Jade Dungeon

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:调用的程序名称

$1n:命令行参数

数字超过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与句点操作符.的区别:

  1. .只能执行脚本;exec可以执行脚本和二进制程序。
  2. .执行结束后把控制交还给原来的脚本,exec不会交还控制状态。
  3. .授予新进程本地变量的访问权限,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 -ltrap -l,或是man 7 signal显示的帮助信息查看信号的信息。

常用信号:

  • EXIT:非真实信号,但是trap中常常用到。表示程序完毕或用户按exit。
  • ( 1)SIGHUPHUP:挂起信号。断开执行。
  • ( 2)SIGINTINT:中断。按CONTROL + C
  • ( 3)SIGQUITQUIT:退出。CONTROL + SHIFT + |CONTROL + SHIT + \
  • ( 9)SIGKILLKill:结束,kill -9命令。该信号无法捕获。
  • (15)SIGTERMTERM:软中断,kill -15kill默认
  • (20)SIGCHLDTSTP:停止,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)