Bash脚本
控制结构
test工具是Bash的内置命令。检验表达式的结果是否为真(0)或是假(非0)。
可以用test
关键字与[]
进行使用:
test $# -eq 0 # 相当于 [ $# -eq 0 ]
关系操作符:
-
-ne
:不等于 -
-eq
:等于 -
-eq
:大于等于 -
-gt
:大于 -
-le
:小于等于 -
-lt
:小于
test判别式:
-
string
:不为空 -
-n string
:长度大于0 -
-z string
:长度为0 -
str1 = str2
-
str1 != str2
-
int1 relop int2
:relop就是之前说的关系操作符
-
file1 -ef file2
:inode相同 -
file1 -nt file2
:file1修改时间更新 -
file1 -ot file2
:file1修改时间更早
-
-e filename
:文件存在 -
-s filename
:文件长度大于0 -
-d filename
:是目录 -
-f filename
:是普通文件 -
-b filename
:是块设备文件 -
-c filename
:是字符设备文件 -
-L filename
:是符号链接 -
-p filename
:文件是命令管道
-
-g filename
:文件setgid位被设置 -
-k filename
:文件sticky位被设置 -
-u filename
:文件setuid位被设置
-
-r filename
:当前用户有读取权限 -
-w filename
:当前用户有写入权限 -
-x filename
:当前用户有执行权限
-
-t file-descriptor
:文件描述符可选值为:0、1或2。如果关联到键盘屏幕则为True -
-G filename
:文件存在,且与当前用户同组别 -
-O filename
:文件存在且被当前用户所拥有
if-then
if test-command then command fi
if test-command; then command fi
例:
echo -n "word 1: " read word1 echo -n "word 2: " read word2 if test "$word1" = "$word2" then echo "Match!" fi
变量$#
代表参数的个数:
if test $# -eq 0 then echo "You must supply at least one argument." exit 1 fi
test -f filename
检查文件是一个普通文件还是一个目录:
if test $# -eq 0 then echo "You must supply at least one argument." exit 1 fi if test -f "$1" then echo "$1 is a regular file in the working directory" else echo "$1 is NOT a regular file in the working directory" fi
if-then-else
if test-command then commands else commands fi
if test-command; then commands else commands fi
if-then-elif
if test-command; then commands elif test-command; then commands ... else commands fi
for-in
for loop-index in arg-list; do commands done
例:
for fruit in apples oranges pears bananas; do echo "$fruit" done for i in *; do # `*`表示当前目录中所有的文件 echo "$i" done
for
默认可以把命令行中参数列表作为迭代的目标:
for loop-index; do commands done
在脚本中可以使用$@
代表调用时传入的参数列表。
例,在for循环中,默认的迭代对象是整个参数列表$@
,可以不写出来:
$cat for_test for arg; do echo "$arg" done $ for_test candy gum chocolate candy gum chocolate
while
格式:
while test-command; do commands done
例子:
$ cat count #!/bin/bash number=0 while [ "$number" -lt 10 ]; do echo -n "$number" # echo -n 输出不换行 ((number += 1)) done echo # 换行 $ bash count 0123456789
until
格式:
until test-command ; do commands done
例:
$ cat until1 #!/bin/bash secretname=jenny name=noname echo "try guss name" echo until [ "$name" = "$secretname" ]; do echo -n "Your gess: " read name done echo "very good." $ bash until1 try guss name Your gess: helen Your gess: barbara Your gess: rachael Your gess: jenny very good.
break 与 continue
略
case
格式:
case test-string in ptn-1) commands-1 ;; ptn-2) commands-2 ;; ... esac
匹配的类型:
-
*
:任意内容,一般作为默认项。 -
?
:任意单个字符。 -
[...]
:定义一组字符。 -
|
:多个条件或分支。
例:
$ cat case2 echo -n "Enter A, B or C: " read letter case "$letter" in a|A) echo "You entered A" ;; b|B) echo "You entered B" ;; c|C) echo "You entered C" ;; *) echo "You did not enter A, B or C" ;; esac
select
语法:
select varname [in arg ...]; do commands done
select时还可以设置提示符PS3
,它是作为提示用户继续输入的信息,例:
#!/bin/bash PS3="Choose fruit: " select FRUIT in apple banana blueberry kiwi organe watermelon STOP; do if [ "$FRUIT" == ""]; then echo -e "Invalid input.\n" continue elif [ $FRUIT = STOP ]; then echo -e "Thank for playing. " break fi echo "You choose $FRUIT. " echo -e "That is choice number $REPLY. \n" done
$ bash fruit 1) apple 3) blueberry 5) organe 7) STOP 2) banana 4) kiwi 6) watermelon Choose fruit: 3 You choose blueberry. That is choice number 3. Choose fruit: 2 You choose banana. That is choice number 2. Choose fruit: 7 Thank for playing.
Here文档
here文档把程序代码本身作为程序的输入。
<<
作为生定向,紧跟后面的一个符号作为分隔符:
$ cat catfile grep -i "$1" <<+ # << 作为生定向,紧跟后面的+作为分隔符 aaaaa bbbbb ccccc ddddd eeeee + $ bash catfile bbb bbbbb $ bash catfile ccc ccccc
文件描述符
Linux进程启动时就包括标准输入输出和错误:0、1、2。
使用文件时打开文件,并分配文件描述符;不用文件时要关闭文件并释放文件描述符。
打开文件描述符:
exec n> outfile exec m< infile
复制文件描述符:
exec n<&m # 复制输入描述符 exec n>&m # 复制输出描述符
关闭文件描述符:
exec n<&-
例:
exec 3 < &0 4 <& 1 # 复制输入和输出 exec 5 < "in.txt" 6 <& 1 # 打开文件,复制输出 exec 7 < "in.txt" 8 > "out.txt" # 打开两个文件, cat <& 3 >& 4 # 把从3里读出的内容写到4里 cat <& 5 >& 6 cat <& 7 >& 8 exec 3<&- 4<&- 5<&- 6<&- 7<&- 8<&- # 关闭文件描述符
参数与变量
变量数组
name=(v1 v2 v3 ...)
例:
$ NAMES=(max helen sam zach) $ echo ${NAMES[2]} # sam
*
与@
都是用来取出整个数组元素的,但是加上双引号以后有区别:
$ A=("${NAMES[*]}") # 所有元素拼成一个 $ B=("${NAMES[@]}") # 所有元素作为数组 $ declare -a .... declare -a NAMES=([0]="max" [1]="helen" [2]="sam" [3]="zach") declare -a A=([0]="max helen sam zach") declare -a B=([0]="max" [1]="helen" [2]="sam" [3]="zach") $ echo ${#NAMES[*]} # 4 显示元素个数 $ echo ${#NAMES[1]} # 5 显示第一个元素的长度 $ NAMES[1]=alex # 设置元素的值 $ echo ${NAMES[*]} max alex sam zach
变量局部性
导出变量
默认变量都是局部的。export
命令让变量对子进程可用。
函数
函数与调用者用同一套环境,所以可以访问调用者定义的变量。
函数中的局部变量
内置命令typeset
声明函数中的变量为局部变量,仅在函数内部有效。
$ function count_down () { > typeset count # 局部变量 > count=$1 > while [ $count -gt 0 ]; do > echo "$count ..." > ((count=count-1)) # 双括号保证shell以算术表达式处理 > sleep 1 > done > echo "Blast off." > } $ count=10 # 外部变量 $ count_down 4 4 ... 3 ... 2 ... 1 ... Blast off.
特殊参数
$$
:PID进程标识
例如:echo是shell内嵌的,所以显示的就是当前进程的标识:
$ echo $$
用进程标识作为文件名的一部分,也是一个常用的手段:
cp example.txt example.$$.txt
$!
:后台进程的进程号
$ sleep 60 & # [1] 1928 $ echo $! # 1928 $ echo $$ # 7552
$?
:退出状态
$?
代表着上一个命令的退出代码。
$ ls text.txt $ echo $? # 0 $ ls no.such.file.txt $ echo $? # 2
在脚本中可通过exit
指定一个退出状态并结束脚本:
exit 7
位置参数
$#
:参数的个数
$0
:调用的程序名称
$1
到n
:命令行参数
数字超过9要加上花括号,如{$12}
。
*
与@
都是用来取出整个数组元素的,但是加上双引号以后有区别:
$ A=("${NAMES[*]}") # 所有元素拼成一个 $ B=("${NAMES[@]}") # 所有元素作为数组
shift
:左移参数命令
左移,并丢弃最左的一个参数。 通常用来循环扫描所有的参数。
#!/bin/bash echo "$1 $2 $3" shift echo "$1 $2 $3" shift echo "$1 $2 $3" shift echo "$1 $2 $3" shift $ bash demoshift a b c a b c b c c
set
:初始化参数
在程序里设置参数:
$ cat demoset #!/bin/bash set this is it echo $1 $2 $3 $ bash demoset this is it
例如可以用来拆解字符串:
$ date 2017年05月22日 16:39:02 $ cat dateset #!/bin/bash set $(date) echo $* echo echo "arg1 : $1" echo "arg2 : $2" echo "arg3 : $3" echo "arg4 : $4" echo "arg5 : $5" echo "arg6 : $6" echo $ bash dateset 2017年05月22日 16:39:16 arg1 : 2017年05月22日 arg2 : 16:39:16 arg3 : arg4 : arg5 : arg6 :
扩充变量与未设置变量
当指定的变量名${name}
找不到的时候,有一些应对机制。
一般情况下找不到变量名,那值就取一个空的字符串。
:-
找不到变量就取设置值
找不到变量名,就返回默认值(结束后变量学是未定度)。
${name:-default}
:-
找不到变量设置一个默认值设置值
找不到变量名,就合建一个值为默认值的变量。
${name:=default}
:?
:找不到变量时报错
报错并结束脚本执行:
${name:?message}
例:
cd $TESTDIR # 如果变量 TESTDIR没有值,以下的写法不会导致程序中止 $ cd ${TESTDIR:?err: val is null} bash: TESTDIR1: err: val is null $ cd ${TESTDIR:?$(date +%T) err: val is null} bash: TESTDIR: 15:03:30 err: val is null
内置命令
Shell的内置命令在shell脚本执行中不会创建新的进程。
显示命令的类型:type
$ type cat who echo if lt cat is hashed (/bin/cat) # 已经执行过,被Shell缓存了 who is /usr/bin/who echo is shell buildin if s shell keyword lt is aliased to 'ls -itrh | tail'
读取用户输入:read
读取用户输入的内容,成功返回0
,读到EOF就返回非零数字。
常用参数:
-
-s
:不回显输入的字符。 -
-a arr
:输入的单词作为数组的一个元素,arr用数组的名字代替。
把输入的内容保存到变量
$ cat read1 echo -n "username: " # -n表示不换行 read firstline echo "username is : $firstline"
变量中的特殊字符会被扩展
echo "username is : $firstline" # 双引号禁止shell扩展 echo username is : $firstline # 如果用户输入了`*`,会被shell扩展
输入提示符
read的参数-p
给用户显示一个输入提示符:
$ cat readla read -p "please input: " str echo "You entered: $str"
直接执行用户输入的内容
变量中的内容可以直接作为命令执行:
$ cat read2 read -p "please input: " str $str $ bash read2 input: date 2017年06月13日 10:58:07
读入的内容分别存入多个变量
$ cat read3 #!/bin/bash read -p "input: " str1 str2 str3 echo "str1 is: $str1" echo "str2 is: $str2" echo "str3 is: $str3" $ bash read3 input: this is something str1 is: this str2 is: is str3 is: something
执行命令:exec
格式:
exec [command] [args]
exec可以创建一个新的进程来执行命令,还可以重定向来自shell内部的文件描述符。
exec与句点操作符.
的区别:
-
.
只能执行脚本;exec可以执行脚本和二进制程序。 -
.
执行结束后把控制交还给原来的脚本,exec不会交还控制状态。 -
.
授予新进程本地变量的访问权限,exec
不能。
exec不会返回控制状态
因为不会返回控制状态,所以一般作为脚本的最后一行:
$ cat exec_demo who exec date echo "This line whil never displayed."
用exec重定向输入和输出
把标准输入重定向到一个文件中:
exec < inputfile
把标准输出和标准错误输出到文件中:
exec > outputfile 2> errfile
这样的方式使用exec,当前进程不会被替代,脚本中exec后面可以跟其他命令。
Linux用/dev/tty
表示用户工作的屏幕,这样脚本把把输出重定向到/dev/tty
上就可以
保证把内容显示给用户面不用关心用户用的是哪个设备
(tty会显示用户正在使用的设备名)。
$ cat to_screen1 echo "message to standard output" echo "message to standard error" 1>&2 echo "message to the user" > /dev/tty $ bash to_screen1 > out 2> err message to the user $ cat out message to standard output $ cat err message to standard error
可以用exec
指定输出到用户屏幕:
exec > /dev/tty
$ cat to_screen2 exec > /dev/tty echo "message to standard output" echo "message to standard error" 1>&2 echo "message to the user" > /dev/tty $ bash to_screen2 > out 2> err message to standard output message to the user $ cat out $ cat err message to standard error
注意在用exec重定向以后,后续的输出都保持重定向。除非再次用exec重定向。
用户的输入也可以用read重定向,这样可以输入来自/dev/tty
(键盘设备):
read name < /dev/tty
或
exec < /dev/tty
信号捕获:trap
通过kill -l
,trap -l
,或是man 7 signal
显示的帮助信息查看信号的信息。
常用信号:
-
EXIT
:非真实信号,但是trap中常常用到。表示程序完毕或用户按exit。 -
( 1)
SIGHUP
或HUP
:挂起信号。断开执行。 -
( 2)
SIGINT
或INT
:中断。按CONTROL + C
-
( 3)
SIGQUIT
或QUIT
:退出。CONTROL + SHIFT + |
或CONTROL + SHIT + \
-
( 9)
SIGKILL
或Kill
:结束,kill -9
命令。该信号无法捕获。 -
(15)
SIGTERM
或TERM
:软中断,kill -15
或kill
默认 -
(20)
SIGCHLD
或TSTP
:停止,CONTROL + Z
-
DEBUG
:每个命令执行之执行trap语句指定命令(实际上是多个信号, 但在trap中很实用)。 -
ERR
:错误。程序没有正常终止(退出状态非0)的命令结束之后执行trap语句指定内容 (实际上是多个信号,但在trap中很实用)。
Shell脚本捕获到信号以后可以进行一些处理。如果捕获到1,2,3,9,15,20 这六个信号都会中止脚本。 由于信号kill(9)是不能被程序处理的,系统会自动终止程序。
trap命令的格式为:
trap ['commands'] [signal]
注意commands代表的命令部分用单引号包起来。这是为了防止内部的变量或符号被 shell扩展。
$ cat inter #!/bin/bash trap 'echo PROGRAM INTERRUPTED; exit 1' INT while true; do echo "Program running. " sleep 1 done $ inter Program Running. Program Running. Program Running. CONTROL-C PROGRAM INTERRUPTED $
在实际应用中,trap到信号以后,一般处理的逻辑是在退出程序前,释放一些资源。 比如删除临时文件,等。
终止进程:kill
语法格式:
kill [-signal] PID
解析命令行选项:getopts
解析命令行参数:
getopts optstring varname [arg...]
optstring指定的参数的形式,一个字母代表一个选项,带冒号后缀的表示这个选项有值。
例:dxo:lt:r
表示-d
、-x
、-o value
、-l
、-t value
、-r
、
例子:
#!/bin/bash echo "Tips:" echo "-c compile" echo "-t test" echo "-a all" echo "-r run main func" echo "-e REPL" while getopts "b:ctrae" arg #选项后面的冒号表示该选项需要参数 do case $arg in c) ctags -R src --exclude=target --exclude=vendor mvn clean compile test-compile ;; t) mvn resources:resources resources:testResources surefire:test -Dtest=UserAuthDaoIntegrationTest,StrengthRecordDaoIntegrationTest,AerobicRecordRecordDaoIntegrationTest ;; a) ctags -R src --exclude=target --exclude=vendor mvn compile test-compile resources:resources resources:testResources surefire:test -Dtest=UserAuthDaoIntegrationTest,StrengthRecordDaoIntegrationTest,AerobicRecordRecordDaoIntegrationTest ;; r) mvn resources:resources scala:run -DmainClass=example.ScalaAppExample ;; e) mvn clean compile resources:resources resources:testResources scala:console ;; b) echo "b's arg:$OPTARG" #参数存在$OPTARG中 ;; ?) #当有不认识的选项的时候arg为? exit 1 ;; esac done
表达式
算术表达式
用let格式
$ VALUE=3 $ VV2=7 $ let "VALUE=VALUE * 10 + VV2" $ echo $VALUE 37
Shell不会扩展等号右边的内容,所以这里的双引号可以省略。
$ let VALUE=VALUE * 10 + VV2
let的每个参数作为 一个独立的表达式,可以在一行上给多个变更进行赋值:
$ let "COUNT = COUNT + 1" VALUE=VALUE*10+VV2
用((expression))
格式
$ VALUE=3 $ VV2=7 $ ((VALUE=VALUE * 10 + NEW)) $ echo $VALUE 37
有多个赋值表达式中,用逗号分开:
((COUNT = COUNT + 1", VALUE=VALUE*10+VV2))
注意算术赋值与算术扩展的区别
$((expression))
是扩展,以表达式的值代表被扩展的内容。
逻辑表达式
语法:
[[ expression ]]
例:
if [[ 30 < $age && $age < 60 ]]; then
或
if [[ 30 -lt $age && $age -lt 60 ]]; then
字符串比较
>
,<
,=
以字母顺序比较。
字符串模式匹配
语法:
${varname op pattern}
op的代表操作符:
-
#
:去除最小匹配前缀 -
##
:去除最大匹配前缀 -
%
:去除最小匹配后缀 -
%%
:去除最大匹配后缀
例:
$ SOURCEFILE=/usr/local/src/prog.c $ echo ${SOURCEFILE#AV} /usr/local/src/prog.c $ echo ${SOURCEFILE##/*/} prog.c qwshan@DST60040 ~/tmp $ echo ${SOURCEFILE%/*} /usr/local/src qwshan@DST60040 ~/tmp $ echo ${SOURCEFILE%%/*} $ echo ${SOURCEFILE%.c} /usr/local/src/prog $ CHOPFIRST=${SOURCEFILE#/*/} $ echo $CHOPFIRST local/src/prog.c $ NEXT=${CHOPFIRST%%/*} $ echo $NEXT local
还能使用字符串长度操作符,${#name}
$ echo $SOURCEFILE /usr/local/src/prog.c $ echo ${#SOURCEFILE} 21
操作符
-
前置后缀操作符:
var++ var-- ++var --var
-
一元:
-var +var
;布尔取反!
,二进制取反~
-
加减乘除,幂指数,取模:
+ - * / ** %
-
位移:
<< >>
-
比较:
> < = >= <= !=
-
位与,位或,位异或:
& | ^
-
布尔逻辑:
&& ||
-
三元操作:
?:
-
赋值:
= *= /= %= += -= <<= >>= &= ^= |-
-
逗号操作:
,
管道
管道操作符优先级是最高的:
$ cmd1 | cmd2 || cmd3 | cmd4 && cmd5 | cmd6
相当于:
$ ((cmd1 | cmd2) || (cmd3 | cmd4)) && (cmd5 | cmd6)