环境变量
全局、局部环境变量
用户变量(局部变量):修改的设置只对某个用户的路径或执行起作用;
系统变量(全局变量):影响范围是整个系统 ;
系统环境变量基本上都是使用全大写字母,以区别于普通用户的环境变量。
查看
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 命令会用到两个环境变量:
- OPTARG:保存带值参数的值
- 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桌面提供了图形化窗口部件。