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类似。