【shell脚本编程大全-笔录01】

环境变量

全局、局部环境变量

​ 用户变量(局部变量):修改的设置只对某个用户的路径或执行起作用;

​ 系统变量(全局变量):影响范围是整个系统 ;

系统环境变量基本上都是使用全大写字母,以区别于普通用户的环境变量。

查看

shell 复制代码
# 查看全局变量
$ env
$ printenv
$ printenv HOME	# 要显示个别环境变量的值,可以使用printenv命令,但是不能用env命令

# 查看当前环境变量,set命令会显示某个特定进程设置的所有环境变量,包括局部变量、全局变量以及用户定义变量
$ set

env、printenv和set之间的差异:

1、set命令会显示出全局变量、局部变量以及用户定义变量。它还会按照字母顺序对结果进行排序。

2、env和printenv命令不会对变量排序,也不会输出局部变量和用户定义变量。在这种情况下,env和printenv的输出是重复的。不过env命令有一个printenv没有的功能,这使得它要更有用一些。

设置变量

shell 复制代码
# 设置局部变量
$ my_variable= "Hello World"

# 设置全局变量,可以通过export命令使局部变量变成全局变量
$ export my_variable

# 删除环境变量
$ unset my_variable

注意:如果在子进程中删除了一个全局环境变量, 这只对子进程有效。该全局环境变量在父进程中依然可用。

设置 PATH

shell 复制代码
$ PATH=$PATH:/home/xx/xx

​ 对PATH变量的修改只能持续到退出或重启系统。这种效果并不能一直持续。

环境变量持久化

​ 当登录Linux系统时,bash shell会作为登录shell启动。登录shell会从5个不同的启动文件里读取命令:

  • /etc/profile
  • $HOME/.bash_profile
  • $HOME/.bashrc
  • $HOME/.bash_login
  • $HOME/.profile

​ /etc/profile文件是系统上默认的bash shell的主启动文件。系统上的每个用户登录时都会执行这个启动文件。另外4个启动文件是针对用户的,可根据个人需求定制。

​ 1、对全局环境变量来说,可能更倾向于将新的或修改过的变量设置放在**/etc/profile**文件中,但这可不是什么好主意。如果你升级了所用的发行版,这个文件也会跟着更新,那你所有定制过的变量设置可就都没有了。

​ 2、最好是在**/etc/profile.d目录中创建一个以.sh结尾的文件**。把所有新的或修改过的全局环境变量设置放在这个文件中。

​ 3、在大多数发行版中,存储个人用户永久性bash shell变量的地方是** H O M E / . b a s h r c ∗ ∗ 文件。这一点适用于所有类型的 s h e l l 进程。但如果设置了 ∗ ∗ B A S H E N V ∗ ∗ 变量,那么除非它指向的是 ∗ ∗ HOME/.bashrc**文件。这一点适用于所有类型的shell进程。但如果设置了**BASH_ENV**变量,那么除非它指向的是** HOME/.bashrc∗∗文件。这一点适用于所有类型的shell进程。但如果设置了∗∗BASHENV∗∗变量,那么除非它指向的是∗∗HOME/.bashrc**,否则你应该将非交互式shell的用户变量放在别的地方。

​ alias命令设置就是不能持久的,可以把alias设置放在 $HOME/.bashrc 启动文件中,使其效果永久化。

数组

​ 要给某个环境变量设置多个值,可以把值放在括号里,值与值之间用空格分隔。

sh 复制代码
$ mytest=(one two three four five)
$ echo ${mytest[0]}
one
$ echo ${mytest[1]}
two
$ echo ${mytest[*]} 
one two three four five
#改变某个索引值位置的值
$ mytest[2]=seven
$ echo ${mytest[*]} 
one two seven four five
#删除
$ unset mytest[2]      //这里只是删除了值,[2]变成空了而已
$ unset mytest

数学运算

expr 命令

​ expr命令允许在命令行 上处理数学表达式,但需要转义特殊字符。

shell 复制代码
#!/bin/bash 
var1=10 
var2=20 
var3=$(expr $var2 / $var1) 
echo The result is $var3

使用方括号 [ ]

​ 不需要转义特殊字符,但只支持整数运算

shell 复制代码
#!/bin/bash 
var1=100 
var2=50 
var3=45 
var4=$[$var1 * ($var2 - $var3)] 
echo The final result is $var4

内建计算器:bc

​ bash计算器能够识别:整数和浮点数、变量、注释、表达式、编程语句、函数

shell 复制代码
#!/bin/bash 
var1=100 
var2=45 
var3=$(echo "scale=4; $var1 / $var2" | bc)	# scale:小数位数,默认0
echo The answer for this is $var3
如果需要进行大量运算,EOF字符串标识了重定向给bc命令的数据的起止
shell 复制代码
#!/bin/bash 
var1=10.46 
var2=43.67 
var3=33.2 
var4=71 

var5=$(bc << EOF 
scale = 4 
a1 = ( $var1 * $var2) 
b1 = ($var3 * $var4) 
a1 + b1 
EOF 
)
echo $var5

退出状态码

查看退出状态

shell 复制代码
$?

Linux退出状态码:

状态码 描述
0 执行成功
1 一般性未知错误(参数有误)
2 不适合的shell命令
126 命令不可执行(无权限)
127 没有找到命令
128 无效的退出参数
128+x 与Linux信号x相关的严重错误
130 通过Ctrl+c终止的命令
255 正常范围之外的退出状态码

自定义退出状态码

​ 默认情况下,shell脚本会以脚本中的最后一个命令的退出状态码退出。

exit 命令允许在脚本结束时指定一个退出状态码。例如:

shell 复制代码
$ cat test13 
#!/bin/bash 
# testing the exit status 
var1=10 
var2=30 
var3=$[$var1 + $var2] 
echo The answer is $var3 
exit 5 # 指定退出状态码
# exit var2	# 可以指定变量

$ chmod u+x test13 
$ ./test13 
The answer is 40 
$ echo $? 
5 

注意:退出状态码最大只能是255,区间为:0~255

字段分隔符 IFS

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

​ 默认情况下,bash shell会将下列字符当作字段分隔符: 空格、制表符、换行符 如果bash shell在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。

​ 可以将IFS的值也可以设为其他,也可以设置多个:

shell 复制代码
# 将IFS的值设为冒号(:)
IFS=: 
# 设置多个,将换行符($'\n')、冒号(:)、分号(;)和双引号(")作为字段分隔符
IFS=$'\n':;"
shell 复制代码
#!/bin/bash 
# reading values from a file 
file="states" 
# 设置字段分割为换行符
IFS=$'\n' 
for f in $(cat $file) 
do 
 echo "Visit beautiful $f" 
done 

​ 在处理代码量较大的脚本时,可能在一个地方需要修改IFS的值,然后忽略这次修改,在脚本的其他地方继续沿用IFS的默认值。一个可参考的安全实践是在改变IFS之前保存原 来的IFS值,之后再恢复它。 这种技术可以这样实现:

shell 复制代码
IFS.OLD=$IFS  
IFS=$'\n'  
[代码...] 
IFS=$IFS.OLD

​ 这就保证了在脚本的后续操作中使用的是IFS的默认值。

判断条件

test 命令

​ 如果test命令中列出的条件成立 ,test命令就会退出并返回 退出状态码0

​ test命令可以判断三类条件:数值比较、字符串比较、文件比较

shell 复制代码
if test condition
then 
 commands
fi

[ ] 替代test命令

​ [ ] 定义测试条件,等同 test 命令。

shell 复制代码
if [ condition ] # condition 前后需要加一个空格
then 
 commands
fi

数值比较

  • -eq 等于
  • -ne 不等
  • -gt 大于
  • -ge 大于等于
  • -lt 小于
  • -le 小于等于

bash shell只能处理整数,不支持浮点值比较。

字符串比较

  • str1 = str2 检查str1是否和str2相同
  • str1 != str 检查str1是否和str2不同
  • str1 < str2 检查str1是否比str2小
  • str1 > str2 检查str1是否比str2大
  • -n str1 检查str1的长度是否不为0
  • -z str1 检查str1的长度是否为0

字符串比较大小时,有两个问题:

1、大于号和小于号必须转义,否则shell会把它们当作重定向符号,把字符串值当作文件名;

2、大于和小于顺序和sort命令所采用的不同,顺序相反;

空的和未初始化的变量也可以用-n、-z判断。

文件比较

  • -d file 检查file是否存在,且是否为目录
  • -f file 检查file是否存在,且是否为文件
  • -e file 检查file目录或文件是否存在
  • -r file 检查file目录或文件是否存在,并可读
  • -s file 检查file目录或文件是否存在,并非空
  • -w file 检查file目录或文件是否存在,并可写
  • -x file 检查file目录或文件是否存在,并可执行
  • -O file 检查file目录或文件是否存在,并属当前用户所有
  • -G file 检查file目录或文件是否存在,并且默认组与当前用户相同
  • file1 -nt file2 检查file1是否比file2新
  • file1 -ot file2 检查file1是否比file2旧

布尔运算( && 、|| )

  • A && B 当A命令执行成功,才执行B
  • A || B 仅当A执行失败,才执行B

布尔逻辑是一种能够将可能的返回值简化为TRUE或FALSE的方法。

[ A && B ] 使用AND布尔运算符来组合两个条件。两个条件都必须满足,then部分的命令才会执行。

[ A || B ] 使用OR布尔运算符来组合两个条件。任意条件为TRUE,then部分的命令就会执行。

双括号 (( ))

​ 双括号命令允许你在比较过程中使用高级数学表达式。 只能进行整数运算,不能对小数(浮点数)或者字符串进行运算。

shell 复制代码
(( expression ))	# expression 可以是任意的数学赋值或比较表达式
  • 表达式可以有多个,多个表达式之间以逗号,分隔。对于多个表达式的情况,以最后一个表达式的值作为整个 (( )) 命令的执行结果: echo $((a=3+5, b=a+10)) 输出:15 ;
  • 可以使用 $ 获取 (( )) 命令的结果,这和使用 获得变量值类似: a = 获得变量值类似:a= 获得变量值类似:a=((10+66);
  • 在 (( )) 内使用变量无需加上 前缀,会自动解析变量名: ( ( a + b ) ) ,但要获取结果时需要加上 c = ∗ ∗ 前缀,会自动解析变量名:((a+b)),但要获取结果时需要加上c=** 前缀,会自动解析变量名:((a+b)),但要获取结果时需要加上c=∗∗**((a+b));

除了支持简单的加减乘除外,还支持其他运算符:

符合 描述
val++ 后增
val-- 后减
++val 先增
--val 先减
! 逻辑求反
~ 位求反
** 幂运算
<< 左位移
>> 右位移
& 位布尔和
| 位布尔或
&& 逻辑和
|| 逻辑或

​ 可以在 if 语句中用双括号命令,也可以在脚本中的普通命令里使用来赋值:

shell 复制代码
$ cat test.sh
#!/bin/bash 

val1=10 
if (( $val1 ** 2 > 90 )) ; then
 (( val2 = $val1 ** 2 )) 
 echo "The square of $val1 is $val2" 
fi 

$ ./test.sh
The square of 10 is 100

注意:不需要将双括号中表达式里的大于号转义。这是双括号命令提供的另一个高级特性。

双方括号 [[ ]]

​ 双方括号命令提供了针对字符串比较的高级特性

shell 复制代码
[[ expression ]]

​ 双方括号里的expression使用了test命令中采用的标准字符串比较。但它提供了test命 令未提供的另一个特性(模式匹配 )。在模式匹配中,双方括号中可以定义一些正则表达式来匹配字符串:

shell 复制代码
$ cat test.sh
#!/bin/bash 

if [[ $USER == r* ]] 
then 
 echo "Hello $USER" 
else 
 echo "Sorry, I do not know you" 
fi 

$ ./test.sh
Hello rich 

注意:不是所有的shell都支持双方括号!

小数比较

shell 复制代码
$ echo "1.8 < 1.6" |bc	# 不成立输出 0
0
$ echo "1.8 > 1.6" |bc	# 成立输出 1
1
shell 复制代码
#!/bin/bash

a=0.23
b=0.9

if [ $(echo "$a > $b" | bc) = 1 ]; then
 echo "a>b"
else
 echo "a<b"
fi

结构化命令

if-then

​ 与其他编程语言不同,shell 的 if 语句会运行 if 后面的命令,如果该命令的退出状态码是0 (该命令成功运行),位于then部分的命令就会被执行。如果该命令的退出状态码是其他值就不会被执行,会继续执行脚本中的下一个命令。

shell 复制代码
if command ; then
	commands
elif command ; then
	commands
else
	commands
fi

shell 复制代码
$ cat test1.sh
#!/bin/bash 
# testing the if statement 
if pwd 
then 
 echo "It worked" 
fi

# 这个脚本在if行采用了pwd命令。如果命令成功结束,再执行echo语句
$ ./test1.sh
/home/Christine 
It worked 

case 命令

​ case命令:为变量每个可能的值指定不同的选项。

shell 复制代码
case variable in 
 pattern1 | pattern2) commands1;; 
 pattern3) commands2;; 
 *) default commands;; 
