Linux之(11)shell入门语法(3)
Author:onceday Date:2022年9月19日
漫漫长路,有人对你微笑过嘛…
本文主要收集整理于以下文档:
- 《Linux命令行与shell脚本编程大全》
- 《鸟哥的Linux私房菜》
文章目录
- Linux之(11)shell入门语法(3)
- 1.引言
- 1.1 捕获信号
- 1.2 后台模式运行脚本
- 1.3 在非控制台下运行脚本
- 1.4 使用jobs查看进程列表
- 1.5 重启停止的作业
- 1.6 进程优先级
- 1.7 定时运行作业
- 1.8 cron时间表
- 1.9 anacron程序
- 2.shell 函数
- 2.1 创建函数
- 2.2 函数返回值
- 2.3 获取函数标准输出
- 2.4 在函数中使用变量
- 2.4.1 向函数中传递参数
- 2.4.2 变量
- 2.4.3 使用数组变量
- 2.5 函数递归
- 2.6 创建库
- 3.文本菜单
- 3.1 创建菜单布局
- 3.2 使用select命令
- 3.3 使用dialog窗口
- 3.4 其他的窗口支持
1.引言
当构建高级脚本时,需要掌握更多的技能。
默认情况下,bash shell会忽略收到的任何SIGQUIT(3)和SIGTERM(15)信号,但是会处理收到的SIGHUP(1)和SIGINT(2)信号。
1.1 捕获信号
可以通过trap
来指定shell脚本要监看并从shell中拦截的Linux信号。如果脚本收到了trap命令中列出的信号,该信号不再由shell处理,而是交由本地处理。
trap commands signals
比如下面就是捕获SIGINT信号:
trap "echo 'sorry! Ctrl-C doesn't work." SIGINT
现在按下Ctrl-C,只会显示上面这条消息了。
也可以捕获脚本退出的信号:
trap "echo Goodbye... " EXIT
无论脚本正常退出还是异常退出,都会捕捉到EXIT,然后执行这段命令。
修改信号捕获很简单,只要在需要修改的信号重新指定捕获命令即可:
trap "echo xxxxx" SIGINT
xxxxxx省略很多xxxxxxx
trap "echo xxxxx" SIGINT
使用两个--
中划线就可以删除,或者说恢复默认信号处理方式:
trap -- SIGINT
1.2 后台模式运行脚本
加上后缀符&
即可在后台模式运行脚本:
onceday@ubuntu:new$ ./te.sh 4 &
[1] 86
onceday@ubuntu:new$ finnal value is 24
[1]+ Done ./te.sh 4
方括号中的数字[1]是shell分配给后台进程的作业号,下一个数是Linux分配给进程的进程ID(PID)。
当结束时,会输出Done这行的信息。
后台进程运行时,也会使用终端显示器来显示STDOUT和STDERR消息。
因此,后台运行的命令输出往往会弄的终端很乱,最好将脚本的输出重定向。
可以同时运行多个后台进程,不过在终端退出时,这些进程也会同时退出。
1.3 在非控制台下运行脚本
可使用nohub命令,会阻断来自其他进程的SIGHUP信号,因此当终端退出时,该后台进程不会退出。
nohup ./test1.sh &
nohub命令会解除终端和进程的关联,进程自然也不会同STDOUT和STDERR联系起来,所有的输出都会保存到nphub.out文件中。
1.4 使用jobs查看进程列表
一些常见的命令行参数如下:
参数 | 描述 |
-l | 列出进程的PID以及作业号 |
-n | 只列出上次shell发出的通知后改变了状态的作业 |
-p | 只列出作业的PID |
-r | 只列出运行中的作业 |
-s | 只列出已停止的作业 |
实例:
onceday@ubuntu:new$ jobs
[1] Running sleep 20 &
[2] Running sleep 20 &
[3] Running sleep 20 &
[4] Running sleep 20 &
[5] Running sleep 20 &
[6]- Running sleep 20 &
[7]+ Running sleep 20 &
带加号+
的作业是默认作业,即在使用作业命令时如果未指定作业号,该作业会被当前作业控制指令的操作对象。
在当前默认作业完成处理后,带减号的作业会成为下一个默认作业。
1.5 重启停止的作业
使用 bg命令可以重新恢复已停止的作业。
bg [n]
当有多个命令时,需要使用n指定作业号。
onceday@ubuntu:new$ sleep 30
^Z
[1]+ Stopped sleep 30
onceday@ubuntu:new$ bg
[1]+ sleep 30 &
onceday@ubuntu:new$ jobs
[1]+ Running sleep 30 &
也可以使用前台模式重启作业:
fg [n]
操作和bg是类似的。
1.6 进程优先级
shell启动的所有进程优先级默认都是相同的,在Linux系统上,优先级是个整数值,从-20最高优先级到+19最低优先级。默认情况下使用优先级0来启动。最低20是最高优先级。
使用nice命令可以设置命令启动时的调度优先级:
nice -n 10 ./test.sh > test.out &
不过该命令对于普通系统用户只能调低优先级。
使用renice
命令可以改变系统上已运行命令的优先级:
renice -n 10 -p pid
和nice 一样,该命令也有很多限制:
- 只能对属于你的进程执行renice
- 只能通过renice降低进程的优先级
- root用户可以通过renice来任意调整进程的优先级
1.7 定时运行作业
使用at命令可以将作业提交到队列中,指定何时运行该作业。
该命令由at的守护进程atd定时来调用,通常位于/var/spool/at
。
命令格式非常简单:
at [-f filename] time
日期格式如下:
- 标准的小时和分钟格式,比如20:36
- AM/PM指示符,比如8:15PM
- 特定可命名时间,比如now、noon、midnight或者teatime(4PM)
- 标准日期格式,比如MMDDYY、MM/DD/YY或DD.MM.YY
- 文本日期,比如Jul 4或Dec 25,
- 也可以时间增量,比如+25min
作业队列会使用小写字母a-z和大写字母A-Z排序来指定优先级,字母排序越高,优先级越低。
at
指令的输出非常不方便,默认是通过sendmail发送的,可在脚本里对输出重定向,这样方便查看。
可使用atq
查看当前系统中有哪些作业在等待。
可使用atrm
命令来删除等待中的作业。
1.8 cron时间表
Linux系统使用cron程序来安排要定期执行的作业,corn程序会在后台运行并检查一个特殊的表(称作cron时间表),从此表中可以获知已安排执行的作业。
命令格式如下:
min hour dayofmonth month dayofweek command
下面是示例:
15 10 * * 1 command //使用了通配符,表示每天10:15分定时执行
每周一5:15PM运行的命令:
15 17 * * 1 command
dayofmonth表示的是每月的天数,1~31天。
如何在每月的最后一天执行,需要一些技巧,如下:
00 12 * * * if ['date +%d -d tomorrow' = 01 ]; then ; command
命令列表需要指定全路径,可以用重定向指定输出的文件。
12 14 * * * /home/onceday/test.sh > test.out
可以使用crontab -l
列出已有的cron时间表。
如果用户的cron时间表文件并不 存在,可以使用crontab -e
添加条目。
也可以使用预定义的cron目录:
/etc/cron.hourly
/etc/cron.daily
/etc/cron.weekly
/etc/cron.monthly
这个适用于对时间无精确要求的场合。
1.9 anacron程序
anacron程序是用于cron的补充的,对于cron时间表,如果关机错误时间,那么也是不会去启动脚本。
但anacron会及时的重启错过执行时间的程序。
anacron的时间目录位于/var/spool/anacron
,它会处理位于cron目录的程序,但不会包含执行间隔小于一天的程序。
anacron有自己的时间表,/etc/anacrontab
来检查作业的执行情况。
时间格式如下:
period delay identifier command
- period定义了作业多久运行一次,以天为单位。
- delay指定系统启动后anacron程序需要等待分钟再运行错过的脚本。
- command条目包含了run-parts程序和一个cron脚本目录名
- identifier是用于表示日志消息和错误邮件的作业的非空字符串,如cron.weekly、cron.monthly。
2.shell 函数
2.1 创建函数
有两种方式可以创建函数:
function name {
commands
}
name
是函数的唯一名称,脚本中定义的每个函数都必须有一个唯一的名称。
第二种方式如下:
name() {
commands
}
使用函数非常简单:
function f1 {
echo "xxxxxxxxx"
}
#调用函数
f1
不能在函数定义之前使用函数,这样会出错。
如果重定义了函数,则新定义的函数会覆盖旧定义的函数,这样会出现隐形bug。
2.2 函数返回值
每个函数在运行结束时都会返回退出状态码。
使用标准变量$?
可以确定之前最近执行的函数的退出码。
默认情况下,函数的退出状态码为该函数最后一条执行命令的退出状态码。
可以使用return
指定返回的状态码:
function f2 {
read -p "get me a word:" word
echo -n "it is your input:"
echo $word
return 10
}
f2
echo "exit code: $? "
执行如下:
onceday@ubuntu:new$ ./te
get me a word:hellp
it is your input:hellp
exit code: 10
必须紧接着脚本执行后使用$?
,因为它只会保存最近一条执行过的语句。
2.3 获取函数标准输出
可以将函数内的echo输出保存到某个具体的变量:
function f2 {
read -p "get me a word:" word
echo -n "it is your input:"
echo $word
return 10
}
result=$(f2)
echo "f2 output: $result "
此时函数f2内的echo的输出内容都会被保存到result中,并且不会输出在屏幕上。但read还是能把提示输出到屏幕上,这是bash shell特别对待的结果。
通过这种方法,还可以返回浮点值和字符串值。
2.4 在函数中使用变量
2.4.1 向函数中传递参数
这部分详细内容可参考《Linux之shell基础语法(2)》第三章《处理用户输入》。
向函数中传递参数很简单,就像是对脚本或者命令传递参数一样:
functionx $var1 12 "xx"
如上就是传递了三个参数进入函数,在函数中调用它们也和脚本是一样的:
function f2 {
echo $[$1+$2]
return 10
}
re=$(f2 2 4)
echo "result:$re"
同理,使用$#
可以检查传进去的参数数量,$@
和$*
包含参数的文本字符串,且$@
可用于for遍历。
需要注意,在函数里面是无法获取函数外面脚本的特殊变量值,如$1
、$@
等。必须要手动传递进去。
2.4.2 变量
全局变量在脚本的任何地方都有效果,如果在脚本的主体部分定义了一个全局变量,那么可以在函数下面读取其值。
对于函数而言,脚本外定义的变量,其内部也能使用和更改,因此及其容易出现问题:
function f2 {
value=$[$temp1+$temp2]
return 10
}
temp1=4
temp2=5
value=1
f2
echo "$value"
运行的结果显示value被改成了9,因此需要在函数里面使用局部变量。
onceday@ubuntu:new$ ./te
9
使用局部变量的方式很简单:
local temp
local temp=$[ $value + 5 ]
这样,函数里面的变量和外部脚本的变量就是分离的,即使名字一样也不会相互干扰。
2.4.3 使用数组变量
当向函数传递数组变量时,只会取其第一个值:
function f2 {
array=$1
echo "$array[*]}"
echo "$@"
echo " "
return 10
}
myarray=(1 2 3 4 5 6 7)
f2 $myarray
f2 ${myarray[*]}
必须把数组的值拆开,然后把这些值都传进去。
onceday@ubuntu:new$ ./te
1
1
1
1 2 3 4 5 6 7
可以使用以下方法在函数内部重建数组变量:
newarray=($(echo "$@"))
for value in ${newarray[*]}
do
commands
done
如果需要从函数中返回数组,也需要先拆开返回,然后再合并。
2.5 函数递归
shell 函数也可以递归调用,前提是必须使用本地变量,或者不造成相互影响的全局变量。
下面是一个阶乘函数的实例:
function factorial {
if [ $1 -eq 1 ]
then
echo 1
else
local temp=$[ $1 - 1 ]
local result=$(factorial $temp)
echo $[ $result * $1 ]
fi
}
factorial 5
factorial可以递归调用自己,直到达到某个退出条件。
2.6 创建库
bash shell允许创建库函数,并且可以在多个脚本中引用:
source file.lib
source net.lib
. ./myfunc.lib
commands
必须使用source来导入库函数文件,这样才能吧库函数加载到当前的shell运行环境。
导入这些库函数文件之后,就可以直接使用名字来调用了。
此外,可以在命令行或者.bashrc
文件中定义函数,具体就不介绍了,和前面的方式一样。
可以去软件 - GNU 工程 - 自由软件基金会下载shtool
,bash shell的脚本函数库玩玩,不过最新版本也是2008年出的,有兴趣可以试试,使用wget ftp://ftp.gnu.org/gnu/shtool/shtool-2.0.8.tar.gz
。
3.文本菜单
3.1 创建菜单布局
在shell脚本的文本界面,也可以创建菜单界面,重点在于case命令。
需要先使用clear
清除菜单元素布局,然后echo -e
以显示非打印字符。
下面是一个菜单实例,echo -n
表示不换行,read -n 1
表示读取一个字符后就返回,无需用户按下回车。
clear
echo
echo -e "\t\t\t system status\n"
echo -e "\t1. Display disk space"
echo -e "\t2. Display user"
echo -e "\t3. Display memory usage"
echo -e "\t0. Exit menu\n\n"
echo -en "\t\tEnter option:"
read -n 1 op
echo " "
echo "you choice is $op"
运行后输出为:
system status
1. Display disk space
2. Display user
3. Display memory usage
0. Exit menu
Enter option:2
you choice is 2
onceday@ubuntu:new$
实际上,可以将这段代码封装成一个菜单函数,而每个选项也可作为一个函数存在,即使其功能未完成实现也无所谓,即桩函数(stub function)。
#!/bin/bash
function show_diskspace {
clear
df -k
}
function show_user {
clear
who
}
function show_memory {
clear
cat /proc/meminfo
}
function menu {
clear
echo
echo -e "\t\t\t system status\n"
echo -e "\t1. Display disk space"
echo -e "\t2. Display user"
echo -e "\t3. Display memory usage"
echo -e "\t0. Exit menu\n\n"
echo -en "\t\tEnter option:"
read -n 1 op
}
while [ 1 ]
do
menu
case $op in
0) break;;
1) show_diskspace;;
2) show_user;;
3) show_memory;;
*) clear
echo "Error option!"
esac
echo -en "\n\n\t\t\tHit any key to continue."
read -n 1 line
done
echo ""
上面这个脚本就是一个完整的菜单脚本,以此为模板可以创建非常复杂的菜单过程。
3.2 使用select命令
select可以协助创建菜单:
select variable in list
do
commands
done
select会为list中的每个选项都创建一个带编号的选项,然后为选项提供一个由PS3
环境变量提供的特殊字符。
下面是一个实例:
#!/bin/bash
function show_diskspace {
clear
df -k
}
function show_user {
clear
who
}
function show_memory {
clear
cat /proc/meminfo
}
PS3="Enter option:"
select option in "show diskspace" "show user" "show memory" "exit"
do
case $option in
"exit")
break;;
"show diskspace")
show_diskspace;;
"show user")
show_user;;
"show memory")
show_memory;;
*) clear
echo "Error option!"
esac
done
echo ""
select选项需要手动输入回车才能停止输入,而且会自动循环,所以需要设立break
条件。
1) show diskspace
2) show user
3) show memory
4) exit
Enter option:1
3.3 使用dialog窗口
dialog包支持在命令行界面创建标准的窗口对话框。
该包可能需要额外安装:
sudo apt-get install dialog
使用方法很简单,如下:
dialog --widget parameters
- widget是窗口的组件名
- parameters是参数,可以指定部件窗口的大小以及部件需要的文本。
输出为两种:
- STDERR,重定向该输出可获取输出内容
- 退出状态码,使用
$?
可获取状态码的值
以下是常见窗口部件:
部件 | 描述 |
calendar | 提供选择日期的日历 |
checklist | 显示多个选项,每个选项都能打开或关闭 |
form | 构建一个带有标签以及文本字段的表单 |
fselect | 提供一个文件选择窗口来浏览选择文件 |
gauge | 显示完成的百分比进度条 |
infobox | 显示一条消息,但不用等待回应 |
inputbox | 提供一个输入文本用的文本表单 |
inputmenu | 提供一个可编辑的菜单 |
menu | 显示可选择的一系列选项 |
msgbox | 显示一条消息,并要求用户选择ok按钮 |
pause | 显示一个进度条来显示暂停期间的状态 |
passwordbox | 显示一个文本框,但会隐藏输入的文本 |
passwordform | 显示一个带标签和隐藏文本字段的表单 |
radiolist | 提供一组菜单选项,但只能选择其中一个 |
tailbox | 用tail命令在滚动窗口中显示文件的内容 |
tailboxbg | 跟tailbox一样,但是在后台模式中运行 |
textbox | 在滚动窗口中显示文件的内容 |
timebox | 提供一个选择小时,分钟和秒数的窗口 |
yesno | 提供一个带有Yes和No按钮的简单消息 |
具体就不介绍了,需要时自行查阅即可。
3.4 其他的窗口支持
在KDE图形化环境下,默认包含kdialog包,可以在KDE桌面上生成类似于dialog式部件的标准窗口。
在HNOME图形化环境下,支持gdialog和zenity,使用方法和dialog类似。