[转帖]Bash脚本编程学习笔记08:函数

bash,脚本,编程,学习,笔记,函数 · 浏览次数 : 0

小编点评

#!/bin/bash # 1. 检测(ping)某一主机的在线状态 ip=$1 if ping -c 1 -q $ip &> /dev/null; then echo \"The host $ip is online.\" return 0 else return 1 fi # 2. 实现乘法口诀表 N=$1 for ((i=1;i<=N;i++)); do for ((j=1;j<=i;j++)); do echo -ne \"$j*$i=$((i*j))\\t\" } echo done echo done # 3. 实现NN乘法口诀表 function fact() { if [ $1 -eq 1 -o $1 -eq 0 ]; then echo 1 else echo $(($1*$(fact $(($1-1))))) fi} } # 4. 求阶乘的计算方法 if [ $1 -eq 1 -o $1 -eq 0 ]; then echo 1 else echo $(($1*$(fact $(($1-1))))) fi

正文

https://www.cnblogs.com/alongdidi/p/bash_function.html

 

官方资料:Shell Functions (Bash Reference Manual)

简介

正如我们在《Bash脚本编程学习笔记06:条件结构体》中最后所说的,我们应该把一些可能反复执行的代码块整合起来,避免反复编写使得代码过于臃肿。

函数正是为了解决这个问题而存在的。函数在定义时,可以将常用的代码整合为一个整体,当我们需要执行的时候,只需要调用这个函数即可。

Bash是过程式编程语言,从上至下顺序执行代码,因此函数定义必须在函数调用之前完成。

函数属于shell的基础特性,即不仅仅是针对于bash,包括csh、sh、ksh和zsh等都具有这里说明的函数特性。

函数会在当前的shell环境下执行,不会创建新的进程(子shell)来解释函数。

函数可以通过“unset -f”命令删除。

 

函数的定义和调用

函数的定义有两种方式。

方式一。

FUNC_NAME () {
    BODY
}

方式二。

function FUNC_NAME [()] {
    BODY
}

[()]:表示小括号可以省略。

函数的调用只需要键入函数名即可,就像键入命令名一样。

复制代码
[root@c7-server ~]# cat function.sh 
#!/bin/bash

# 函数定义 test_func() { echo "This is just for test." }
# 函数调用 test_func [root@c7-server ~]# bash function.sh This is just for test.
复制代码

示例:编写一个脚本,接收一个用户名参数,输出用户名、UID和默认shell。(要求以函数的形式)

复制代码
#!/bin/bash

userinfo() {
    if ! id $username &> /dev/null; then
        echo "The user $username is not exists."
    else
        grep "^$username\>" /etc/passwd | cut -d : -f 1,3,7
    fi
}

username=$1
userinfo
复制代码

示例:将《Bash脚本编程学习笔记06:条件结构体》中最后的服务脚本中冗余的代码改写为函数,即函数式编程。

复制代码
#!/bin/bash
#
# chkconfig: - 50 50
# Description: test service script
#

prog=$(basename $0)
lockfile="/var/lock/subsys/$prog"

start() {
    if [ -e $lockfile ]; then
        echo "The service $prog has already started."
    else
        touch $lockfile
        echo "The service $prog starts finished."
    fi
}
stop() {
    if [ ! -e $lockfile ]; then
        echo "The service $prog has already stopped."
    else
        rm -f $lockfile
        echo "The service $prog stops finished."
    fi
}
restart() {
    if [ -e $lockfile ]; then
        rm -f $lockfile
        touch $lockfile
        echo "The service $prog restart finished."
    else
        touch $lockfile
        echo "The service $prog starts finished."
    fi
}
status() {
    if [ -e $lockfile ]; then
        echo "The service $prog is running."
    else
        echo "The service $prog is not running."
    fi
}

case $1 in
    start)
        start
    ;;
    stop)
        stop
    ;;
    restart)
        restart
    ;;
    status)
        status
    ;;
    *)
        echo "Usage: $prog {start|stop|restart|status}"
        exit 1
    ;;