esac

​ case 匹配不同的某值,如果匹配成功则执行它下面的命令,直到 ;; 为止

shell 复制代码
#!/bin/bash 
echo '输入 1 到 3 之间的数字:'
echo '你输入的数字为:'
read Num
case $Num in
	1) echo '你选择了 1' ;;	# 2|3) 值内容可以加上|可以有多个值
	2) echo '你选择了 2' ;;
	3) echo '你选择了 3' ;;
	*) echo '你没有输入 1 到 3 之间的数字' ;;	# 如以上都不匹配,则执行这条
esac			#结束
	echo "test case end"	#输出内容

for 循环

shell 复制代码
#!/bin/bash 
list="A B C D"
list=$list" E"	# 这是向变量中存储的已有文本字符串尾部添加文本的一个常用方法
for test in $list
do 
	echo The next number is $test 
done
echo The last number is $test	# for结束后,test变量会一直保持最后一次迭代的值

# 从命令读取值遍历
#!/bin/bash 
file="test.txt" 
for f in $(cat $file) 
do 
 echo "Visit beautiful $f" 
done 

​ for命令默认用空格、制表符、换行符 来划分列表中的每个值。如果在单独的数据值中有空格,就必须用双引号将这些值圈起来。

​ 也可以将do语句和for语句放在同一行,但必须用分号将其同列表中的值分开:for var in list; do

用通配符遍历目录

