Bash Cookbook 学习笔记

2017-11-15 11:21:21来源:segmentfault作者:jimhs人点击

分享
Read Me

本文是以英文版<bash cookbook> 为基础整理的笔记,或速记,力求脱水


cookbook特点是实用,有很多可以复用的代码框架


假设读者已有一定的脚本基础知识,没有也没关系


我没找到中文版,所以决定diy,按自己的风格来写一遍


类似bash是啥等问题,不涉及。。

另推荐两本比较好的教材:

<Linux Shell Scripting Cookbook> 有中文版。零基础的读者可以先看这本


<Advanced Bash-Scripting Guide> 旧版的有中文。圣经级


争取周更一章 【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眼里,一切皆文件


文件描述符
























类型标识描述符编号
标准输入STDIN0
标准输出STDOUT1
标准错误STDERR2
用户自定义 3...

I/O常用符号速查表







































命令备注
命令 <输入.in读入
命令 >输出.out覆盖写
命令 >l输出.out在noclobber作用域内强制覆盖写
命令 >>输出.out追加写
命令 <<EOF 输入 EOF将"输入"内嵌到脚本内
命令a l 命令b l 命令c单向管道流
命令a l tee 输出a l 命令bT型管道流 (三通接头)
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 answerread -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 answerread -p "$prompt" answer
case "$answer" in
[yY1] ) CHOICE='y';;
[nN0] ) CHOICE='n';;
* ) CHOICE="$answer";;
esac
} # 结束# 主脚本只负责业务单元:
# 不断返回一个包的时间值给用户确认或修改,直到新值满足要求
until [ "$CHOICE" = "y" ]; doprintf "%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 herefor 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变量的
因为两个控制台是相互隔离的运行环境


逻辑
算术

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台