esac
复制代码

 

函数的输出和退出状态码

函数的输出指的是函数体中执行的命令的输出,STDOUT和STDERR都会输出。其中也包括了我们使用echo或者printf的显式的STDOUT。

如果函数中没有return语句的话,那么函数的退出状态码是函数体中最后一条命令的退出状态码。

如果函数中有return语句的话,那么当函数体自上而下执行到return时,函数会立即停止并返回,函数的退出状态码就是return指定的退出状态码。

return [n]

如果return没有指定退出状态码的话,那么就是return的上一条命令的退出状态码。

注意不要和exit语句混淆。return用户终止函数的执行并返回,而exit是终止了整个脚本的执行并退出。后者的作用域更广。

 

函数的位置参数

函数和脚本一样,都可以接收参数作为位置参数,然后在函数中引用这些参数。也支持引用与位置参数有关的特殊变量($#, $*, $@)。

向函数传递位置参数和向脚本传递位置参数的方式是一样的。

my_func() {
    echo "$1 $2"
}

my_func arg1 arg2

记住,传参时,不要写成类似C语言的风格,会报错的。

my_func(arg1, arg2)

 

练习

1、编写一个脚本,批量添加用户,用户传参给脚本,参数是欲添加的用户名前缀,用户名其余部分使用数字补全。

复制代码
#!/bin/bash

userAdd() {
    if id $1 &> /dev/null; then
        return 1
    else
        useradd $1
        return 0
    fi
}

for i in {01..03}; do
    username="$1$i"
    userAdd $username
    retval=$?
    if [ $retval -eq 0 ]; then
        echo "The user $username has been added."
    elif [ $retval -eq 1 ]; then
        echo "The user $username was existed."
    fi
done
复制代码

这里有一点需要注意,在for循环体中,一定要在函数调用后立即将函数的退出状态码(return)获取并存入变量中(如该示例中的retval),而后在对该状态码做判断。

如果直接多次判断$?的值的话,那么第一次的$?的值是函数的退出状态码,到了第二次,就会变成了上一句echo语句了。

2、编写一个脚本,通过函数检测(ping)某一主机的在线状态。需检测192.168.152.1~192.168.152.254这个范围。

复制代码
#!/bin/bash

ping_test() {
    ip=$1
    if ping -c 1 -q $ip &> /dev/null; then
        echo "The host $ip is online."
        return 0
    else
        return 1
    fi
}

for i in 192.168.152.{1..254}; do
    ping_test $i
done
复制代码

Linux中的ping命令,默认是发送无限的请求数据包持续ping的,需要使用-c指定包的数量。这个脚本不太好,因为ping遇到不通的情况会等待一段时间,导致脚本比较耗时。

3、编写一个脚本,通过函数实现乘法口诀表,注意,不是九九乘法口诀表,而是NN乘法口诀表,N作为用户参数传入脚本。

复制代码
#!/bin/bash

multi() {
    N=$1
    for ((i=1;i<=N;i++)); do
        for ((j=1;j<=i;j++)); do
            echo -ne "$j*$i=$((i*j))\t"
        done
        echo
    done
}

multi $1
复制代码

注意:这个脚本有瑕疵,在输出时,当N数值过大的时候,使用“\t”制表符会使显示较不人性化。

 

 

函数中的变量作用域

《Bash脚本编程学习笔记01:变量与多命令执行》中我们说变量有三种类型并说明了其作用域。

  • 本地变量:仅当前shell有效(即当前bash进程)。
  • 环境变量:当前shell及其子shell(即当前bash进程即其子bash进程)。
  • 局部变量:在某部分代码片段中有效(例如函数)。

结合作用域的概念,我们来看下面这个示例。

复制代码
#!/bin/bash

name=tom

set_name() {
    name=jerry
    echo $name
}

set_name
echo $name
复制代码

我们应该会认为这样吧?

set_name --输出--> jerry
echo $name --输出--> tom

其实,真实的结果是:

[root@c7-server ~]# bash func_scope.sh
jerry
jerry

造成这种结果的原因是,局部变量是需要手动定义的,而不是其出现在函数体中就是局部变量了。

可以通过内置命令local来定义局部变量,local仅可以在函数内部使用!

local [option] name[=value] …

因此我们对脚本进行改写,函数体中的变量明确使用local命令定义,就可以验证我们此前说的变量作用域的理论了。

复制代码
[root@c7-server ~]# cat func_scope.sh
#!/bin/bash

name=tom

set_name() {
    local name=jerry
    echo $name
}

set_name
echo $name
[root@c7-server ~]# bash func_scope.sh
jerry
tom
复制代码

函数中的变量作用域,是一个难点,我其实没搞太明白。上面的示例其实是一个非常简单的示例,在复杂的情况下,例如函数A调用函数B时,local会变得很有帮助。具体遇到的时候,建议大家再翻阅一下官方的手册。

 

函数的递归

一个函数可以调用另一个函数,而当一个函数调用自身时,就叫做函数的递归。

递归不会无限递归,一般会有一个边界,在边界处会返回,而后根据递归的顺序逐一按照相反的顺序返回。

函数的递归很好地解决了计算阶乘(factorial)和斐波那契(fibonacci)数列。

阶乘

阶乘是由基斯顿·卡曼发明的数学运算。一个正整数的阶乘是所有小于等于该数的正整数的积,记作“n!”。0和1的阶乘都是1。

n!=1*2*3*4*...*n

阶乘存在一个规律。

5!=1*2*3*4*5
4!=1*2*3*4
3!=1*2*3
2!=1*2
1!=1

因此。

5!=4!*5
4!=3!*5
3!=2!*3
...
n!=(n-1)!*n=n*(n-1)!

我们就可以把阶乘定义为一个函数并通过递归的方式来实现阶乘的计算。

复制代码
#!/bin/bash

fact() {
    if [ $1 -eq 1 -o $1 -eq 0 ]; then
        echo 1
    else
        echo $(($1*$(fact $(($1-1)))))
    fi
}

fact $1
复制代码

斐波那契数列

斐波那契数列是数学家莱昂纳多·斐波那契以兔子繁殖为例引入的,又作兔子数列。

该数列F,第一项和第二项都为1,从第二项开始,每一项的值都是前两项之和。

数列F
F(1)=1
F(2)=1
F(3)=F(1)+F(2)
...
F(n)=F(n-2)+F(n-1)
1 1 2 3 5 8 13 21 34 ...

因此我们可以把求斐波那契数列的第N项的值写为一个函数。

复制代码
[root@c7-server ~]# cat func_factorial.sh 
#!/bin/bash

fact() {
    if [ $1 -eq 1 -o $1 -eq 0 ]; then
        echo 1
    else
        echo $(($1*$(fact $(($1-1)))))
    fi
}

fact $1
[root@c7-server ~]# vim func_fibo.sh
[root@c7-server ~]# bash func_fibo.sh 1
1
[root@c7-server ~]# bash func_fibo.sh 2
1
[root@c7-server ~]# bash func_fibo.sh 3
2
[root@c7-server ~]# bash func_fibo.sh 4
3
[root@c7-server ~]# bash func_fibo.sh 5
5
[root@c7-server ~]# bash func_fibo.sh 6
8
[root@c7-server ~]# bash func_fibo.sh 7
13
[root@c7-server ~]# bash func_fibo.sh 8
21
复制代码

不过这种方式仅仅是求得第N项的值,我们可以改为打印这个斐波那契数列。

复制代码
[root@c7-server ~]# cat func_fibo.sh 
#!/bin/bash

fibo() {
    if [ $1 -eq 1 -o $1 -eq 2 ]; then
        echo -n "1 "
    else
        echo -n "$(($(fibo $(($1-2)))+$(fibo $(($1-1))))) "
    fi
}

for i in $(seq $1); do
    fibo $i
done
echo
[root@c7-server ~]# bash func_fibo.sh 10
1 1 2 3 5 8 13 21 34 55
复制代码

与[转帖]Bash脚本编程学习笔记08:函数相似的内容:

[转帖]Bash脚本编程学习笔记08:函数

https://www.cnblogs.com/alongdidi/p/bash_function.html 官方资料:Shell Functions (Bash Reference Manual) 简介 正如我们在《Bash脚本编程学习笔记06:条件结构体》中最后所说的,我们应该把一些可能反复执行

[转帖]Bash脚本编程学习笔记02:脚本基础和bash配置文件

脚本基础 参考资料:Shell Scripts (Bash Reference Manual) 不严谨地说,编程语言根据代码运行的方式,可以分为两种方式: 编译运行:需要先将人类可识别的代码文件编译成机器可运行的二进制程序文件后,方可运行。例如C语言和Java语言。 解释运行:需要一个编程语言的解释

[转帖]Bash脚本编程学习笔记03:算术运算

https://www.cnblogs.com/alongdidi/p/bash_arithmetic_expression.html 简介 Bash所支持的算术运算和C语言是一样的,这里指的是操作符(operator)以及它们的优先级(precedence)、结合性(associativity)和

[转帖]Bash脚本编程学习笔记04:测试命令test、状态返回值、位置参数和特殊变量

Bash脚本编程学习笔记04:测试命令test、状态返回值、位置参数和特殊变量 我自己接触Linux主要是大学学习的Turbolinux --> 根据《鸟哥的Linux私房菜:基础篇》(第三版) --> 马哥的就业班课程。给我的感觉是这些课程对于bash的讲解,理论上是不够的,但是限于时间、篇幅和精

[转帖]Bash脚本编程学习笔记04:测试命令test、状态返回值、位置参数和特殊变量

https://www.cnblogs.com/alongdidi/p/test_exitStatus_positionalAndSpecialParameter.html 我自己接触Linux主要是大学学习的Turbolinux --> 根据《鸟哥的Linux私房菜:基础篇》(第三版) --> 马

[转帖]Bash脚本编程学习笔记03:算术运算

https://www.cnblogs.com/alongdidi/p/bash_arithmetic_expression.html 简介 Bash所支持的算术运算和C语言是一样的,这里指的是操作符(operator)以及它们的优先级(precedence)、结合性(associativity)和

[转帖]Bash脚本编程学习笔记05:用户交互与脚本调试

https://www.cnblogs.com/alongdidi/p/read_and_bash_debug.html 用户交互 在《学习笔记04》中我们有提到位置参数,位置参数是用来向脚本传递参数的一种方式。还有一种方式,是read命令。 [root@c7-server ~]# read nam

[转帖]Bash脚本编程学习笔记06:条件结构体

简介 在bash脚本编程中,条件结构体使用if语句和case语句两种句式。 if语句 单分支if语句 if TEST; then CMD fi TEST:条件判断,多数情况下可使用test命令来实现,返回值为0的话则执行CMD,否则就离开该条件结构体,脚本继续往下执行。 [root@c7-serve

[转帖]Bash脚本编程学习笔记07:循环结构体

https://www.cnblogs.com/alongdidi/p/bash_loop_construct.html 本篇中涉及到算术运算,使用了$[]这种我未在官方手册中见到的用法,但是确实可用的,在此前的博文《Bash脚本编程学习笔记03:算术运算》中我有说明不要使用,不过自己忘记了。大家还

[转帖]Bash脚本编程学习笔记09:数组

https://www.cnblogs.com/alongdidi/p/bash_array.html 数组简介 在bash脚本编程当中,变量是存储单个元素的内存空间;而数组是存储多个元素的一段连续的内存空间。 数组由数组名和下标构成,如下。 ARRAY_NAME[SUBSCRIPT] 数组按照下标