shell 复制代码
# 遍历目录文件,支持多个目录
for file in /home/rich/.b* /home/rich/badtest/*
do 
 if [ -d "$file" ]	# 将$file变量用双引号圈起来,预防有带空格的目录
 then 
   echo "$file is a directory" 
 elif [ -f "$file" ] 
 then 
   echo "$file is a file" 
 else 
   echo "$file doesn't exist" 
 fi 
done 

​ 在Linux中,目录名和文件名中包含空格当然是合法的。要适应这种情况,应该将$file变量用双引号圈起来。如果不这么做,遇到含有空格的目录名或文件名时就会有错误产生:

​ ./test6: line 6: [: too many arguments

for迭代( i++ )

shell 复制代码
#!/bin/bash 
for (( i=1; i <= 10; i++ )) 
do 
 echo "The next number is $i" 
done

$ ./test8 
The next number is 1 
The next number is 2 
...

# 可以使用多个变量
#!/bin/bash 
# multiple variables 
for (( a=1, b=10; a <= 10; a++, b-- )) 
do 
 echo "$a - $b" 
done 

while 循环

​ while命令允许定义一个要测试的命令(条件),只要定义的测试命令返回非0退出状态码时,while命令会停止循环。

​ while命令允许 你在while语句行定义多个测试命令只有最后一个测试命令不成立时停止循环。

shell 复制代码
#!/bin/bash 
var1=10 
while echo $var1 
 [ $var1 -ge 0 ] 	# 注意:每个测试命令都在单独的一行上
do 
 echo "This is inside the loop" 
 var1=$[ $var1 - 1 ] 
done 

until 循环

until 命令和while 命令工作的方式完全相反。until命令要求你指定一个通常返回非零退出状态码的测试命令。只有测试命令的退出状态码不为0,bash shell才会执行循环中列出的命令。 一旦测试命令返回了退出状态码0,循环就结束了。

​ 和while命令类似,可以指定的多个测试命令,只有在最后一个测试命令成立时停止循环

shell 复制代码
#!/bin/bash 
var1=100 
until echo $var1 
 [ $var1 -eq 0 ] 
do 
 echo Inside the loop: $var1 
 var1=$[ $var1 - 25 ] 
done 

循环处理文件数据

​ 通过修改IFS环境变量,就能强制for命令将文件中的每行都当成单独的一个条目来处理,即便数据中有空格也是如此。一旦从文件中提取出了单独的行,可能需要再次利用循环来提取行中的数据。

shell 复制代码
#!/bin/bash 
# 遍历/etc/passwd文件中的数据
IFS=$'\n'
for entry in $(cat /etc/passwd) 
do 
 echo "Values in $entry --" # 先换行符间隔遍历
 IFS=:
 	for value in $entry 	# 然后冒号间隔遍历
 do 
   echo " $value" 
 done
done

​ 这个脚本使用了两个不同的IFS值来解析数据。第一个IFS值解析出/etc/passwd文件中的单独的行。内部for循环接着将IFS的值修改为冒号,允许你从/etc/passwd的行中解析出单独的值。

控制循环

break

跳出内部循环 :在处理多个循环时,break命令会终止所在的最内层的循环。

​ 即使内部循环通过break命令终止了,外部循环依然继续执行。

shell 复制代码
#!/bin/bash 
# breaking out of an inner loop 
for (( a = 1; a < 4; a++ )) ; do 
 echo "Outer loop: $a" 
 for (( b = 1; b < 100; b++ )) ; do 
   if [ $b -eq 4 ] 
   then 
 	break 
   fi 
   echo " Inner loop: $b" 
 done 
done

break n

跳出外部循环 :有时你在内部循环,但需要停止外部循环( break n )。

​ 其中n指定了要跳出的循环层级。默认n为1,表明跳出的是当前的循环。如果你将 n设为2,break命令就会停止下一级的外部循环。

shell 复制代码
#!/bin/bash 
# breaking out of an outer loop 
for (( a = 1; a < 4; a++ )) ; do 
 echo "Outer loop: $a" 
 for (( b = 1; b < 100; b++ )) ; do 
   if [ $b -gt 4 ] 
   then 
     break 2 
   fi 
   echo " Inner loop: $b" 
 done
done

continue

跳过某次循环 :可以提前中止某次循环中的命令,但并不会完全终止整个循环。

​ 和break命令一样,continue命令也可以通过参数 n 指定哪一级循环:continue n

shell 复制代码
#!/bin/bash 
for (( var1 = 1; var1 < 15; var1++ )) ; do 
 if [ $var1 -gt 5 ] && [ $var1 -lt 10 ] 
 then 
   continue 
 fi 
 echo "Iteration number: $var1" 
done 

处理循环的输出

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

shell 复制代码
for file in /home/rich/* ; do 
 if [ -d "$file" ] 
 then 
   echo "$file is a directory" 
 elif 
   echo "$file is a file" 
 fi 
done > output.txt	# 输出重定向到某个文件中
shell 复制代码
for state in "A B" C E D 2 1 ; do 
 echo "$state" 
done | sort 	# 对输出进行排序

脚本参数及选项

参数$

参数 说明
$0 当前脚本文件名
$n 传递给脚本或函数的参数,n表示第几个参数。
$# 传递到脚本或函数的参数个数
$* 传递给脚本或函数的所有参数。如 "$*" 被双引号包含时:会将所有的参数看做一份数据,如 "$1 $2 ... $n"
$@ 传递给脚本或函数的所有参数。如 "$@" 被双引号包含时:每个参数都看作一份数据,如 "$1" " 2 " ... " 2" ... " 2"..."n"
$$ 当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID
$! 后台运行的最后一个进程的ID号
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 上个命令的退出状态,或函数的返回值。0表示没有错误,其他任何值表明有错误。

最后一个参数:KaTeX parse error: Expected '}', got '#' at position 3: {!#̲} 注意:当命令行上没有任何参...#的值为0,但${!#}变量会返回命令行用到的脚本名。

$* 和 $@ 的区别

​ 当 \* 和 @ 不被双引号 ' " " ' 包围时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔。但是当它们被双引号 ' " " ' 包含时,就会有区别了: 1 、 ' " @ 不被双引号\`" "\`包围时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔。 但是当它们被双引号\`" "\`包含时,就会有区别了: 1、\`" @不被双引号'""'包围时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔。但是当它们被双引号'""'包含时,就会有区别了:1、'"\*"`会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据。 ​ 2、`"@"`仍然将每个参数都看作一份数据,彼此之间是独立的。

特殊变量:$?

​ $? 是一个特殊变量,用来获取上一个命令的退出状态,或者上一个函数的返回值。

shell 复制代码
# 获取上一个命令的退出状态
if [ "$1" == 100 ]
then
   exit 0  #参数正确,退出状态为0
else
   exit 1  #参数错误,退出状态1
fi

# 获取函数的返回值
function add(){
    return `expr $1 + $2`
}
add 23 50  #调用函数
echo $?  #获取函数返回值:73

命令状态码详情参见:《自定义退出状态码》

函数返回值详情参见:《返回值》

移动变量(shift)

​ shift命令会根据它们的相对位置来移动命令行参数。在使用shift命令时,默认情况下它会将每个参数变量向左移动一个位置。所以,变量$3 的值会移到$2中,变量$2的值会移到$1中,而变量$1的值则会被删除(注意,变量$0的值,也 就是程序名,不会改变)。

​ 这可以很好的遍历命令行参数,尤其是在不知道到底有多少参数时。可以只操作第一个参数,移动参数,然后继续操作第一个参数。

shell 复制代码
# 遍历所有命令行参数
while [ -n "$1" ] ; do	# 当第一个参数的长度为零时,循环结束
 echo "Parameter $1"
 shift	# 也可以一次性移动多个位置(shift n),n指明要移动的位置数
done

shift n

​ 也可以一次性移动多个位置,只需要给shift命令提供一个参数,指明要移动的位置数就行了。

shell 复制代码
$ cat test14.sh
#!/bin/bash 
echo 
echo "Before moving parameters: $*" 
shift 2 # 向左移动两位
echo "After moving first parameter: $1" 

$ ./test14.sh 1 2 3 4 5
Before moving parameters: 1 2 3 4 5 
After moving first parameter: 3 

通过使用shift命令的参数,就可以轻松地跳过不需要的参数。

处理选项

处理简单选项

​ 遍历所有参数内嵌case命令达到不同命令执行不同的处理。

shell 复制代码
# 在提取每个单独参数时,用case语句来判断某个参数是否为选项
while [ -n "$1" ] ; do 
 case "$1" in 
 -a) echo "Found the -a option" ;; 
 -b) echo "Found the -b option" ;; 
 -c) echo "Found the -c option" ;; 
 *) echo "$1 is not an option" ;; 
 esac 
 shift 
done 

$ ./xx.sh -a -b -c -d
Found the -a option 
Found the -b option 
Found the -c option 
-d is not an option 
# 不管选项按什么顺序出现在命令行上,这种方法都适用
$ ./xx.sh -d -c -a

分离参数和选项

​ shell会用双破折线来表明选项列表结束。在双破折线(--)之后,脚本就可以放心地将剩下的命令行参数当作参数

shell 复制代码
# 提取选项
while [ -n "$1" ] 
do 
 case "$1" in 
 -a) echo "Found the -a option" ;; 
 -b) echo "Found the -b option";; 
 -c) echo "Found the -c option" ;; 
 --) shift 
 break ;; 
 *) echo "$1 is not an option";; 
 esac 
 shift 
done 
# 提取参数
count=1 
for param in $@ 
do 
 echo "Parameter #$count: $param" 
 count=$[ $count + 1 ] 
done 

​ 遇到双破折线时,脚本用break命令来跳出while循环。由于过早地跳出了循环,需要再加一条shift命令来将双破折线移出参数变量。

shell 复制代码
# 不带"--"时,所有参数都会当作命令行参数
$./xx.sh -c -a -b test1 test2 test3
Found the -c option 
Found the -a option 
Found the -b option 
test1 is not an option 
test2 is not an option 
test3 is not an option

# 当脚本遇到"--"时,它会停止处理选项,并将剩下的参数都当作命令行参数
$./xx.sh -c -a -b -- test1 test2 test3
Found the -c option
Found the -a option
Found the -b option
Parameter #1: test1
Parameter #2: test2
Parameter #3: test3

处理带值的选项

shell 复制代码
# -b选项还需要一个额外的参数值
while [ -n "$1" ] ; do
 case "$1" in
 -a) echo "Found the -a option";;
 -b) param="$2"
   echo "Found the -b option, with parameter value $param"
   shift ;;
 -c) echo "Found the -c option";;
 --) shift
   break ;;
 *) echo "$1 is not an option";;
 esac
 shift
done

count=1
for param in "$@"
do
 echo "Parameter #$count: $param"
 count=$[ $count + 1 ]
done

# 执行
$./xx.sh -a -b test1 -d
Found the -a option
Found the -b option, with parameter value test1
-d is not an option
# 更换带值参数的位置也可以
$ ./xx.sh -b test1 -a -d
Found the -b option, with parameter value test1 
Found the -a option 
-d is not an option 

​ 在这个例子中,case语句定义了三个它要处理的选项。-b选项还需要一个额外的参数值。 由于要处理的参数是$1,额外的参数值就应该位于$2(因为所有的参数在处理完之后都会被移出)。只要将参数值从$2变量中提取出来就可以了。当然,因为这个选项占用了两个参数位,所以你还需要使用shift命令多移动一个位置。

​ 但还有一些限制,比如想将多个选项放进一个参数中,不过 getopt 可以解决;

shell 复制代码
$ ./xx.sh -ac
-ac is not an option

getopt

​ getopt命令是一个在处理命令行选项和参数 时非常方便的工具。它能够识别命令行参数, 从而在脚本中解析它们时更方便。getopt命令可以接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式

​ 命令格式: getopt optstring parameters

​ 在每个需要参数值的选项字母后加一个冒号。getopt命令会基于你定义的optstring解析提供的参数

shell 复制代码
$ getopt ab:cd -a -b test1 -cd test2 test3
-a -b test1 -c -d -- test2 test3

​ optstring定义了四个有效选项字母:a、b、c和d。冒号(:)被放在了字母b后面,代表b选项需要一个参数值。当getopt命令运行时,它会检查提供的参数列表(-a -b test1 -cd test2 test3),并基于提供的optstring进行解析。注意,它会自动将-cd选项分成两个单独的选项,并插入双破折线来分隔行中的额外参数。

​ 如果指定了一个不在optstring中的选项,默认情况下,getopt命令会产生一条错误消息。

shell 复制代码
$ getopt ab:cd -a -b test1 -cde test2 test3
getopt: invalid option -- e 
 -a -b test1 -c -d -- test2 test3 
 
# 在命令后加-q选项,可以忽略这条错误消息
$  getopt -q ab:cd -a -b test1 -cde test2 test3
 -a -b 'test1' -c -d -- 'test2' 'test3' 

注意:getopt命令选项必须出现在optstring之前。

在脚本中使用getopt

​ 可以在脚本中使用getopt来格式化脚本所携带的任何命令行选项或参数:set命令实现用getopt命令生成的格式化后的版本来替换已有的命令行选项和参数。

​ set命令的选项之一是双破折线(--),它会将命令行参数替换成set命令的命令行值。然后,该方法会将原始脚本的命令行参数传给getopt命令,之后再将getopt命令的输出传给set命令,用getopt格式化后的命令行参数来替换原始的命令行参数,如下所示:

shell 复制代码
set -- $(getopt -q ab:cd "$@") 

​ 现在原始的命令行参数变量的值会被getopt命令的输出替换,而getopt已经为我们格式化好了命令行参数。利用该方法,就可以写出能处理命令行参数的脚本:

shell 复制代码
#!/bin/bash
set -- $(getopt -q ab:cd "$@") 

while [ -n "$1" ] ; do 
 case "$1" in 
 -a) echo "Found the -a option" ;; 
 -b) param="$2" 
   echo "Found the -b option, with parameter value $param" 
   shift ;; 
 -c) echo "Found the -c option" ;; 
 --) shift 
   break ;; 
 *) echo "$1 is not an option";; 
 esac
 shift 
done 

count=1 
for param in "$@" 
do 
 echo "Parameter #$count: $param" 
 count=$[ $count + 1 ] 
done

# 可以处理合并选项了
$./xx.sh -ac	
Found the -a option 
Found the -c option 
# 
$ ./test18.sh -a -b test1 -cd test2 test3
Found the -a option 
Found the -b option, with parameter value 'test1' 
Found the -c option 
Parameter #1: 'test2' 
Parameter #2: 'test3' 

# 存在问题:getopt命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。
$ ./xx.sh -a -b test1 -cd "test2 test3" test4
Found the -a option 
Found the -b option, with parameter value 'test1' 
Found the -c option 
Parameter #1: 'test2 
Parameter #2: test3' 
Parameter #3: 'test4' 

​ 但getopt命令并不擅长处理带空格和引号的参数值。它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。不过还有另外一个办法能解决这个问题------ [getopts](#更高级的 getopts)。

更高级的 getopts

​ getopts 内建于bash shell。它比 getopt 多了一些扩展功能。

​ 与getopt不同,前者将命令行上选项和参数处理后只生成一个输出,而 getopts 命令能够和已有的shell参数变量配合默契。

​ 每次调用它时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于0的退出状态码。这让它非常适合用解析命令行所有参数的循环中。

shell 复制代码
getopts optstring variable
  • 有效的选项字母都会列在optstring中,如果选项要求有个参数值,就在其后加一个冒号。
  • 要去掉错误消息的话,可以在optstring之前加一个冒号。
  • getopts命令将当前参数保存在命令行中定义的 variable 中。

​ getopts 命令会用到两个环境变量:

  1. OPTARG:保存带值参数的值
  2. OPTIND:保存下一个要处理的参数索引

​ 当调用getopts时如果发现OPTIND索引处的选项有输入值则设置OPTARG并返回true即便没有输入值若索引处存在选项也会返回true,同时为OPTIND递增1

shell 复制代码
while getopts :ab:c opt ; do 
 case "$opt" in
 a) echo "Found the -a option" ;; 
 b) echo "Found the -b option, value: $OPTARG";; 
 c) echo "Found the -c option" ;; 
 *) echo "Unknown option: $opt";; 
 esac 
done

# ./xx.sh -ab test1 -c
Found the -a option 
Found the -b option, with value test1 
Found the -c option

​ while语句定义了getopts命令,指明了要查找哪些命令行选项,以及每次迭代中存储它们的变量名(opt)。

​ getopts命令解析命令行选项时会移除开头的单破折线 (-),所以在case定义中不用单破折线

shell 复制代码
# 可以在参数值中包含空格
$./xx.sh -b "test1 test2" -a
Found the -b option, with value test1 test2 
Found the -a option 

# 可以将选项字母和参数值放在一起使用,而不用加空格
$ ./xx -abtest1
Found the -a option 
Found the -b option, with value test1

# 能够将命令行上找到的所有未定义的选项统一输出成问号
$ ./xx.sh -acde
Found the -a option 
Found the -c option 
Unknown option: ? 
Unknown option: ?

​ 在getopts处理每个选项时会将OPTIND环境变量值增一。在getopts完成处理时,可以使用shift命令和OPTIND值来移动参数。(OPTIND为下一个要处理的参数索引)

shell 复制代码
#!/bin/bash 
while getopts :ab:cd opt ; do 
 case "$opt" in 
 a) echo "Found the -a option" ;; 
 b) echo "Found the -b option, with value $OPTARG" ;; 
 c) echo "Found the -c option" ;; 
 d) echo "Found the -d option" ;; 
 *) echo "Unknown option: $opt" ;; 
 esac 
done 
# 
shift $[ $OPTIND - 1 ] 
# 
echo 
count=1 
for param in "$@" 
do 
 echo "Parameter $count: $param" 
 count=$[ $count + 1 ] 
done 

$ ./xx.sh -a -b test1 -d test2 test3
Found the -a option 
Found the -b option, with value test1 
Found the -d option 
Parameter 1: test2 
Parameter 2: test3 

​ 分析 OPTIND 的变化。

shell 复制代码
# 观察 OPTIND 的变化
#!/bin/bash
while getopts :ab:cd opt ; do
 case "$opt" in
 a) echo "Found the -a option;  a_OPTIND:$OPTIND" ;;
 b) echo "Found the -b option , with value $OPTARG;  b_OPTIND:$OPTIND" ;;
 c) echo "Found the -c option;  c_OPTIND:$OPTIND" ;;
 d) echo "Found the -d option;  d_OPTIND:$OPTIND" ;;
 *) echo "Unknown option: $opt ;  e_OPTIND:$OPTIND" ;;
 esac
done
#
echo "OPTIND:$OPTIND"
shift $[ $OPTIND - 1 ]

echo
count=1
for param in "$@"
do
 echo "Parameter $count: $param"
 count=$[ $count + 1 ]
done

# ./test1.sh -ad -c test2 test3 test4
Found the -a option;  a_OPTIND:1	# 下个参数为d,索引位置为1
Found the -d option;  d_OPTIND:2	# 下个参数为c,索引位置为2
Found the -c option;  c_OPTIND:3	# 下个参数为test2,索引位置为3
OPTIND:3

Parameter 1: test2
Parameter 2: test3
# ./test1.sh -adc test2 test3 test4
Found the -a option;  a_OPTIND:1	# 下个参数为d,索引位置为1
Found the -d option;  d_OPTIND:1	# 下个参数为c,索引位置为1
Found the -c option;  c_OPTIND:2	# 下个参数为test2,索引位置为2
OPTIND:2

Parameter 1: test2
Parameter 2: test3

用户输入 read

接收输入

read命令从标准输入(键盘)或另一个文件描述符中接受输入。在收到输入后,read命令会将数据放进一个变量:

shell 复制代码
echo -n "Enter your name: " 	# -n 去除字符串末尾换行符
read name	# 把用户输入保存到name变量
echo "Hello $name"

# -p :可直接在rend命令行指定提示信息
read -p "Please enter your age: " age 

​ 也可以指定多个变量;输入的每个 数据值都会分配给变量列表中的下一个变量,如果变量数量不够,剩下的数据就全部分配给最后一个变量。

shell 复制代码
read -p "Enter: " A B
echo " $A, $B"
# 执行1
Enter: a b
a, b... 
# 执行2
Enter: a
a,  ...
# 执行3
Enter: a b c d
a, b c d

​ 在read 命令行中不指定变量 。read命令会将它收到的任何数据都放进特殊环境变量REPLY中。

shell 复制代码
read -p "Enter your name: " 
echo Hello $REPLY, welcome to my program.

# 执行
Enter your name: Christine
Hello Christine, welcome to my program.

等待时长

-t 参数指定 read 命令等待输入的秒数,当计时结束时,read命令返回一个非零退出状态。可以使用结构化语句来处理。

shell 复制代码
# 5秒内没有输入,则停止等待
if read -t 5 -p "Please enter your name: " name ; then 
 echo "Hello $name, welcome to my script" 
else 
 echo 
 echo "Sorry, too slow! " 
fi

输入字数限制

-n 参数设置 read 命令计数输入的字符。当输入的字符数目达到预定数目时自动退出 ,并将输入的数据赋值给变量。

​ 如 -n 后接数值 1,表示 read 命令只要接收到一个字符就退出。只要按下一个字符进行回答,read 命令立即接受输入并将其传给变量,无需按回车键。

shell 复制代码
read -n1 -p "Do you want to continue [Y/N]?" answer
case $answer in
Y | y)
  echo "fine ,continue";;
N | n)
  echo "ok,good bye";;
*)
  echo "error choice";;
esac

隐藏终端显示

-s 选项能够使 read 命令中输入的数据不显示在命令终端上(实际上,数据是显示的,只是 read 命令将文本颜色设置成与背景相同的颜色)。

shell 复制代码
read -s -p "Enter your password: " pass 
echo 
echo "Your password $pass"

按行读取文件 read line

​ 每次调用 read 命令都会读取 文件中的 "一行 " 文本。当文件没有可读的行时,read 命令将以非零状态退出

​ 最常见的方法是对文件使用cat 命令,将结果通过管道 直接传给含有read 命令的while命令:

shell 复制代码
# cat 命令的输出作为read命令的输入,read读到的值放在line中
cat test.txt | while read line
do
   echo "$line"
done

​ while循环会持续通过read命令处理文件中的行,直到read命令以非零退出状态码退出。

输出/输入重定向

标准文件描述符

​ Linux系统将每个对象当作文件处理。这包括输入和输出进程。Linux用文件描述符(filedescriptor)来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次最多可以有九个文件描述符。出于特殊目的,bash shell保留了前三个文件描述符(0、1和2)。

Linux的标准文件描述符:

文件描述符 缩写 描述
0 STDIN 标准输入(键盘输入)
1 STDOUT 标准输出(显示器输出)
2 STDERR 标准错误(显示器输出)

以上文件描述符的输出输入都可以通过重定向,强制改变。

STDIN

​ STDIN文件描述符代表shell的标准输入 。对终端界面来说,标准输入键盘。shell从STDIN文件描述符对应的键盘获得输入,在用户输入时处理每个字符

​ 在使用输入重定向符号(<)时,Linux会用重定向指定的文件来替换标准输入文件描述符。 它会读取文件并提取数据,就如同它是键盘上键入的。

shell 复制代码
$ cat < testfile.txt

STDOUT

​ STDOUT文件描述符代表shell的标准输出 。在终端界面上,标准输出 就是终端显示器 。shell的所有输出(包括shell中运行的程序和脚本)会被定向到标准输出中,也就是显示器。

​ 默认情况下,大多数bash命令会将输出导向STDOUT文件描述符。不过可以用输出重定向来改变。

shell 复制代码
$ ls -l > test2 	# 输出重定向符号(>)
$ cat test2 
total 20 
-rw-rw-r-- 1 rich rich 53 2014-10-16 11:30 test 
-rw-rw-r-- 1 rich rich 0 2014-10-16 11:32 test2 

​ 通过输出重定向符号 (>),通常会显示到显示器的所有输出会被shell重定向到指定文件。

​ 也可以将数据追加 到某个文件。这可以用 >> 符号来完成。

shell 复制代码
$ who >> test2 
$ cat test2 
total 20 
-rw-rw-r-- 1 rich rich 53 2014-10-16 11:30 test 
-rw-rw-r-- 1 rich rich 0 2014-10-16 11:32 test2 	
rich pts/0 2014-10-17 15:34 (192.168.1.2)	# 追加的内容

​ 当命令生成错误消息时,shell并未将错误消息重定向到输出文件。

shell 复制代码
# 执行错误后,test3文件创建成功了,只是里面是空的
$ ls -al badfile > test3 
ls: cannot access badfile: No such file or directory 
$ cat test3 
$ 

shell对于错误消息的处理是跟普通输出分开的。如果出现了错误信息,这些信息是不会出现在日志文件中的。

STDERR

​ STDERR文件描述符代表shell的标准错误输出 。运行的程序和脚本出错时,生成的错误消息都会发送到STDERR。

​ 默认情况下,STDERR文件描述符会和STDOUT文件描述符指向同样的地方(终端显示)。

重定向错误

只重定向错误

shell 复制代码
# 将错误信息重定向到test4文件中
$ ls -al badfile 2> test4 
$ cat test4 
ls: cannot access badfile: No such file or directory 

​ 可以看到,STDERR 文件描述符被设成2 。可以选择只重定向错误消息,将该文件描述符值放在重定向符号前,注意之间不能有空格(2>)。

​ 用这种方法,shell会只重定向错误消息,而非普通数据。这里是另一个将STDOUT和STDERR消息混杂在同一输出中的例子:

shell 复制代码
$ ls -al test badtest test2 2> test5 
# ls命令的正常STDOUT输出仍然会发送到默认的STDOUT文件描述符,也就是显示器
-rw-rw-r-- 1 rich rich 158 2014-10-16 11:32 test2 

$ cat test5 	# 错误信息被重定向导test5中
ls: cannot access test: No such file or directory 
ls: cannot access badtest: No such file or directory 

重定向错误和数据

​ STDERR 和 STDOUT 分别重定向到不同文件

shell 复制代码
$ ls -al test test2 2> test6 1> test7 
$ cat test6 
ls: cannot access test: No such file or directory 

$ cat test7 
-rw-rw-r-- 1 rich rich 158 2014-10-16 11:32 test2 

​ 也可以将 STDERR 和 STDOUT 的输出重定向到同一个文件 。为此bash shell 提供了特殊的重定向符号 &>

shell 复制代码
$ ls -al test test2 test3 badtest &> test7 
$ cat test7 
ls: cannot access test: No such file or directory 
ls: cannot access badtest: No such file or directory
-rw-rw-r-- 1 rich rich 158 2014-10-16 11:32 test2 
-rw-rw-r-- 1 rich rich 0 2014-10-16 11:33 test3 

当使用 &> 符时,命令生成的所有输出都会发送到同一位置,包括数据和错误。

注意:为了避免错误信息散落 在输出文件中,相较于标准输出,bash shell自动赋予了错误消息更高的优先级

在脚本中-输出重定向

临时重定向

​ 如果有意在脚本中生成错误消息,可以将单独的一行输出重定向到STDERR。你所需要做的是使用输出重定向符来将输出信息重定向到STDERR文件描述符。在重定向到文件描述符时,必须在文件描述符数字之前加一个&:

shell 复制代码
$ cat test8
#!/bin/bash 
echo "This is an error" >&2 	# 把输出重定向STDERR文件描述符(2)
echo "This is normal output" 

# 直接执行看不出什么
$ ./test8 
This is an error 
This is normal output

​ 默认情况下,Linux会将STDERR导向STDOUT。但是,如果在运行脚本时重定向了STDERR,脚本中所有导向STDERR的文本都会被重定向

shell 复制代码
$ ./test8 2> test9 
This is normal output

$ cat test9 
This is an error

永久重定向

​ 如果脚本中有大量数据需要重定向,那重定向每个echo语句就会很烦琐。exec 命令可以定义某段代码的输出重定向。

shell 复制代码
$ cat test11 
#!/bin/bash 
# 这里以下的STDERR重定向到了testerror文件
exec 2>testerror
echo "This is the start of the script" 
echo "now redirecting all output to another location" 
# 这里以下的STDOUT重定向到了testout文件(之前的代码还是正常标准输出)
exec 1>testout 
echo "This output should go to the testout file" 
echo "but this should go to the testerror file" >&2 

$ ./test11 
This is the start of the script 
now redirecting all output to another location 

$ cat testout 
This output should go to the testout file 

$ cat testerror 
but this should go to the testerror file

在脚本中-输入重定向

​ 可以使用与脚本中重定向STDOUT和STDERR相同的方法来将STDIN从键盘重定向到其他位置。exec命令允许你将STDIN重定向到Linux系统上的文件中:exec 0< testfile

shell 复制代码
$ cat testfile
AAA
BBB
CCC

$ cat test.sh
exec 0< testfile
count=1
while read line ; do
 echo "Line #$count: $line"
 count=$[ $count + 1 ]
done

$ ./test.sh
Line #1: AAA
Line #2: BBB
Line #3: CCC

自定义重定向

​ 在脚本中重定向输入和输出时,并不局限于这3个默认的文件描述符。在shell 中最多可以有9个打开的文件描述符其他6个从3~8的文件描述符均可用作输入或输出重定向。可以将这些文件描述符中的任意一个分配给文件,然后在脚本中使用它们。本节将介绍如何在脚本中使用其他文件描述符。

创建 输出文件描述符

​ 可以用exec命令来给输出分配文件描述符。和标准的文件描述符一样,一旦将另一个文件描述符分配给一个文件,这个重定向就会一直有效,直到你重新分配。这里有个在脚本中使用其他文件描述符的简单例子。

shell 复制代码
$ cat test
#!/bin/bash 
# using an alternative file descriptor 
exec 3>testout 
echo "This should display on the monitor" 
echo "and this should be stored in the file" >&3 
echo "Then this should be back on the monitor" 

$ ./test
This should display on the monitor 
Then this should be back on the monitor 
# 重定向到文件描述符3的那行echo语句的输出却进入了testout文件
$ cat testout 
and this should be stored in the file

重定向 文件描述符

​ 现在介绍怎么恢复已重定向的文件描述符 。你可以分配另外一个文件描述符给标准文件描述符,反之亦然。这意味着你可以将STDOUT的原来位置重定向到另一个文件描述符,然后再利用该文件描述符重定向回STDOUT。

​ 强调!在重定向到文件描述符时,必须在文件描述符数字之前加一个 &

shell 复制代码
$ cat test14 
#!/bin/bash 

exec 3>&1 
exec 1>test14out 
echo "This should store in the output file" 
echo "along with this line." 
exec 1>&3 
echo "Now things should be back to normal" 

$ ./test14 
Now things should be back to normal 
$ cat test14out 
This should store in the output file 
along with this line. 

​ 首先,脚本将文件描述符3重定向到文件描述符 1 的当前位置,也就是STDOUT。这意味着任何发送给文件描述符3的输出都将出现在显示器上

​ 第二个exec命令将STDOUT重定向到文件,shell现在会将发送给STDOUT的输出直接重定向到输出文件中。但是,文件描述符3仍然指向STDOUT原来的位置,也就是显示器。如果此时将输出数据发送给文件描述符3,它仍然会出现在显示器上,尽管STDOUT已经被重定向了。

​ 在向STDOUT(现在指向一个文件)发送一些输出之后,脚本将STDOUT重定向到文件描述符 3 的当前位置(现在仍然是显示器)。这意味着现在STDOUT又指向了它原来的位置:显示器。

​ 这个方法可能有点叫人困惑,但这是一种在脚本中临时重定向输出,然后恢复默认输出设置的常用方法。

创建 输入文件描述符

​ 可以用和重定向输出文件描述符同样的办法重定向输入文件描述符。在重定向到文件之前,先将STDIN文件描述符保存到另外一个文件描述符,然后在读取完文件之后再将STDIN恢复到它原来的位置。

shell 复制代码
$ cat test15 
#!/bin/bash 
# redirecting input file descriptors 
exec 6<&0 
exec 0< testfile 
count=1 
while read line 
do 
 echo "Line #$count: $line" 
 count=$[ $count + 1 ] 
done 
exec 0<&6 

read -p "Are you done now? " answer 
case $answer in 
Y|y) echo "Goodbye";; 
N|n) echo "Sorry, this is the end.";; 
esac 

$ ./test15
Line #1: This is the first line. 
Line #2: This is the second line. 
Line #3: This is the third line. 
Are you done now? y 
Goodbye

​ 在这个例子中,文件描述符6用来保存STDIN的位置。然后脚本将STDIN重定向到一个文件。read命令的所有输入都来自重定向后的STDIN(也就是输入文件)。

​ 在读取了所有行之后,脚本会将STDIN重定向到文件描述符6,从而将STDIN恢复到原先的位置。该脚本用了另外一个read命令来测试STDIN是否恢复正常了。这次它会等待键盘的输入。

创建 读写文件描述符

​ 也可以打开单个文件描述符来作为输入和输出 。可以用同一个文件描述符对同一个文件进行读写。

​ 不过用这种方法时,需要特别注意。由于是对同一个文件进行数据读写,shell会维护一个内部指针,指明在文件中的当前位置。任何读或写都会从文件指针上次的位置开始。

shell 复制代码
$ cat test16 
#!/bin/bash 

exec 3<> testfile 
read line <&3 
echo "Read: $line" 	# 此时读取完第一行,内部指针在testfile第二行第一个字符开始
echo "This is a test line" >&3	# 输入会覆盖指针之后的内容

$ cat testfile 		
This is the first line. 
This is the second line. 
This is the third line. 

$ ./test16 
Read: This is the first line. 

$ cat testfile 
This is the first line. 
This is a test line

当脚本向文件中写入数据时,它会从文件指针所处的位置开始。read命令读取了第一行数据,所以它使得文件指针指向了第二行数据的第一个字符。在echo语句将数据输出到文件时,它会将数据放在文件指针的当前位置,覆盖了该位置的已有数据。

关闭 文件描述符

​ 如果创建了新的输入或输出文件描述符,shell会在脚本退出时自动关闭它们。然而在有些情况下,需要在脚本结束前手动关闭文件描述符。要关闭文件描述符,将它重定向到特殊符号 &-

shell 复制代码
# 关闭文件描述符3
exec 3>&-

​ 有个例子来说明当尝试使用已关闭的文件描述符时会怎样:

shell 复制代码
$ cat badtest 
exec 3> test17file 
echo "This is a test line of data" >&3 
exec 3>&- 
echo "This won't work" >&3 

# 使用已关闭的文件描述符会报错
$ ./badtest 
./badtest: 3: Bad file descriptor

​ 在关闭文件描述符后,如果在脚本中打开了同一个输出文件,shell 会用一个新文件来替换已有文件。这意味着如果输出数据,它就会覆盖已有文件。

shell 复制代码
$ cat test17 
#!/bin/bash 

exec 3> test17file 
echo "This is a test line of data" >&3 
exec 3>&- 
cat test17file 
exec 3> test17file 
echo "This'll be bad" >&3 

$ ./test17 
This is a test line of data		# cat test17file 

$ cat test17file 
This'll be bad 

列出打开的文件描述符(lsof)

参数 描述
-p 允许指定进程ID(PID)
-d 允许指定要显示的文件描述符编号

​ 要想知道进程的当前 PID,可以用特殊环境变量 $$(shell会将它设为当前PID)。-a 选项用来对其他两个选项的结果执行布尔AND运算。

shell 复制代码
$ /usr/sbin/lsof -a -p $$ -d 0,1,2,3,4,5,6,7

COMMAND PID  USER FD TYPE DEVICE SIZE NODE NAME
bash 	3344 rich 0u CHR  136,0 	  2    /dev/pts/0
bash 	3344 rich 1u CHR  136,0 	  2    /dev/pts/0
bash 	3344 rich 2u CHR  136,0 	  2    /dev/pts/0
描述
COMMAND 正在运行的命令名的前9个字符
PID 进程的PID
USER 进程属主的登录名
FD 文件描述符号以及访问类型(r代表读,w代表写,u代表读写)
TYPE 文件的类型(CHR代表字符型,BLK代表块型,DIR代表目录,REG代表常规文件)
DEVICE 设备的设备号(主设备号和从设备号)
SIZE 如果有的话,表示文件的大小
NODE 本地文件的节点号
NAME 文件名

特殊文件:/dev/null

​ null文件里什么都没有。shell输出到null文件的任何数据都不会保存。在Linux系统上null文件的标准位置是 /dev/null 。重定向到该位置的任何数据都会被丢掉, 不会显示。

shell 复制代码
$ ls -al > /dev/null 
$ cat /dev/null
$ ls -al badfile test16 2> /dev/null 
-rwxr--r-- 1 rich rich 135 Oct 29 19:57 test16* 

tee 命令

​ 将输出同时发送到显示器和日志文件,不用将输出重定向两次,只要用特殊的tee命令就行。

​ tee命令相当于管道的一个T型接头。它将从STDIN过来的数据同时发往两处。一处是 STDOUT,另一处是tee命令行所指定的文件名。

shell 复制代码
# 由于tee会重定向来自STDIN的数据,可以用它配合管道命令来重定向命令输出
$ date | tee testfile 
Sun Oct 19 18:56:21 EDT 2014 
$ cat testfile 
Sun Oct 19 18:56:21 EDT 2014 

​ 输出出现在了STDOUT中,同时也写入了指定的文件中。默认情况下,tee命令会在每次使用时覆盖输出文件内容。

shell 复制代码
# -a选项,可将数据追加到文件中
$ date | tee -a testfile 

​ 利用这个方法,既能将数据保存在文件中,也能将数据显示在屏幕上:

shell 复制代码
$ cat test22 
#!/bin/bash 
# using the tee command for logging 
tempfile=test22file 
echo "This is the start of the test" | tee $tempfile 
echo "This is the second line of the test" | tee -a $tempfile 
echo "This is the end of the test" | tee -a $tempfile 

$ ./test22 
This is the start of the test 
This is the second line of the test 
This is the end of the test 

$ cat test22file 
This is the start of the test 
This is the second line of the test 
This is the end of the test 

实例

​ 文件重定向常见于脚本需要读入文件和输出文件时。这个样例脚本两件事都做了。它读取.csv 格式的数据文件,输出SQL INSERT语句来将数据插入数据库。

​ shell脚本使用命令行参数指定待读取的.csv文件。.csv格式用于从电子表格中导出数据,所以你可以把数据库数据放入电子表格中,把电子表格保存成.csv格式,读取文件,然后创建INSERT 语句将数据插入MySQL数据库。

shell 复制代码
$ cat members.csv 
Blum,Richard,123 Main St.,Chicago,IL,60601 
Blum,Barbara,123 Main St.,Chicago,IL,60601 
Bresnahan,Christine,456 Oak Ave.,Columbus,OH,43201 
Bresnahan,Timothy,456 Oak Ave.,Columbus,OH,43201 
shell 复制代码
$cat test23 
#!/bin/bash 
outfile='members.sql' 
IFS=',' 
# read 获取输入 < members.csv
while read lname fname address city state zip ; do 
 cat >> $outfile << EOF
 INSERT INTO members (lname,fname,address,city,state,zip) VALUES 
('$lname', '$fname', '$address', '$city', '$state', '$zip'); 
EOF
done < ${1}
#done < $1

done < ${1} 当运行程序test23时,$1代表第一个命令行参数。它指明了待读取数据的文件。read语句会使用IFS字符解析读入的文本,我们在这里将IFS指定为逗号。

​ **cat >> o u t f i l e < < E O F ∗ ∗ 这条语句中包含一个输出追加重定向(双大于号)和一个输入追加重定向(双小于号)。输出重定向将 c a t 命令的输出追加到由 outfile << EOF** 这条语句中包含一个输出追加重定向(双大于号)和一个输入追加重定向(双小于号)。输出重定向将cat命令的输出追加到由 outfile<<EOF∗∗这条语句中包含一个输出追加重定向(双大于号)和一个输入追加重定向(双小于号)。输出重定向将cat命令的输出追加到由outfile变量指定的文件中。cat命令的输入不再取自标准输入,而是被重定向到脚本中存储的数据。EOF符号标记了追加到文件中的数据的起止。

INSERT INTO 语句其中的数据会由变量来替换,变量中内容则是由read语句存入的。所以基本上while循环一次读取一行数据,将这些值放入INSERT语句模板中,然后将结果输出到输出文件中。

shell 复制代码
$ ./test23 members.csv

$ cat members.sql 
 INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('Blum', 
 'Richard', '123 Main St.', 'Chicago', 'IL', '60601'); 
 INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('Blum', 
 'Barbara', '123 Main St.', 'Chicago', 'IL', '60601'); 
 INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('Bresnahan', 
 'Christine', '456 Oak Ave.', 'Columbus', 'OH', '43201'); 
 INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('Bresnahan', 
 'Timothy', '456 Oak Ave.', 'Columbus', 'OH', '43201');

创建临时文件 mktemp

​ Linux系统有特殊的目录,专供临时文件使用。Linux使用/tmp目录来存放不需要永久保留的文件。大多数Linux发行版配置了系统在启动时自动删除/tmp目录的所有文件。系统上的任何用户账户都有权限在读写/tmp目录中的文件。

mktemp 命令可以在/tmp目录中创建一个唯一的临时文件。shell会创建这个文件,但不用默认的umask值。它会将文件的读和写权限分配给文件的属主,并将你设成文件的属主。一旦创建了文件,你就在脚本中有了完整的读写权限,但其他人没法访问它(当然,root用户除外)。

创建本地临时文件

​ 默认情况下,mktemp会在本地目录中创建一个文件。要用mktemp命令在本地目录中创建一个临时文件,只要指定一个文件名模板就行了。模板可以包含任意文本文件名,在文件名末尾加上6个大写的X即可。

​ mktemp命令会用6个字符码替换这6个X,从而保证文件名在目录中是唯一的。你可以创建多个临时文件,它可以保证每个文件都是唯一的。

shell 复制代码
$ mktemp testing.XXXXXX 
testing.1DRLuV 

​ 在脚本中使用mktemp命令时,将文件名保存到变量中,这样就能在后面的脚本中引用了:

shell 复制代码
#!/bin/bash 
tempfile=$(mktemp test19.XXXXXX) 
exec 3>$tempfile 
echo "This script writes to temp file $tempfile" 
echo "This is the first line" >&3

在 /tmp 目录创建临时文件

-t 选项会强制mktemp命令来在系统的临时目录来创建该文件,并返回用来创建临时文件的全路径。

shell 复制代码
$ mktemp -t test.XXXXXX 
/tmp/test.xG3374 

​ 由于mktemp命令返回了绝对路径,可以在Linux系统上的任何目录下引用该临时文件:

shell 复制代码
$ cat test
#!/bin/bash 
# creating a temp file in /tmp 
tempfile=$(mktemp -t tmp.XXXXXX) 
echo "This is a test file." > $tempfile 
echo "This is the second line of the test." >> $tempfile 
echo "The temp file is located at: $tempfile" 
cat $tempfile 
rm -f $tempfile

$ ./test
The temp file is located at: /tmp/tmp.Ma3390 
This is a test file. 
This is the second line of the test. 

创建临时目录

-d 选项告诉mktemp命令来创建一个临时目录而不是临时文件。

shell 复制代码
$ cat test21 
#!/bin/bash 
# using a temporary directory 
tempdir=$(mktemp -d dir.XXXXXX) 
cd $tempdir 
tempfile1=$(mktemp temp.XXXXXX) 
tempfile2=$(mktemp temp.XXXXXX) 
exec 7> $tempfile1 
exec 8> $tempfile2 

控制脚本

处理信号(类似捕获异常)

Linux 信号

​ Linux系统和应用程序可以生成超过30个信号,Linux编程时会遇到的最常见的Linux系统信号(signals)如下:

信号 描述
1 SIGHUP 挂起进程
2 SIGINT 终止进程
3 SIGQUIT 停止进程
9 SIGKILL 无条件终止进程
15 SIGTERM 尽可能终止进程
17 SIGSTOP 无条件停止进程,但不是终止进程
18 SIGTSTP 停止或暂停进程,但不终止进程
19 SIGCONT 继续运行停止的进程

​ 默认情况下,bash shell会忽略收到的任何SIGQUIT (3)和SIGTERM (5)信号(正因为这样,交互式shell才不会被意外终止)。但是bash shell会处理收到的SIGHUP (1)和SIGINT (2)信号。

​ 如果bash shell收到了SIGHUP信号,比如当你要离开一个交互式shell,它就会退出。但在退出之前,它会将SIGHUP信号传给所有由该shell所启动的进程(包括正在运行的shell脚本)。

​ 通过SIGINT信号,可以中断shell。Linux内核会停止为shell分配CPU处理时间。这种情况发 生时,shell会将SIGINT信号传给所有由它所启动的进程,以此告知出现的状况。

​ shell会将这些信号传给shell脚本程序来处理。而shell脚本的默认行为是忽略这些信号。它们可能会不利于脚本的运行。要避免这种情况,可以脚本中加入识别信号的代码,并执行命令来处理信号。

生成信号

​ bash shell允许用键盘上的组合键生成两种基本的Linux信号。这个特性在需要停止或暂停失控程序时非常方便。

中断进程
Ctrl+C 组合键会生成SIGINT信号,并将其发送给当前在shell中运行的所有进程。

shell 复制代码
# 在超时前按下Ctrl+C组合键,就可以提前终止sleep命令
$ sleep 100
^C 
$ 

暂停进程
Ctrl+Z 组合键会生成一个SIGTSTP信号,停止shell中运行的任何进程。

停止(stopping)进程跟终止(terminating)进程不同:停止进程会让程序继续保留在内存中,并能从上次停止的位置 继续运行。

sh 复制代码
# 停止sleep命令
$ sleep 100
^Z 
[1]+ Stopped sleep 100 
$ 

[1] 方括号中的数字是shell分配的作业号 (job number)。shell将shell中运行的每个进程称为作业,并为每个作业分配唯一的作业号。它会给第一个作业分配作业号1,第二个作业号2,以此类推。

​ 如果shell会话中有一个已停止的作业,在退出shell时,bash会提醒:

shell 复制代码
$ sleep 100
^Z 
[1]+ Stopped sleep 100 
$ exit 
exit 
There are stopped jobs. 

# 可以用ps命令来查看已停止的作业
$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 
0 S 501 2431 2430 0 80 0 - 27118 wait pts/0 00:00:00 bash 
0 T 501 2456 2431 0 80 0 - 25227 signal pts/0 00:00:00 sleep 
0 R 501 2458 2431 0 80 0 - 27034 - pts/0 00:00:00 ps 
# 在S列中(进程状态),ps命令将已停止作业的状态为显示为T。这说明命令要么被跟踪,要么被停止了

​ 如果在有已停止作业存在的情况下,要退出 shell 只要再输入一遍exit命令就行了,shell会退出并终止已停止作业。或者利用kill命令终止已停止作业(PID)。

捕获信号

trap命令允许指定 shell 脚本要监看并从shell中拦截的Linux信号。如果脚本收到了trap命令中列出的信号,该信号不再 由shell处理,而是交由本地处理。

shell 复制代码
trap commands signals

​ 在trap命令行上,只需列出想要shell执行的命令,以及一组用空格分开的待捕 获的信号。可以用数值或Linux信号名来指定信号。

​ 下面例子,展示了如何使用trap命令来忽略 SIGINT 信号,并控制脚本的行为。

shell 复制代码
$ cat test1.sh
#!/bin/bash 
# 每次 Ctrl-C(SIGINT 终止进程时)的时候,提示这句话 
trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT 
# 
echo This is a test script 
# 
count=1 
while [ $count -le 5 ] 
do 
 echo "Loop #$count" 
 sleep 1 
 count=$[ $count + 1 ] 
done 
# 
echo "This is the end of the test script" 

​ 本例中用到的trap命令会在每次检测到SIGINT信号时显示一行简单的文本消息。捕获这些信号会阻止用户用bash shell组合键Ctrl+C来停止程序。

shell 复制代码
$ ./test1.sh
This is a test script 
Loop #1 
Loop #2 
Loop #3 
^C Sorry! I have trapped Ctrl-C 
Loop #4 
^C Sorry! I have trapped Ctrl-C 
Loop #5  
This is the end of the test script 

​ 每次使用Ctrl+C组合键,脚本都会执行trap命令中指定的echo语句,而不是处理该信号停止该脚本。

捕获脚本退出

​ 除了在shell脚本中捕获信号,也可以在shell脚本退出时进行捕获。这是在shell完成任务时执行命令的一种简便方法。 要捕获shell脚本的退出,只要在trap命令后加上EXIT信号就行。

shell 复制代码
$ cat test2.sh
#!/bin/bash 
trap "echo Goodbye..." EXIT 
count=1 
while [ $count -le 5 ] 
do 
 echo "Loop #$count" 
 sleep 1 
 count=$[ $count + 1 ] 
done 

$ ./test2.sh
Loop #1 
Loop #2 
Loop #3 
Loop #4 
Loop #5 
Goodbye... 
# 当脚本运行到正常的退出位置时,捕获就被触发了,shell会执行在trap命令行指定的命令。如果提前退出脚本,同样能够捕获到EXIT
$ ./test2.sh
Loop #1 
Loop #2 
Loop #3 
^CGoodbye... 

​ 因为SIGINT信号并没有出现在trap命令的捕获列表中,当按下Ctrl+C组合键发送SIGINT信号时,脚本就退出了。但在脚本退出前捕获到了EXIT,于是shell执行了trap命令。

修改或移除捕获

​ 要想在脚本中的不同位置进行不同的捕获处理,只需重新使用带有新选项的trap命令。

shell 复制代码
trap "echo ' Sorry... Ctrl-C is trapped.'" SIGINT 
...
# 修改,只需要再次调用就行
trap "echo ' I modified the trap!'" SIGINT
...

删除捕获设置

​ 删除已设置好的捕获。只需要在trap命令与希望恢复默认行为的信号列表之间加上两个破折号就行了。

shell 复制代码
trap -- SIGINT 

移除信号捕获后,脚本按照默认行为来处理SIGINT信号。

后台运行脚本

​ 只要在执行命令后加个 & 符就行:

shell 复制代码
$ ./xxx.sh &
[1] 3231  	

​ 回显的内容中,方括号中的数字是shell分配给后台进程的作业号。下一个数是Linux系统分配给进程的进程 ID(PID)。Linux系统上运行的每个进程都必须有一个唯一的 PID。

​ 注意,当后台进程运行时,它仍然会使用终端显示器来显示STDOUT和STDERR消息。最好是将后台运行的脚本的STDOUT和STDERR进行重定向,避免杂乱的输出。

运行多个后台作业

shell 复制代码
$ ./test6.sh &
[1] 3568 
$ This is Test Script #1 
$ ./test7.sh &
[2] 3570 
$ This is Test Script #2 
$ ./test8.sh &
[3] 3573 
$ And...another Test script 

​ 每次启动新作业时,Linux系统都会为其分配一个新的作业号和PID。

​ 通过ps命令,可以看到 所有脚本处于运行状态。

shell 复制代码
$ ps
 PID TTY TIME CMD 
 2431 pts/0 00:00:00 bash 
 3568 pts/0 00:00:00 test6.sh 
 3570 pts/0 00:00:00 test7.sh 
 3573 pts/0 00:00:00 test8.sh 
 3574 pts/0 00:00:00 sleep 
 3575 pts/0 00:00:00 sleep 
 3577 pts/0 00:00:00 sleep 
 3578 pts/0 00:00:00 sleep 
 3579 pts/0 00:00:00 ps

注意:

在终端会话中使用后台进程时,每一个后台进程都和终端会话(pts/0)终端联系在一起。如果终端会话退出,那么后台进程也会随之退出。

在非控制台下运行脚本

​ 如果希望运行在后台模式的脚本在登出控制台后能够继续运行到结束,即使退出了终端会话。这可以用nohup 命令来实现。

​ nohup 命令运行了另外一个命令来阻断所有发送给该进程的SIGHUP信号。这会在退出终端会话时阻止进程退出。

shell 复制代码
$ nohup ./test1.sh &
[1] 3856 
$ nohup: ignoring input and appending output to 'nohup.out'

​ 由于nohup命令会解除终端与进程的关联,进程也就不再同STDOUT和STDERR联系在一起。为了保存该命令产生的输出,nohup命令会自动将STDOUT和STDERR的消息重定向到一个名为 nohup.out 的文件中。

​ nohup.out 文件包含了通常会发送到终端显示器上的所有输出。

说明:

如果使用nohup运行了另一个命令,该命令的输出会被追加到已有的nohup.out文件中。当运行位于同一个目录中的多个命令时一定要当心,因为所有的输出都会被发送到同一个nohup.out文件中。

作业控制

​ 启动、停止、终止以及恢复作业的这些功能统称为作业控制。通过作业控制,就能完全控制shell环境中所有进程的运行方式了。

查看作业

​ jobs 命令可以查看shell当前正在处理的作业。

shell 复制代码
$ cat test10.sh
#!/bin/bash 
echo "Script Process ID: $$" 
count=1 
while [ $count -le 10 ] 
do 
 echo "Loop #$count" 
 sleep 10 
 count=$[ $count + 1 ] 
done 
echo "End of script..." 

​ 脚本用**$$**变量来显示Linux系统分配给该脚本的PID,然后进入循环,每次迭代都休眠10秒。可以从命令行中启动脚本,然后使用Ctrl+Z组合键来停止脚本。

shell 复制代码
$ ./test10.sh
Script Process ID: 1897 
Loop #1 
Loop #2 
^Z 
[1]+ Stopped ./test10.sh

# 然后利用 & 将另外一个作业作为后台进程启动
$ ./test10.sh > test10.out &
[2] 1917 

​ jobs命令会显示这两个已停止/运行中的作业,以及它们的作业号和作业中使用的命令。

shell 复制代码
$ jobs -l
[1]+ 1897 Stopped ./test10.sh 
[2]- 1917 Running ./test10.sh > test10.out &
参数 描述
-l 列出进程的PID以及作业号
-n 只列出上次shell发出的通知后改变了状态的作业
-p 只列出作业的PID
-r 只列出运行中的作业
-s 只列出已停止的作业

​ 带加号 的作业会被当做默认作业。在使用作业控制命令时,如果未在命令行指定任何作业号,该作业会被当成作业控制命令的操作对象。

​ 当前的默认作业完成处理后,带减号 的作业成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业,不管shell中有多少个正在运行的作业。

重启作业

​ 在bash作业控制中,可以将已停止的作业作为后台进程或前台进程重启。

shell 复制代码
# 以后台模式重启作业
$ bg [作业号]

# 以前台模式重启作业
$ fg [作业号]

调度优先级

​ 在多任务操作系统中(Linux就是),内核负责将CPU时间分配给系统上运行的每个进程。调度优先级(scheduling priority)是内核分配给进程的CPU时间(相对于其他进程)。在Linux系统 中,由shell启动的所有进程的调度优先级默认都是相同的。

​ 调度优先级是个整数值,从 -20(最高优先级)到 +19(最低优先级)。默认情况下,bash shell 以优先级0来启动所有进程。

nice 命令

​ 可以设置命令启动时的调度优先级。注意!必须将nice命令和要启动的命令放在同一行中。

shell 复制代码
$ nice -n 10 ./test4.sh > test4.out &
[1] 4973 

# 查看进程信息(ni:优先级)
$ ps -p 4973 -o pid,ppid,ni,cmd
 PID PPID NI CMD 
 4973 4721 10 /bin/bash ./test4.sh 

注意:

只能通过nice降低进程的优先级,root 用户或 sudo权限不受限制;

renice 命令

​ 可以改变系统上已运行命令的优先级(指定运行进程的PID)。

shell 复制代码
$ ./test11.sh &
[1] 5055 

$ ps -p 5055 -o pid,ppid,ni,cmd
 PID PPID NI CMD 
 5055 4721 0 /bin/bash ./test11.sh 

$ renice -n 10 -p 5055
5055: old priority 0, new priority 10 

$ ps -p 5055 -o pid,ppid,ni,cmd
 PID PPID NI CMD 
 5055 4721 10 /bin/bash ./test11.sh

注意:

renice 只能对属于你的进程执行;

只能通过renice降低进程的优先级,root 用户或sudo权限不受限制;

定时执行

at :在预设时间执行

crontab :定期执行执行(错过时间点的任务不执行)

anacron:错过的任务也能执行

函数 function

创建

shell 复制代码
# 第一种
function name { 
 commands 
} 

# 第二种
name() { 
 commands 
} 

调用

shell 复制代码
$ cat test1
#!/bin/bash 

function func1 { 
 echo "This is an example of a function" 
}

# 调用
func1

返回值

​ bash shell会把函数当作一个小型脚本,运行结束时会返回一个退出状态码。有3种不同的方法来为函数生成退出状态码。

默认退出状态码

​ 默认情况下,函数的退出状态码是函数中最后一条命令返回的退出状态码 。在函数执行结束后,可以用标准变量 $? 来确定函数的退出状态码。

shell 复制代码
$ cat test4 
#!/bin/bash 

func1() { 
 echo "trying to display a non-existent file" 
 ls -l badfile 
} 
func1 
echo "The exit status is: $?" 

$ ./test4
trying to display a non-existent file 
ls: badfile: No such file or directory 
The exit status is: 1
# 函数的退出状态码是1,这是因为函数中的最后一条命令(ls)没有成功运行。但无法知道函数中其他命令中是否成功运行。

# 看下面的例子
$ cat test4b 
#!/bin/bash 

func1() { 
 ls -l badfile 
 echo "This was a test of a bad command" 
} 
func1 
echo "The exit status is: $?" 

$ ./test4b 
ls: badfile: No such file or directory 
This was a test of a bad command 
The exit status is: 0
# 由于函数最后一条语句echo运行成功,该函数的退出状态码就是0,尽管其中有一条命令并没有正常运行。所以使用函数的默认退出状态码是存在问题的。

return 命令

​ return命令:退出函数返回特定的退出状态码。return命令允许指定一个整数值来定义函数的退出状态码。

shell 复制代码
$ cat test5 
#!/bin/bash 
function dbl { 
 echo "doubling the value" 
 return 66 
} 
dbl 
echo "The new value is $?" 

​ 函数一结束就取返回值;如果在用 ? 变量提取函数返回值之前执行了其他命令,函数的返回值就会丢失。( ?变量提取函数返回值之前执行了其他命令,函数的返回值就会丢失。( ?变量提取函数返回值之前执行了其他命令,函数的返回值就会丢失。(? 变量会返回执行的最后一条命令的退出状态码)

退出状态码必须是0~255;任何大于256的值都会产生一个错误值。

接收返回值

​ 正如可以将命令的输出保存到shell变量中一样,也可以对函数的输出采用同样的处理办法。

sheLl 复制代码
# 将fun01函数的输出(返回值)赋给$result变量
result=$(fun01)

在函数中使用变量

函数传参

​ 函数可以使用标准的参数环境变量来表示命令行上传给函数的参数。例如,函数名会在 $0 变量中定义,函数命令行上的任何参数都会通过$1、2等定义。也可以用特殊变量#来判断传给函数的参数数目。然后函数可以用参数环境变量 来获得参数值。

​ 脚本中指定函数时,必须将参数和函数放在同一行,格式如下:

​ fun1 $value1 $value2 n1 n2 ...

shell 复制代码
#!/bin/bash
# 函数传参
function addem {
  echo $[ $1 + $2 ]
}
echo -n "Adding: "
value=$(addem 10 15)
echo $value

# 执行
$./test.py
Adding: 25

​ 由于函数使用特殊参数环境变量作为自己的参数值,因此它无法直接获取脚本在命令行中的参数值。

shell 复制代码
function addem { 
 echo $[ $1 + $2 ] 
} 
echo -n "Adding : "
value=$(addem) 
echo $value

$ ./test 10 15 
Adding :
./test.py: line 3: +  : syntax error: operand expected (error token is "+  ")

​ 尽管函数也使用了$1和$2变量,但它们和脚本主体中的$1和$2变量并不相同。要在函数中使用这些值,必须在调用函数时手动将它们传过去。

shell 复制代码
function addem { 
 echo $[ $1 + $2 ] 
} 
echo -n "Adding : "
value=$(addem $1 $2)	# 通过将$1和$2变量传给函数,它们就能跟其他变量一样供函数使用了
echo $value

$ ./test 10 15 
Adding : 25

全局变量

​ 全局变量在shell脚本中任何地方都有效。默认情况下,在脚本中定义的任何变量都是全局变量。在函数外定义的变量可在函数内正常访问。

shell 复制代码
function func1 {
 temp=$[ $temp + 10 ]
}
temp=4
echo "old_temp:$temp"
func1
echo "new_temp:$temp"

# 执行后
old_temp:4
new_temp:14

​ A变量在函数外定义并被赋值。当函数被调用时,该变量及其值在函数中都依然有效。如果变量在函数内被改变了,那么在函数外再引用该变量时,A变量是改变后的值。

​ 这样可能存在一些问题,尤其是如果想在不同的shell脚本中使用函数的话。

局部变量

​ 声明局部变量:local temp

local 关键字保证了变量只局限在该函数中。如果脚本中在该函数之外有同样名字的变量,那么shell将会保持这两个变量互不影响。

shell 复制代码
function func1 {
 local temp=$[ $temp + 10 ]	# local 声明局部变量
}
temp=4
echo "old_temp:$temp"
func1
echo "new_temp:$temp"

# 执行后
old_temp:4
new_temp:4

​ 现在,在func1函数中使用 t e m p 变量时,并不会影响在脚本主体中赋给 temp变量时,并不会影响在脚本主体中赋给 temp变量时,并不会影响在脚本主体中赋给temp变量的值。

数组参数

​ 将数组变量当作单个参数传递的话,函数只会取数组变量的第一个值。

shell 复制代码
$ cat badtest3 
#!/bin/bash
function testit { 
 echo "The parameters are: $@" 
} 
 
myarray=(1 2 3 4 5) 
echo "The original array is: ${myarray[*]}" 
testit $myarray 

$ ./badtest3 
The original array is: 1 2 3 4 5 
The parameters are: 1 

​ 要解决这个问题,必须将数组变量的值分解成单个的值,然后将这些值作为函数参数使用。

shell 复制代码
$ cat test10 
#!/bin/bash 

function testit {
 echo "The parameters are: $@"
 local newarray=$@ 
 echo "The new array value is: ${newarray[*]}" 
} 	
myarray=(1 2 3 4 5) 
echo "The original array is ${myarray[*]}" 
testit ${myarray[*]} 

$ ./test10 
The original array is 1 2 3 4 5
The parameters are: 1 2 3 4 5
The new array value is: 1 2 3 4 5
shell 复制代码
# 案例:数组求和
$ cat test11
function addarray { 
 local sum=0 
 local newarray=$@
 for value in ${newarray[*]} ; do 
  sum=$[ $sum + $value ] 
 done 
 echo $sum 
} 
myarray=(1 2 3 4 5) 
echo "The original array is: ${myarray[*]}" 
arg1=$(echo ${myarray[*]}) 
result=$(addarray $arg1) 
echo "The result is $result" 

$ ./test11 
The original array is: 1 2 3 4 5 
The result is 15 

递归

​ 函数可以调用自己来得到结果。通常递归函数都有一个最终可以迭代到的基准值。

​ 递归算法的经典例子是计算阶乘:x! = x * (x-1)!

shell 复制代码
$ cat test13 
#!/bin/bash 
function factorial { 
 if [ $1 -eq 1 ] ; then 
  echo 1 
 else 
  local temp=$[ $1 - 1 ] 
  local result=$(factorial $temp) 
  echo $[ $result * $1 ] 
 fi 
} 
read -p "Enter value: " value 
result=$(factorial $value) 
echo "The factorial of $value is: $result" 

$ ./test13 
Enter value: 5 
The factorial of 5 is: 120 

创建函数库(类似模块)

​ 创建函数库文件,包含一些函数,然后在其他脚本中通过source命令引用该库文件。

shell 复制代码
$ cat myfuncs 
# my script functions 
function addem { 
 echo $[ $1 + $2 ] 
} 

source 命令会在当前shell上下文中执行命令,而不是创建一个新shell。可以用source命令来在shell脚本中运行库文件脚本。

​ source命令有个快捷的别名,称作点操作符:. ./myfuncs

shell 复制代码
$ cat test14 
#!/bin/bash 

# 使用相应路径访问该文件
. ./myfuncs

value1=10 
value2=5 
result1=$(addem $value1 $value2) 
echo "The result of adding them is: $result1" 

$ ./test14 
The result of adding them is: 15 

在命令行上使用函数

在命令行上创建函数

​ 因为shell会解释用户输入的命令,所以可以在命令行上直接定义一个函数:

shell 复制代码
# 单行方式定义函数
$ function divem { echo $[ $1 / $2 ]; } 
$ divem 100 5 
20

# 多行方式来定义函数
$ function multem { 
> echo $[ $1 * $2 ] 
> } 
$ multem 2 5 
10 

在命令行上创建函数时。如果给函数起了个跟内建命令或另一个命令相同的名字,函数将会覆盖原来的命令。

在.bashrc 文件中定义函数

​ 在命令行上直接定义shell函数的明显缺点是退出shell时,函数就消失了。

​ 持久生效的话,将函数定义在主目录下的 .bashrc 文件中,这个文件在每次启动一个新shell的时候,都会由shell重新载入。

shell 复制代码
$ cat .bashrc 
# .bashrc 
# Source global definitions 
if [ -r /etc/bashrc ]; then 
 . /etc/bashrc 
fi 
# 直接定义函数
function addem { 
 echo $[ $1 + $2 ] 
}
# 读取函数文件
. /home/xxx/myfuncs

该文件中的函数会在下次启动新bash shell时生效。随后就能在系统上任意地方使用这个函数了。

shell还会将定义好的函数传给子shell进程;所以任何shell脚本中都能使用,甚至都不用对库文件使用source。

GNU shtool 脚本函数库

​ GNU shtool shell脚本函数库提供了一些简单的shell脚本函数,可以用来完成日常的shell功能,例如处理临时文件和目录或者格式化输出显示。

​ 使用教程:https://blog.csdn.net/Qiu_SaMa/article/details/120607394

shell 复制代码
$ tar -zxvf shtool-2.0.8.tar.gz
$ ./confifgure 
$ make 
# 使用
# shtool [options] [function [options] [args]] 

图形化脚本编程

创建菜单布局

​ 可以用这个模板创建任何shell脚本菜单界面。它提供了一种跟用户交互的简单途径。

echo 命令中包含制表符和换行符,必须用 -e 选项;

-en 选项会去掉末尾的换行符;;

read -n 选项来限制只读取一个字符;

shell 复制代码
$ cat menu1 
#!/bin/bash 

function diskspace { 
 clear	# 清屏
 df -k 
} 
function whoseon { 
 clear 
 who 
} 
function memusage { 
 clear 
 cat /proc/meminfo 
} 

# 菜单展示
function menu { 
 clear 
 echo 
 echo -e "\t\t\tSys Admin Menu\n" 
 echo -e "\t1. Display disk space"
 echo -e "\t2. Display logged on users" 
 echo -e "\t3. Display memory usage" 
 echo -e "\t0. Exit program\n\n" 
 echo -en "\t\tEnter option: " 
 read -n 1 option 
} 

# 菜单逻辑
while [ 1 ] 
do 
 menu 
 case $option in 
 0) 
  break ;; 
 1) 
  diskspace ;; 
 2) 
  whoseon ;; 
 3) 
  memusage ;; 
 *) 
  clear 
  echo "Sorry, wrong selection";; 
 esac 
 echo -en "\n\n\t\t\tHit any key to continue" 
 read -n 1 line 
done 
clear 
shell 复制代码
                        Sys Admin Menu

        1. Display disk space
        2. Display logged on users
        3. Display memory usage
        0. Exit program


                Enter option:

select 工具

​ 创建文本菜单的一半工夫都花在了建立菜单布局和获取用户输入。select 命令只需要一条命令就可以创建出菜单,然后获取输入的答案并自动处理。

​ list 参数是由空格分隔的文本选项列表,这些列表构成了整个菜单。select命令会将每个列表项显示成一个带编号的选项,然后为选项显示一个由PS3环境变量定义的特殊提示符。

PS3:定制shell脚本的select提示,默认提示 #?

shell 复制代码
$ cat smenu1
#!/bin/bash 

function diskspace { 
 clear 
 df -k 
} 
function whoseon { 
 clear 
 who 
} 
function memusage { 
 clear 
 cat /proc/meminfo 
} 

PS3="Enter option: " 
select option in "Display disk space" "Display logged on users" "Display memory usage" "Exit program" 
do 
 case $option in 
 "Exit program") 
  break ;; 
 "Display disk space") 
  diskspace ;; 
 "Display logged on users") 
  whoseon ;; 
 "Display memory usage") 
  memusage ;; 
 *) 
 clear 
 echo "Sorry, wrong selection";; 
 esac 
done 
clear

$ ./smenu1 
1) Display disk space 3) Display memory usage 
2) Display logged on users 4) Exit program 
Enter option: 

​ 在使用select命令时,记住,存储在变量中的结果值是整个文本字符串而不是跟菜单选项相关联的数字。文本字符串值才是你要在case语句中进行比较的内容。

制作窗口 dialog

dialog 能够用ANSI转义控制字符在文本环境中创建标准的窗口对话框。dialog 命令使用命令行参数来决定生成哪种窗口部件(widget)。部件是dialog包中窗口元素类型的术语。

dialog包支持的标准部件:

部 件 描 述
calendar 提供选择日期的日历
checklist 显示多个选项(其中每个选项都能打开或关闭)
form 构建一个带有标签以及文本字段(可以填写内容)的表单
fselect 提供一个文件选择窗口来浏览选择文件
gauge 显示完成的百分比进度条
infobox 显示一条消息,但不用等待回应
inputbox 提供一个输入文本用的文本表单
inputmenu 提供一个可编辑的菜单
menu 显示可选择的一系列选项,返回选项号
msgbox 显示一条消息,并要求用户选择OK按钮
pause 显示一个进度条来显示暂定期间的状态
passwordbox 显示一个文本框,但会隐藏输入的文本
passwordform 显示一个带标签和隐藏文本字段的表单
radiolist 提供一组菜单选项,但只能选择其中一个
tailbox 用tail命令在滚动窗口中显示文件的内容
tailboxbg 跟tailbox一样,但是在后台模式中运行
textbox 在滚动窗口中显示文件的内容
timebox 提供一个选择小时、分钟和秒数的窗口
yesno 提供一条带有Yes和No按钮的简单消息

要在命令行上指定某个特定的部件,需使用双破折线格式:

shell 复制代码
dialog --widget parameters
# widget:部件名
# parameters:部件窗口的大小以及部件需要的文本

每个dialog部件都提供了两种形式的输出:

1、STDERR

2、退出状态码

​ 可以通过dialog命令的退出状态码来确定用户选择的按钮。可以用标准的 $? 变量来确定dialog部件中具体选择了哪个按钮。

​ 如果部件返回了数据,比如菜单选择,那么dialog命令会将数据发送到STDERR。可以用标准的bash shell方法来将STDERR输出重定向到另一个文件或文件描述符中。

shell 复制代码
# 将文本框中输入的文本重定向到age.txt文件中
$ dialog --inputbox "Enter your age:" 10 20 2>age.txt 

部件案例

1、msgbox部件

​ msgbox 部件是对话框中最常见的类型。它会在窗口中显示一条消息,直到用户单击OK按钮后才消失。

shell 复制代码
$ dialog --title Testing --msgbox "This is a test" 10 20 

2、yesno部件

​ yesno 允许用户选择 yes 或 no。 它会在窗口底部生成两个按钮:一个是Yes,一个是No。

​ dialog命令的退出状态码会根据用户选择的按钮来设置。如果用户选择了No按钮,退出状态码是1;如果选择了Yes按钮,退出状态码就是0。

shell 复制代码
$ dialog --title "Please answer" --yesno "Is this thing on?" 10 20 

3、inputbox部件

​ inputbox部件为用户提供了一个简单的文本框区域来输入文本字符串。dialog命令会将文本字符串的值发给STDERR。必须重定向STDERR来获取用户输入。

​ inputbox提供了两个按钮:OK和Cancel。如果选择了OK按钮,命令的退出状态码就是0;反之,退出状态码就会是1。

shell 复制代码
[user@hostname]$ dialog --inputbox "Enter your age:" 10 20 2>age.txt 
[user@hostname]$ echo $? 
0 
[user@hostname]$ cat age.txt
12[user@hostname]$

​ 在使用cat命令显示文本文件的内容时,该值后面并没有换行符。这有助于将文件内容重定向到shell脚本中的变量里,以提取用户输入的字符串

4、textbox部件

​ textbox 部件是在窗口中显示大量信息的极佳办法。它会生成一个滚动窗口来显示由参数所指定的文件中的文本。

shell 复制代码
# 可以用方向键来左右或上下滚动/etc/passwd的内容
$ dialog --textbox /etc/passwd 15 45

5、menu部件

​ menu 部件可以创建文本菜单 。只要为每个选项提供一个选择标号和文本就行了。dialog命令会将选定的菜单标号发送到STDERR,可以根据需要重定向STDERR。

shell 复制代码
$ dialog --menu "Sys Admin Menu" 20 30 4 1 "Display disk space" 2 "Display users" 3 "Display memory usage" 4 "Exit" 2> test.txt

# 参数"30"后的参数"4"定义了在窗口中一次显示的菜单项总数为4条

6、fselect部件

​ 可以用fselect部件来浏览文件的位置并选择文件。

shell 复制代码
$ dialog --title "Select a file" --fselect $HOME/ 10 50 2>file.txt

​ fselect 选项后的第一个参数是窗口中使用的起始目录位置。fselect部件窗口由左侧的目录列表、右侧的文件列表(显示了选定目录下的所有文件)和含有当前选定的文件或目录的简单文本框组成。可以手动在文本框键入文件名,或者用目录和文件列表来选定(使用空格键选择文件,将其加入文本框中)。

dialog 选项

​ dialog 选项可以重写对话窗口中的任意按钮标签。该特性允许创建任何需要的窗口。

​ 除了标准部件,还可以在dialog命令中定制很多不同的选项。如 -title 可以设置窗口标题。另外还有许多其他的选项可以全面定制窗口外观和操作。dialog命令中可用的选项,见下表:

选项 描述
--add-widget 继续下个对话框,直到按下Esc或Cancel按钮
--aspect ratio 指定窗口宽度和高度的宽高比
--backtitle title 指定显示在屏幕顶部背景上的标题
--begin x y 指定窗口左上角的起始位置
--cancel-label label 指定Cancel按钮的替代标签
--clear 用默认的对话背景色来清空屏幕内容
--colors 在对话文本中嵌入ANSI色彩编码
--cr-wrap 在对话文本中允许使用换行符并强制换行
--create-rc file 将示例配置文件的内容复制到指定的file文件中①
--defaultno 将yes/no对话框的默认答案设为No
--default-item string 设定复选列表、表单或菜单对话中的默认项
--exit-label label 指定Exit按钮的替代标签
--extra-button 在OK按钮和Cancel按钮之间显示一个额外按钮
--extra-label label 指定额外按钮的替代标签
--help 显示dialog命令的帮助信息
--help-button 在OK按钮和Cancel按钮后显示一个Help按钮
--help-label label 指定Help按钮的替代标签
--help-status 当选定Help按钮后,在帮助信息后写入多选列表、单选列表或表单信息
--ignore 忽略dialog不能识别的选项
--input-fd fd 指定STDIN之外的另一个文件描述符
--insecure 在password部件中键入内容时显示星号
--item-help 为多选列表、单选列表或菜单中的每个标号在屏幕的底部添加一个帮助栏
--keep-window 不要清除屏幕上显示过的部件
--max-input size 指定输入的最大字符串长度。默认为2048
--nocancel 隐藏Cancel按钮
--no-collapse 不要将对话文本中的制表符转换成空格
--no-kill 将tailboxbg对话放到后台,并禁止该进程的SIGHUP信号
--no-label label 为No按钮指定替代标签
--no-shadow 不要显示对话窗口的阴影效果
--ok-label label 指定OK按钮的替代标签
--output-fd fd 指定除STDERR之外的另一个输出文件描述符
--print-maxsize 将对话窗口的最大尺寸打印到输出中
--print-size 将每个对话窗口的大小打印到输出中
--print-version 将dialog的版本号打印到输出中
--separate-output 一次一行地输出checklist部件的结果,不使用引号
--separator string 指定用于分隔部件输出的字符串
--separate-widget string 指定用于分隔部件输出的字符串
--shadow 在每个窗口的右下角绘制阴影
--single-quoted 需要时对多选列表的输出采用单引号
--sleep sec 在处理完对话窗口之后延迟指定的秒数
--stderr 将输出发送到STDERR(默认行为)
--stdout 将输出发送到STDOUT
--tab-correct 将制表符转换成空格
--tab-len n 指定一个制表符占用的空格数(默认为8)
--timeout sec 指定无用户输入时,sec秒后退出并返回错误代码
--title title 指定对话窗口的标题
--trim 从对话文本中删除前导空格和换行符
--visit-items 修改对话窗口中制表符的停留位置,使其包括选项列表
--yes-label label 为Yes按钮指定替代标签

--create-rc:dialog命令支持运行时配置。该命令会根据配置文件模板创建一份配置文件。dialog启动时会先去检查是否设置了 DIALOGRC 环境变量,该变量会保存配置文件名信息。如果未设置该变量或未找到该文件,它会将 $HOME/.dialogrc作为配置文件。如果这个文件还不存在的话,就尝试查找编译时指定的GLOBALRC文件,也就 是/etc/dialogrc。如果这个文件也不存在的话,就用编译时的默认值。

--backtitle:是为脚本中的菜单创建公共标题的简便办法。如果你为每个对话窗口都指定了该选项,那么它在你的应用中就会保持一致。

在脚本中应用

要在脚本中是使用dialog,需要遵循两个规则:

1、如果有Cancel或No按钮,检查dialog命令的退出状态码;

2、重定向STDERR来获得输出值;

使用dialog部件来生成系统管理菜单:

shell 复制代码
#!/bin/bash 

# 创建临时文件
temp=$(mktemp -t test.XXXXXX) 	# 保存df和meminfo命令的输出,用于textbox部件显示
temp2=$(mktemp -t test2.XXXXXX)	# 保存菜单选项号

function diskspace { 
 df -k > $temp 
 dialog --textbox $temp 20 60 
} 
function whoseon { 
 who > $temp 
 dialog --textbox $temp 20 50 
} 
function memusage { 
 cat /proc/meminfo > $temp 
 dialog --textbox $temp 20 50 
}

# 执行完每个函数之后,脚本都会返回继续显示菜单
# 只有选择"0"或者选择"Cancel"按钮才会退出循环
while [ 1 ] 
do 
dialog --menu "Sys Admin Menu" 20 30 10 1 "Display disk space" 2 "Display users" 3 "Display memory usage" 0 "Exit" 2> $temp2
# 检查dialog命令的退出状态码,选择Cancel按钮(1)时直接退出
if [ $? -eq 1 ] 
then 
 break 
fi 

selection=$(cat $temp2)
case $selection in 
1) 
 diskspace ;; 
2) 
 whoseon ;; 
3) 
 memusage ;; 
0) 
 break ;; 
*) 
 dialog --msgbox "Sorry, invalid selection" 10 30 
esac 
done

rm -f $temp 2> /dev/null 
rm -f $temp2 2> /dev/null

使用图形

​ 如果想给交互脚本加入更多的图形元素:KDE 和 GNOME 桌面环境都扩展了dialog命令的思路,包含了可以在各自环境下生成X Window图形化部件的命令。

​ kdialog和zenity包,它们各自为KDE和GNOME桌面提供了图形化窗口部件。

相关推荐
blessing。。1 小时前
I2C学习
linux·单片机·嵌入式硬件·嵌入式
2202_754421541 小时前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
运维&陈同学2 小时前
【zookeeper03】消息队列与微服务之zookeeper集群部署
linux·微服务·zookeeper·云原生·消息队列·云计算·java-zookeeper
周末不下雨3 小时前
win11+ubuntu22.04双系统 | 联想 24 y7000p | ubuntu 22.04 | 把ubuntu系统装到1T的移动固态硬盘上!!!
linux·运维·ubuntu
哎呦喂-ll3 小时前
Linux进阶:环境变量
linux
Rverdoser3 小时前
Linux环境开启MongoDB的安全认证
linux·安全·mongodb
PigeonGuan4 小时前
【jupyter】linux服务器怎么使用jupyter
linux·ide·jupyter
东华果汁哥4 小时前
【linux 免密登录】快速设置kafka01、kafka02、kafka03 三台机器免密登录
linux·运维·服务器
咖喱鱼蛋4 小时前
Ubuntu安装Electron环境
linux·ubuntu·electron