参数输入
传递参数
向 shell 脚本传递数据的最基本方法是使用命令行参数
$ ./addem 10 30
bash shell 会将所有的命令行参数都指派给称作位置参数(positional parameter)的特殊变量。位置变量的名称都是标准数字:$0 对应脚本名,$1 对应第一个命令行参数,$2 对应第二个命令行参数,以此类推,直到$9。
#!/bin/bash # Using one command-line parameter # factorial=1 for (( number = 1; number <= $1; number++ )) do factorial=$[ $factorial * $number ] done echo The factorial of $1 is $factorial exit
$ ./positional1.sh 5
参数之间是以空格分隔的,要想在参数值中加入空格,必须使用引号(单引号或双引号均可)。
可以使用位置变量$0 获取在命令行中运行的 shell 脚本名,这里有一个潜在的问题。如果使用另一个命令来运行 shell 脚本,则命令名会和脚本名混在一起;如果运行脚本时使用的是绝对路径,那么位置变量$0 就会包含整个路径。
basename 命令可以返回不包含路径的脚本名
#!/bin/bash # Using basename with the $0 command-line parameter # name=$(basename $0) # echo This script name is $name. exit
特殊参数
特殊变量$#含有脚本运行时携带的命令行参数的个数。你可以在脚本中的任何地方使用这个特殊变量,就跟普通变量一样:
#!/bin/bash # Counting command-line parameters # $# 表示参数个数(不包含$0) if [ $# -eq 1 ] then fragment="parameter was" else fragment="parameters were" fi echo $# $fragment supplied. exit
$*变量和$@变量可以轻松访问所有参数,它们各自包含了所有的命令行参数。
$*变量会将所有的命令行参数视为一个单词。这个单词含有命令行中出现的每一个参数。基本上,$*变量会将这些参数视为一个整体,而不是一系列个体。
$@变量会将所有的命令行参数视为同一字符串中的多个独立的单词,以便你能遍历并处理全部参数。这通常使用 for 命令完成。
#!/bin/bash # Testing different methods for grabbing all the parameters # echo echo "Using the \$* method: $*" echo echo "Using the \$@ method: $@" echo exit
shift命令会根据命令行参数的相对位置进行移动。在使用shift命令时,默认情况下会将每个位置的变量值都向左移动一个位置。因此,变量$3的值会移入$2,变量$2的值会移入$1,而变量$1的值则会被删除。
这是遍历命令行参数的另一种好方法,尤其是在不知道到底有多少参数的时候。
#!/bin/bash ## Shifting through the parameters echo echo "Using the shift method:" count=1 while [ -n "$1" ] # 检查参数长度是否大于0 do echo "Parameter #$count = $1" count=$[ $count + 1 ] shift done echo
处理选项
处理简单的选项,可以用shift实现
#!/bin/bash # Extract command-line options # echo 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 echo exit
$ ./extractoptions.sh -a -b -c -d
分离参数和选项:在 Linux 中,处理这个问题的标准做法是使用特殊字符将两者分开,该字符会告诉脚本选项何时结束,普通参数何时开始,这个特殊字符是双连字符(–)。
#!/bin/bash # Extract command-line options and parameters # echo 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 # echo count=1 for param in $@ do echo "Parameter #$count: $param" count=$[ $count + 1 ] done echo exit
在遇到双连字符时,脚本会用 break 命令跳出 while 循环。由于提前结束了循环,因此需要再加入另一个 shift 命令来将双连字符移出位置变量。
$ ./extractoptionsparams.sh -a -b -c – test1 test2 test3
使用getopt命令
getopt 命令在处理命令行选项和参数时非常方便。它能够识别命令行参数,简化解析过程,格式如下:
getopt optstring parameters
optstring 是这个过程的关键所在。它定义了有效的命令行选项字母,还定义了哪些选项字母需要参数值。
首先,在 optstring 中列出要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后面加一个冒号。getopt 命令会基于你定义的 optstring 解析提供的参数。
$ getopt ab:cd -a -b BValue -cd test1 test2 # (out)-a -b BValue -c -d -- test1 test2 $
optstring 定义了 4 个有效选项字母:a、b、c 和 d。冒号(:)被放在了字母 b 后面,因为 b 选项需要一个参数值。
注意,它会自动将-cd 分成两个单独的选项,并插入双连字符来分隔命令行中额外的参数。
可以在脚本中使用 getopt 命令来格式化脚本所携带的任何命令行选项或参数,getopt 命令生成的格式化版本替换已有的命令行选项和参数,这得求助于set 命令。
set 命令有一个选项是双连字符(–),可以将位置变量的值替换成 set 命令所指定的值。
具体做法是将脚本的命令行参数传给 getopt 命令,然后再将 getopt 命令的输出传给 set命令,用 getopt 格式化后的命令行参数来替换原始的命令行参数,如下所示:
set -- $(getopt -q ab:cd "$@") # getopt 的-q 选项可以忽略错误消息
#!/bin/bash # Extract command-line options and values with getopt # set -- $(getopt -q ab:cd "$@") # echo 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 # echo count=1 for param in $@ do echo "Parameter #$count: $param" count=$[ $count + 1 ] done exit
$ ./extractwithgetopt.sh -ac
$ ./extractwithgetopt.sh -c -d -b BValue -a test1 test2
使用getopts命令
getopt 与 getopts 的不同之处在于,前者在将命令行中选项和参数处理后只生成一个输出,而后者能够和已有的 shell 位置变量配合默契。
getopts 每次只处理一个检测到的命令行参数。在处理完所有的参数后,getopts 会退出并返回一个大于 0 的退出状态码。这使其非常适合用在解析命令行参数的循环中。getopts 命令的格式如下:
getopts optstring variable
optstring 值与 getopt 命令中使用的值类似。有效的选项字母会在 optstring 中列出,如果选项字母要求有参数值,就在其后加一个冒号。不想显示错误消息的话,可以在 optstring之前加一个冒号。getopts 命令会将当前参数保存在命令行中定义的 variable 中。
#!/bin/bash # Extract command-line options and values with getopts # echo while getopts :ab:c opt do case "$opt" in a) echo "Found the -a option" ;; b) echo "Found the -b option with parameter value $OPTARG";; c) echo "Found the -c option" ;; *) echo "Unknown option: $opt" ;; esac done exit
$ ./extractwithgetopts.sh -ab BValue -c
while 语句定义了 getopts 命令,指定要查找哪些命令行选项,以及每次迭代时存储它们的变量名(opt)。getopts 命令会移除起始的连字符,所以在 case 语句中不用连字符。
选项标准化
在 Linux 中,有些选项字母在某种程度上已经有了标准含义。如果能在 shell 脚本中支持这些选项,则你的脚本会对用户更友好。
选项 | 描述 |
---|---|
-a | 显示所有对象 |
-c | 生成计数 |
-d | 指定目录 |
-e | 扩展对象 |
-f | 指定读入数据的文件 |
-h | 显示命令的帮助信息 |
-i | 忽略文本大小写 |
-l | 产生长格式输出 |
-n | 使用非交互模式(批处理) |
-o | 将所有输出重定向至指定的文件 |
-q | 以静默模式运行 |
-r | 递归处理目录和文件 |
-s | 以静默模式运行 |
-v | 生成详细输出 |
-x | 排除某个对象 |
-y | 对所有问题回答 yes |
用户输入
read命令从标准输入(键盘)或另一个文件描述符中接受输入。获取输入后,read命令会将数据存入变量。下面是该命令最简单的用法:
#!/bin/bash ## Using the read command echo -n "Enter your name: " read name echo "Hello $name, welcome to my script." exit
read 命令也提供了-p 选项,允许直接指定提示符:
#!/bin/bash # Using the read command with the -p option # read -p "Please enter your age: " age days=$[ $age * 365 ] echo "That means you are over $days days old!" exit
-t 选项会指定 read 命令等待输入的秒数。如果计时器超时,则 read 命令会返回非 0 退出状态码:
#!/bin/bash # Using the read command with a timer # if read -t 5 -p "Enter your name: " name then echo "Hello $name, welcome to my script." else echo echo "Sorry, no longer waiting for name." fi exit
#!/bin/bash # Using the read command for one character # read -n 1 -p "Do you want to continue [Y/N]? " answer # case $answer in Y | y) echo echo "Okay. Continue on...";; N | n) echo echo "Okay. Goodbye" exit;; esac echo "This is the end of the script." exit
本例中使用了-n 选项和数值 1,告诉 read 命令在接收到单个字符后退出。只要按下单个字符进行应答,read 命令就会接受输入并将其传给变量,无须按 Enter 键。
无显示读取
有时需要从脚本用户处得到输入,但又不想在屏幕上显示输入信息(比如密码)。
-s 选项可以避免在 read 命令中输入的数据出现在屏幕上(其实数据还是会被显示,只不过 read 命令将文本颜色设成了跟背景色一样)
#!/bin/bash # Hiding input data # read -s -p "Enter your password: " pass echo echo "Your password is $pass" exit
从文件中读取
可以使用 read 命令读取文件。每次调用 read 命令都会从指定文件中读取一行文本。需要借助cat命令:
#!/bin/bash # Using the read command to read a file # count=1 cat $HOME/scripts/test.txt | while read line do echo "Line $count: $line" count=$[ $count + 1 ] done echo "Finished processing the file." exit
脚本控制
处理信号
Linux 利用信号与系统中的进程进行通信。Linux 系统和应用程序可以产生超过 30 个信号。下面表格列出了在 shell 脚本编程时会遇到的最常见的 Linux 系统信号。
信号 | 值 | 描述 |
---|---|---|
1 | SIGHUP | 挂起(hang up)进程 |
2 | SIGINT | 中断(interupt)进程 |
3 | SIGQUIT | 停止(stop)进程 |
9 | SIGKILL | 无条件终止(terminate)进程 |
15 | SIGTERM | 尽可能终止进程 |
18 | SIGCONT | 继续运行停止的进程 |
19 | SIGSTOP | 无条件停止,但不终止进程 |
20 | SIGTSTP | 停止或暂停(pause),但不终止进程 |
在默认情况下,bash shell 会忽略收到的任何 SIGQUIT(3)信号和 SIGTERM(15)信号(因此交互式 shell 才不会被意外终止)。但是,bash shell 会处理收到的所有 SIGHUP(1)信号和SIGINT(2)信号。
bash shell 允许使用键盘上的组合键来生成两种基本的 Linux 信号。这个特性在需要停止或暂停失控脚本时非常方便。
- 中断进程 :Ctrl+C 组合键会生成 SIGINT 信号,并将其发送给当前在 shell 中运行的所有进程。
- 暂停进程:Ctrl+Z 组合键会生成 SIGTSTP 信号,停止 shell 中运行的任何进程。停止(stopping)进程跟终止(terminating)进程不同,前者让程序继续驻留在内存中,还能从上次停止的位置继续运行。
trap 命令可以指定 shell脚本需要侦测并拦截的 Linux 信号。如果脚本收到了 trap 命令中列出的信号,则该信号不再由shell 处理,而是由本地处理。
trap 命令的格式如下:
trap commands signals
在 trap 命令中,需要在 commands 部分列出想要 shell 执行的命令,在 signals 部分列出想要捕获的信号(多个信号之间以空格分隔)。指定信号的时候,可以使用信号的值或信号名。
#!/bin/bash #Testing signal trapping # 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
要想在脚本中的不同位置进行不同的信号捕获处理,只需重新使用带有新选项的 trap 命令即可:
#!/bin/bash #Modifying a set trap # trap "echo ' Sorry...Ctrl-C is trapped.'" SIGINT # count=1 while [ $count -le 3 ] do echo "Loop #$count" sleep 1 count=$[ $count + 1 ] done # trap "echo ' I have modified the trap!'" SIGINT # 修改捕获信号 # count=1 while [ $count -le 3 ] do echo "Second Loop #$count" sleep 1 count=$[ $count + 1 ] done # exit
也可以移除已设置好的信号捕获。在 trap 命令与希望恢复默认行为的信号列表之间加上两个连字符即可
#!/bin/bash #Removing a set trap # trap "echo ' Sorry...Ctrl-C is trapped.'" SIGINT # count=1 while [ $count -le 3 ] do echo "Loop #$count" sleep 1 count=$[ $count + 1 ] done # trap -- SIGINT # 恢复默认的信号捕获 echo "The trap is now removed." # count=1 while [ $count -le 3 ] do echo "Second Loop #$count" sleep 1 count=$[ $count + 1 ] done # exit
也可以在 trap 命令后使用单连字符来恢复信号的默认行为。单连字符和双连字符的效果一样。
后台运行脚本
以后台模式运行 shell 脚本非常简单,只需在脚本名后面加上&即可:
$ ./backgroundscript.sh &
在非控制台下运行脚本
有时候,即便退出了终端会话,也想在终端会话中启动 shell 脚本,让脚本一直以后台模式运行到结束。这可以用 nohup 命令来实现。
nohup 命令能阻断发给特定进程的 SIGHUP 信号。当退出终端会话时,这可以避免进程退出。
nohup ./testAscript.sh &
作业控制
作业控制包括启动、停止、“杀死”以及恢复作业。通过作业控制,能完全控制 shell 环境中所有进程的运行方式。
jobs 是作业控制中的关键命令,该命令允许用户查看 shell 当前正在处理的作业。
#!/bin/bash #Testing job control # echo "Script Process ID: $$" # $$变量来显示 Linux 系统分配给该脚本的 PID # count=1 while [ $count -le 5 ] do echo "Loop #$count" sleep 10 count=$[ $count + 1 ] done # echo "End of script..." exit
$ jobs # 可以查看 当前运行的脚本
选项 | 描述 |
---|---|
-l | 列出进程的 PID 以及作业号 |
-n | 只列出上次 shell 发出通知后状态发生改变的作业 |
-p | 只列出作业的 PID |
-r | 只列出运行中的作业 |
-s | 只列出已停止的作业 |
定时运行作业
at 命令允许指定 Linux 系统何时运行脚本。该命令会将作业提交到队列中,指定 shell 何时运行该作业。at 的守护进程 atd 在后台运行,在作业队列中检查待运行的作业。
atd 守护进程会检查系统的一个特殊目录(通常位于/var/spool/at 或/var/spool/cron/atjobs),从中获取 at 命令提交的作业。在默认情况下,atd 守护进程每隔 60 秒检查一次这个目录。如果其中有作业,那么 atd 守护进程就会查看此作业的运行时间。如果时间跟当前时间一致,就运行此作业。
at 命令的基本格式非常简单:
at [-f filename] time
在默认情况下,at 命令会将 STDIN 的输入放入队列。你可以用-f 选项指定用于从中读取命令(脚本文件)的文件名。
time 选项指定了你希望何时运行该作业。如果指定的时间已经过去,那么 at 命令会在第二天的同一时刻运行指定的作业。指定时间的方式非常灵活。at 命令能识别多种时间格式。
- 标准的小时和分钟,比如 10:15。
- AM/PM 指示符,比如 10:15 PM。
- 特定的时间名称,比如 now、noon、midnight 或者 teatime(4:00 p.m.)。
- 标准日期,比如 MMDDYY、MM/DD/YY 或 DD.MM.YY。
- 文本日期,比如 Jul 4 或 Dec 25,加不加年份均可。
- 时间增量:
- Now + 25 minutes
- 10:15 PM tomorrow
- 10:15 + 7 days
atq 命令可以查看系统中有哪些作业在等待
调度需要定期运行的脚本
Linux 系统使用 cron 程序调度需要定期执行的作业。cron 在后台运行,并会检查一个特殊的表(cron 时间表),从中获知已安排执行的作业。
cron 时间表通过一种特别的格式指定作业何时运行,其格式如下:
minutepasthour hourofday dayofmonth month dayofweek command
cron 时间表允许使用特定值、取值范围(比如 1~5)或者通配符(星号)来指定各个字段。如果想在每天的 10:15 运行一个命令,可以使用如下 cron 时间表字段:
15 10 * * * /home/christine/backup.sh > backup.out
每个用户(包括root用户)都可以使用自己的cron时间表运行已安排好的任务。Linux提供了crontab命令来处理cron时间表。要列出已有的cron时间表,可以用-l选项:
$ crontab -l
在默认情况下,用户的cron时间表文件并不存在。可以使用-e选项向cron时间表添加字段。在添加字段时crontab命令会启动一个文本编辑器。
如果创建的脚本对于执行时间的精确性要求不高,则用预配置的cron脚本目录会更方便。预配置的基础目录共有4个:hourly、daily、monthly和weekly。
$ ls /etc/cron.*ly
anacron 程序
cron 程序唯一的问题是它假定 Linux 系统是 7×24 小时运行的。除非你的 Linux 运行在服务器环境,否则这种假设未必成立。
如果某个作业在 cron 时间表中设置的运行时间已到,但这时候 Linux 系统处于关闭状态,那么该作业就不会运行。当再次启动系统时,cron 程序不会再去运行那些错过的作业。为了解决这个问题,许多 Linux 发行版提供了 anacron 程序。
anacron 程序只处理位于 cron 目录的程序,比如/etc/cron.monthly。它通过时间戳来判断作业是否在正确的计划间隔内运行了。每个 cron 目录都有一个时间戳文件,该文件位于/var/spool/anacron:
$ ls /var/spool/anacron
anacron 时间表的基本格式和 cron 时间表略有不同:
period delay identifier command
period 字段定义了作业的运行频率(以天为单位)。anacron 程序用该字段检查作业的时间戳文件。delay 字段指定了在系统启动后,anacron 程序需要等待多少分钟再开始运行错过的脚本
$ cat /etc/anacrontab
# /etc/anacrontab: configuration file for anacron # See anacron(8) and anacrontab(5) for details. SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin HOME=/root LOGNAME=root # These replace cron's entries 1 5 cron.daily run-parts --report /etc/cron.daily 7 10 cron.weekly run-parts --report /etc/cron.weekly @monthly 15 cron.monthly run-parts --report /etc/cron.monthly