Linux命令行与shell脚本编程大全 3rd 第二部分命令实验记录
Chapter 11: Basic Script building
Using Multiple Commands
使用分号分隔命令
1 | date ; who |
Creating a Script File
- 新建文件 mysh.sh
- 填入内容
- 设置环境
- 运行
However, the fi rst line of a shell script fi le is a special case, and the pound sign followed by the exclamation point tells the shell what shell to run the script under
Shell script 的第一行表示你想要用哪个 Shell 运行你的脚本
1 |
|
尝试运行 mysh.sh
,运行失败 bash: mysh.sh: command not found
这是你有两种选择
- 将包含脚本的目录添加到 PATH 中,eg:
export PATH=$PATH:path_to_folder
- 使用相对或绝对路径调用脚本, eg:
./mysh.sh
PS: 发现直接用 sh mysh.sh
即可,还省去了赋权的操作
这里直接使用相对路径调用 ./mysh.sh
, 运行失败 bash: ./mysh.sh: Permission denied
。通过 ls -l mysh.sh
查看权限, 发现并没有权限,然后赋权,再次尝试,运行成功。
1 | ls -l mysh.sh |
Displaying Messages
使用 echo
打印信息
- 当输出内容中没有什么特殊符号时,可以直接在 echo 后面接你要的内容
- 当输出内容中包含单/双引号时,需要将 echo 的内容包含在 双/单引号中
- 默认 echo 是会换行的,使用
echo -n xxx
取消换行
Using Variables
set
: 打印当前环境的所有环境变量, 输出内容包括 PATH,HOME 等
1 | set |
在你的 sh 脚本中,可以调用这些变量
1 |
|
当你在输出语句中想要打印 $
这个特殊字符时,只需要在前面加斜杠
1 | echo "This book cost $15" |
PS: ${variable}
也是合法的,这种声明起到强调变量名的作用
变量名限制:最多 20 个字符,可以是英文,数组或者下划线,区分大小写。使用等号链接,中间不允许有空格
Command substitution
你可以通过一下方式将 cmd 输出的值赋给你的变量
`
反引号符(backtick character)$()
表达式
1 | tdate=`date` |
这只是初级用法,更骚的操作是将输出定制之后用作后续命令的输出,比如下面这个例子,将文件夹下的所有目录写进 log 文件,并用时间戳做后缀 today=$(date +%y%m%d); ls -al /usr/bin > log.$today
Caution: Command substitution creates what’s called a subshell to run the enclosed command. A subshell is a separate child shell generated from the shell that’s running the script. Because of that, any variables you create in the script aren’t available to the subshell command.
Command substitution 这种使用方法会创建一个子 shell 计算变量值,子 shell 运行的时候是看不到你外面的 shell 中定义的变量的
Redirecting Input and Output
Output redirection
可以使用大于号(greater-than symbol)将输出导入文件
1 | date > test6 |
如果文件已经存在,则覆盖原有内容
1 | who > test6 |
使用两个大于号(double greater-than symbol)来做 append 操作
1 | date >> test6 |
Input redirection
使用小于号(less-htan symbol)将文件中的内容导入输入流 command < inputfile
1 | wc < test6 |
除了从文件导入,从命令行直接导入多行也是可行的,术语叫做 inline input redirection
。这个之前在之前阿里云 setup 环境的时候操作过的。使用两个小于号(<<) + 起止描述符实现
1 | wc << EOF |
Pipes
使用方式 command1 | command2
Don’t think of piping as running two commands back to back. The Linux system actually runs both commands at the same time, linking them together internally in the system. As the fi rst command produces output, it’s sent immediately to the second command. No inter-mediate fi les or buffer areas are used to transfer the data.
pipe 中的命令是同时执行的,变量传递不涉及到中间变量
1 | ls |
pipe 可以无限级联 cmd1 | cmd2 | cmd3 | ...
Performing Math
Bash 中提供了两种方式进行计算
The expr command
1 | expr 1 + 5 |
expr 支持常规算数运行 + 与或非 + 正则 + 字符串操作等
这个 expr 表达式有点尴尬,他的一些常规运算是要加反斜杠的,简直无情
1 | expr 1 * 6 |
这个还算好的,如果在 sh 文件中调用,表达式就更操蛋了
1 |
|
Using brackets
Bash 中保留 expr
是为了兼容 Bourne shell,同时它提供了一种更简便的计算方式,$[ operation ]
1 | var1=$[1 + 5] |
方括号表达式可以自动完成计算符号的识别,不需要用反斜杠做转义符,唯一的缺陷是,他只能做整数运行
1 | var1=100 |
A floating-point solution
bc: bash calculation, 他可以识别一下内容
- Number(integer + floating point)
- Variables(simple variables + arrays)
- Comments(# or /* … */)
- Expressions
- Programming statements(such as if-then statements)
- Functions
1 | bc |
可以通过 scale 关键字指定精确位, 默认 scale 为 0, -q
用于跳过命令文字说明
1 | bc -q |
如前述,bc 可以识别变量
1 | bc -q |
你可以通过 Command substitution,在 sh 脚本中调用 bc, 形式为 variable=$(echo "options; expression" | bc)
1 | cat test9 |
脚本中定义的变量也可以使用
1 | cat test10 |
当遇到很长的计算表达式时,可以用 << 将他们串起来
1 | cat test12 |
PS: 在上面的脚本中我们用了 Command substitution 所以中间的变量前面都是要加 $
的,当终端调用 bc
时就不需要了
Exiting the Script
There’s a more elegant way of completing things available to us. 每个命令结束时,系统都会分配一个 0-255 之间的整数给他
Checking the exit status
Linux 使用 $?
表示上一条命令的执行状态
1 | date |
运行正常,返回 0。如果有问题,则返回一个非零
1 | asd |
常见错误码表
Code | Desc |
---|---|
0 | Success |
1 | General unknown error |
2 | Misuse of shell command |
126 | The cmd can’t execute |
127 | Cmd not found |
128 | Invalid exit argument |
128+x | Fatal err with Linux singal x |
130 | Cmd terminated with Ctrl+C |
255 | Exit status out of range |
The exit command
exit 关键字让你可以定制脚本的返回值
1 | cat test13 |
PS: 这里看出来差别了,如果我直接用 sh test13 执行的话,结果是 0
这里需要指出的是,exit code 最大为 255 如果超出了,系统会自己做修正
1 | cat test14 |
Chapter 12: Using Structured Commands
本章内容主要包括 loigc flow control 部分
Working with the if-then Statement
if-then 是最基本的控制方式,format 如下, 判断依据是 command 的 exit code, 如果是 0 则表示 success,其他的则为 fail.
1 | if command |
positive sample 如下
1 | cat test1.sh |
negative sample 如下
1 | cat test2.sh |
PS: if-then
可以改一下 format,将 if-then 写在一行,看上去更贴近其他语言的表现形式
1 | if command; then |
then 中可以写代码段,如下
1 | cat test3.sh |
PS:由于是 mac 系统,有点出入,但是目的还是达到了。顺便测了一下缩进,将 echo 的缩进去了一样 work
Exploring the if-then-else Statement
格式如下,
1 | if command |
改进 test3.sh 如下
1 | # #!/usr/local/bin/bash |
Nesting if
1 | if command1 |
写脚本检测帐户是否存在,然后检测用户文件夹是否存在
1 | cat test5.sh |
Tips: Keep in mind that, with an elif statement, any else statements immediately following it are for that elif code block. They are not part of a preceding if-then statement code block.(elif 之后紧跟的 else 是一对的,它不属于前面的 if-then)
多个 elif 串连的形式
1 | if command1 |
Trying the test Command
if-then 条件判断只支持 exit code,为了使它更通用,Linux 提供了 test 工具集,如果 test 判定结果为 TRUE 则返回 0 否则非 0,格式为 test condition
, 将它和 if-then 结合,格式如下
1 | if test condition |
如果没有写 condition,则默认为非 0 返回值
1 | cat test6.sh |
将测试条件替换为变量,输出 True 的语句
1 | cat test6.sh |
测试条件还可以简写成如下形式
Be careful; you must have a space after the first bracket and a space before the last bracket, or you’ll get an error message.
1 | if [ condition ] |
test condition 可以测试如下三种情景
- Numeric comparisons
- String comparisons
- File comparisions
Using numeric comparisons
Comparison | Description |
---|---|
n1 -eq n2 | Check if n1 is equal to n2 |
n1 -ge n2 | Check if n1 greater than or equal to n2 |
n1 -gt n2 | Check if n1 is greater than n2 |
n1 -le n2 | Check if n1 is less than or equal to n2 |
n1 -lt n2 | Check if n1 less than n2 |
n1 -ne n2 | Check if n1 is not equal to n2 |
test condition 对变量也是有效的,示例如下
1 | cat numeric_test.sh |
但是 test condition 有个缺陷,它不能测试浮点型数据
1 | value1=5.55 |
Caution: Remember that the only numbers the bash shell can handle are integers.
Using string comparisons
Comparison | Description |
---|---|
str1 = str2 | Check if str1 is the same as string str2 |
str1 != str2 | Check if str1 is not the same as string str2 |
str1 < str2 | Check if str1 is less than str2 |
str1 > str2 | Check if str1 is greater than string str2 |
-n str1 | Check if str1 has a length greater than zero |
-z str1 | Check if str1 has a length of zero |
1 | cat test8.sh |
在处理 <
和 >
时,shell 会有一些很奇怪的注意点
<
和>
必须使用转义符号,不然系统会把他们当作流操作<
和>
的用法和 sort 中的用法时不一致的
针对第一点的测试,测试中,比较符号被当成了流操作符,新生产了一个 hockey 文件。这个操作 exit code 是 0 所以执行了 true 的 loop. 你需要将条件语句改为 if [ $val1 \> $val2 ]
才能生效
1 | cat badtest.sh |
针对第二点,sort 和 test 对 string 的比较是相反的
1 | cat test9b.sh |
PS: 这里和书本上有出入,我在 MacOS 里测试两者是一致的,大写要小于小些,可能 Ubantu 上不一样把,有机会可以测一测
Note: The test command and test expressions use the standard mathematical comparison symbols for string compari-sons and text codes for numerical comparisons. This is a subtle feature that many programmers manage to get reversed. If you use the mathematical comparison symbols for numeric values, the shell interprets them as string values and may not produce the correct results.
test condition 的处理模式是 数字 + test codes(-eq); string + 运算符(</>/=),刚好是交叉的,便于记忆
对 -n
和 -z
进行测试,undefined 的变量默认长度为 0
1 | cat test10.sh |
Using file comparisons
Comparison | Description |
---|---|
-d file | Check if file exists and is a directory |
-e file | Check if file or directory exists |
-f file | Check if file exists and is a file |
-r file | Check if file exists and is readable |
-s file | Check if file exists and is not empty |
-w file | Check if file exists and is writable |
-x file | Check if file exists and is executable |
-O file | Check if file exists and is owned by the current user |
-G file | Check if file exists and the default group is the same as the current user |
file1 -nt file2 | Check if file1 is newer than file2 |
file1 -ot file2 | Check if file1 is older than file2 |
测试范例如下
1 | mkdir test_folder |
Considering Compound Testing
组合条件
- [ condition1 ] && [ condition2 ]
- [ condition1 ] || [ condition2 ]
1 | [ -f tmp_file ] && [ -d $HOME ] && echo true || echo false |
Working with Advanced if-then Features
if-then 的增强模式
- Double parentheses for mathematical expressions(双括号)
- Double square brackets for advanced string handling functions(双方括号)
Using double parentheses
创括号是针对算数运算的
test command 只提供了简单的算术运算,双括号提供的算力更强,效果和其他语言类似,格式 (( expression ))
. 除了 test 支持的运算,它还支持如下运算
Comparison | Description |
---|---|
val++ | Post-incremnet |
val– | Post-decrement |
++val | Pre-increment |
–val | Pre-decrement |
! | Logical negation |
~ | Bitwise negation |
** | Exponentiation |
<< | Left bitwise shift |
>> | Right bitwise shift |
& | Bitwise Boolean AND |
| | Bitwise Boolean OR |
&& | Logical AND |
|| | Logical OR |
测试范例如下
1 | # ** 次方操作 |
Using double bracket
双方括号是针对字符运算的,格式为 [[ expression ]]
. 除了 test 相同的计算外,他还额外提供了正则的支持
Note: bash 是支持双方括号的,但是其他 shell 就不一定了
1 | [[ $USER == i* ]] && echo true || echo false |
PS: 双等(==)表示 string 符合 pattern,直接用等号也是可以的
Considering the case Command
对应 Java 中的 switch-case 语法, 格式如下. 当内容和 pattern 匹配时,就会执行对应的语句
1 | case variable in |
1 | cat test26.sh |
Issues
Issue1: 写脚本的时候,发现一个很奇怪的问题
1 | # 未赋值的变脸 -n 会返回 true ?! |
以后可以的话都用增强型把,容错率更高
More Structured Commands
这章介绍了其他一些流程控制的关键词
The for Command
1 | for var in list |
PS: for 和 do 也可以写一起(for var in list; do),和 if-then 那样
Reading values in a list
1 | cat test1.sh |
当 for 结束后变量还会存在
1 | cat test1b.sh |
Reading complex values in a list
当 list 中包含一些标点时,结果可能就不是预期的那样了
1 | cat badtest1.sh |
解决方案:
- 给引号加转义符(for test in I don't know if this'll work, append some thing more?)
- 将 string 用双引号包裹(for test in I don”‘“t know if this”‘“ll work, append some thing more?)
for
默认使用空格做分割,如果想要连词,你需要将对一个的词用双引号包裹起来
1 | cat badtest2.sh |
Reading a list from a variable
1 | cat test4.sh |
PS: list=$list" Connecticut"
是 shell 中 append 字符串的常见操作
Reading values from a command
结合其他命令,计算出 list 的值
1 | echo a b c > states |
Changing the field separator
有一个特殊的环境变量叫做 IFS(internal field separator). 他可以作为分割 field 的依据。默认的分割符有
- A space
- A tab
- A newline
如果你想要将换行作为分割符,你可以使用 IFS=$'\n'
Caution: 定制 IFS 之后一定要还原
测试环节:如何打印当前 IFS 的值?
1 | echo -n "$IFS" | hexdump |
Caution: 变量和引号之间的关系:
- 单引号,所见即所得。写什么即是什么
- 双引号,中间的变量会做计算
- 没符号,用于连续的内容,如果内容中带空格,需要加双引号
脚本中 IFS.OLD=$IFS
的赋值语句经常会抛异常 ./test_csv.sh: line 3: IFS.OLD=: command not found
但是写成 IFS_OLD
的话就可以运行,可能是点号的形式会变成其他一些什么调用也说不定。以后为了稳定,还是用下划线的形式把
1 | IFS_OLD=$IFS |
其他定制分割符 IFS=:
, 或者多分割符 IFS=$'\n':;"
Reading a directory using wildcards
1 | cat test6.sh |
PS: 在这个例子中有一个很有意思的点,在 test 中,将变量 file 使用双引号包裹起来了。这是因为 Linux 中带空格的文件或文件夹是合法的,如果没有引号,解析就会出错
Caution: It’s always a good idea to test each file or directory before trying to process it.
The C-Style for command
C 语言中 for 循环如下
The C language for command
1 | for (i=0; i<10; i++) |
bash 中也提供了类似的功能, 语法为 for (( variable assignment; condition; iteration process ))
例子:for(( a=1; a<10; a++ ))
限制:
- The assignment of the variable value can contain space
- The variable in the condition isn’t preceded with a dollar sign
- The equation for the iteration process doesn’t use the expr command format
这种用法,对我倒是很亲切,但是和之前用过的那些变量赋值之类的语句确实有一些语法差异的。这个语句中各种缩进,空格都不作限制
1 | cat test8.sh |
Using multiple variables
for 中包含多个参数
1 | cat test9.sh |
The while Command
1 | while test command |
示例
1 | cat test10.sh |
Using multiple test commands
while 判断的时候可以接多个条件,但是只有最后一个条件的 exit code 起决定作用。就算第一个条件我直接改为 cmd
每次都抛错,循环照常进行
还有,每个条件要新起一行, 当然用分号隔开也是可以的
1 | cat test11.sh |
The until Command
语意上和 while 相反,但是用法一致
1 | until test commands |
1 | cat test12 |
Nesting Loops
循环嵌套,很常见
1 | cat test14 |
while + for 的例子。树上的例子 for 中 边界条件是 $var2<3;
和之前表现的语法不一样,试了一下,有无 $
都是可以的
1 | cat test14 |
Looping on File Data
Stackoverflow 上看到一篇解释 IFS=$’\n’ 的帖子,挺好。一句话就是 $'...'
的语法可以表示转义符
通常来说,你会需要遍历文件中的内容,这需要两个知识点
- Using nested loops
- Changing the IFS environment variable
通过设置 IFS 你可以在包含空格的情况下处理一行内容. 下面是处理 /etc/passwd
文件是案例
1 | cat test1 |
Controlling the loop
通过 break, continue 控制流程
The break command
打断单层循环, 这个语法适用于任何循环语句,比如 for, while, until 等
1 | cat test17 |
打断内层循环
1 | cat test19 |
在内部循环执行过程中,打断外层循环,这个特性倒是很新颖,Java 中没见过 Haha
break n
默认是 1,打断当前的循环,设置成 2 就是打断外面一层直接退出。下面例子中,我们通过在 inner for 中 break 2 直接退出了外层 for 循环
1 | cat test20 |
The continue command
提前结束循环,继续下一次循环. 下面例子中,当当前变量 3 < x < 8 时跳过打印. 前面介绍的循环体都适用,如 for, while 和 until
1 | cat test21 |
和 break 一样,continue 也支持 continue n
来跳过循环。测试用例中,当外层变量值 2 < x < 4 时,跳过打印
1 | cat test22 |
Processing the Output of a Loop
for
中打印的语句可以在 done
后面接文件操作符一起导入,还有这种功能。。。那我之前写脚本用的 printf 不是显得有点呆
1 | cat test23 |
PS: 试了一下,echo -n
也是 OK 的
同理,done
后面还可以接其他的命令, 这个扩展很赞
1 | cat test24 |
Practical Examples
一些实用的脚本范例
Finding executable files
通过遍历 PATH 中的路径,统计处你可以运行的 commands 列表
1 | cat test25 |
PS: 这个 more
就用的很灵性!!
Creating multiple user accounts
将需要创建的新用户写到文件中,并通过脚本解析文件,批量创建
1 |
|
Chapter 14: Handling User Input
这章主要讲如何在脚本中做交互
Passing Parameters
Reading parameters
bash 会将传入的所有变量都赋给 positional parameters. 这些位置变量以 $
开头,$0
为脚本名称,$1
为第一个参数,以此类推
根据传入参数计算斐波那契额终值
1 | cat test1 |
多参数调用案例
1 | cat test2 |
字符串作为参数
1 | cat test3 |
如果字符串之间有空格,需要用引号包裹起来
当参数数量超过 9 个的时候,你需要用花括号来调用
1 | at ./test4 |
Reading the script name
`$0 代表了脚本文件的名字
1 | cat test5 |
不一样的调用方式,得到的第 0 参数值会不一样,如果像统一得到文件名,可以使用 basename 命令
1 | cat test5b |
Testing parameters
当脚本中需要用到参数,但是参数没有给,则脚本会抛异常,但是我们可以更优雅的处理这种情况
1 | cat test7 |
Using Special Parameter Variables
Counting parameters
$#
用于统计参数数量
1 | cat test8 |
根据上面的特性我们可以试着发散一下思路,尝试拿到最后一个参数
1 | cat badtest1 |
尝试失败,语法上来说,花括号中间是不允许有 $
符号的,你可以用叹号表达上面的意思
1 | # #!/usr/local/bin/bash |
Grabbing all the data
你可以使用 $*
或者 $@
拿到所有的参数,区别如下
$*
会将所有的参数当作一个变量对待$@
会将所有的参数当作类似数组那种概念,分开对待。也就是说,你可以在 for 中循环处理
1 | cat test11 |
看上去没区别。。。这里需要结合 for 来观察
1 | cat test12 |
Being Shify
我们可以通过 shift
关键字将参数左移,默认左移一位.
PS: note that the value for variable $0
, the program name, remains unchanged
PPS: Be careful when working with the shift command. When a parameter is shifted out, its value is lost and can’t be recovered.
1 | cat test13 |
移动多个位置测试
1 | cat test14 |
Working with Options
介绍三种添加 Options 的方法,Options 顾名思义,就是命令中的可选参数。
Finding your options
单个依次处理法: 可以使用 case + shift 的语法识别 options。将预先设置的 Options 添加在 case 的过滤列表中,然后遍历 $1
识别它
1 | cat test15 |
Options parameters 分开处理法: 我们可以认为的在两种参数中间添加一个分割符,比如 --
作为 options 的结束和 parameter 的开始. 在脚本中现实的识别并处理它。
PS: 突然意识到 $0
是不算在 $*
和 $@
中的
下面的例子中,如果没有 --
,则所有参数都在第一个 do-while 中处理了。加了之后会在两个 loop 中处理
1 | cat test16 |
带值的 options 处理: 有些命令中 options 是带值的,比如 ./testing.sh -a test1 -b -c -d test2
。这是我们就需要在脚本中识别可选参对应的值
下面的例子中 -b test1
是一个带值的可选参数,我们在 识别到 -b
后立即拿到 $2
即为对应的值
PS: 但是怎么看,bash 中添加可选参数都很麻烦啊,如果是可选参数带多个值呢,那不是还得加逻辑。。。
1 | cat test17 |
Using the getopt command
介绍 getopt
工具函数,方便处理传入的参数
Looking at the command format getopt
可以接受一系列的 options 和 parameters 并以正确的格式返回, 语法如下 getopt optstring parameters
Tips getopt 还有一个增强版 getopts, 后面章节会介绍
测试 getopt, b 后面添加了冒号表示它是带值的可选参数. 如果输入的命令带有未定义的参数,则会给出错误信息。如果想要忽略错误信息,则需要 getopt 带 -q 参数
1 | getopt ab:cd -a -b test1 -cd test2 test3 |
PS: MacOS 的 bash 是不支持 -q 参数的!使用 docker 绕过了这个限制 诶嘿 ╮( ̄▽ ̄””)╭
Using getopt in your scripts 这里有一个小技巧,我们需要将 getopt 和 set 配和使用 set -- $(getopt -q ab:cd "$@")
1 |
|
PS: 最后一个例子中可以看到 getopt 并不能很好的处理字符串, “test2 test3” 被分开解析了。幸运的是,我们有办法解决这个问题
Advancing to getopts
getopts
是 getopt
的增强版本,格式如下 getopts optstring variable
, optstring 以冒号开始
如下所示,getopts
- 自动为我们将每个参数封装到 opt 变量中
- 删选的时候省区了
-
- 提供了内置的
$OPTARG
代表可选参数的值 - 将为定义的参数类型用问好替换
1 | cat test19 |
getopts
还内置了一个 OPTIND 变量,可以在处理每个参数的时候自动 +1. OPTIND 变量初始值为 1,如果要取 params 部分,则 shift $[ $OPTIND - 1 ]
下面例子中,
1 | cat test20 |
Standardizing Options
介绍 shell 中参数表示的 comman sense
Option | Description |
---|---|
-a | Shows all objects |
-c | Produces a count |
-d | Specifies a directory |
-e | Expands an object |
-f | Specifies a file to read data from |
-h | Displays a help message for the command |
-i | Ignores text case |
-l | Produces a long format version of the output |
-n | Uses a non-interactive(batch) mode |
-o | Specifies an output file to redirect all output |
-q | Run in quiet mode |
-r | Processes directories and files recursively |
-s | Runs in silent mode |
-v | Produces verbose output |
-x | Excludes an object |
-y | Answers yes to all questions |
Getting User Input
bash 提供了 read 方法来作为用户输入
Reading basics
read
可以从键盘或文件中获取输入
1 | cat test21 |
上面的例子中,cmd 会将所有的输入看作一个变量处理
带用户提示的输入
1 | cat test22 |
和第一个实验对照,cmd 也可以将所有输入当作 list 处理
1 | cat test23 |
如果你没有为 read 指定变量,bash 会自动将这个值赋给环境变量 $REPLY
1 | cat test24 |
Timing out
默认情况下 read 会一直阻塞在那里,等待用户输入,但是我们也可以设置一个等待时间
1 | cat test25 |
read 还可以指定输入的长度, 设定好长度后,你输入对应长度的内容,他立马就执行下去了
1 | cat test26 |
Reading with no display
在输入一些敏感信息时,你不希望他显示在屏幕上,可以用 -s 参数
1 | cat test27 |
Reading from a file
Linux 系统中,可以通过 read 命令从文件中按行读取。当读完后,返回非 0
1 | cat test28 |
PS: 如果文件的末行没有换行,则最后一行并不会被处理
Presenting Data
这章主要向你展示更多的输出流处理技巧
Understanding Input and Output
现在为止,我们主要采取两种输出流展示方式
- 终端屏显
- 重定向到文件
目前为止我们只能将全部内容一起输出到文件或屏幕,在下面的小节中,我们将尝试将内容分开处理
Standard file descriptors
Linux 系统通过 file descriptor 来指代每一个文件对象,这个 decriptor 是一个唯一的非负的整数。每个进程同一时间允许至多 9 个打开的文件。bash 中将 0,1 和 2 用于特定的用途
File descriptor | Abbreviation | Description |
---|---|---|
0 | STDIN | Standard input |
1 | STDOUT | Standard output |
2 | STDERR | Standard error |
STDIN 标准输入,比如终端的键盘输入和 <
的文件输入. 很多 bash 命令接收 STDIN 的输入,比如 cat, 如果你没有指定文件,他就会接收键盘输入
1 | $ cat |
STDOUT shell 的标准输出就是 terminal monitor.
STDERR 当运行命令出异常了,可以使用这个 descriptor 导流
Redirecting errors
Redirecting error only
像前面表哥所示,STDERR 的文件描述符是 2,你可以在 redirection symbol 前加上这个标识符来指定导向
1 | ls -al badfile 2> test4 |
下面的例子中,badfile 不存在,所以错误信息写入 test5 中,test4 存在,所以在屏幕上显示
1 | ls -al test4 badfile 2> test5 |
Redirecting errors and data
如果你想将正常和异常的信息都输出到文件,你需要指定两个输出
1 | ls |
如果你想将这两种信息都导入一个文件,bash 提供了一个特殊的 redirection symbol 来做这个事情 &>
1 | ls -al test5 badfile &> test8 |
Redirecting Output in Scripts
通过 STDOUT 和 STDERR 你可以将输出导入任何 file discriptors. 有两种方式可以重定向输出
- Temporarily redirecting each line
- Permanently redirecting all comands in the script
Temorary redirections
这段文字的描述有点蹩脚,还是直接用案例说明把。假如你想将你的 echo 内容输出到 STDERR 指定的流中,需要怎么做?这种用法就是打印自己的 err log 啊, 可以使用 >&2
的格式
下面的例子中,test8 中指定第一个 echo 通过 STDERR 输出,第二个 STDOUT 输出。当直接调用时,由于两个输出默认都是打在公屏上的,所以没什么区别,但是当我指定 err 输出到 test9 时,区别就出现了。只有错误信息导到 test9 了
1 | cat test8 |
Permanent redirections
上面的情况适合少量打 log 的情况。如果你有好多 err 需要重新导向,你可以这么做
exec 会启动一个新的 shell,下例中新启动的 shell 会将 STDOUT 的内容都发送的 testout 文件中去
1 | cat test10 |
你可以在程序中间做这样的操作. 下面的例子中,我们在开头部分指定 err 输出到 testerror 文件,接着打印两个普通输出。再指定普通输出,输出到文件,最后指定 err 输出到 err 文件
1 | cat test11 |
当你改变了 STDOUT 或者 STDERR 后,要再改回来就不是那么容易了,如果你需要切换这些流,需要用到一些技巧,这些将在后面的 Creating Your Own Redirection 章节讲到
Redirecting Input in Scripts
和输出流一样,我们可以通过定向符号操纵输入流 exec 0< testfile
下面的例子中,我们以文件中的命令代替键盘输入
1 | cat test12 |
Creating Your Own Redirection
shell 中最多只能有 9 个 file descriptor, 我们已经用了 0,1,2.下面我们将使用 3-8 自定义我们自己的 file descriptor.
Creating output file descriptors
1 | cat test13 |
这个概念听起来有点复杂,但是其实很直接了当的,用法也和前面的默认文件描述符是一致的。
Redirecting file descriptors
下面的例子中我们会做重定向的切换。开始时,我们用 3 号 descriptor 代替 1 的位置。就是所有的 echo 都会输入到 3 号中,之后,我们还原,echo 就又输出到屏幕了。这个其实只用到了一个语法 3>&1
,这个语句就是用 3 代替 1, 还原的时候顺序翻一下即可
1 | cat test14 |
Creating input file descriptors
和输出流一样,输入流也可以用上面的这个技巧。下面的例子中,我们先用 6 号代替原始的 0 号键盘输入,做完操作后将它还原
1 | cat test15 |
Creating a read/write file descriptor
感觉这个例子有点。。。鸡肋。虽然实用性不高,但是挺有趣
下面的例子中,我们会将 3 同时设置为输入输出描述符。先读取一行,再输入一行。读取一行后,位置定位到第二行开头,这个时候写我们自己的内容,他会覆盖之前的内容。
1 | cat test16 |
Closing file descriptors
新创建的 file descriptors 都会在脚本结束时自动关闭。但是如果你想在接本结束前手动关闭,需要做什么?
关闭的格式如下 exec 3>&-
, 下面的实验中我们将 3 号指向文件,输出内容,再关闭它,再试着输出内容。可以看到,关闭后再输出会抛异常
1 | cat badtest |
除此之外还有一个更重要的细节需要注意,如果你在一个脚本中,关闭后再使用同一个 file descriptor 的话。它会将之前写的内容覆盖掉
下面实验中,我们先用 3 号描述符将信息写入文件,关闭后 cat 输出,再打开它写东西。最后发现之前写的被覆盖了
1 | cat test17 |
Listing Open File Descriptors
lsof
可以列出整个系统所有发开的 file descriptor, 这个在权限方面有些争议。MacOS 也有这个命令
1 | which lsof |
显示当前进程的文件描述符使用情况
1 | lsof -a -p $$ -d 0,1,2 |
lsof 输出说明
Column | Description |
---|---|
COMMAND | The first nine characters of the name of the command in the process |
PID | The process ID of the process |
USER | The login name of the user who owns the process |
FD | The file descriptor number and access type. r-read, w-write, u-read/write |
TYPE | The type of file. CHR-character, BLK-block, DIR-directory, REG-regular file |
DEVICE | The device numbers(major and minor) of the device |
SIZE | If available, the size of the file |
NODE | The node number of the local file |
NAME | The name of the file |
作为对比,下面是一个文件中的 file descriptor 的信息
1 | cat test18 |
Suppressing Command Output
有些时候,你并不想看到任何异常输出,比如后台运行的时候。这时你可以将 STDERR 的内容输出到 null file 中去,位置是 /dev/null
1 | ls -al > /dev/null |
你也可以将输入指定到 null file. 这样做可以快速清空一个文件,算是 rm + touch 的简化版
1 | cat testfile |
Using Temporary Files
Linux 系统预留了 /tmp
文件夹放置临时文件, 设置提供了专用命令 mktemp
来创建临时文件,这个命令创建的文件在 umask 上给创建者所有权限,其他人则没有权限
Creating a local temporary file
零时文件名字中的末尾的大写 X 会被替换成随机数
1 | mktemp testing.XXXXXX |
实验脚本中,我们创建一个临时文件并写入内容,然后关闭流,并 cat 一下。最后移除文件。把异常信息丢掉不显示
1 | cat test19 |
Creating a temporary file in /tmp
前面的临时文件都是创建在当前文件夹下的,下面介绍在 tmp 文件夹下的创建办法,其实就是加一个 -t 的参数。。。结果和书上有区别,并该是 MacOS 定制过
1 |
|
1 | cat test20 |
Creating a temporary directory
-d 创建临时文件夹
1 | cat test21 |
Logging Messages
有时你可能想一个流即打印到屏幕上,也输出到文件中,这个时候,你可以使用 tee
1 | date | tee testfile |
注意,tee 默认会覆盖原有内容
1 | who | tee testfile |
之前 date 的内容被覆盖了,你可以用 -a 做 append 操作
1 | date | tee -a testfile |
实操
1 | cat test22 |
Practical Example
解析一个 csv 文件,将其中的内容重组成一个 SQL 文件用作 import
1 | <!-- cat members.csv --> |
主要语法说明
- done < ${1}: 将命令行中给的文件做输入
- read lname…: 以逗号为分割,每个字段给个名字,方便后面调用
- cat >> $outfile << EOF: cat 会拿到一行的内容,并对这些内容做替换放到 outfile 中去. 这个用法和 tee myfile << EOF.. 有异曲同工之妙
1 | cat test23 |
Script Control
Handling Signals
Signal | Name | Description |
---|---|---|
1 | SIGHUP | Hangs up |
2 | SIGINT | Interrupts |
3 | SIGQUIT | Stops running |
9 | SIGKILL | Unconditionally terminates |
11 | SIGSEGV | Produces segment violation |
15 | SIGTERM | Terminates if possible |
17 | SIGSTOP | Stops unconditionally, but doesn’t terminate |
18 | SIGTSTP | Stops or pauses, but continues to run in background |
19 | SIGCONT | Resumes execution after STOP or TSTP |
默认情况下,bash shell 会忽略 QUIT 和 TERM 这两个信息,但是可以识别 HUP 和 INT。
Generating signals
通过键盘操作你可以产生两种信号
Interrupting a process Ctrl + C
可以生成 SIGINT 信号并把它发送到任何终端正在执行的进程中
Pausing a process Ctrl + Z
停止一个进程
1 | sleep 100 |
当有 stop 的进程是,你是不能退出 bash 的可以用 ps 查看. 对应的 job 的 S(status) 为 T。你可以使用 kill 杀死它
1 | ps -l |
Trapping signals
脚本中我们可以指定需要忽略的 signal, 格式为 trap command signals
。
下面的示例中,我们在脚本中指定忽略 Ctrl + C
发出的 SIGINT 信号,运行过程中即使按下组合键脚本继续运行
1 | cat test1.sh |
Trapping a script exit
trap 命令还可以做到,当脚本结束时执行命令的效果,即使是通过 Ctrl + C 结束也会被出发。
1 | cat test2.sh |
Modifying or removing a trap
下面示例中展示了如何修改 trap 的动作。我们先定义当遇到 SIGINT 时 echo 的内容。当 5 秒循环后修改 echo 的内容。通过触发 SIGINT 查看改动是否生效
1 | cat test3.sh |
你也可以通过 trap -- SIGINT
移除定义的 trap
1 | cat test3b.sh |
Tip 上面的去除也可以用单横线 trap - SIGINT
Running scripts in Background Mode
试想一下下面的情形,如果你的脚本需要执行比较长的时间,如果你在终端运行了它,那么你就没有终端可用了。你可以使用 background 的模式跑类似的脚本,ps
显示那些 process 很多都是后台运行的
Running in the background
想要后台运行脚本是很简单的,只需要在调用脚本时后面接一个 ampersand symbol &
即可
下面程序中,我们声明了一个计时器,并在后台运行。运行时他会给出 PID 信息。当脚本执行结束时会打印 done 的信息
PS: bash 测试的时候要我会车才会打印,zsh 自动打印
1 | cat test4.sh |
当使用 background mode 的时候,他还是用的 STDOUT 和 STDERR
1 | cat test5.sh |
Running multiple background jobs
如果你想启动多个 background job 只需要终端运行多个 xx.sh &
即可。每次系统多会分配一个 job id 和 process id 给后台进程,可以通过 ps 查看
1 | ./test5.sh & |
Running Scripts without a Hang-Up
通过 nohup
命令,你可以让脚本始终在后台运行,即使关闭终端也行
nohup
会将 script 和 STDOUT, STDERR 解绑。自动将输出绑定到 nohup.out 文件。如果你在一个文件夹下启动多个 nohup process, 他们的输出会混在一起
1 | nohup ./test1.sh & |
Controlling the Job
job control 即 开始/通知/kill/重启 jobs 的动作
Viewing jobs
jobs
cmd 让你可以查看 shell 正在运行的 jobs
下面的例子中,我们启动一个计时器脚本。第一次运行,中间通过 Ctrl + Z stop 它。第二次采用后台运行。然后通过 jobs 命令观察这两个 job 的状态。jobs -l 可以显示 PID
1 | cat test10.sh |
jobs 命令的可选参数
Parameter | Description |
---|---|
-l | List the PID of the process along with the job number |
-n | Lists only jobs that have changed their status since the last notification from the shell |
-p | Lists only the PIDs of the jobs |
-r | Lists only the running jobs |
-s | Lists only stopped jobs |
jobs 列出的信息可以看到加号和减号。+
表示 default job. -
表示即将变成 default job 的 job。同一时间,只有一个带 加号 的 job 和一个带 减号 的 job。
下面实验中,我们启动三个后台脚本并观察 jobs 状态
1 | ./test10.sh > test10a.out & |
Restarting stopped jobs
通过 bash 的 job control 你可以重新启动停止的脚本,启动方式有 background 和 foreground 两种,后者会接管终端
当有多个 script 停止时,可以使用 bg + num 的方式启动对应的脚本
1 | cat test11.sh |
bg
和 fg
的最主要的区别。如果用 bg, 你还可以在当前终端运行命令,如果是 fg 你需要等命令全部执行完了才能继续运行
Being Nice
Linux 系统中各 process 都有优先级,从 -20 到 19 不等。 shell 启动的 process 默认都是 0。19 是最低优先级的。可以通过 Nice guys finish last 方便记忆
Using the nice command
当需要指定优先级时,可以通过使用 nice
命令指定优先级等级
1 | # MacOS 不支持 cmd column |
Using the renice command
当 process 运行是,可以通过 renice
调整优先级
1 | ./test11.sh & |
和 nice 一样,renice 也有以下限制
- 只能 renice 你自己 own 的 processes
- renice 只能将优先级调低
- root 用户可以用 renice 调整到任何等级
Running Like Clockwork
这章我们将会使用 at
和 corn
命令让我们的脚本定时运行
Scheduling a job using the at command
at
让你可以定时的在系统中运行脚本,大多数 Linux 系统会在启动时开启一个 atd 的守进程,定时 check 并运行目标路径(/var/spool/at) 下的脚本
Understanding the at command format
at
的基本格式很简单 at [-f filename] time
. at 可以识别多种时间格式
- A standard hour and minute, such as 10:15
- An AM/PM indicator, such as 10:15PM
- A specific named time, such as now, noon, midnight or teatime(4PM)
同时你可以指定特定格式的日期
- A standard date format, such as MMDDYY, MM/DD/YY, or DD.MM.YY
- A text date, such as Jul 4 or Dec 25, with or withour the year
- A time increment:
- Now + 25 minutes
- 10:15PM tomorrow
- 10:15 + 7 days
当使用 at 命令的时候,对应的 job 提交到 job queue 中。系统中有 26 种 job 可选,队列名字为字母大小写的 a-z
Note 以前还有一个 batch 命令可以让你在 low useage 状态下运行 script,现在它只是通过调用 at + b queue 完成的定时脚本。队列的字母顺序越靠后,优先级越低。默认 job 优先级为 a
Retrieving job output
Linux 系统中,当 job 运行时是没有监测的地方的。系统会将内容记录到邮件中并发送给联系人
1 | cat test13.sh |
如果你系统没有配置邮箱,那就收不到邮件了,你可以直接指定输入到文件
PS: 这个实验失败,运行后我并没有看到 out 文件
1 | cat test13b.sh |
Listing pending jobs
显示所有 pendng 的 job, 我还以为也是用 jobs 呢,忙乎了半天
1 | atq |
Removing jobs
删除 at queue 中的 job
1 | atrm 1 |
Mac 是不是有什么特殊设置啊, 之前启动的 at job 都 block 了
Scheduling regular scripts
at 只能配置一次性 job, 如果要配置可重复的 job,可以用 cron. cron 在后台运行,他会检查 cron tables 看哪些 job 需要运行
Looking at the cron table
cron job 的语法:min hour dayofmonth month dayofweek command
示例如下
1 | # * 表示 每 的意思 |
Note 怎么设置每月最后天 run 的 job? 可以通过检查明天是不是第一天解决这个问题,示例:00 12 * * * if [
date +%d -d tomorrow= 01 ] ; then ; command
解释:每天中午检查一下明天是不是下个月的第一天,如果是则执行 command
cron job 必须指出脚本的全路径 15 10 * * * /home/rich/test4.sh > test4out
Building the cron table
显示当前用户的 cron job
1 | crontab -l |
View cron directories
mac 下没有这些配置,先跳过
Starting scripts with a new shell
书中说的是 set shell features, 所以下面这一段讲的是配置问题
启动 shell 时配置文件加载顺序如下,当前面的被发现时,后面的就会被忽略
- $HOME/.bash_profile
- $HOME/.bash_login
- $HOME/.profile
这里用的是 runs the .bashrc
所以感觉 rc 文件更像是添加什么新功能的感觉(主管臆测,就我本人,感觉什么东西都塞到 rc 中了,也能 work)
每次 bash shell 启动时都会运行 .bashrc
中的内容