shell 基本学习笔记
Shell 为用户提供了简单的与操作系统交互的接口,并原生的支持重定向,文件操作等特性,从而可以更容易的完成某些脚本任务。Shell 脚本实际上是 shell 支持的一系列语句的集合,用于执行一系列的任务。这里主要记录下 Missing Semester of Your CS Education 以及鸟叔的 Linux 私房菜中关于 Shell 以及其脚本的内容,同时 man bash 也是重要的参考来源,作为介绍和笔记。
shell 脚本执行
以当前目录下的 test.sh 脚本为例,可以通过多种方式进行 shell 脚本的执行。
./test.sh #通过直接指定脚本位置执行,需要 test.sh 具有执行权限 bash test.sh #使用 bash 来解释执行 ./test.sh 文件中的脚本,此时不需要 test.sh 具备可执行权限 source test.sh #从 test.sh 中读取命令并进行支持
在上述三种执行方式中,前两种均在新的进程中执行脚本,故而不会对父进程产生影响,而第三种使用 source 命令的执行方式则是直接在当前进程中执行对应的语句,所以发生的改变在当前进程中可见。
shell 脚本的调试
在通过 bash 执行某一个脚本时,如 bash test.sh 时,可以通过 bash 自带的参数进行调试,比较有用的是 -n 和 -x 参数。
bash -n test.sh #对 test.sh 进行语法检查而不实际执行 bash -x test.sh #将 test.sh 执行过程中实际展开的内容输出
Shebang - shell 脚本的开始
脚本开始位置的 "#!" 字符序列被称为 Shebang,当一个以 Shebang 开头的文件被当作 shell 脚本时,程序加载器(program loader)将 "#!" 所在初始行的其他内容(比较常见的比如"/bin/bash")作为 interpreter directive. 加载器会执行对应的解释器并将原始脚本的执行路径作为参数传递给该执行器。注意,上述 Shebang 的作用说明不止可以采用 shell 作为解释器,如可以在 Shebang 部分指定 python 解释器,从而执行 python 脚本(#!/user/bin/env python)。
Bash 中一些注意点:
1.使用转移字符(escape character)来消除某些字符的特殊含义。在 shell 中,字符 '\' 被当作转移字符。
2.除在终端上逐行执行命令之外,shell 一般支持简单的脚本语言,从而对变量,控制流,循环等进行支持。
3.多条命令可以使用 ";" 进行分割。
变量
shell 中变量区分大小写,默认情况下,shell 中的变量默认为字符型,通过 "=" 进行赋值,通过 "$" 进行访问。通过 declare 命令可以声明数值类型的变量,同时需要注意 bash 仅支持整数类型的计算。若不进行赋值操作,则变量的默认值为 null string.
var=test //变量 var 被赋值为 test var= //默认 var 为空 $var //访问变量 var ${var} //访问变量 var
这里需要注意在赋值语句的左右两边不应该加入空格,因为shell使用空白字符作为token分界,若插入空格会被视为类似命令调用的形式。相较于第一种变量访问方法,第二种写法可以更容易区分变量,如 $vartest 与 ${vat}test,前者会对变量 vartest 进行展开,而后者则会对 var 进行展开。
shell 中支持对特殊字符(主要是通配符 ? 和 *),变量以及命令的拓展使用。
*.conf // 执行是会进行通配符展开,* 可以匹配任意多个字符,如 test.conf, a.conf 均可匹配 ?.conf // ? 可以匹配单个字符,如 a.conf, b.conf $var // 访问变量中的值 $(command) // 命令替换,返回值为 command 执行的结果 `command` // 命令替换的另一种写法,使用 `` 符号
shell globbing
在书写 shell 脚本时,可以使用通配符(wildcard)包括 "*" 和 “?” 来对具有相同模式的名字进行拓展,其中 "*" 可以匹配任意数量的字符,而 "?" 匹配一位字符。同时还可以使用 "{}" 来针对某些子串进行同一类型的扩展。
rm foo* //会删除类似 foo1, foo12 等相似的 mv ×{.py, .sh} folder //会移动所有以 .sh 和 .py 结尾的文件 touch {foo, bar}/{a..h} //创建 foo.a - foo.h 以及 bar.a - bar.h
命令替换(comman substitution)
使用 $(CMD) 获得执行命令 CMD 的输出结果。如 $(date) 获得 date 命令执行的结果。
过程替换([rpcess substitution)
<(CMD) 会执行 CMD 并将结果存放到临时文件中,同时将 <(CMD) 替换为临时文件的名字。
diff <(ls foo) <(ls bar) //比较 foo 目录和 bar 目录下的文件的不同
引用
对应的,shell 中支持三种不同的引用(quote)的方法,可以对 shell 默认对特殊字符,变量和命令的拓展行为进行限定。主要是双引号 "",单引号 '',反斜杠 \.
处理 | 示例 | |
"xxx" | 支持对变量展开 | "$test" - 会展开变量test的值 |
不对通配符进行处理 | "*.conf" - 将 * 视为普通字符 | |
支持命令替换 | "$(ls)" - 将 ls 作为命令执行后返回结果 | |
'xxx' | 不对变量展开 | "$test" - 将$test视为普通字符串 |
不对统配符进行处理 | "*.conf" - 将 * 视为普通字符 | |
不进行命令替换 | "$(ls)" - 将 $(ls) 视为普通字符串 | |
\x | 通过 \ 对特殊字符进行转移 | 可以对换行,特殊字符如 '$','*','?' 以及引号等进行转义 |
这里需要注意 \ 进行转移的转义操作仅作用于紧邻其后的一个字符, \ 与待转义字符间不要有空格,否则会被视为对空格的转义。
数值计算
默认情况下 shell 中的内容被视为字符串,不会进行数值操作,想要使用数值操作主要有两种方法,一种是将变量声明为整数类型,通过 declare 命令实现,另一种是使用 "((xxx))" 符号包括运算内容,其中的部分会视为数值计算而不是普通的字符串操作。
注意不能使用形如 test=((expr)) 的赋值方式,这种情况会将 "=" 右侧的内容视为字符串赋值内容,而 "(" 是无法作为普通字符使用的(除非使用 \ 进行转义操作)。正确的做法是 test=$((expr)),使用 $ 进行展开。
((expr)) //expr会按照算数计算的方式进行求值 $((expr)) //获得expr的计算结果 declare -i test=5 //定义 test 为整数类型,可以对其进行整数操作 test=$test+5 //变量值为 10
如果对整数类型如上面的 test 赋值其他类型时,被赋值内容会被转换为整数类型,如 test=a,此时 test 的值为 0(注意不是 a 的 ascii 码值)。
控制语句
shell 支持循环,条件判断等操作进行程序流程的控制。
条件语句
shell 脚本中可以使用与操作符 "&&" 和或操作符 "||" 来进行程序控制,常用于对函数的返回值进行处理等操作。注意这两个操作符和 C/C++ 中的操作符含义略有不同。
command1 && command2 //当且仅当 command1 执行结果为 0, command2 执行 command1 || command2 //当且仅当 command1 执行结果非 0, command2 执行
shell 脚本支持比较丰富的比较操作,通过 "[]" (test)或 "[[]]" (test contruct) 操作符来使用,操作结果返回 true 或者 false。其支持的比较参数可以直接参考 test 命令所支持的操作,具体可以参考 man test 内容(事实上 shell 中的写法就是对应的 test 命令去除 test 后的形式)。其中比较常见的操作如下所示:
//字符串比较 str1 = str2 //字符串相等,等号两边的空格是必须的 str1 != str2 //字符串不相等 //数字比较 int1 -eq int2 // =(equal) int1 -ge int2 // >=(greater equal),另外还有 gt(greater than),le(less equal),lt(less than),ne(not equal) //文件比较 file1 -ef file2 //file1 和 file2 有相同的设备和inode号 file1 -nt file2 //file1 的 mtime 较之 file2 新 file1 -ot file2 //file1 较之 file2 旧 //文件特性判断 -d FILE //文件存在且可执行 -w FILE //文件存在且可写入 -e FILE //文件存在 //多重判断 -o //任意一个条件成立,-e file1 -o -e file2, file1 或 file2 任意一个存在即返回 true -a //条件同时成立则返回 true ! //结果取反,如 ! -e file1
上述语句返回的结果为 true 或 false,常用于 if 条件判断语句等位置。
另外对于上述判断的使用建议在双重括号中,如判断某个文件是否存在可以使用 [[ $? -eq 0 ]] 来判断上一条语句的返回结果是否为 0.关于 "[]" 与 "[[]]" 的区别可以参考 What is the difference between test, [ and [[ ?
一个使用"[[]]"而不是"[]"的例子是,若在条件中设置对应的字符串的比较,如 $test1 = $test2,则需要使用 [[ $test1 = $test2 ]] 的格式,若使用 "[]" 进行操作,则当其中一个字符串如 $test1 为空时,会出现形如 [ = xxx] 的格式,会出现报错 "[: =: unary operator expected".
判断语句
if 判断语句,其中条件判断语句可以使用上面介绍的条件判断语法进行。同时,除了使用类似 -o/-a 这种复合条件判断语句外,在 if 语句的判断语句中还可以使用 &&(与) 和 ||(或) 来连接多个中括号的条件判断。如 [ -e file1 -a -e file2 ] 的功能与 [ -e file1 ] && [ -e file2 ] 的功能相同。
if [条件判断1]; then xxxxx elif [条件判断2]; then //只要时有条件判断,均需要接上 then 关键字 xxxxx else xxxxx fi //结束条件判断
使用 case ...in .. esac 关键字来进行判断匹配。
case $1 in // case 与 in 均为关键字,$1 为进行匹配的内容 "1") //括号 ")" 前为进行匹配的内容 // statements ;; //表示程序段的结束 ×) //"*" 表示默认匹配内容,实际起类似通配符的作用 //statements ;; esac //case 语句结束
函数
shell 中包含有对函数的简单支持,在被调用之前,函数的定义应该是可见的。函数的基础定义如下所示。
function funcName() {
//statements
}
在调用函数,可以直接函数名+参数的形式进行调用。如 funcName 1 以参数为 1 进行函数调用。在函数的内部通过与 shell 脚本访问参数类似的方式进行参数访问,其中 $1 表示的是函数调用的第一个参数,而不再是 shell 脚本的第一个参数。
循环语句
shell 脚本中包含有 while 、util 和 for 类型的循环。
while 循环表示判断条件为成立时进行循环,而 util 循环表示判断条件成立时就终止循环。
while [条件判断] //条件为真时循环继续 do xxxx //statements done util [条件判断] //条件为假时循环继续 do xxxx //statements done
另外 shell 支持两种语法的 for 语句循环,具体如下所示:
for var in con1 con2 con3 ... // $var 依次取 con1 ... con3 进行循环,in 后面接可迭代对象即可 do xxxx //statements done for (( 初始值; 限制值; 执行步长 )) do xxxx //statements done
参数使用
shell 脚本中,默认通过对规定的字符组合来访问传递给脚本的参数,比较常见的访问符如下所示。更多 shell 中使用特殊字符的例子可以参考 Advanced Bash-Scripting Guide - Chapter 3. Special Characters
$0 //文件名 $1-$9 //脚本的第 1 到第 9 个参数 $@ //所有参数,独立的所有的参数 $* //所有参数通过分隔字符隔开,分隔符默认为空格,形如 "$1 $2 $3" $# //参数的数量 $? //前一条命令返回值 $$ //执行当前脚本的进程号 !! //上一条完整命令,常见使用场景是上条指令由于权限问题失败时,使用 sudo !! $_ //上一条命令的最后一个参数,在 shell 中可以使用 Esc + . 获得 $$ //当前 shell 的进程 pid $! //前一个后台进程的 pid,注意是后台进程,如通过在命令后加 & 执行的命令
另外 shell 脚本中可以通过 shift n 的形式跳过参数中的前 n 个,对应的对参数的访问形式( $x 中 x 的编号)会发生变化。
shell 脚本中常用的命令
read
read 命令用于读取来自键盘的输入,有用的参数如下所示。其中多个参数可以混用。read 为 bash 本身自带的命令,可以通过 read --help 查看命令帮助。
read -t time val1 //等待 time 时间,将用户输入存放至变量 val1 read -p description val2 //输出 description 并等待用户输入,存储至 val2 read -s //用户输入不显示在终端上 read var1 var2 <<< "$variable" //将 varible 变量中的值读取到 var1 和 var2 中,使用 $IFS 中的值作为分割符,可以自行进行修改
printf
进行简单的格式化输出,使用方式与 C/C++ 中的格式化输出类似。
printf "path name %s" $PATH //可用的格式化符包括 %s 字符串,%d 数字,%f 浮点数等
declare
declare 用于设置变量,修改变量的属性等操作。
declare [options] variable[=value] -a #将变量声明为数组 array -i #将变量声明为整形,之后会对该类型进行算数运算,而不是视为字符串 -r #将变量声明为只读类型 -x #将变量 export,即设置为全局变量 #将上述参数的 "-" 替换为 "+" 即可关闭对应的属性
其他
用户可以通过多种方式对变量进行默认值的设置,便于输出等情况。即当变量之前未设置时,使用其默认的值。
echo ${var:=default} //通过 := 设置默认值 echo ${var=-default} //通过 =- 设置默认值
参考
1. man page
2. Linux Shell Scripting Tutorial - varible