Linux-Shell函数和图形菜单

背景

此前讲述的都是单一脚本运行所有的命令,其中不乏一些重复的代码,如果是少量的都还好,但是一旦面对大批量重复的代码时就会显得很啰嗦,所以此处就讲到在Linux中使用Shell函数,从而降低重复代码的编写工作。

同样,为了便于更加友好的使用Shell脚本,还可以创建图形菜单,本文也对该内容进行了简要描述。

全文笔记来源于《Linux命令行与Shell编程大全》

内容

Shell函数

与其它语言类似,在shell中也可以编写函数,它的定义方式如下:

1
2
3
4
5
6
7
8
9
# 方式1:借助关键字funcation
funcation name {
commands
}

# 方式2:直接定义
name() {
commands
}

比如:

1
2
3
4
5
6
7
8
funcation print {
echo "who you are"
}

# 也可以
print() {
echo "who you are"
}

当我们需要调用函数的时候,则直接写函数名即可,比如:

1
2
# 针对上面的例子
print

需要注意的地方有两处:第一,函数必须先定义才能够调用,否则报错;第二,函数名必须唯一,否则后重名的函数会重写掉前面同名的函数。

那么作为一个函数,其目的是为了处理单一功能的需求,有时则需要返回数据。在shell中,shell函数同样也可以返回数据,返回数据有三种方式:

  1. 直接获取默认的退出状态码:$?。该状态码记录的是函数中最后一条命令执行的返回状态码,不能够代表函数内部没有错误

    1
    2
    3
    4
    5
    6
    function name {
    echo "this is a file"
    }

    name
    echo "function run is ok? $?"
  2. 利用return获取自定的退出状态码:return num。它记录的也只是最后一条命令执行的返回状态码,不代表函数内部没有错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function name {
    read -p "please enter a number" value
    return $[ $value*2 ]
    }

    name
    # 使用return切记:函数一调用完就要直接获取其退出状态码,否则$?记录的都是上一条命令的返回状态码
    echo "function run is ok? $?"

    # 使用return语句还有一点需要注意:return的值必须在0~255,这个是命令的返回状态码限制的
  3. 返回指定的数据:echo data。前两种方式返回的内容太渣,我们必须要返回带有艺术的东西时,就使用echo吧!!!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #!/bin/bash
    function name {
    read -p "please enter a number:" value
    value=$[ $value*2 ]
    echo $value
    }

    # 使用命令互换接住函数的返回值
    data=$( name )
    echo $data

    这种方式有一点需要注意:在函数中,凡是直接输出到STDOUT上的,都是被当作返回值返回,所以如果不是我们需要的数据,则在函数中利用重定向将不需要的数据过滤。

函数中,还有一个概念就是参数传递,在shell的函数中,参数是通过命令行的方式进行传递的,格式如下:

1
2
# 切记函数名与参数必须位于同一行中
name var1 var2 var3 ...

在函数中,如果需要引用到传递的参数,则可以使用$1~$9获取,$0表示该函数名,同样$#表示传递进去的参数总数,$@$*获取所有传递进去的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function name {
if [ $# -eq 2]
then
result=$[ $1*$2]
elif [ $# -lt 2 ]
then
result="var is less than 2"
else
result=0
for i in $@
do
result=$[ $result+i]
done
fi
echo $reult
}

# 此处切记,如果是从命令行传递过来的参数是无法直接被引用到函数中的
res=$( name 1 2 )
echo "this result is $res"

其实在shell中,也分全局变量和局部变量,在shell脚本的主体部分定义的变量都是全局变量,在整个shell中(包括函数中)都可以访问,而在函数内部定义的变量则只能在函数内部进行访问。

1
2
3
4
5
6
7
function name {
var=1
echo $[ $var+$var2 ]
}
var2=2
data=$( name )
echo "the result is $data"

同样还可以借助关键字local在函数内部标识变量为局部变量

1
2
3
4
5
6
7
8
9
function name {
# 看这里
local var
var=1
echo $[ $var+$var2 ]
}
var2=2
data=$( name )
echo "the result is $data"

之前讲过在shell中,也有列表类型的变量,如果我们想将列表类型的变量传递到函数中,则仅仅通过变量名传递是不行的,需要在函数中依次获取列表变量的值,组成一个新的列表后才能正常的使用。

1
2
3
4
5
6
7
8
function name {
local newarray
newarray=( $@ ) # --> 注意此处
echo "${newarray[*]}" # --> 此处就可以将新的数组进行返回
}
array=( 1 2 3 4 5 6 7 8 9 )
data=$( name ${array[*]} ) # --> 注意此处
echo "the result is $data"

讲到函数,就会说到一个概念:递归调用,这个在shell中也可以实现,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function mi {
if [ $1 -gt 2 ]
then
local temp=$( mi $[ $1-1] )
echo $[ $temp * $1 ]
else
echo 1
fi
}

read -p " please enter a number: " num
data=$( mi $num )
echo "the result is $data"

上面定义函数的方式是在一个shell脚本之中进行定义,当有多个脚本需要使用同一个函数的时候,此时该方法就不适用了。所以就引入了一个概念:公共库函数,即将函数部分独立成一个单独的shell脚本文件,然后在需要使用到函数的部分引入这个公共库函数文件即可。

定义公共库函数的脚本内容就全是function的函数,可以忽视,但是如果需要在shell脚本中引用公共库函数,则就需要借助命令source

1
2
3
4
5
6
7
8
9
10
11
# source后面添加公共库函数文件的路径
source ./file
read -p " please enter a number: " num
data=$( mi $num )
echo "the result is $data"

# source又名点操作符,所以可以用.代替,因此上述内容又可简写为
. ./file
read -p " please enter a number: " num
data=$( mi $num )
echo "the result is $data"

其实,函数不仅仅支持在shell脚本中,在命令行中也可以定义函数并调用

1
2
3
4
5
# 格式都是一样的,只是需要换行的部分则需要用;代替, 并且每一行命令的结束都要尾部追加;
function name {local tmp; tmp=$[ $1*$2 ]; echo $tmp;}

# 使用的时候,则直接函数名+参数
name 1 2

当然这样的方式很不友好,所以对于一些确实需要在命令行下运行的命令,一般都直接写到.bashrc文件中,因为这个文件在shell运行的时候就会自动加载,这样就避免了重复的劳作。

图形菜单

在图形界面中,程序运行都会有一个菜单桌面,比较友好,其实在shell中,也可以编辑图形菜单,本文记录两种方式,都是基于命令行的。最简单的就是文本菜单了,菜单的选择借助于case命令了,页面的信息清理借助于clear命令比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 创建菜单函数
function menu {
clear
echo
# -e:可以将\t等转义字符进行输出出来
echo -e "1.\t查看磁盘的使用情况"
echo -e "2.\t查看当前目录下的文件占用情况"
echo -e "3.\t查看当前登录的用户"
echo -e "4.\t退出当前的Shell程序"
echo -en "\n\n\t\tPlease enter your choose: "
read -n 1 option # --> 这里读取的option变量可以在主函数中使用,我也不是很明白
}

function disk {
clear
echo -e "云服务器的磁盘空间使用如下:\n"
df -h
}

function amount {
clear
echo -e "当前目录:$(pwd)使用情况如下:\n"
du -h
}

function loguer {
clear
echo -e "当前系统下登录的用户有如下:\n"
who
}

while [ 1 ] # --> 相当于while true
do
menu
case $option in
1 ) disk;;
2 ) amount;;
3 ) loguer;;
4 ) clear
break;;
* ) clear
echo "your choose is error";;
esac
echo -e "\n\n\t\tEnter any key to continue"
read -en 1 line
done

