【读书笔记】Linux命令行与Shell脚本编程大全

Linux命令行与Shell脚本编程大全

5.2 shell 的父子关系

命令分组

Command Grouping 主要有两种形式:

  • 一种以小括号包括,命令之间以冒号分隔。也被称为 进程列表
    • 注意,至少要有两条命令才会被认为是进程列表:(command;command[;command;command...])
  • 另一种以大括号包括,语法为 { command;[command;command;...] } ;
    • 注意与花括号之间必须留有空格,同时最后一个命令后仍然需要一个分号。

两种命令分组的区别在于,使用进程列表将会创建出一个子shell来执行分组中的命令,而另一种形式则不会如此。

协程

coproc 是一个 shell 关键字,其用法为 coproc [name] command,它将在后台生成一个子 shell,并在其中执行 command 。协程的名字默认为 COPROC,当显式命名时,command 部分应采用命令分组的形式(注意若使用进程列表,将产生嵌套的子 shell )。

11.4 使用变量

命令替换

为了将命令的输出提取出来以赋给变量,可以使用如下两种形式执行命令:

  • 使用反引号字符(`)
  • $() 格式

下面的脚本将当前日期及时间嵌入到特定字串中输出:

#!/bin/bash

my_var=`date`	# or my_var=$(date)
echo "The date and time are: " $my_var

11.7 执行数学运算

expr 命令

Bourne shell 提供了一个特别的命令,即expr,来处理数学表达式,该命令能够识别少数的数字及字符串操作符,此外该命令对数字的支持仅限于整数。并且,应注意到,在shell中使用时还需要对一些操作符进行转义。

bash shell 中,为了避免转义操作符带来的麻烦,应采用这种形式 $[ operation ] 来执行数学表达式。

浮点运算

z shell 提供了完整的浮点数算术支持。而在bash中,为了避开数学运算的整数限制,常见的方案是使用内建的bash计算器,即bc

下面是一个在脚本中使用bc的简单例子:

#!/bin/bash

res=$( echo "scale=2; var1=3.1415; var1 / 2" | bc )
echo "Result is $res"

bc有一个内建变量 scale ,默认值为0。该变量控制着在含有除法的浮点数运算中,结果所保留的小数位数。浮点乘法不受其影响。

12.4 test 命令

if-then 语句中的 test 命令
if command
then
	commands
fi
if command; then
	commands
fi

if-then 语句首先执行 command 部分并测试其退出状态码( exit status ),如果为0,则执行 commands 部分。test 命令,提供了测试更多条件的途径。test condition 如果 condition 成立,test 命令就会返回退出状态码 0,否则返回非零的退出状态码。

bash shell 提供了 test 命令的另一种更简便的写法,注意 condition 与方括号间的空格是必须的。

if [ condition ]
then
	commands
fi
可判断的三类条件
  • 数值比较
    • n1 -op n2 , op: eq ne ge gt le lt
    • 无法处理浮点值
  • 字符串比较
    • str1 op str2, op: = != < >
    • -op str1, op: n z
    • 进行大小比较时,需要对 op 进行转义,此外,该比较使用标准ASCII顺序进行(sort命令使用的是系统的本地化语言设置中定义的排序顺序。对于英语,本地化设置制定了在排序中小写字母在大写字母前)。
  • 文件比较
    • -op file, op: d e f r s w x O G
    • file1 -op file2, op: nt ot
    • 其中 s 检查file是否存在并非空;O 检查file是否存在并属当前用户所有;G 检查file是否存在并默认组与当前用户的相同;nt 表示 newer then ,ot 表示 older then ,注意 nt 和 ot 不会关心文件是否存在。
复合测试条件

if-then 语句允许使用布尔逻辑来组合测试,有两种布尔运算符可用:

[ condition1 ] op [ condition2 ], op: && ||

变量参与

如果测试中的操作数包含了变量,那么建议使用引号将变量替换表达式引住。这是因为,如果变量为空(这在使用命令替换给变量赋值时可能会发生),那么变量替换将使得某些测试缺少操作数,从而造成错误;或者如果变量的值中包含空格,那么对于测试,将会出现非法或多余的参数。

通过使用 [ "$var" op val ] 这种形式,可以明确告知 test 命令,那里有一个参数,即便参数是空值或包含空格。

12.6 if-then 的高级特性

用于数学表达式的双括号

(( expression )),其中可使用各种各样的数学运算符,且不需要对诸如 < > 这样的操作符进行转义。

此外,由于 expression 可以是任意的数学赋值或比较表达式,故也可将双括号命令用作给变量赋值的普通命令。

用于高级字符串处理功能的双方括号

[[ expression ]],expression 使用了 test 命令中采用的标准字符串比较,同时提供了额外的特性:模式匹配,通过使用双等号操作符来使用这一特性。

12.7 case 语句

case variable in
pattern1 | pattern2) commands1;;
pattern3) commands2;;
*) default_commands;;
esac

shell 中的 case 不需要 "break" 语句即可跳转出去。

13.1 for 命令

内部字段分隔符 ( Internal Field Separator )

IFS 环境变量定义了 bash shell 用作字段分隔符的一系列字符。默认的字段分隔符有:

  • 空格
  • 制表符
  • 换行符
for 命令的格式
for var in list
do
	commands
done
for var in list; do
	commands
done
C 语言风格的 for 命令

for (( variable assignment ; condition ; iteration process ))

注意,有些部分没有遵循 bash shell 的标准:

  • 变量赋值可以有空格
  • 条件中的变量不以 $ 开头
  • 迭代过程的算式未用 expr 命令格式

下面是一个采用该风格但没有实际意义的例子:

for (( a = 1, b = 10; a<=10 && b > 6; a++, b-- ))
do
	echo "$a - $b"
done
seq 命令

并不是所有的 shell 都像 bash 一样支持 C 语言风格的 for 命令,如此,就需要写出一大串数字序列作为 list ,不过,幸好有 seq 命令可以帮助我们生成数字序列。

seq [option]... [first] [increment] last

默认 first 和 increment 为 1 。

13.3 while 命令

while test_command
do
	commands
done

其中 test_command 与 if-then 语句中该部分的用法相同。此外,可以定义多个 test_command,但只有最后一个测试命令的退出状态码被用来决定是否结束循环,注意,每个测试命令都应出现在单独的一行上。

下面是一个定义有多个 test_command 但没有什么实际意义的脚本:

#!/bin/bash

var=5
while echo $var
	[ $var -gt 0 ]
do
	echo "still in the loop"
	(( var = $var - 1 ))
done

输出为:

5
still in the loop
4
still in the loop
3
still in the loop
2
still in the loop
1
still in the loop
0

13.4 until 命令

until test_command
do
	commands
done

与 while 命令类似,until 命令也可以定义多个测试命令,但只有最后一个命令的退出状态码起作用。

13.7 控制循环

break 命令

break n,默认情况下,n 为1,表示立即结束当前层的循环;若 n 为2,则该命令会停止下一级的外部循环。

continue 命令

continue n,n 默认为1,表示跳过当前循环中剩余的命令。

13.8 处理循环的输出

可以对循环的输出使用管道或进行重定向,只需在 done 命令后添加一个处理命令即可。

14.1 命令行参数

位置参数 ( positional parameter )

bash shell 将被称为位置参数的特殊变量对应分配给输入到命令行中的所有参数,包括 shell 所执行的脚本名称。位置参数变量是标准的数字,其中 $0 是脚本程序名,$1 对应第一个参数,以此类推,直到第九个参数 $9,对于这之后更多的参数,需要在变量数字周围加上花括号,如第十一个参数对应 ${11}

获取脚本名

$0 变量所保存的通常是带有路径的脚本名,而非 basename 。不过,使用外部命令 basename 即可提取出想要的不带路径的脚本名。

14.2 特殊参数变量

参数统计

特殊变量 $# 统计了命令行中输入的参数的个数(注意,这不包含程序名,不要混淆了),可以通过该变量直接获取最后一个参数,不过应写成这种形式:${!#}(注意,当没有输入任何参数时,获取到的是程序名,而不是参数)。

下面的脚本将遍历并输出所有参数,其采用了 C 语言风格的 for 命令,并使用了嵌套的参数替换:

#!/bin/bash
for (( i = 1; i <= $#; i++ ))
do
	echo "param: ${!i}"
done
一次获取所有的参数

特殊变量 $* 将所有参数作为一个整体以当成一个单词来保存;而特殊变量 $@ 将所有参数当作同一字符串中的多个独立的单词来保存,从而使得可以直接通过 for 命令来遍历这些参数。

14.4 处理选项

shift 命令

默认情况下,该命令会将除 $0 外的每个位置参数变量向左移动一个位置,其中变量 $1 的值将在每次移动中被遗弃,取而代之是 $2 的值(如果有的话)。可以给该命令提供一个参数,指明每次要移动的距离。

注意,该命令同时也影响着特殊变量 $#, $* 以及 $@ 的值。

选项与参数

选项 是跟在单破折线(-)后的单个字母,对于 shell 脚本的内部处理来说,它是特殊的参数,而 参数 对应着普通的参数。

当同时使用选项与普通参数时,标准的处理方式是使用特殊字符将两者分开,该字符会告诉脚本何时结束选项以及普通参数何时开始。对 Linux 来说,这个特殊字符是双破折线(--)。

选项的特殊情况
  • 带值的选项:有些选项会带上一个额外的参数值,如 -m value
  • 合并的选项:将多个选项放到了同一个参数之中,如 -ac
  • 选项与值合写:如 -mValue
getopt 命令

用户在命令行中往往采用更为舒适的输入习惯来输入选项及普通参数,这给脚本对选项的处理带来了难度。通过使用 getopt 命令,可将习惯性的输入格式化为标准的脚本选项及普通参数输入。

getopt optstring parameters

optstring 定义了命令行中有效的选项字母,并定义了哪些选项需要参数值。首先列出所有有效的选项字母,而后在带有参数值的选项字母后加上一个冒号。该命令会基于 optstring 来解析 parameters ,并返回其标准形式。

$ getopt ab:cd -acb val1 -d val2 val3
-a -c -b val1 -d -- val2 val3

在脚本中使用该命令时,往往搭配 set 命令来将脚本的命令行参数替换为其标准形式,只需要在脚本开始处添加这样一条语句:set -- $(getopt -qu optstring "$@") ,其中 -q 是 getopt 命令的选项,表示忽略错误消息,比如发现了无效的选项;而 -- 是 set 命令的一个选项(真是很不标准的一个选项),使得 set 将其所在脚本对应的命令行参数替换为该选项的参数值。

-q 选项导致的错误

当 getopt 命令仅带上 -q 选项后,除了会忽略错误消息外,其输出还会发生微小的变化:

$ getopt -q ab:cd -acb val1 -d val2 val3
-a -c -b 'val1' -d -- 'val2' 'val3'

当脚本想要输出 val2 时,实际上会输出 'val2' (虽然 echo 'hello' 仅会打印出 hello)。如果不想要这种输出,可以再添加一个 -u 选项来避免输出被引号包围,事实上,-u 选项往往是必要的,因为在数值比较中,i<10 是有效的,而 i<'10' 显然是无效的。

此外,也可以不带任何选项,而只需将标准错误重定向到 /dev/null 即可。

实例

下面的脚本带有三个选项,-d 要求打印出分界符,-u 及其参数值表示一个单元的内容,-n 及其参数值决定要将该单元打印多少次,默认为 1 。

#!/bin/bash

set -- $(getopt -qu u:n:d "$@")

num=1
delimit=no

while [ -n "$1" ]
do
	case "$1" in
		-u)
			unit=$2
			shift	;;
		-n)
			num=$2
			shift	;;
		-d)
			delimit=yes	;;
		--)
			shift
			break	;;
		*)
			echo "some error"
			exit 1	;;
	esac
	shift
done

if [ $delimit = yes ]; then
	echo "->"
fi
if [ -z "$unit" ]; then
	echo "error: need unit"
else
	for (( i=0; i<$num; i++ ))
	do
		echo -n "$unit"
	done
	echo
fi
if [ $delimit = yes ]; then
	echo "<-"
fi

if [ $# -gt 0 ]; then
	echo "[$#]remained param: $*"
fi
$ ./necho.sh -du\& -n5 "hello world"
->
&&&&&
<-
[2]remained param: hello world
$ getopt -qu u:n:d -du"a b" -n5 "hello world"
 -d -u a b -n 5 -- hello world

可见,getopt 命令可以将合并选项拆开,也可以处理选项与其参数值间没有空格的情况,但是,它却无法把 hello worlda b 当成是一个参数来处理。

更高级的 getopts 命令

getopts optstring variable

optstring 与 getopt 的类似,首先列出有效的选项字母,而后在需要的参数值的选项后加上冒号。不像 getopt ,getopts 命令一次只处理一个选项,当处理完所有选项后,它会返回一个大于0的退出状态码。

getopts 命令将当前选项保存在 variable 中(不带单破折线),如果该选项有参数值的话,则将参数值保存在 OPTARG 中。此外,环境变量 OPTIND 始终保存着下一个要解析的命令行参数的位置,位置索引从1开始,因此,当解析完毕后,只需要 shift $[ $OPTIND - 1 ] 即可方便地处理余下的普通参数。

可以在 optstring 之前加上一冒号,这会使得 getopts 在发现错误时保持静默,但这并不意味着它不会处理错误。

  • 当发现无效选项时,variable 被设置为 ?,而选项字母被存放在 OPTARG 中;
  • 当选项缺少要求的参数值时,variable 被设置为 :,同时该选项字母会被存放在 OPTARG 中。

若 optstring 不以冒号开始,

  • 当发现无效选项或选项缺少要求的参数值时,variable 被设置为 ?,且 OPTARG 会被 unset ,此外还会打印出一条诊断消息。

用 getopts 重写上面实例的选项处理部分:

#!/bin/bash

num=1
delimit=no

while getopts :u:n:d opt
do
	case "$opt" in
		u) unit=$OPTARG	;;
		n) num=$OPTARG	;;
		d) delimit=yes	;;
		:) echo "Required argument for $OPTARG is not found."
			exit 1	;;
		*) echo "[$opt] Unknown option: $OPTARG"	;;
	esac
done

shift $[ $OPTIND - 1 ]

# - - -
$ ./necho.sh -hdu"a b" -n5 "hello world"
[?] Unknown option: h
->
a ba ba ba ba b
<-
[1]remained param: hello world

脚本正确地识别出选项 -u 的参数为 a b,这是因为 getopts 正确地认为其是一个参数。此外,hello world被认为是一个参数,这是因为对于命令行参数来说,确是如此。

但 getopts 命令并不总能识别出缺少参数的选项:

$ ./necho.sh -du
Required argument for u is not found.
$ ./necho.sh -u -d
-d

文件描述符与重定向

重定向符号

[fd]{<|>|<>}{[ ]file|&[ ]fd}

[src]{<|>|<>}{dst},若没有给出 src 部分,默认 >1><0<

永久重定向

默认情况下,重定向都是临时的,可以通过 exec 命令在脚本执行期间永久重定向,例如

#!/bin/bash

echo "[1]This output should be shown at screen"
tmpfile=$( mktemp out.XXXXXX )
echo "[1]This output should go to the $tmpfile file" >> $tmpfile
echo "[2]This output should be shown at screen"
exec 3>& 1
exec >> $tmpfile
echo "[2]This output should go to the $tmpfile file"
exec >&3
echo "[3]This output should be shown at screen"
exec <$tmpfile
echo "The content of the $tmpfile file:"
while read
do
	echo $REPLY
done
echo "That's over"
rm $tmpfile
$ ./test.sh
[1]This output should be shown at screen
[2]This output should be shown at screen
[3]This output should be shown at screen
The content of the out.rmmGdo file:
[1]This output should go to the out.rmmGdo file
[2]This output should go to the out.rmmGdo file
That's over
关闭文件描述符

要关闭文件描述符,将其重定向到特殊文件描述符 - 即可。

17 函数

17.1 创建与使用函数

创建函数
function name {
	commands
}
name() {
	commands
}

函数名是唯一的,如果后定义的函数使用了重复的函数名,之前的函数将会被覆盖,且这一切不会有任何提示。

使用函数

像使用一般的命令一样,直接键入函数名即可调用该函数。且函数在使用前应该被定义,否则将产生错误消息。

17.2 返回值

使用退出状态码

可以将函数的退出状态码当成是返回值,默认情况下,函数的退出状态码就是函数中最后一条命令的退出状态码。通过在函数中使用 return 命令,可以退出函数并指定一个整数值作为退出状态码。

由于退出状态码的取值为 0~255,所以 return 一个非法的值时,将产生一个错误值(这并不会产生任何错误消息)。

使用标准输出

通过命令替换可以将函数的标准输出赋给变量,这是一种获取函数返回值的更好的方法。

17.3 函数中的变量

传递参数

bash shell 将函数当作是小型的脚本来对待,这意味着可以像普通脚本那样向函数传递参数。

变量的作用域

默认情况下,在脚本中任何位置定义的任何变量都是全局变量。但对于函数中的变量来说,情况有些特殊。

  • 当函数通过命令替换执行时,由于这一过程创建了子 shell 来执行函数,所以虽然函数仍然可以通过继承来获取到父 shell 中的变量的值,但函数的执行将不会影响到全局环境,这包括对全局变量值的更改以及定义新的变量。
  • 当函数以正常方式执行时,则会影响到全部环境。
局部变量

local locVar or local locVar=expr

注意,与初始化全局变量不同,使用 local 声明或定义局部变量的操作将更新特殊变量 $?,这是因为 local 是一个内建命令,而命令具有退出状态码。

如果想通过命令替换给一个局部变量赋值,同时获取到该命令的退出状态码的话,应该使用如下方式:

local locVar
locVar=`command(s)`
exitStatus=$?
实例 —— 阶乘运算
#!/bin/bash

factorial() {
	if [ $1 -eq 1 ]; then
		echo 1
	else
		local ret=` factorial $[ $1 - 1 ]`
		echo $[ $ret * $1 ]
	fi
}

read -p "Input a number: " num
ret=` factorial $num `
echo "Result: $ret"
exit 0

使用库

可以创建一个只包含有函数的公用库文件,而后在多个脚本中引用该文件。

source 命令

该命令会在当前 shell 上下文中执行命令,而不是创建一个新的 shell。可以通过该命令在脚本中运行库文件,这样就可以使用库中的函数了。

source 命令有一个别名,即 点操作符 ( dot operator ),写作 .

18.1 文本菜单

select 命令
select variable in list
do
	commands
done

list 是由空格分隔的文本选项列表,select 将每个列表项显示成一个带编号的选项,而后循环进行以下步骤:

  • 打印出由 PS3 环境变量定义的提示符并等待用户输入;
  • 获取输入并将与输入的编号对应的文本选项存入到 variable 中(若输入无效,则 [ -z $variable ] 将为真);
  • 执行 commands,如果遇到 break ,则退出循环。
实例
#!/bin/bash

PS3="Enter your chioce: "

clear

select option in	\
	"Exit program"	\
	"Display memory usage"	\
	"Display disk space"	\
	"Display date and time"
do
	case $option in
		"Exit program")	break	;;
		"Display memory usage")	free -h	;;
		"Display disk space")	df -h	;;
		"Display date and time")	date	;;
		*)	echo "Sorry, wrong selection"	;;
	esac
done

clear
exit 0

18.2 TUI

用于 C 语言

为了绘制 TUI (Text-based User Interface ),可以使用 ncurcesnewt 库,只是注意在使用前确认一下是否安装了其 -dev 包即可。

适合 shell 脚本的

使用 dialogwhiptail 命令,即可在终端中绘制各种窗口来制作 TUI 。在 apt 中,对 whiptail 包的描述为:

Displays user-friendly dialog boxes from shell scripts

Whiptail is a "dialog" replacement using newt instead of ncurses. It provides a method of displaying several different types of dialog boxes from shell scripts. This allows a developer of a script to interact with the user in a much friendlier manner.

whiptail
选项的类别

whiptail 命令的选项分为两类:option、box-option 。其中,后者决定了要绘制的窗口及其内容,而前者用于调整后者的一些细节。

哪一个按钮

该命令通过退出状态码来告知脚本是哪一个按钮被选择了,当选择 yes-buttonok-button 时,返回 0;而当选择 no-buttoncancel-button 时,返回 1(当有错误发生或按下 ESC 时,退出状态码将是 255)。

获取输入

inputbox 会将用户输入的文本输出到 STDERR 中,同样,menu 也会将用户所选项的 tag 文本输出到 STDERR 中,这使得无法通过命令替换来将用户的输入或菜单选择存储到变量中。解决这一问题的其中一个方法是,临时交换 STDERR 与 STDIN 。

实例

下面的实例在上面文本菜单的基础上,提供了 TUI 。

#!/bin/bash

esCheck() {
    if [ $? -ne 0 ]; then
        echo "exit from $1." >&2
        exit $2
    fi
}

# inputUser validUser
inputUser() {
    local user
    user=`whiptail --inputbox "Input your user name please." 8 40 "w" 3>&1 1>&2 2>&3`
    esCheck "inputbox" 1
    if [ "$user" != $1 ]; then
		echo "Illegal user" >&2
		exit 1
    fi
    return 0
}

# inputPassword chanceTimes correctPassword
inputPassword() {
    local password
    local chanceTimes=$[ $1 - 1 ]
    while [ $chanceTimes -ge 0 ]
    do
        password=`whiptail \
            --title "password dialog" \
            --passwordbox "Input your secret password." 8 60 \
            3>&1 1>&2 2>&3`
        esCheck "passwordbox" 1
	if [ "$password" = $2 ]; then
	    return 0
	else
	    if [ $chanceTimes = 0 ]; then
		echo "No chance for password inputing." >&2
		exit 1
	    fi
	    whiptail \
		--title "remained chances: $chanceTimes" \
		--msgbox "The password is wrong, try it again." 8 40
	    esCheck "msgbox" 1
	fi
		chanceTimes=$[ $chanceTimes - 1 ]
    done
}

# menu :opt
menu() {
    local opt
    local es
    opt=`whiptail \
	--title "menu dialog" \
	--menu "Select your chioce" \
	25 60 20 \
	"e" "Exit program" \
	1   "Display memory usage" \
	2   "Display disk space" \
	3   "Display date and time" \
	3>&1 1>&2 2>&3`
    es=$?
    echo $opt
    return $es
}

inputUser $USER
inputPassword 3 "123456"
temp=` mktemp tmp.XXXXXX `
while :
do
    opt=`menu`
    esCheck "menu" 1
    case $opt in
	"e" )	break	;;
	1   )	free -h > $temp
    		whiptail --title "Memory usage" --textbox $temp 25 90 --scrolltext	;;
	2   )	df -h > $temp
			whiptail --title "Disk space" --textbox $temp 25 60 --scrolltext	;;
	3   )	whiptail --title "Date and time" --msgbox "`date`" 8 60	;;
	*   )	echo "This message shouldn't be shown!" >&2; exit 1	;;
    esac
done
rm $temp
for i in `seq 100`
do
    sleep 0.1
    echo $i
done | whiptail --gauge "Please wait for the program to exit..." 8 60 0
exit 0
posted @ 2020-09-14 20:30  Char-z  阅读(226)  评论(0编辑  收藏  举报