walker's code blog

coder, reader

从查找文件并移动的shell命令说开去

一个不能更常见的需求: 从一大堆下载目录(或别的目录)里, 查找指定的文件, 并移动/复制到指定的文件夹, 如果用鼠标点开一个个的文件夹, 还有文件夹里的文件夹, 估计要累死, 当然, 即使自己不会, 也很容易查到两个shell命令:

find path_A -name "*AAA*" -print0 | xargs -0 -I {} mv {} path_B
find path_A -maxdepth 1 -name "*AAA*" -exec mv {} path_B \; 

都能达到目的, 第二条命令容易懂很多(-maxdepth去掉就是recrusive search), 去exec一个mv命令, 记得加上表示语句结束的分号就好了, 我的关注点在第一条, 趁机学学xargs吧.

查到这篇文章说的不错, 先摘几个要点:

echo 'main' | cat test.cpp

这条命令并不会把main输出, 因为管道确实将其作为标准输入给了cat命令作为标准输入, 但因为有了test.cpp这个命令行参数, cat命令就没有去读标准输入的参数了.
其实基本上linux的命令中很多的命令的设计是先从命令行参数中获取参数,然后从标准输入中读取,反映在程序上,命令行参数是通过main函数int main(int argc,char*argv[])的函数参数获得的,而标准输入则是通过标准输入函数例如C语言中的scanf读取到的。他们获取的地方是不一样的。例如:

echo 'main' | cat

这条命令中cat会从其标准输入中读取内容并处理,也就是会输出 'main' 字符串。echo命令将其标准输出的内容 'main' 通过管道定向到 cat 的标准输入中。

cat

如果仅仅输入cat并回车,则该程序会等待输入,我们需要从键盘输入要处理的内容给cat,此时cat也是从标准输入中得到要处理的内容的,因为我们的cat命令行中也没有指定要处理的文件名。大多数命令有一个参数-如果直接在命令的最后指定 -则表示从标准输入中读取,例如:

echo 'main' | cat -

这样也是可行的,会显示 'main' 字符串,同样输入 cat -直接回车与输入 cat直接回车的效果也一样,但是如果这样呢:

echo 'main' | cat test.cpp -

同时指定test.cpp 和 - 参数,此时cat程序会先输出test.cpp的内容,然后输出标准输入'main'字符串,如果换一下顺序变成这样:

echo 'main' | cat - test.cpp

则会先输出标准输入'main'字符串,然后输出test.cpp文件的内容。如果去掉这里的-参数,则cat只会输出test.cpp文件的内容。另外如果同时传递标准输入和文件名,grep也会同时处理这两个输入,例如:

echo 'main' | grep 'main' test.cpp -

此处同上, 如果不加-, 则只会在test.cpp中搜索"main", 加了-, 则会在文件和标准输出中都检查关键字.

另外很多程序是不处理标准输入的,例如kill,rm这些程序如果命令行参数中没有指定要处理的内容则不会默认从标准输入中读取。所以:

echo '516' | kill

这种命里是不能执行的。

echo 'test' | rm -f

这种也是没有效果的。

有时候我们的脚本却需要echo '516' | kill这样的效果,例如ps -ef | grep 'ddd' | kill这样的效果,筛选出符合某条件的进程pid然后结束。这种需求对于我们来说是理所当然而且是很常见的,那么应该怎样达到这样的效果呢。有几个解决办法:

kill `ps -ef | grep 'ddd'`    

这个时候实际上等同于拼接字符串得到的命令,其效果类似于kill $pid

for procid in $(ps -aux | grep "some search" | awk '{print $2}'); do kill -9 $procid; done   

其实与第一种原理一样,只不过需要多次kill的时候是循环处理的,每次处理一个

ps -ef | grep 'ddd' | xargs kill  

OK,使用了xargs命令,铺垫了这么久终于铺到了主题上。xargs命令可以通过管道接受字符串,并将接收到的字符串通过空格分割成许多参数(默认情况下是通过空格分割) 然后将参数传递给其后面的命令,作为后面命令的命令行参数

###xargs与管道的区别

