Read Me
本文是以英文版<bash cookbook> 为基础整理的笔记,或速记,力求脱水
cookbook特点是实用,有很多可以复用的代码框架
假设读者已有一定的脚本基础知识,没有也没关系
我没找到中文版,所以决定diy,按自己的风格来写一遍
类似bash是啥等问题,不涉及。。
-
另推荐两本比较好的教材:
争取周更一章 【11.12 更新第二章:变量】
一、基本定义和I/O
约定格式
# 注释:前导的$表示命令提示符 # 注释:无前导的第二+行表示输出 # 例如: $ 命令 参数1 参数2 参数3 # 行内注释 输出_行一 输出_行二 $ cmd par1 par1 par2 # in-line comments output_line1 output_line2
获取帮助
天助自助者
命令查询 man help
# cmd表示任意命令 $ man cmd # 手册第7章(这一章是内容总览) $ man 7 cmd $ cmd -h $ cmd --help # 查看bash内置命令的帮助文档 $ help builtin-cmd
删除 rm
# 文件删除前询问确认(误删文件会很麻烦的) $ rm -i abc.file rm: remove regular file 'abc.file'?
命令(精确)查找 type which locate
# 从$PATH的路径列表中查找:可执行的别名、关键字、函数、内建对象、文件等。 $ type ls ls is aliased to `ls -F -h` $ type -a ls # 查找全部(All)匹配的命令 ls is aliased to `ls -F -h` ls is /bin/ls $ which which /usr/bin/which # 也用于判断命令是bash内置(built-in)或是外部的(external) $ type cd cd is a shell builtin
# 从cron job维护的系统索引库中查找。 $ locate apropos /usr/bin/apropos /usr/share/man/de/man1/apropos.1.gz /usr/share/man/es/man1/apropos.1.gz /usr/share/man/it/man1/apropos.1.gz /usr/share/man/ja/man1/apropos.1.gz /usr/share/man/man1/apropos.1.gz # slocate (略)
命令(模糊)查找 apropos
# 从man手册中查找匹配的命令关键字。 $ apropos music cms (4) - Creative Music System device driver $ man -k music # 效果同上 cms (4) - Creative Music System device driver
输入/输出
在linux眼里,一切皆文件
文件描述符
类型 | 标识 | 描述符编号 |
---|---|---|
标准输入 | STDIN | 0 |
标准输出 | STDOUT | 1 |
标准错误 | STDERR | 2 |
用户自定义 | 3... |
I/O常用符号速查表
命令 | 备注 |
---|---|
命令 <输入.in | 读入 |
命令 >输出.out | 覆盖写 |
命令 >l输出.out | 在noclobber作用域内强制覆盖写 |
命令 >>输出.out | 追加写 |
命令 <<EOF 输入 EOF | 将"输入"内嵌到脚本内 |
命令a l 命令b l 命令c | 单向管道流 |
命令a l tee 输出a l 命令b | T型管道流 (三通接头) |
2 >&1 | &的意思是,将1解释为描述符而不是文件名 |
2 >&3- | -的意思是 : 自定义描述符3用完后释放 |
I/O的流向
$ 命令 1>输出文件.out 2>错误日志.err # 单向管道流 $ cat my* | tr 'a-z' 'A-Z' | uniq | awk -f transform.awk | wc # 通过tee实现管道分流,将uniq的输出写入x.x,同时也传给awk处理 $ ... uniq | tee /tmp/x.x | awk -f transform.awk ... # 对于不接受标准输入作为参数的命令,比如rm # 此时无法像这样写管道流 $ find . -name '*.c' | rm # 解决办法是,将输入通过$(...)打包为子进程 $ rm $(find . -name '*.class') # 通过引入一个自定义的临时描述符3,可以实现STDOUT和STDERR的对调 $ ./myscript 3>&1 1>stdout.logfile 2>&3- | tee -a stderr.logfile # 简化的结构 $ ./myscript 3>&1 1>&2 2>&3
单行多命令 sub-shell
# 一是用{},因为花括号是保留字,所以前后括号与命令间都要留一个空格 $ { pwd; ls; cd ../elsewhere; pwd; ls; } > /tmp/all.out # 二是用(),bash会把圆括号内的序列打包为一个子进程(sub-shell) # 子进程是个很重要的概念,这里暂不展开 # 如果说bash是个壳,sub-shell就是壳中壳了 # 类比python的闭包 $ (pwd; ls; cd ../elsewhere; pwd; ls) > /tmp/all.out
here document
here document是个linux脚本语言所特有的东西
对这个专有名词,我在网上也没找到现成的翻译
这里的here可以理解为"here it is"
即把原本需要从外部引用的输入文件
用一对EOF标识符直接包裹进脚本
这样就免去了从命令行再多引入一个外部文件的麻烦
如果把输入文件比作脚本需要的电池
就相当于“自带电池”的概念了(又借用了python的词)
# bash会对内容块内一些特殊标识进行不必要的解析和转义,进而可能导致一些异常行为 # 所以作为一个良好的习惯,建议改用<<\EOF,或<<'EOF',甚至可以是<<E\OF $ cat ext # here is a "here" document ## 巧妙的双关语 grep $1 <<EOF mike x.123 sue x.555 EOF $ $ ext 555 sue x.555 $
# tab缩进:<<-'EOF' # -(减号)会告知bash忽略EOF块内的前导tab标识 # 最后一个EOF前内务必不要留多余的空格,否则bash将无法定位内容块结束的位置 $ cat myscript.sh ... grep $1 <<-'EOF' lots of data can go here it's indented with tabs to match the script's indenting but the leading tabs are discarded when read EOF # 尾巴的EOF前不要有多余的空格 ls ... $
获取用户输入 read
# 直接使用 $ read # 通过-p参数设置提示符串,并用ANSWER变量接收用户的输入 $ read -p "给个答复 " ANSWER # 输入与接收变量的对应原则: # 类比python中元组的解包(平行赋值) # 参数: PRE MID POST # 输入比参数少:one # 参数: PRE(one),MID(空),POST(空) # 输入比参数多:one two three four five # 参数: PRE(one),MID(two),POST(three four five) $ read PRE MID POST # 密码的接收 # -s关闭明文输入的同时,也屏蔽了回车键,所以通过第二句显式输出一个换行 # # $PASSWD以纯文本格式存放在内存中,通过内核转储或查看/proc/core等方式可以提取到 $ read -s -p "密码: " PASSWD ; printf "%b" "\n"
一些实用的脚本框架
# 文件名: func_choose # 根据用户的输入选项执行不同命令 # 调用格式: choose <默认(y或n)> <提示符> <选yes时执行> <选no时执行> # 例如: # choose "y" \ # "你想玩个游戏吗?" \ # /usr/games/spider \ # 'printf "%b" "再见"' >&2 # 返回: 无 function choose { local default="$1" local prompt="$2" local choice_yes="$3" local choice_no="$4" local answer read -p "$prompt" answer [ -z "$answer" ] && answer="$default" case "$answer" in [yY1] ) exec "$choice_yes" # 错误检查 ;; [nN0] ) exec "$choice_no" # 错误检查 ;; * ) printf "%b" "非法输入 '$answer'!" esac } # 结束
# 文件名: func_choice.1 # 把处理用户输入的逻辑单元从主脚本中剥离,做成一个有标准返回值的函数 # 调用格式: choice <提示符> # 例如: choice "你想玩个游戏吗?" # 返回: 全局变量 CHOICE function choice { CHOICE='' local prompt="$*" local answer read -p "$prompt" answer case "$answer" in [yY1] ) CHOICE='y';; [nN0] ) CHOICE='n';; * ) CHOICE="$answer";; esac } # 结束 # 主脚本只负责业务单元: # 不断返回一个包的时间值给用户确认或修改,直到新值满足要求 until [ "$CHOICE" = "y" ]; do printf "%b" "这个包的时间是 $THISPACKAGE\n" >&2 choice "确认? [Y/,<新的时间>]: " if [ -z "$CHOICE" ]; then CHOICE='y' elif [ "$CHOICE" != "y" ]; then # 用新的时间覆写THISPACKAGE相关的事件 printf "%b" "Overriding $THISPACKAGE with ${CHOICE}\n" THISPACKAGE=$CHOICE fi done # 这里写THISPACKAGE相关的事件代码
# 以下总结三种常用的预定义行为: # 1. 当接收到'n'之外的任何字符输入时,向用户显示错误日志 choice "需要查看错误日志吗? [Y/n]: " if [ "$choice" != "n" ]; then less error.log fi # 2. 只有接收到小写'y',才向用户显示消息日志 choice "需要查看消息日志吗? [y/N]: " if [ "$choice" = "y" ]; then less message.log fi # 3. 不论有没有接收到输入,都向用户做出反馈 choice "挑个你喜欢的颜色,如果有的话: " if [ -n "$CHOICE" ]; then printf "%b" "你选了: $CHOICE" else printf "%b" "没有喜欢的颜色." fi
二、命令/变量/逻辑/算术
命令
抛开窗口和鼠标的束缚
运行的机制 $PATH
# 当输入任意一条命令时 $ cmd # bash会遍历在环境变量$PATH定义的路径,进行命令匹配 # 路径串用冒号分隔。注意最后的点号,表示当前路径 $ echo $PATH /bin:/usr/bin:/usr/local/bin:. # 做个小实验: $ $ bash # 首先,开一个bash子进程 $ cd # 进到用户的home路径 $ touch ls # 创建一个与ls命令同名的空文件 $ chmod 755 ls # 赋予它可执行权限 $ PATH=".:$PATH" # 然后把当前(home)路径加入PATH的头部 $ # 这时,在home路径下执行ls命令时,会显示一片空白 # 因为你所期望的ls已经被自创的ls文件替换掉了 # 如果去到其他路径再执行ls,一切正常 # 实验做完后清理现场 $ cd $ rm ls $ exit # 退出这个bash子进程 $ # 所以,安全的做法是,只把当前路径附在PATH的尾部,或者干脆就不要附进去
# 一个实用的建议: # 可以把自己写的所有常用脚本归档在一个自建的~/bin目录里 PATH=~/bin:$PATH
# 通过自定义的变量操作命令: # 比如定义一个叫PROG的通用变量 $ FN=/tmp/x.x $ PROG=echo $ PROG $FN $ PROG=cat $ PROG $FN
变量的取名是很有讲究的。有些程序,比如InfoZip,会通过$ZIP和$UNZIP等环境变量传参给程序。如果你在脚本中擅自去定义了一个类似ZIP='/usr/bin/zip'的变量,会怎么想也想不明白:为什么在命令行工作得好好的,到了脚本就用不了? 所以,一定要先去读这个命令的使用手册(RTFM: Read The Fxxking Manual)。
运行的顺序 串行 并行
三种让命令串行的办法
# 1. 不停的手工输入命令,哪怕前一条还没执行完,Linux也会持续接收你的输入的 # 2. 将命令串写入一个脚本再批处理 $ cat > simple.script long medium short ^D # 按Ctrl-D完成输入 $ bash ./simple.script # 3. 更好的做法是集中写在一行: # 顺序执行,不管前一条是否执行成功 $ long ; medium ; short # 顺序执行,前一条执行成功才会执行下一条 $ long && medium && short
命令的并行
# 1. 用后缀&把命令一条条手工推到后台 $ long & [1] 4592 $ medium & [2] 4593 $ short $ # 2. 写在一行也可以 $ long & medium & short [1] 4592 [2] 4593 # [工作号] 进程号 $ $ kill %2 # 关闭medium进程,或者kill 4593 $ fg %1 # 把long进程拉回前台 $ Ctrl-Z # 暂停long进程 $ bg # 恢复long进程,并推到后台
linux其实并没有我们所谓“后台”的概念。当说“在后台执行一条命令”时,实际上发生的是,命令与键盘输入脱开。然后,控制台也不会阻塞在该命令,而是会显示下一条命令提示符。一旦命令“在后台”执行完,该显示的结果还是会显示回屏幕,除非事先做了重定向。
# 不挂断地运行一条后台命令 $ nohup long & nohup: appending output to 'nohup.out' $
用&运行一条后台命令时,它只是作为bash的一个子进程存在。当你关闭当前控制台时,bash会广播一个挂断(hup)信号给它的所有子进程。这时,你放在后台的long命令也就被“意外”终止了。通过nohup命名可以避免意外的发生。如果决意要终止该进程,可以用kill,因为kill发送的是一个SIGTERM终止信号。控制台被关闭后,long的输出就无处可去了。这时,nohup会被输出追加写到当前路径下的nohup.out文件。当然,你也可以任意指定这个重定向的行为。
脚本的批量执行
# 如果有一批脚本需要运行,可以这样: for SCRIPT in /path/to/scripts/dir/* do if [ -f $SCRIPT -a -x $SCRIPT ] then $SCRIPT fi done # 这个框架的一个好处是,省去了你手工维护一个脚本主清单的麻烦 # 先简单搭个架子,很多涉及robust的细节还待完善
返回状态 $?
用$?接收命令返回
# $?变量动态地存放“最近一条”命令的返回状态 # 惯例:【零值】正常返回;【非零值】命令异常 # 取值范围: 0~255,超过255会取模 $ badcommand it fails... $ echo $? 1 # badcommand异常 $ echo $? 0 # echo正常 $ $ badcommand it fails... $ STAT=$? # 用静态变量捕获异常值 $ echo $STAT 1 $ echo $STAT 1 $
$?结合逻辑判断
# 例如: # 如果cd正常返回,则执行rm cd mytmp if [ $? -eq 0 ]; then rm * ; fi # 更简洁的表达: # A && B:逻辑与 # 如果cd正常返回,则执行rm $ cd mytmp && rm * # A || B:逻辑或 # 如果cd异常返回,则打印错误信息并退出 cd mytmp || { printf "%b" "目录不存在.\n" ; exit 1 ; } # 如果不想写太多的逻辑判断,在脚本中一劳永逸的做法是: set -e # 遇到任何异常则退出 cd mytmp # 如果cd异常,退出 rm * # rm也就不会执行了
变量
一些常识 $
-
变量是:
存放字符串和数字的容器
可以比较、改变、传递
不需要事先声明
# 主流的用法是,全用大写表示变量,MYVAR # 以上只是建议,写成My_Var也可以 # 赋值不能有空格 变量=值 # 因为bash按空格来解析命令和参数 $ MYVAR = more stuff here # 错误 $ MYVAR="more stuff here" # 正确 # 变量通过$来引用 # 抽象来看,赋值语句的结构是:左值=右值 # 通过$,告诉编译器取右值 # 而且,$将变量和同名的字面MYVAR做了区分 $ echo MYVAR is now $MYVAR MYVAR is now more stuff here for FN in 1 2 3 4 5 do somescript /tmp/rep$FNport.txt # 错误 $FNport被识别为变量 somescript /tmp/rep${FN}port.txt # 正确 {}界定了变量名称的范围 done
导出和引用 export
# 查看当前环境定义的所有变量 $ env $ export -p
导出变量的正确方式
# 可以把导出声明和赋值写在一起 export FNAME=/tmp/scratch # 或者,先声明导出,再赋值 export FNAME FNAME=/tmp/scratch # 再或者,先赋值,再声明导出 FNAME=/tmp/scratch export FNAME # 赋过的值也可以修改 export FNAME=/tmp/scratch FNAME=/tmp/scratch2
正确的理解变量引用
# 通过上边的声明,我们有了一个FNAME的环境变量 $ export -p | grep FNAME declare -x FNAME="/tmp/scratch2" # 我们暂称它是父脚本 # 现在如果父脚本内开了(调用)一个子脚本去访问和修改这个变量,是可以的 # 但是,这个修改行为,对于父脚本是透明的 # 因为子脚本访问和修改的,只是从父脚本copy过来的环境变量复本 # 这是单向的继承关系,也是linux的一种设计理念(或称为安全机制) # 父脚本有没有什么办法去接收到这个改动呢? # 唯一的取巧办法是: # 让子脚本将修改echo到标准输出 # 然后,父脚本再通过shell read的方式去读这个值 # 但是,从维护的角度来讲,并不建议这样做 # 如果真的需要这么做,那原来的设计就有问题
所谓环境,指的是当前环境,也即当前控制台
如果你新开一个bash控制台,是根本看不到这个FNAME变量的
因为两个控制台是相互隔离的运行环境