上面的方式在使用的时候就已经比较友好了,但是在创建菜单的时候还是浪费较多的内容在编辑菜单上,其实shell提供了一个易于上手的菜单编辑命令:select ... in ...,格式如下:

1
2
3
4
5
6
# PS3是select ... in ...中特殊的环境变量,用于输入的提示符
PS3="Enter your choose:"
select option in list
do
commands
done

比如将上面的例子改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function disk {
clear
echo -e "云服务器的磁盘空间使用如下:\n"
df -h
}

function amount {
clear
echo -e "当前目录:$(pwd)使用情况如下:\n"
du -h
}

function loguser {
clear
echo -e "当前系统下登录的用户有如下:\n"
who
}

PS3="Enter your option:"
select option in "查看磁盘的使用情况" "查看当前目录下的文件占用情况" "查看当前登录的用户" "退出当前的Shell程序"
do
case $option in
"查看磁盘的使用情况" ) disk;;
"查看当前目录下的文件占用情况" ) amount;;
"查看当前登录的用户" ) loguser;;
"退出当前的Shell程序" ) clear
break;;
* ) echo -e "Your choose is error";;
esac
done

上述的内容只是基于文本模式的,和图形桌面是一点关系也没有,不过也已经非常的友好了。但是还有另外一个库:dialog,它应该算是命令行窗口下的图形菜单了,首先可能我们需要进行安装“:

1
sudo apt install dialog

然后就是dialog使用的格式:

1
2
3
4
# 它可以在任意的位置运行哈
# widget:dialog的部件名
# parameters:定义部件窗口的大小及部件需要的文本
dialog --widget parameters

关于dialog的运行,有需要说明之处:

  1. dialog有两种输出方式:命令退出状态码STDERR
  2. 对于选择类部件,如果用户选择的是yes/ok,则退出状态码为0;若选择的是Cancel/no,则退出状态码为1
  3. 对于有返回值的部件,则会将返回值直接输出到STDERR上,此时则需要用户进行重定向获取。

其次是部件及参数的n多种类:https://www.cnblogs.com/klb561/p/9043142.html

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/bin/bash
# using dialog to create a menu
temp=$( mktemp -t test.XXXXXX )
temp2=$( mktemp -t test2.XXXXXX )

function diskspace {
df -k > $temp
dialog --textbox $temp 20 60 # 这个就很灵性了,因为$temp是个文件,它却能直接读取文件的内容
}

function whoseon {
who > $temp
dialog --textbox $temp 20 50
}

function memusage {
cat /proc/meminfo > $temp
dialog --textbox $temp 20 50
}

function showwho {
whoami > $temp
dialog --textbox $temp 20 50
}

function cpu {
daedecode > $temp
dialog --textbox $temp 20 50
}

while [ 1 ]
do
dialog --menu "Sys Admin Menu" 20 30 10 1 "Display disk space" 2 "Display users" 3 "Display memory usage" 4 "Display whoami" 5 "Display cpu info" 0 "Exit" 2> $temp2

if [ $? -eq 1 ]
then
break
fi
selection=$(cat $temp2)
case $selection in
1 ) diskspace ;;
2 ) whoseon ;;
3 ) memusage ;;
4 ) showwho ;;
5 ) cpu ;;
0 ) break ;;
* ) dialog --msgbox "Sorry, invalid selection" 10 30;;
esac
done
rm -f $temp 2> /dev/null
rm -f $temp2 2> /dev/null