echo '--help' | cat
echo '--help' | xargs cat
```	
第一句输出`--help`, 第二句相当于执行了`cat --help`, 所以管道是把前面的输出当成后面的输入, 而`xargs`则是把前面的输出当成了后面的命令行参数.

`xargs`的命令参数可以查我给的引用原文, 说得详细且有实例, 或者看下面的简单介绍:

-0,--null:以\0作为分隔符,接受到的特殊字符将当作文本符号处理;
-d:指定分段的分隔符,默认分隔字符为空白字符; -a,--arg-file=file:指定命令标准输入的来源文件; -e'FLAG' 或者-E 'FLAG':指定一个终止符号,当xargs命令匹配到第一个FLAG后,停止传递,并退出命令; -p:每当xargs执行一个分段时,询问一次用户是否执行; -t:表示先打印执行的命令再输出; -n NUM:表示一个分段包含的参数个数,参数之间以分隔符隔开,默认是将所有的参数当作一个分段输出; -i:用于将分段分批传递给其后的{}进行输出,分段会替换{}所在的位置进行输出; -I "FLAG":可指定分段的替换符号,分段会分批替换到符号所在的位置进行输出执行; -L:指定每次执行的最大的非空行的行数;

我们来说回"查找并移动"这个原始需求.

首先, 前面铺垫的那么多`-`与标准输入的内容其实与`find`命令并无多大关系. 我们看这里面用到的三个参数

### -print0
用过`find`都知道它的结果是以换行符分隔的, 而加上`-print0`选项则可以把它换成`\0`(其实就是`NUL`)来分隔. 嗯, 不是空格, 但是至少变成了一行, 有点命令行参数的意思了吧?

### -0
就是`--null`, 以`null`为分隔符, 因为我们在前面设置`find`的输出为`null`, 这里当然要设置相应的分隔符. 如果仔细读了前面的参数表, 会发现其实它就是`-d '\0'`的简化版.

### -I
这个命令的英文说明看得我云里雾里, 一贯的不说人话风格, 我还是用一个实例来说明它的用法吧

我在一个目录里建了几个文件, 用`find`把它找出来并用`xargs`把它`echo`出来:

$find . -name "*.txt" -print0 | xargs -p -0 echo echo ./c.txt ./b.txt ./a.txt?...y ./c.txt ./b.txt ./a.txt

    
注意, 我加了一个`-p`参数, 这是为了在执行命令前先把命令打印出来, 这样一来你有机会检查生成的命令最终是不是你想要的, 另一方面也能检查你的命令是否执行了多次.

根据上面的演示, 我们发现一个问题, 就是如果是执行`mv file path/`这样的命令, 也就是说我们需要在命令**中间**插入管道过来的参数, 是不行的, 似乎应该用占位符.

反向学习, 我们既然已经知道了`-I replstr`是正确答案, 那就尝试一下吧:

$find . -name "*.txt" -print0 | xargs -p -0 -I {} echo {} "HELLO" echo ./c.txt HELLO?...y ./c.txt HELLO echo ./b.txt HELLO?...y ./b.txt HELLO echo ./a.txt HELLO?...y ./a.txt HELLO


首先, 我们发现, 我们成功地在`echo`和`HELLO`间插入了管道过来的参数, 其次, 它还把参数用分隔符自行拆开了一次执行一个(又有点类似于添加了`-n 1`的选项的意思).

现在我们明白了, 网上查到的那条命令最终就是执行了N次`mv FILE /path`, 这就是`-I {}`.

Furthermore, 我们把标准答案里那高大上的`{}`换一下如何?

$ find . -name "*.txt" -print0 | xargs -p -0 -I 'M' echo 'M' "HELLO" echo ./c.txt HELLO?...y ./c.txt HELLO echo ./b.txt HELLO?...y ./b.txt HELLO echo ./a.txt HELLO?...y ./a.txt HELLO

$ find . -name "*.txt" -print0 | xargs -p -0 -I M echo M "HELLO" echo ./c.txt HELLO?...y ./c.txt HELLO echo ./b.txt HELLO?...y ./b.txt HELLO echo ./a.txt HELLO?...y ./a.txt HELLO


这里我分别用了`'M'`和`M`, 都不影响其作为占位符的作用, 不要被那故弄玄虚的`{}`给迷惑了. 之所以用`{}`应该还是它更好被辨识和表义, 并不是大括号本身是什么语法.

error--IB-Designables--Failed-to-update-auto-layout-status

首先, 了解一下 IBInspectable / IBDesignable 这是让 Xcode能在设计时就体现你代码对 UI 进行的修改, 以及在设计器里能动态增加你对视图添加的属性的控件的特性(是的, 只是 xcode 的特性, 并不是语言特征)

然后, 用这个的人多半碰到了这类问题:

error: IB Designables: Failed to update auto layout status: Interface Builder Cocoa Touch Tool raised a "NSInternalInconsistencyException" exception: Could not load NIB in bundle: 'NSBundle

网上可能有一大堆讨论相关问题的贴子, 解决方法不尽其数, 可能都不适用你, 所以我的这篇也可能不适用你, 最好还是善用搜索, 我谨提供一种思路.

网上有让你修改工程配置的, 寻找崩溃日志的, 解决我这个问题的, 是在Storyboard 界面里选中出问题的 View(你一定要至少知道是给哪个 view 添加上 IB_DESIGNABLE才导致的问题, 可以通过逐个移除这个声明以测试), 再在菜单里选择: Editor - Debug Selected Views 这个时候就会模拟IB_DESIGNABLE进行 debug, 再加上全局异常断点, 代码就会在崩溃处命中了.

我的问题是我用了两个属性, 然后在 view 的 drawWithRect:方法中, 这两个属性都为空, 而我的属性是在initWithCoder:中初始化的. 所以我再添加initWithFrame:, 在其中解决, 顺利解决.

这说明三个问题:

  1. IB_DESIGNABLE目前的实现还有 bug, 真实启动是跑initWithCoder:的, 它在设计器里绘图的时候却走了initWithFrame:, 你可能不得不为了对付这个 bug 而添加一次同样的代码
  2. initWithFrame:的时候 frame 是{0, 0, 0, 0}可别忘了, 有时候这个也是崩溃原因
  3. 如果我把drawWithRect:中要用到的属性提前初始化一样可以避免这样的问题, 怎样做? 配合IBInspectable, 然后在设计器中给属性设初始值

El-Captain设置环境变量

这里说的不是设置变量给bash/shell来用, 而是给程序使用, 比如, chromium自36版以后, 就不再内置google api keys, 官方文档说明你打包的时候没有添加key的话, 可以在runtime添加, 比如在系统的环境变量里添加进去.

Providing Keys at Runtime

If you prefer, you can build a Chromium binary (or use a pre-built Chromium binary) without API keys baked in, and instead provide them at runtime. To do so, set the environment variables GOOGLE_API_KEY, GOOGLE_DEFAULT_CLIENT_ID and GOOGLE_DEFAULT_CLIENT_SECRET to your "API key", "Client ID" and "Client secret" values respectively.

至于key哪来的请自行google, 我们不去申请key的话, 还是拿来主义:

export GOOGLE_API_KEY="AIzaSyCkfPOPZXDKNn8hhgu3JrA62wIgC93d44k"

export GOOGLE_DEFAULT_CLIENT_ID="811574891467.apps.googleusercontent.com" export GOOGLE_DEFAULT_CLIENT_SECRET="kdloedMFGdGla2P1zacGjAQh"

关于如何在mac上设置环境变量, 有这么一篇雄文, 我一般是直接编辑~/.bash_profile文件, 这次不生效了, 改来改去都没用, 于是换关键词: yosemite/el captain下如何设置环境变量, 立刻就有答案

头两个答案都可以, 第一个是使用了setenv VARIABLENAME=VALUE这种语法, 第二个是直接在一个文件里编辑, 然后使之生效, 我直接用了第二种, 因为文本随时可编辑, 可查看

1, Create an environment.plist file in ~/Library/LaunchAgents/ with this content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>my.startup</string>
  <key>ProgramArguments</key>
  <array>
    <string>sh</string>
    <string>-c</string>
    <string>
        launchctl setenv GOOGLE_API_KEY AIzaSyCkfPOPZXDKNn8hhgu3JrA62wIgC93d44k
        launchctl setenv GOOGLE_DEFAULT_CLIENT_ID 811574891467.apps.googleusercontent.com
        launchctl setenv GOOGLE_DEFAULT_CLIENT_SECRET kdloedMFGdGla2P1zacGjAQh
    </string>
  </array>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>

2, You can add many launchctl commands inside the <string></string> block.可见, 我们只需要在string标签里写需要的内容就行了, 本例是一系列google api keys.

3, The plist will activate after system reboot. You can also use launchctl load ~/Library/LaunchAgents/environment.plist to launch it immediately.

-bin-bash和-bin-sh的区别

教程地址

快捷键:

ctrl+L: 清除屏幕并移到顶部
ctrl+U: 删到行首(剪) ctrl+A: 移到行首
ctrl+K: 删到行尾(剪) ctrl+E: 移到行尾
ctrl+b, f, (左移, 右移)
alt+b,f (前, 后移一个单词)
ctrl+1 等同于clear
ctrl+d delete, ctrl+w 删到单词首
alt+t 词换位, ctrl+t 字符换位 (与前一个)
alt+d, alt+backspace 剪切至词首, 词尾
ctrl+y 粘贴
$ ’Something wrong happend’ >&2

解释:

>代表重定向输出

&表示接下来的是一个文件描述符数字(file descriptor number) 2表示stderr 所以就是把上述字符串输出到stderr里去的意思, 如果是$ ”str” <2 (没有&), 则会输出到一个叫2的文件里去

echo

-n: 可以取消结尾的换行 -e: 解释字符串里的转义符

扩展(通配符)

$ ~/walker # 表示用户目录下的walker目录
$ ~walker # 表示名为walker的目录
$ ~+ # 扩展为当前目录, 等同于 pwd
$ echo d{a, e, I, o, u}g 输出: dag, deg, dig, dog, dug # 大括号内不要有空格(否则会当成参数)
$ echo {j{p,pe}g, png} —> 嵌套

波浪线, 方括号的括号都是基于”路径”的, 如果当前路径没有匹配到对应的文件名, 则会变成字符串原样输出, 而大括号则不然, 是基于”逻辑”的, 只管扩展, 不会去探测扩展后对应的路径存不存在, 因此可能报错文件不存在. 如echo [a,b].txt, 如果不存在a.txt, b.txt, 则会变成”[a,b].txt”这样一个输出, 而{a,b}.txt一定会扩展成a.txt, b.txt 例外: 在用..来扩展时, 如果系统无法理解, 则不会扩展, 如{1..5}会扩展成1,2,3,4,5, 但{ab..123}, 则会变成字符串 但是前导0不参与路径匹配: {01…5} # 01,02,03,04,05 (几个零都可以) 步长:{0..8..2} (未测试成功) # 要打开哪个shopt开关? 活用:

$ echo .{mp{3..4},m4{a,b,p,v}} # 匹配了: .mp3 .mp4 .m4a .m4b .m4p .m4v
$ mkdir {2007..2009}-{01..12}  # 建了2007-2009每年12个目录
for I in {1..4}

注意惊叹号的使用(类似正则里的^)

$ echo ${!S*} # 返回所有以S开头的”变量名”, 如SHELL, SSH..等

另两种转义(string interpolation):

$ echo date is $(date) # 即包在$(…)中
$ echo date is `date` # 包在反引号中

但是要计算2+2, 只有echo $((2+2)) 这种形式, 反引号就不行了

[[:alnum:]], [[:digit:]]等预置的字符类扩展见: https://wangdoc.com/bash/expansion.html 很丰富, 建议详读.

(?, \*, +, @, !)则为匹配的个数, 分别是(0或1, 0或多, 一或多, 一个, 非一个), 如song@(.)mp3等同于song.mp3, 是的, 不同于正则, 它是先规定个数, 再设定匹配字串

注: 需要打开shopt -s extglob

双引号碰到$, 反引号和反斜杠都会自动扩展, 所以echo “$SHELL” 等同于echo echo \$SHELL 双引号能保留”输出”的格式, 比如 echo `cal`, 格式就没了, 自己试试看? 而echo “$(cal)”则可以保留格式:

$ echo "$(cal)"
      六月 2020
日 一 二 三 四 五 六
    1  2  3  4  5  6
 7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30

大段文字输入可以用

$command << token
your long inputs
token

等同于: echo "your long inputs" | command 即把echo的输出作为command的输入, 这个一般用于多行文本

texts里面可以进行使用变量, 但是如果把token用双引号包起来就不能解释变量了.

如果只是简单字符串, 用下面更明确: $ command <<< ‘text’ 如: $ cat <<< “hello world”, 一样等同于 $ echo “hello world” | cat <<< 还有一个作用就是把变量值用这种方式能变成标准输入, 这样被”计算”出来的值也能用于只接受标准输入的命令了, 比如read

变量

printenv PATHecho $PATH等同 解释变量中的变量, 比如$PATH, 不是想象中的$嵌套: ${${myvar}}, 应该这么用

$ myvar=PATH
$ echo ${!myvar}, # 即多加一个惊叹号

$?: 上一个命令的退出码(0成功, 1失败) $$: 当前Shell进程的ID $_: 上一个命令的最后一个参数 $!: 最后一个后台执行的异步命令的进程ID $0: 当前Shell的名称 $-: 当前shell的启动参数, $@, $# 表示脚本的参数数量

$?命令除了取出上一个命令的返回值, 也可以取出上一个函数的返回值

${varname-:value} 取值, 如果不存在则返value, 但不赋值 ${varname=:value} 取值, 如果不存在则返value, 顺便赋值 ${varname+:value} 如果有值则返value(而不是值本身), 没有值则为空, 所以这个时候的value一般用一个标识符号就好了 ${varname?:value} 取值, 如果不存在就报错并把value作为错误错误打印出来 比如 $ filename=${$1:?”filename missing”} 从脚本中取第一个参数作为文件名, 发现没有文件名就报错退出

变量都是字符串, 可以用declare来进行一些限定

$ declare -I v1=13 v2=14 v3=v1+v2 # 声明为integer
$ echo $v3
# 这样更快: 
$ let v=13+14  # 如果习惯了=两边加空格, 则包到引号里: $ let “v = 13 + 14”

字符串

${#”string”} 长度 ${varname:offset:length} 切片(变量名_不需要_美元符号) 删除: (# 和 ## 的区别就是贪婪与否的区别)

$ phone="555-456-1414"
$ echo ${phone#*-}
> 456-1414
$ echo ${phone##*-}
> 1414

替换: ${variable/#pattern/string} 注意, # 左边多了一个 / , 右边多了替换字串 以上, 都是从头匹配, 从尾部匹配把 # 换成 % 任意位置匹配则换成/, 所以就成了你们最熟悉的语法:varname/search/replace

这个时候再回头看/# , /%, 不过是/语法的修饰符罢了(限定起始方向)

${varname^^}, ${varname,,} # 转大写, 转小写

数值运算

逗号是求值, 如 $ echo $((foo = 1+2, 3*4)) 输出为12, 但foo的值是3, 依次计算, 输出是逗号后面的

expr命令等同于双括号: expr 3+5$((3+5))同义

行操作

Bash内置Readline库, 默认采用Emacs快捷键, 切换:

$ set -o vi 或 $ set -o emacs

切换目录/堆栈

不管你cd到哪个了哪个目录, 想回到cd前的目录, 用cd -就行了 pushd , popd则可以把目录推到堆栈里, 演示:

$ pushd 2
/test/1/2 /test/1
$ pushd 3
/test/1/2/3 /test/1/2 /test/1
$ pushd 4
/test/1/2/3/4 /test/1/2/3 /test/1/2 /test/1
$ dirs  # 其实每一次pushd都会把当前堆栈dirs出来
/test/1/2/3/4 /test/1/2/3 /test/1/2 /test/1
$ cd /tmp
$ dirs # 观察, cd其实只是把顶层给改了, 不会增减层级
/tmp /test/1/2/3 /test/1/2 /test/1
$ cd /usr
$ dirs  # 验算
/usr /test/1/2/3 /test/1/2 /test/1

现在你知道 了, cd永远只是更改顶栈, 大多数情况下, 你可以用pushd来替换cd, 这样你就有了后退权了 此时你再popd, 目录会顺利切到/test/1/2/3, 不管你进行过多少次cd, 第二层都不会变并且能直接pop出来 $小练习$ 如果你查看堆栈, 要从第4个开始后退(0为起始), 那么可以把从3开始(不是4)的记录提到顶层来(然后再popd): $ pushd +3 (加号不可省)

注意, 此时0, 1, 2都还在, 只是挪到了尾巴

popd +3则不是”移动”, 而是删除了, 意思是正向删除3个, 如果不带+, 则理解为删除3以后的所有堆栈(即从4开始)

注意: 为什么要从第4个开始退要把从3开始的移到顶层呢?

因为如下dirs: /1, /2, /3 , 你做popd, 是会回到/2的 可见顶栈永远表示的是"当前"目录. 所以你自然无法跳到当前目录. 而pushd, popd+数字改动的只是堆栈表, 不是目录, 即虽然你的目录没变, 但是系统认为你在第三层, 这个时候再后退(popd)自然到了目录表里的下一层/4.

脚本

shebang行#!/usr/bin/env bash 的写法是为了避免#!/bin/bash 这种写法时bash不在bin目录 source命令可以 用一个点来表示: . ~/.bash_profile 读用户的输入:

$ read firstname lastname
$ “you input: $firstname, $lastname” # 如果read后没有给变量名, 则由默认的$REPLY来取出

读文件:

while read myline  # 每次读一行
do
  echo "$myline"
done < $filename  # 注意这里特殊的传参方式, 同时, 如果不传入文件路径, 就是一个无限循环了(read)

存数组:

$ read -a varname. #  -a 参数把用户的多个输入全存到`varname`这个数组里了

其它有用的:

# -e 参数使得用户在输入的时候能用tab补全(包含所有readline库快捷键),
# 如果没有这个参数输入文本的时候是不能使用快捷键的
$ read -e -p “please input the path to the file” 
# -s 可以隐藏用户的输入, 通常用于密码
$ read -s  -p “input password”
# -p 显然就是能直接显示输入前的提示了

条件判断

if里面的test命令 test expression, [ expression], [[ expression ]] 是等价的(第三种支持正则) 空格不能省 [ -? file ] 查看文件状态有非常多的表达式(参数), 具体参阅https://wangdoc.com/bash/condition.html 推荐阅读 for循环 for [ test ] in list; do … done 其中的in list如果省略, 则代表所有脚本参数”$@“:

$ for filename; do echo “$filename”; done
# 等同于
$ for filename in “$@“; do echo “$filename”; done

同理, 如果是用在函数中, 则等于所有函数参数. 用双括号, 变量也无需加$for (( i=0; i<5; i+=1 )); do echo $i; done case in, 如果希望一个匹配后继续做下一个匹配(passthrough), 每一个case 的结尾用;;&而不是;; (多了一个&)

select

select生成的菜单, 选择并执行命令后, 要自行在do-done体内用break退出, 否则会一直要你选择

数组

以下方式声明数组

$ names=(hatter [5]=duchess Alice), 指定了0, 5, 6, 其它为空字符串
$ mp3s=( *.mp3 )
$ declare -a ARRAYNAME
$ read -a ARRAYNAME

读取的时候: $ echo ${array[1]} 大括号不可省 @仍然是返回所有元素: $ echo ${array[@]} 但是在for…in中, 要把整个表达式放双引号中:

$ activities=( swimming "water skiing" canoeing "white-water rafting" surfing )
$ for act in “${activities[@]}”; do….; done

不然其中有”water”, “skiing”,”white-water", rafting”等都会被拆开(bug吧? 字符串也拆) 把@换成*, 加上双引号, 则会一个个字符返回 拷贝数组最方便的方法:

$ hobbies=( “${activities[@]}” diving ) # 顺便演示了为数组添加成员

直接赋值给一个数组(即没有指定索引), 则是赋给第0个组员, 同理, 使用数组名也是使用的0号组员

# 以下@可以换成*
$ echo ${#array[@]} # #仍然用以计数, 但是如果传的是具体索引, 则返回的是对应项的字符串长度
$ echo ${!array[@]} 用以返回有值的索引 (为空的不返回) # 活用的话遍历数组更高效
$ echo ${array[@]:2:3} # 切片
$ arr+=(3 4 5) # 追加
$ unset arr[2] # 删除 , 或: 
$ arr[2]= # 或
$ arr[2]=‘’
# 以上三者等效, 

根据上面知识$ arr= 表示删除第一个成员, 但是unset arr 则是清空整个数组了 也可以用字符串做索引, 就成了字典了:

$ declare -A colors  # 变成大写即可
$ colors[“red”]=“#ff0000”

set命令

单独一个set会显示所有环境变量和Shell函数 以下都可以以set -xxx 的方式写在脚本头任何位置, 就当一个即时开关使用吧 也可以在调用bash脚本前传入比如: bash -eux script.sh -u: 遇到不存在的变量就报错, 而不是忽略 与 -o nounset 等价 -x: 每一个命令执行前会先打印出来 等同于 -o xtrace, 关闭用set +x (组合起来用就是一个小环境) -e: 有错误就中止 等同于 -o errexit -o pipefail: 即使在管道中, 有错也中止(-e 在管道中会失效) -n: -o noexec 不执行命令只检查语法 -f: -o noglob 不对通配符进行文件名(路径)扩展 可用+f 关闭 -v: -o verbose 打印shell接收到的每一行输入 可用+v 关闭 $ set -euxo pipefail 一般这么四个连用

shopt

即: shell option 同set, 直接shopt也可以列出所有参数, -s, -u分别是是打开, 关闭某个参数 shopt 参数名, 可直接查询该参数是否打开关闭, 但是如果是用于编程, 因为返回是字符串不好判断, 所以提供了-q参数(返回0/1, 分别表示打开/关闭)

$ if shopt -q globstar; then …; fi

除错

# 先看目录存不存在, 然后再进入, 然后再打印出来将要删除的文件, 
# 这是最安全的删除方法
# 否则一旦目录不存在, 不同的写法会有不同的问题
[[ -d $dir_name ]] && cd $dir_name && echo rm *  

如果在执行bash脚本前加入-x参数, 则每一条命令执行前都会打印出来 # 等同于set -x

或者写在脚本的shebang行里也行

每一条命令会同上一个标识符作前缀, 默认是+, 可以用export PS4=‘$LINENO +’这种方式自定义(比如现在就加上了行号) $几个环境变量$ $LINENO: 这个变量在哪, 打印的就是这一行的行号 $FUNCNAME: 返回一个数组, 函数调用的名称堆栈, 最里层(即本函数)的是0 $BASH_SOURCE: 返回一个数组, 函数调用的脚本堆栈, 即每层调用的脚本是哪一个, 最里层(即本文件)的是0 $BASH_LINENO: 返回一个数组, 函数每一次被调用时在该脚本的行号, 同样也是从最里层开始 例:

${BASH_SOURCE[1] = main.sh  # [0] 是文件本身, 所以要[1]
${BASH_LINENO[0] = 17  # 调用来源的行号  —> 所以调用来源的行号的索引永远比调用来源(文件)的索引要小1
${FUNCNAME[0]} = hello  # 本方法(或者说”被调用的方法”)

上例代表在 main.sh的17行调用了hello()方法 $小练习$

#!/bin/bash
source lv2.sh   # 引入外部脚本
function lv1method()
{
    echo ---------lv1------------
    i=0
    for v in "${BASH_LINENO[@]}"; do
        echo "bash_line_no[$((i++))]: $v"
    done
    i=0
    for v in "${FUNCNAME[@]}"; do
        echo "func_name[$((i++))]: $v"
    done
    i=0
    for v in "${BASH_SOURCE[@]}"; do
        echo "bash_source[$((i++))]: $v"
    done
    lv2method # 调用外部脚本的方法
}

以上脚本, 多做几次嵌套, 打印出来看看索引之间的关系 输出:

---------lv1------------
bash_line_no[0]: 5
bash_line_no[1]: 0
func_name[0]: lv1method
func_name[1]: main
bash_source[0]: lv1.sh
bash_source[1]: entry.sh
---------lv2------------
bash_line_no[0]: 21
bash_line_no[1]: 5
bash_line_no[2]: 0
func_name[0]: lv2method
func_name[1]: lv1method
func_name[2]: main
bash_source[0]: lv2.sh
bash_source[1]: lv1.sh
bash_source[2]: entry.sh
---------lv3------------
bash_line_no[0]: 19
bash_line_no[1]: 21
bash_line_no[2]: 5
bash_line_no[3]: 0
func_name[0]: lv3method
func_name[1]: lv2method
func_name[2]: lv1method
func_name[3]: main
bash_source[0]: lv3.sh
bash_source[1]: lv2.sh
bash_source[2]: lv1.sh
bash_source[3]: entry.sh

临时文件

安全的用法:

trap 'rm -f "$TMPFILE"’ EXIT  # 退出时删除临时文件)
TMPFILE=$(mktemp) || exit 1  # 用mktemp命令建立临时文件可以只有本人能读, 如果失败就退出
echo "Our temp file is $TMPFILE”

参数: -d: 创建的是目录 -p: 指定目录 -t: 指定模板 如 mktemp -t aaa.XXXXXXX 能生成/tmp/aaa.yZ1HgZV(与X个数相同) trap是用来响应系统信号的, 如ctrl+c产生中断信号SIGINT $ trap -l 列出所有信号(自己打印出来看看) trap的格式: $ trap [动作] [信号1] [信号2] ... trap 命令接的信号有如下

  • HUP:编号1,脚本与所在的终端脱离联系。
  • INT:编号2,用户按下 Ctrl + C,意图让脚本中止运行。
  • QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本。
  • KILL:编号9,该信号用于杀死进程。
  • TERM:编号15,这是kill命令发出的默认信号。
  • EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。

如果trap要执行多条命令, 可以封装到函数里, 命令的位置写函数:$ trap func_name EXIT

启动环境

登录session依次启动如下脚本:

  • /etc/profile
  • /etc/profile.d # 目录下的所有.sh文件
  • ~/.bash_profile # 如果有, 则中止
  • ~/.bash_login # 如果有, 则中止 此为C shell 初始化脚本
  • ~/.profile # Bourne shell 和 Korn shell 初始化脚本

通过$ bash - -login 参数, 可以强制执行以上脚本 非登录session

  • /etc/bash.bashrc # 所有用户都执行
  • ~/.bashrc # 当前用户的

启动参数: -n: 不执行脚本, 只检查语法 -v: 执行语句前先输出 -x: 执行语句后输出该语句

~/.bash_logout 退出时要执行的命令 $ include /etc/inputrc~/.inputrc里加这一行, 可以在里面自定义快捷键

命令提示符

上面提到过$PS4能修改set -x时打印的每句语句前面的+号 命令提示符默认的$符号(根用户#号)则可以用$PS1来修改, 怎么改参考https://wangdoc.com/bash/prompt.html $PS2表示的是输入时折行的提示符, 默认为> $PS3表示使用select命令时系统输入菜单的提示符

iOS屏幕滚动时Timer保持工作的几种方式

iOS当前线程的RunLoop在TableView等scrollView滑动时将DefaultMode切换到了TrackingRunLoopMode。因为Timer默认是添加在RunLoop上的DefaultMode上的,当Mode切换后Timer就停止了运行。 如这样:

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "HH:mm:ss"
    self.timeLabel.text = "\(dateFormatter.string(from: Date()))"
}

本文记录如下四种方式:

  • 将NSTimer添加到当前线程所对应的RunLoop中的commonModes中。
  • 通过Dispatch中的TimerSource来实现定时器。
  • 是开启一个新的子线程,将NSTimer添加到这个子线程中的RunLoop中,并使用DefaultRunLoopModes来执行。
  • 使用CADisplayLink来实现。

CommonModes

override func awakeFromNib() {
    super.awakeFromNib()

    let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "HH:mm:ss"
        self.timeLabel.text = "\(dateFormatter.string(from: Date()))"
    }

    RunLoop.current.add(timer, forMode: .commonModes)
}

子线程/异步 + DefaultMode

override func awakeFromNib() {
    super.awakeFromNib()
    DispatchQueue.global().async {
        let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "HH:mm:ss"
            DispatchQueue.main.async {
                self.timeLabel.text = "\(dateFormatter.string(from: Date()))"
            }
        }
        RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
        RunLoop.current.run()
    }
}

DispatchTimerSource

GCD的知识点

override func awakeFromNib() {
    let queue: DispatchQueue = DispatchQueue.global()   //也可以用mainQueue来实现
    let source = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: 0), queue: queue)
    let timer = UInt64(1) * NSEC_PER_SEC

    source.scheduleRepeating(deadline: DispatchTime.init(uptimeNanoseconds: UInt64(timer)), interval: DispatchTimeInterval.seconds(Int(1)), leeway: DispatchTimeInterval.seconds(0))

    let timeout = 1
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "HH:mm:ss"
    source.setEventHandler {
        if(timeout < 0) {
            source.cancel()
        }

        DispatchQueue.main.async {
            self.timeLabel.text = "\(dateFormatter.string(from: Date()))"
        }
    }
    source.resume()
}

CADisplayLink

CADisplayLink可以添加到RunLoop中,RunLoop的每一次循环都会触发CADisplayLink所关联的方法。在屏幕不卡顿的情况下,每次循环的时间时1/60秒。

override func awakeFromNib() {
    super.awakeFromNib()
    DispatchQueue.global().async {
        let displayLink = CADisplayLink(target: self, selector: #selector(self.update))
        displayLink.add(to: RunLoop.current, forMode: .defaultRunLoopMode)
        RunLoop.current.run()
    }
}

func update() {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "HH:mm:ss"
    let time = "\(dateFormatter.string(from: Date()))"

    if time != currentTime {
        currentTime = time
        DispatchQueue.main.async {
            self.timeLabel.text = self.currentTime
        }
    }
}

详细内容请阅读原文

MacOS添加自启动脚本

MacOS下添加自启动脚本有很多方法, 在一篇知乎文章中了解到Launchd替代了过去的init, rc, init.d, rc.d, SystemStarter, inted/xinetd, watchdogd等, 建议用Launchd. 当然还有别的Automator, Apple Script等方式(底层未研究), 感兴趣的自己搜索, 我选择了直接Launchd, 结合so上的这篇文章:

  1. 编写自己的脚本, 添加可执行权限chmod a+x myscript.sh
  2. 编写Launchd配置文件(.plist文件)
  3. 结合上述两篇文章, 确定在系统启动还是用户启动时运行脚本, 我选择的是用户目录(~/Library/LaunchAgents/)
  4. load这个配置: launchctl load -w ~/Library/LaunchAgents/com.service.name.plist
  5. 登入登出测试, 或: launchctl start com.service.name

注:

  1. 可执行脚本里的路径有空格需要转义
  2. 但plist文件里<string>标签里的目录如果有空格, 不需要转义
  3. load-w参数参见这篇文章
  4. 如果出错, 运行Console应用查看日志, 或参考这篇文章, 定向日志输出文件

即在.plist文件里添加:

<key>StandardOutPath</key>
<string>/var/log/mylog.log</string>

附: .plist文件示例

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
    	<key>Label</key>
    	<string>com.service.name</string>
    	<key>ProgramArguments</key>
    	<array>
    		<string>/path/to/my/script.sh</string>
    	</array>
    	<key>RunAtLoad</key>
    	<true/>
    </dict>
</plist>

如果执行的脚本就一句话, 你可能希望直接在.plist文件里运行, 而不是额外再多生成一个脚本吧? (source)

<key>ProgramArguments</key>
<array>
        <string>/bin/bash</string>
        <string>-c</string>
        <string>ls -1 | grep *.txt | echo &gt; allTextFiles</string>
</array>

继续, 如果还想以root来执行脚本, 综合起来, 我的实现如下:

cp com.run.udp2raw.plist /Library/LaunchDaemons
cd /Library/LaunchDaemons
sudo launchctl load -w com.run.udp2raw.plist
sudo launchctl start com.run.udp2raw

其中udp2raw对应的命令是需要root权限的, 实测通过. 我选择的是/Library/LaunchDaemons/

注: 唯一要注意的地方, 就是最后两行, loadstart命令都需要加sudo. 没有加的时候没有报错, 但是没有运行成功.

附: folders and usage

|------------------|-----------------------------------|---------------------------------------------------|
| User Agents      | ~/Library/LaunchAgents            | Currently logged in user
|------------------|-----------------------------------|---------------------------------------------------|
| Global Agents    | /Library/LaunchAgents             | Currently logged in user
|------------------|-----------------------------------|---------------------------------------------------|
| Global Daemons   | /Library/LaunchDaemons            | root or the user specified with the key UserName
|------------------|-----------------------------------|---------------------------------------------------|
| System Agents    | /System/Library/LaunchAgents      | Currently logged in user
|------------------|-----------------------------------|---------------------------------------------------|
| System Daemons   | /System/Library/LaunchDaemons     | root or the user specified with the key UserName
|------------------|-----------------------------------|---------------------------------------------------|

apple-store链接格式文档

The app on Appstore has specific URL format http://itunes.apple.com/[country-code]/app/[app-name]/id+[id_value]?mt=[1...12] country-code can be us for united statesin for india etc mt stands for Media Type Value for mt can be anything from 1 to 12 and each assigned to specific category i. 8 for iOS apps ii. 12 for Mac apps

来源: stackoverflow

Fixing-Chrome-58+-[missing_subjectAltName]-with-openssl-when-using-sel

原文链接

说在前面

  1. createselfsignedcertificate.sh文件里的sudo删掉了
  1. server.csr.cnfdn里面的内容请改成自己的
  2. v3.ext里面的DNS.1也更改为自己的server
  3. 本来我只想绑一个固定的 IP, 基本通过, 但是在mac的chrome58下, 仍然过不了, 最终还是通过域名解决

上一个在 chrome58下终于变绿的图片

原文转载

Since version 58, Chrome requires SSL certificates to use SAN (Subject Alternative Name) instead of the popular Common Name (CN), thus CN support has been removed.If you're using self signed certificates (but not only!) having only CN defined, you get an error like this when calling a website using the self signed certificate:

Here's how to create a self signed certificate with SAN using openssl

First, lets create a root CA cert using createRootCA.sh:

#!/usr/bin/env bash
mkdir ~/ssl/openssl genrsa -des3 -out ~/ssl/rootCA.key 2048
openssl req -x509 -new -nodes -key ~/ssl/rootCA.key -sha256 -days 1024 -out ~/ssl/rootCA.pem

Next, create a file createselfsignedcertificate.sh:

#!/usr/bin/env bash
openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key -config <( cat server.csr.cnf )
openssl x509 -req -in server.csr -CA ~/ssl/rootCA.pem -CAkey ~/ssl/rootCA.key -CAcreateserial -out server.crt -days 500 -sha256 -extfile v3.ext

Then, create the openssl configuration file server.csr.cnf  referenced in the openssl command above:

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn

[dn]
C=US
ST=New York
L=Rochester
O=End Point
OU=Testing Domain
[email protected]
CN = localhost

Now we need to create the v3.ext file in order to create a X509 v3 certificate instead of a v1 which is the default when not specifying a extension file:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost

In order to create your cert, first run createRootCA.sh which we created first. Next, run createselfsignedcertificate.sh to create the self signed cert using localhost as the SAN and CN. After adding the rootCA.pem to the list of your trusted root CAs, you can use the server.key and server.crt in your web server and browse https://localhost%C2%A0using Chrome 58 or later:

You can also verify your certificate to contain the SAN by calling

openssl x509 -text -in server.crt -noout

Watch for this line Version: 3 (0x2) as well as X509v3 Subject Alternative Name: (and below). Happy self signing!

将共享文件夹用作Time-Machine并加密

怎么在局域网创建一个共享文件夹不在此文讨论范围内, 比如windows文件夹简单右键共享一下, 就能走完本教程. 以下是在macOS上设置Time Machine的操作.

#Step 1: 各种命名 没什么用的第一步, 如果你有多台电脑, 那就最好用名字和MAC地址来作备份的名字, 送佛送到西:

MAC_ADDRESS=`ifconfig en0 | grep ether | awk '{print $2}' | sed 's/://g'`
SHARE_NAME=`scutil --get ComputerName`
IMG_NAME=${SHARE_NAME}_${MAC_ADDRESS}.sparsebundle
echo $IMG_NAME

#Step 2: 创建并加密一个镜像 复制粘贴前记得更改一下'MAXSIZE', 设为自己想要的大小. 为了怕人不看文字直接复制, 我设定了一个合理的350G

MAXSIZE=350g
hdiutil create -size $MAXSIZE -type SPARSEBUNDLE -nospotlight -volname "Backup of $SHARE_NAME" -fs "Case-sensitive Journaled HFS+" -verbose unencrypted_$IMG_NAME
hdiutil convert -format UDSB -o "$IMG_NAME" -encryption AES-128 "unencrypted_$IMG_NAME"
rm -Rf "unencrypted_$IMG_NAME"

注意两点:

  1. 该脚本先创建了一个未加密的image(其实是一个文件夹), 随后加密, 过程中会询问密码, 最后删除未加密的image
  2. 文件会创建在用户主目录, 如果空间不够, 可以读一下hdiutil的文档, 自行设定到远程共享文件夹去. 如果按本脚本, 那么请自行移动到共享目录

#Step 3: 设置Time Machine 双击共享文件夹里的镜像, 输入上一步设置的密码, 此时会mount到本地, 菜单栏上的Time Machine的选择备份文件夹功能里应该能看到这个盘, 但是你不能用它, 我们用命令来关联:

defaults write com.apple.systempreferences TMShowUnsupportedNetworkVolumes 1
sudo tmutil setdestination "/Volumes/Backup of $SHARE_NAME"

此时再打开时光机器, 就可以看到已经自动关联上了(你无需去选择备份硬盘). 有一个小问题, 就是即使我这么操作下来, 即使mount的时候需要输入密码, 备份的时候还是提示往一个没有加密的盘里备份. 也就是说, 我们以为encrypt了, 只是对image而言, 备份还是不加密的. 可见我们还是没有找到像一些NAS系统里那样能被自动发现, 正常加密的方案

参考:

source
create an sparse image
encrypt it
convince Time Machine to use it

备注: 请活学活用, 比如我就没用那些名字变量, 直接写死了镜像路径和文件名

用CALayer绘图,添加动画和渐变

如果CALayer只有一个简单的 path, 那么直接给 path 赋值是最简单的:

shapeLayer = [CAShapeLayer layer];
shapeLayer.bounds = self.bounds;
shapeLayer.anchorPoint = CGPointMake(0, 0);

CGFloat endAngle = (1+_percentage)*M_PI;
shapeLayer.path = [UIBezierPath bezierPathWithArcCenter:center
                                                 radius:radius
                                             startAngle:startAngle
                                               endAngle:endAngle
                                              clockwise:YES].CGPath;
shapeLayer.strokeColor = _highlightColor.CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = arcWidth;
shapeLayer.lineCap = kCALineCapRound;
[self.layer addSublayer:shapeLayer];         

对 线条类的 path 可以应用strokeEnd属性来绘制动画:

CASpringAnimation *pathAnimation = [CASpringAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
pathAnimation.mass = 4.0f;              // 物体质量 1
pathAnimation.stiffness = 200;          // 弹簧刚性 100
pathAnimation.damping = 20;             // 弹簧阻尼 10
pathAnimation.initialVelocity = 1.0f;  // 初始速度 0
pathAnimation.duration = pathAnimation.settlingDuration;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[shapeLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];

再加点渐变吧

// 增加渐变图层
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.bounds;
gradientLayer.colors = gradientColorSet;
gradientLayer.startPoint = CGPointMake(1,0);
gradientLayer.endPoint = CGPointMake(0, _percentage);

[self.layer addSublayer:gradientLayer];
// [self.layer addSublayer:shapeLayer]; // 移除之前的图层
gradientLayer.mask = shapeLayer; // 当作渐变图层的 mask

组合效果如下:

要绘制弧形, 对照这个图就很简单了:

补充知识:

1, CALayer的动画用不了animateWithDuration:animations:completion:怎么办?

因为这是UIView的方法, 你要把它加到一个CATransaction里面去

2, 即使加到CATransaction里面了, 怎么我对frame做的动画还是没有生效?

因为frame是一个复合属性, 它由position, bounds等属性决定, 所以你只是用错了属性.

示例:

    [CATransaction begin];
    [CATransaction setCompletionBlock:^{
        // 完成回调
    }];
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"bounds.size.width"];
    animation.duration = self.defaultLayoutTransitionDuration;
    animation.fromValue = @(0.0f); 
    animation.toValue = @(finalFrame.size.width); 
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    [line.layer addAnimation:animation forKey:@"lineLayerAnimation"];
    line.bounds = finalFrame;
    [CATransaction commit];

其它有关 CALayer 的不同生命周期里绘制的解说请参考这篇文章, 全文转载如下

在iOS中绘图,可以使用UIView,也可以使用CALayer。实际上,UIView也是由底层的CALayer完成绘制的工作

#UIView和CALayer的关系

每个UIView内部都有一个CALayer对象,由它来完成绘制的工作。和view一样,layer也是一个树形的结构

当不需要自定义组件的时候,用UIView的API就足以胜任,把需要的子view通过addSubview()方法放到view的层次里即可;但是如果需要自己绘制一些图形,就需要在UIView的drawRect()方法或是CALayer的相关方法中,调用CoreGraphics的API来画图

跟几个朋友也讨论过这个问题,我认为用layer来画是更好的办法,因为相对于view,layer是更轻量级的组件,可以节省系统资源。同时layer是动画的基本单元,加动画特效也更容易。并且view负责响应手势等,把绘制的代码都放在layer里,逻辑上也更加清晰

但是需要注意,layer不能直接响应触摸事件,所以手势识别还是需要通过view来完成 在UIView中绘图

在UIView中绘图非常简单,当调用

self.setNeedsDisplay()

iOS系统会自动调用view上的drawRect()方法,可以在drawRect()方法中绘制图形 在CALayer中绘图

在layer中绘图,生命周期比view复杂一些

首先也是调用layer上的setNeedsDisplay()触发的

#display

首先会进入layer的display()方法,在这里可以把CGImage赋给layer的contents,那么会直接把该CGImage作为此layer的样式,不会进入后续的方法

// 绘图方法
override func display() {

    if let img = getFrameImage(wheelStyle) {
        contents = img.CGImage
    }        
}

#displayLayer

如果没有实现display()方法,或者调用了super.display(),并且设置了layer的delegate,那么iOS系统会调用delegate的displayLayer()方法

let myLayer : MyLayer = MyLayer()
myLayer.delegate = self;
myLayer.frame = bounds;

override func displayLayer(layer: CALayer) {

    if let img = getFrameImage(wheelStyle) {
        contents = img.CGImage
    }
}

#drawInContext

如果没有设置delegate,或者delegate没有实现displayLayer()方法,那么接下来会调用layer的drawInContext方法

override func drawInContext(ctx: CGContext) {

    CGContextSetLineWidth(ctx, 1);
    CGContextMoveToPoint(ctx, 80, 40);
    CGContextAddLineToPoint(ctx, 80, 140);
    CGContextStrokePath(ctx);
}

#drawLayerInContext

如果layer没有实现drawInContext方法,那么接下来就会调用delegate的drawLayerInContext方法

override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
    CGContextSetLineWidth(ctx, 1);
    CGContextMoveToPoint(ctx, 80, 40);
    CGContextAddLineToPoint(ctx, 80, 140);
    CGContextStrokePath(ctx);
}

#总结

所以,一般来说,可以在layer的display()或者drawInContext()方法中来绘制

在display()中绘制的话,可以直接给contents属性赋值一个CGImage,在drawInContext()里就是各种调用CoreGraphics的API

假如绘制的逻辑特别复杂,希望能从layer中剥离出来,那么可以给layer设置delegate,把相关的绘制代码写在delegate的displayLayer()drawLayerInContext()方法。这2个方法与display()drawInContext()是分别一一对应的