摘要:不挂断地运行一条后台命令用运行一条后台命令时,它只是作为的一个子进程存在。当你关闭当前控制台时,会广播一个挂断信号给它的所有子进程。通过命名可以避免意外的发生。
Read Me
本文是以英文版
2017.11.23 更新完【基础】,内容涵盖bash语法等知识点。
本系列其他两篇,与之互为参考
【中级】内容包括工具、函数、中断及时间处理等进阶主题。传送门
【高级】内容涉及脚本安全、bash定制、参数设定等高阶内容。传送门
所有代码在本机测试通过
Debian GNU/Linux 9.2 (stretch)
GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
约定格式# 注释:前导的$表示命令提示符 # 注释:无前导的第二+行表示输出 # 例如: $ 命令 参数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一、基本定义和I/O
在linux眼里,一切皆文件输入/输出 文件描述符【简表】
类型 | 标识 | 描述符编号 |
---|---|---|
标准输入 | STDIN | 0 |
标准输出 | STDOUT | 1 |
标准错误 | STDERR | 2 |
用户自定义 | 3... |
命令 | 备注 |
---|---|
命令 <输入.in | 读入 |
命令 >输出.out | 覆盖写 |
命令 >l输出.out | 在noclobber作用域内强制覆盖写 |
命令 >>输出.out | 追加写 |
命令 <将"输入"内嵌到脚本内 |
|
命令a l 命令b l 命令c | 单向管道流 |
命令a l tee 输出a l 命令b | T型管道流 (三通接头) |
2 >&1 | &的意思是,将1解释为描述符而不是文件名 |
2 >&3- | -的意思是 : 自定义描述符3用完后释放 |
$ 命令 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.outhere document
here document是个linux脚本语言所特有的东西
对这个专有名词,我在网上也没找到现成的翻译
这里的here可以理解为"here it is"
即把原本需要从外部引用的输入文件
用一对EOF标识符直接内嵌进脚本
这样就免去了从命令行再多引入一个外部文件的麻烦
如果把输入文件比作脚本需要的电池
就相当于让脚本“自带电池”(借用了python的词)
# bash会对内容块内一些特殊标识进行不必要的解析和转义,进而可能导致一些异常行为 # 所以作为一个良好的习惯,建议改用< # 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" " "【应用】接收用户输入
# 文件名: 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 " >&2 choice "确认? [Y/,<新的时间>]: " if [ -z "$CHOICE" ]; then CHOICE="y" elif [ "$CHOICE" != "y" ]; then # 用新的时间覆写THISPACKAGE相关的事件 printf "%b" "Overriding $THISPACKAGE with ${CHOICE} " 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" "目录不存在. " ; 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变量的
因为两个控制台是相互隔离的运行环境基本用法
# 访问脚本的第{1}个参数 $ cat simplest.sh echo ${1} $ ./simplest.sh you see what I mean you $ ./simplest.sh one more time one $ # 索引是单数的时候,花括号可以省略 # $10其实是$1+0 $ cat tricky.sh echo $1 $10 ${10} $ ./tricky.sh I II III IV V VI VII VIII IX X XI I I0 X $#!/bin/bash # actall.sh 批量更改文件权限 for FN in $* do echo changing $FN chmod 0750 $FN done $ ./actall.sh abc.tmp another.tmp allmynotes.tmp # $*相当于把参数序列按原样放入循环 for FN in abc.tmp another.tmp allmynotes.tmp do echo changing $FN chmod 0750 $FN done处理意外的空格 "" $@
首先要感谢苹果公司。是苹果,普及了文件名带空格的写法。
当你给文件命名时,可以优雅地写成My Report或是Our Dept Data
而不是丑陋的MyReport或是Our_Dept_Data了。
这给脚本的处理带来不便。因为空格对于脚本而言,是个最基础的分隔符# 用引号包裹参数 $ touch "Oh the Waste" # 先建一个名称含空格的演示文件 $ cat quoted.sh ls -l "${1}" # 这个引号告诉ls,将脚本传进来的参数视为整体 $ $ ./quoted.sh "Oh the Waste" # 这个引号告诉脚本,将用户给的参数视为整体 -rw-r--r-- 1 jimhs jimhs 0 Nov 11 22:57 Oh the Waste $# 处理循环的时候,前边例子中的$*识别不了带空格的参数,这时要使用加引号的"$@" # 先建三个名称含空格的演示文件 $ touch abc.tmp "yet another".tmp "all my notes".tmp #!/bin/bash # actall.sh 批量更改文件权限 for FN in "$@" do echo changing $FN chmod 0750 $FN done $ ./actall.sh *.tmp计数 $#
#!/bin/bash # check_arg_count 计算参数个数 # if [ $# -lt 3 ] then # 参数少于三个 elif [ $# -gt 3 ] # 参数多于三个 else # 参数等于三个 fi # 注意三者的不同含义 ${#} # 参数的个数 ${#VAR} # VAR值的长度 ${VAR#alt} # 替换访问下一个参数 shift
# 专业的脚本往往会涉及两种参数: # 一种作为选项开关,用于控制脚本的行为;另一种才是待处理处理 # 选项开关一旦被解析完成,就可以丢弃 # 这时,shift就派上了用场 #!/bin/bash # actall.sh 批量更改文件权限 # 通过-v开关控制echo行为 VERBOSE=0; if [[ $1 = -v ]] then VERBOSE=1; shift; # 移位: -v参数处理完可以丢弃了 fi for FN in "$@" do if (( VERBOSE == 1 )) then echo changing $FN fi chmod 0750 "$FN" done $ ./actall.sh -v *.tmp # 这个例子比较简单,只能处理单个的选项 # 实际应用中,会涉及更复杂的情况 # 例如多参数,myscript -a -p,这时参数顺序应该不影响行为 # 以及重复的参数,是该忽略还是提示错误。等等 # 后续谈到getopts时,会给出解决方案数组变量 [ ]
# bash可以处理一维数组变量 $ MYRA=(first second third home) $ echo runners on ${MYRA[0]} and ${MYRA[2]} runners on first and third # 如果只写$MYRA,就表示${MYRA[0]}默认值 ${:- := =}基本用法
# ${:-}用于设置命令行参数的默认值 # 位置1的参数如果为空,则使用/tmp FILEDIR=${1:-"/tmp"} # 变量的默认值 # 如果HOME变量没有设置,则设置为/tmp cd ${HOME:=/tmp} # 做个小实验 $ echo ${HOME:=/tmp} /home/jimhs $ unset HOME # 删除HOME $ echo ${HOME:=/tmp} /tmp $ echo $HOME /tmp $ cd ; pwd /tmp $ # 为了方便记忆和区分,可以这样理解: # ${HOME:=/tmp}用等号(=),表示赋值和返回这个值 # ${1:-"/tmp"}只有半个等号(-),只是返回,没有赋值。因为位置参数本身也没法被赋值空值 null
# ${=}用于允许空值的情况 # 空值null,也即""这样的空字符串 # 空值,和不存在是两个概念 # 继续上边的实验 $ echo ${HOME=/tmp} # 不会替换,因为HOME值存在 /home/jimhs $ HOME="" $ echo ${HOME=/tmp} # 也不会替换,因为HOME值为"" $ unset HOME $ echo ${HOME=/tmp} # 会替换,因为HOME变量不存在了 /tmp $ echo $HOME /tmp $不只是常量
关于${:=}等号的右边,除了常量,还可以放什么? 引用bash手册的原话是 “is subject to tilde expansion, parameter expansion, command substitution, and arithmetic expansion.”# tilde expansion 波浪符展开 (不能带引号"") ${BASE:=~uid17} # parameter expansion 参数展开 ${BASE:=${HOME}} # command substitution 命令替代 cd ${BASE:="$(pwd)"} # arithmetic expansion 算术运算 echo ${BASE:=/home/uid$((ID+1))}参数缺失 ${:?}# ${:?}可以检查参数是否提供,该如何处理 #!/bin/bash # check_unset_parms 检查参数是否设置 USAGE="使用格式:myscript 路径 文件 方式“ FILEDIR=${1:?"错误。未提供路径。"} FILESRC=${2:?"错误。未提供文件。"} CVTTYPE=${3:?"错误。${USAGE}"} # 运行的效果是这样的: $ ./myscript /tmp /dev/null ./myscript: line 5: 3: 错误。使用格式:myscript 路径 文件 方式 $ # 对于第三个分支,可以通过嵌入子进程$()实现更复杂的功能 CVTTYPE=${3:?"错误。$USAGE. $(rm $SCRATCHFILE)"} # 也可以牺牲点紧凑,写成下边这样更易读的结构 if [ -z "$3" ] then echo "错误。$USAGE" rm $SCRATCHFILE fi刚才运行时的提示,显示了行号和错误内容,像个脚本错误。虽然其实是用户自己使用不当造成的。所以,出于用户友好的考虑,商业级脚本并不会这样使用。对脚本编写和调试人员来说,倒是蛮实用的。部分替代 ${% # //}
./myscript: line 5: 3: 错误。使用格式: myscript 路径 文件 方式# ${%}可以对变量内容做部分的修改 #!bin/bash # suffixer 把后缀.bad改成.bash for FN in *.bad do mv "${FN}" "${FN%bad}bash" done # 分解来看各个步骤: # # FN=subaddon.bad NOBAD="${FN%bad}" # NOBAD="subaddon." NEWNAME="${NOBAD}bash" # NEWNAME="subaddon.bash" mv "${FN}" "${NEWNAME}" # mv "subaddon.bad" "subaddon.bash"# 用vi和sed等编辑器的替代格式可以吗?比如这样: mv "${FN}" "${FN/.bad/.bash}" # 编辑器中,末尾还要加个/来封闭这条语句 # 但这里不需要了,因为右半花括号}已经做了句法封闭 # ${FN/.bad/.bash}会把我们的subaddon.bad替换为subashdon.bad # 如果写成${FN//.bad/.bash},就会替换为subashdon.bash # 因为这两种写法,都无法像%那样实现替换位置的锚定。字符串操作【简表】算术
${...} 动作 name:number:number 提取子串,起始位置:长度 #name 返回字串长度 name#pattern 删除最短匹配,左向右 name##pattern 删除最长匹配,左向右 name%pattern 删除最短匹配,右向左 name%%pattern 删除最长匹配,右向左 name/pattern/string 替代第一处匹配 name//pattern/string 替代所有匹配 bash支持整数运算语法 ((...)) let# 双括号内的表达式各项间对空格不敏感 # 各变量不需要再用$前缀,位置参数除外 COUNT=$(( COUNT + 5+MAX * 2 - $2)) # 除法取整数部分。不支持浮点运算 $ echo $((- 7/- 3)) 2 # 多条表达式可以通过逗号级联 # 逗号运算符返回的是它右边的值,这里是3 $ echo $(( X+=5 , Y*=3 )) 3 # 双括号内的括号和星号已经是算术运算符的本义了 # 所以不需要再用进行转义 Y=$(( ( X + 2 ) * 10 )) # 双括号与let等效 # let语句的整条表达式内不能有空格 let Y=(X+2)*10运算符【全表】三、测试/流程控制 测试 [...] [[...]]
算术运算符 描述 用法 等价于 = 赋值 a=b a=b *= 乘 a*=b a=(a*b) /= 除 a/=b a=(a/b) %= 余数 a%=b a=(a%b) += 加 a+=b a=(a+b) -= 减 a-=b a=(a-b) <<= 左位移 a<<=b a=(a< >>= 右位移 a>>=b a=(a>>b) &= 按位与 a&=b a=(a&b) ^= 按位异或 a^=b a=(a^b) l= 按位或 al=b a=(alb)
测试体有三类
file 文件
string 字符串
expr 算术表达式
使用说明
运算符分为单目,和双目两种
各运算符,用于条件测试,或在结构体[ ... ]和[[ ... ]]内使用
多个运算符之间可以通过-a(逻辑与)和-o(逻辑或)进行级联
也可以成组地用于转义过的( ( ... ) )内部
用于字符串比较的<和>,以及结构体[[ ... ]],在bash 2.0版之前不可用
正则表达式=~,只适用于bash 3.0及后续版本的结构体[[ ... ]]内部
测试运算符【全表】条件 单分支 if...then...else
测试运算符 真值 -a file 文件存在, 弃用, 同 -e -b file 文件存在,且为 块设备 -c file 文件存在,且为 字符设备 -d file 文件存在,且为 目录 -e file 文件存在; 同 -a -f file 文件存在,且为 常规文件 -g file 文件存在 且已 设置setgid位 -G file 文件存在,且由 有效组id拥有 -h file 文件存在,且为 符号链接, 同 -L -k file 文件存在 且已 设置粘滞位 -L file 文件存在,且为 符号链接, 同 -h -N file 文件自从上次读后做过修改 -O file 文件存在,且由 有效用户id拥有 -p file 文件存在,且为 管道或命名管道(FIFO文件) -r file 文件存在,且为 可读 -s file 文件存在,且为 非空 -S file 文件存在,且为 套接字 -t N 文件描述符N指向终端 -u file 文件存在 且已 设置setuid位 -w file 文件存在,且为 可写 -x file 文件存在,且为 可执行, 或是可搜索的目录 fileA -nt fileB fileA 文件修改时间 晚于 fileA fileA -ot fileB fileA 文件修改时间 早于 fileA fileA -ef fileB fileA 和 fileB 指向同一文件 -n string 字符串非空 -z string 字符串长度为零 stringA = stringB stringA 匹配 stringB (POSIX版) stringA == stringB stringA 匹配 stringB stringA != stringB stringA 不匹配 stringB stringA =~ regexp stringA 匹配 扩展正则表达式regexp stringA < stringB stringA 小于 stringB 字典序 stringA > stringB stringA 大于 stringB 字典序 exprA -eq exprB exprA 等于 exprB exprA -ne exprB exprA 不等于 exprB exprA -lt exprB exprA 小于 exprB exprA -gt exprB exprA 大于 exprB exprA -le exprB exprA 小于等于 exprB exprA -ge exprB exprA 大于等于 exprB exprA -a exprB exprA 真 且 exprB 真 exprA -o exprB exprA 真 或 exprB 真 基本结构
#紧凑的写法: if list; then list; [ elif list; then list; ] ... [ else list; ] fi #更具可读性的写法: if (( $# < 3 )) then # 分支1 exit 1 elif (( $# > 3 )) then # 分支2 exit 2 else # 参数个数等于3时。。 fi测试体
# 1. 方括号: # 主要用于文件测试 if [ -d file ] # 也可用于简单的算术测试 # 如果算术表达式本身含括号,则还需要对括号进行转义或加引号区别 # 这时就不如用双圆括号方便 if [ $# -lt 3 ] # 2. 双圆括号: # 只限于算术表达式 if (( $# < 3 )) # 3. 命令: # 各种命令的exit返回值可用于测试: if ls; pwd; cd $1; then echo success; # cd成功时 else echo failed; # cd失败时 fi【应用】逆波兰表达式
【摘自百度百科】逆波兰表达式(Reverse Polish Notation),简称为RPN,由J. Lukasiewicz (12/21/1878 – 02/13/1956)发展而来,在避免使用括号的情况下,完成表达式的有优先级的运算。RPN表达式由操作数(operand)和运算符(operator)构成,不使用括号,即可表示带优先级的运算关系,但是须使用元字符,如空格。一般在计算机中,使用栈操作进行RPN表达式的计算。遇到操作数就入栈,遇到运算符,就对当前栈顶元素进行相应的一元或者二元运算。#!/bin/bash # rpncalc.sh # # 实现简单的RPN命令行整数计算 # 普通写法:(5+4)*2 # 逆波兰写法:5 4 + 2 * # # 先检查参数个数是否正确(不能小于3,或者为偶数) if [ ( $# -lt 3 ) -o ( $(($# % 2)) -eq 0 ) ] then echo "用法: ./rpncalc 操作数 操作数 运算符 [ 操作数 运算符 ] ..." echo "乘法用x或*表示" exit 1 fi ANS=$(($1 ${3//x/*} $2)) # 用x或*通配乘法 shift 3 # 循环直至参数耗尽,返回最终结果$ANS while [ $# -gt 0 ] do ANS=$((ANS ${2//x/*} $1)) shift 2 done echo $ANS如果不喜欢逆波兰表达式,可以有另一种方案:function calc { awk "BEGIN {print "结果: " $* }"; }这个函数通过awk内置的计算功能实现,且支持浮点数。比较使用,可以存到/etc/bashrc或~/.bashrc$ calc 2 + 3 + 4.5 结果: 9.5 # 括号和乘号因为是bash的元字符,所以需要用取消转义 $ calc (2+2-3)*4 结果: 4 # 或者直接将整个表达式包裹进单引号内 $ calc "(2+2-3)*4.5" 结果: 4.5多分支 case...incase语句的强大,归功于右半括号):不光可以进行简单的字符串比较,还可以实现复杂的模式匹配基本结构
case $FN in *.gif) gif2png $FN ;; # 双分号告诉bash,该分支到此结束(break) *.png) pngOK $FN ;; *.jpg) jpg2gif $FN ;; *.tif | *.TIFF) tif2jpg $FN ;; *) printf "格式无法识别: %s" $FN # 都不匹配时执行这里(default) ;; esac # esac比end-case省字符# 等价于: if [[ $FN == *.gif ]] then gif2png $FN elif [[ $FN == *.png ]] then pngOK $FN elif [[ $FN == *.jpg ]] then jpg2gif $FN elif [[ $FN == *.tif || $FN == *.TIFF ]] then tif2jpg $FN else printf "格式无法识别: %s" $FN fi # elif比elseif省字符循环 whilebash不同于其他编程语言,比如c或java,的地方是:当循环测试体返回零值时,判断为真。因为在bash的语境中,零代表一切正常,非零才代表异常。这点与其他语言正好相反。基本结构
# 1.无限循环 # 注意:因为expr非零,所以((expr))为零,所以while判断为真 while (( 1 )) { ... } # 2.算术测试 # 各变量前无需再加$前缀 while (( COUNT < MAX )) do some stuff let COUNT++ done # 3.文件测试 # 方括号用法同if语句 while [ -z "$LOCKFILE" ] do 循环体 done # 4.读取文件输入 # 文件末尾的值是-1,此时while非真,退出循环 while read lineoftext do process $lineoftext done如何将文件传给while-read循环
# 1.文件重定向给脚本 $ 脚本 <文件 # 2.对于固定的文件,可以直接在脚本内做重定向 while read lineoftext do 循环体 done < 文件 # 3.或者通过管道的方式 # 注意:管道左边的cat和右边的整个循环体都将是独立的子进程 # 循环内定义的任何变量,出循环体后都无法继续使用了 # 反斜线进行分行,使结构更具可读性 cat 文件 | while read lineoftext do 循环体 done【应用】清理垃圾文件
当使用版本控制命令svn查看一个工作目录内的文件变动时,常见的输出格式如下。
每个文件的前缀标识:M有修改、A新添加、?无法识别。
标识?的文件通常为运行后留下的临时或其他碎片文件。$ svn status bcb M bcb/amin.c ? bcb/dmin.c ? bcb/mdiv.tmp A bcb/optrn.c M bcb/optson.c ? bcb/prtbout.4161 ? bcb/rideaslist.odt ? bcb/x.maxc $有两种方法清理这些垃圾文件:方法一
# svn输出|grep取前缀为?的行|cut取出文件名|rm删除 svn status mysrc | grep "^?" | cut -c8- | while read FN; do echo "$FN"; rm -rf "$FN"; done方法二
# 因为bash根据空格作为参数分隔符 # 所以,分别用两个参数TAG和FN接收前缀标识和文件名 svn status mysrc | while read TAG FN do if [[ $TAG == ? ]] then echo $FN rm -rf "$FN" fi doneforfor常用于需要计数的循环基本结构
# 采用类c的风格 for (( expr1 ; expr2 ; expr3 )) ; do list ; done # 支持多变量 for (( i=0, j=0 ; i+j < 10 ; i++, j++ )) do echo $((i*j)) done浮点数循环
# 可以结合seq命令实现浮点数循环 # 要注意seq的参数顺序,递增量是写在中间的 $ seq 1.0 .03 1.1 1.00 1.03 1.06 1.09# for结构 for fp in $(seq 1.0 .01 1.1) do echo $fp; 其他语句 done # 先通过一个$()子进程展开seq列表,全部展开完了再传给for处理 # 原seq列表内的"换行"都被子进程替换为了空格 # 这样,每一个浮点值,传给for的时候都变成了字符串 "1.01" "1.02"# while结构 seq 1.0 .01 1.1 | while read fp do echo $fp; 其他语句 done # seq与while通过管道|连接,各自为独立的子进程,是并行的关系 # 这样,对于特别大而耗时的序列,seq的处理不会阻塞while循环 # 而前一个的for结构,是串行的关系,存在阻塞的隐患早期的循环风格
# 早期的bash, for循环只能写成展开的形式 # 类c的风格是从版本2.04往后才有的 for i in 1 2 3 4 5 6 7 8 9 10 do echo $i doneselect...inselect可以对一个列表进行循环选择,实现简单的目录功能$ cat dblist testDB simpleInventory masterInventory otherDB#!/bin/bash # dbinit.sh DBLIST=$(cat dblist) select DB in $DBLIST do echo 初始化数据库: $DB # 其他操作 done$ ./dbinit.sh 1) testDB 2) simpleInventory 3) masterInventory 4) otherDB #? 2 初始化数据库: simpleInventory #? $select会对列表各项添加数字编号。以#?作为提示输入符,无限循环列表,直到用户按Ctrl+D结束输入并退出。对于系统默认的#?提示符,可以通过修改PS3变量进行个性化。(注:PS1就是标准控制台的输入提示符,PS2是跨行输入时显示的提示符。)#!/bin/bash # dbinit.sh PS3="0 inits >" select DB in $DBLIST do if [ $DB ] then echo 初始化数据库: $DB PS3="$((i++)) inits >" # 其他操作 fi done
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/23672.html
摘要:不挂断地运行一条后台命令用运行一条后台命令时,它只是作为的一个子进程存在。当你关闭当前控制台时,会广播一个挂断信号给它的所有子进程。通过命名可以避免意外的发生。 showImg(https://segmentfault.com/img/bVZARf?w=314&h=413); Read Me 本文是以英文版 为基础整理的笔记,力求脱水 2017.11.23 更新完【基础】,内容涵盖b...
摘要:传送门中级内容包括工具函数中断及时间处理等进阶主题。反斜杠用于禁用别名扩展功能。删除所有的别名,可以消除隐患。敏感信息哈希表当前运行环境下,执行过的命令会被添加到哈希表,用于提高再次调用时的访问速度。更改权限用于修改目录及文件权限。 showImg(https://segmentfault.com/img/bVZARf?w=314&h=413); Read Me 本文是以英文版 为基...
摘要:传送门中级内容包括工具函数中断及时间处理等进阶主题。反斜杠用于禁用别名扩展功能。删除所有的别名,可以消除隐患。敏感信息哈希表当前运行环境下,执行过的命令会被添加到哈希表,用于提高再次调用时的访问速度。更改权限用于修改目录及文件权限。 showImg(https://segmentfault.com/img/bVZARf?w=314&h=413); Read Me 本文是以英文版 为基...
摘要:传送门中级内容包括工具函数中断及时间处理等进阶主题。反斜杠用于禁用别名扩展功能。删除所有的别名,可以消除隐患。敏感信息哈希表当前运行环境下,执行过的命令会被添加到哈希表,用于提高再次调用时的访问速度。更改权限用于修改目录及文件权限。 showImg(https://segmentfault.com/img/bVZARf?w=314&h=413); Read Me 本文是以英文版 为基...
摘要:本文是以英文版为基础整理的笔记,力求脱水更新完中级。空格被默认作各列的分隔符,也可以通过开关进行自定义。这样的数组也叫作关联数组,或称为映射,或者是哈希表。空行表示段落的结束会匹配空行。 showImg(https://segmentfault.com/img/bVZARf?w=314&h=413); Read Me 本文是以英文版 为基础整理的笔记,力求脱水 2018.01.21 ...
阅读 3531·2021-10-14 09:43
阅读 3169·2021-08-25 09:38
阅读 422·2019-08-30 15:55
阅读 1196·2019-08-30 13:05
阅读 2120·2019-08-29 16:05
阅读 382·2019-08-29 12:58
阅读 2684·2019-08-29 12:34
阅读 2725·2019-08-27 11:03