Read Me
- 本文是以英文版<bash cookbook> 为基础整理的笔记,力求脱水
- 2017.11.23 更新完【基础】,内容涵盖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... |
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变量的
因为两个控制台是相互隔离的运行环境
参数的访问计数 ${} * @ {#}
- 基本用法
# 访问脚本的第{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<<b) |
>>= | 右位移 | 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及后续版本的结构体[[ ... ]]内部
测试运算符【全表】
测试运算符 | 真值 |
---|---|
-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...then...else
- 基本结构
#紧凑的写法:
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...in
case语句的强大,归功于右半括号):不光可以进行简单的字符串比较,还可以实现复杂的模式匹配
- 基本结构
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省字符
循环
while
bash不同于其他编程语言,比如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
done
for
for常用于需要计数的循环
- 基本结构
# 采用类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
done
select...in
select可以对一个列表进行循环选择,实现简单的目录功能
$ 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。