当前位置 博文首页 > RtxTitanV的博客:Shell编程之函数

    RtxTitanV的博客:Shell编程之函数

    作者:[db:作者] 时间:2021-07-06 21:41

    本文主要对Shell中的函数进行简单总结,另外本文所使用的Linux环境为CentOS Linux release 8.2.2004,所使用的Shell为bash 5.1.0(1)-release

    一、函数定义

    Shell函数是一种对命令进行分组,把一组命令与单个名称相关联,以便以后使用该名称来执行的方式。它们就像常规命令一样执行。当使用Shell函数名作为简单命令名时,将执行与该函数名相关联的命令列表。Shell函数在当前Shell环境中执行,没有创建新的进程来执行。

    函数定义的语法如下:

    # 写法一
    # function为Shell保留字,是可选的,如果提供了function保留字,()是可选的,可以省略
    # fname为函数名称,在默认模式下,函数名可以是任何不被引用(unquoted)且不包含$的shell单词
    # 当Shell处于POSIX模式下,函数名必须是有效的Shell名称,并且不能与特殊内置命令(special builtins)同名
    # compound-command是函数体,通常是包含在{和}之间的命令列表,但也可以是复合命令
    # 如果使用了function保留字,但没有提供(),则需要使用{}
    # 每当函数名被指定为命令名时函数体就会被执行(函数调用)
    # redirections为重定向,与Shell函数相关联的任何重定向都是在函数执行时执行的
    function fname [()] compound-command [ redirections ]
    # 写法二
    # 没有function保留字,不能省略()
    fname () compound-command [ redirections ]
    

    几个概念解释:

    • Bash的POSIX模式(Bash POSIX Mode):在启动Bash时指定--posix选项或在Bash运行时执行set -o posix,都会使它改变那些与POSIX标准不一致的行为,从而更使Bash更符合POSIX标准。
    • 特殊内置命令:由于历史原因,POSIX标准将一些内置命令归入到特殊类别。当Bash不是在POSIX模式下执行时,这些内置命令的行为与其他Bash内置命令没有区别。
    • POSIX的特殊内置命令如下:break:.continueevalexecexitexportreadonlyreturnsetshifttrapunset
    • 重定向:在执行命令之前,它的输入和输出可以使用Shell解释的特殊符号来重定向。

    注意:由于历史原因,在最常见的用法中,函数体周围的大括号{}与函数体之间必须用空白符或换行符分开。这是因为大括号是保留字并且只有当它们与命令列表用空格或其他shell元字符将隔开时才会被识别为保留字。此外,当使用大括号时,列表必须以分号、&或换行符结束。

    除非出现语法错误或已经存在同名的只读函数,否则函数定义的退出状态为0。

    标准写法(推荐)的函数定义示例如下:

    #!/bin/bash
    
    # 标准写法的函数定义
    function func() {
        echo "这是一个函数"
    }
    

    提供了function保留字,可以省略()。示例如下:

    #!/bin/bash
    
    # 函数定义,提供function保留字,可以省略()
    function func {
        echo "这是一个函数"
    }
    

    没有function保留字,不能省略()。示例如下:

    #!/bin/bash
    
    # 函数定义,没有function保留字,不能省略()
    func() {
       echo "这是一个函数"
    }
    

    函数定义的时候可以嵌套。示例如下:

    #!/bin/bash
    
    function func1() {
        echo "执行函数func1"
        function func2() {
            echo "执行函数func2"
            function func3() {
                echo "执行函数func3"
            }
        }
    }
    

    二、函数调用

    在函数定义之后,可以调用函数,调用函数时可以给它传递参数,也可以不传递,如果不传递参数,直接给出函数名即可:

    # 函数调用,不传递参数
    fname
    

    除了DEBUGRETURN陷阱(trap)不会被继承外,shell执行环境的所有其他方面在函数和其调用者之间都是相同的,除非函数已经使用内置命令declare赋予了它trace属性,或者通过内置命令set启用了-o functrace选项(在这种情况下,所有函数都会继承DEBUGRETURN陷阱),ERR陷阱不会被继承,除非启用了shell的-o errtrace选项。

    不传递参数的函数调用的示例如下:

    #!/bin/bash
    
    # 函数定义
    function func() {
        echo "这是一个函数"
    }
    # 函数调用
    func
    

    执行结果:
    1
    不能把函数调用放到函数调用之前。示例如下:

    #!/bin/bash
    
    # 函数调用
    func
    # 函数定义
    function func() {
        echo "这是一个函数"
    }
    

    执行结果:
    2
    由于Shell脚本是按顺序从上往下执行,函数调用在函数定义之前会先执行函数调用,由于执行func时函数还未定义,Shell会将func当成一个命令,由于在$PATH路径下找不到该命令,所以报func命令找不到的错误。所以函数调用需放在函数调用之后。

    函数定义的时候可以嵌套,调用时应按执行顺序,先调用外层的函数,再依次调用内层函数。示例如下:

    #!/bin/bash
    
    function func1() {
        echo "执行函数func1"
        function func2() {
            echo "执行函数func2"
            function func3() {
                echo "执行函数func3"
            }
        }
    }
    func3
    func2
    func1
    func3
    func2
    func3
    

    执行结果:
    3

    三、函数传参

    调用函数时给它传递参数,多个参数之间以空格分隔:

    # 函数调用,并向它传递参数
    fname param1 param2 param3 ...
    

    当函数被执行时,函数的参数在其执行过程中成为位置参数。扩展为位置参数数量的特殊参数#会被更新以反映变化。特殊参数0不变。在函数执行时,变量FUNCNAME的第一个元素被设置为函数的名称。

    位置参数和特殊参数见下表:

    位置参数说明
    n(n>=1)用除了单个0以外一个或多个数字表示的参数。位置参数是在shell启动时由其参数赋值的,也可以使用内置命令set来重新赋值,位置参数不能用赋值语句进行赋值,可以用内置命令setshift来设置和取消它们。当执行shell函数时,位置参数会被暂时替换为传递给函数的参数。第n个位置参数可以表示为${n},当n只由一个数字组成时,可以表示为$n,当n不只由1个数字组成时,必须表示为${n}
    特殊参数说明
    0($0)shell或shell脚本的名称,通常为shell脚本文件名。
    #($#)位置参数的个数,用十进制表示。
    *($*)从1开始的所有位置参数。当它没出现在双引号内时,每个位置参数都会扩展为一个单独的单词,在执行该操作的上下文环境中,这些单词会进一步进行单词拆分(word splitting)和文件名扩展。当它出现在双引号内时,它会扩展为一个包含每个参数的单词,每个参数的值由特殊变量IFS的第一个字符分隔。如果IFS未设置,则参数之间用空格分隔。如果IFS为空,则将参数连接起来,参数中间不使用分隔符。
    @($@)从1开始的所有位置参数。在执行单词拆分的上下文环境中,将每个位置参数扩展为一个单独的单词;如果不在双引号内,这些单词将会进行单词拆分。在不执行单词拆分的上下文环境中,将每个位置参数扩展为一个单独的单词,每个位置参数之间用空格分隔。当它出现在双引号内,并且进行了单词拆分时,每个参数扩展为一个单独的单词。当没有位置参数时,"$@"$@扩展为空,即它们会被删除。
    ?($?)最近(上一个)前台执行的命令的退出状态。
    $($$)当前shell进程ID。在子Shell(subshell)中(如命令组合()),它是启动子Shell的Shell进程ID,而不是子Shell的进程ID。
    !($!)最近放入后台的作业的进程ID,无论是作为异步命令执行还是使用内置命令bg
    -($-)当前的选项,这些选项是在调用时指定的,或是通过set命令指定的,或是shell本身设置的。

    调用函数时给它传递参数,在函数中使用位置参数来接收传给函数的参数。示例如下:

    #!/bin/bash
    
    # 函数定义
    function func() {
        # 函数执行时,变量FUNCNAME的第一个元素被设置为函数的名称
        echo "执行函数的名称:${FUNCNAME[0]}"
        echo "shell脚本的名称:$0"
        echo "传给函数的第一个参数:$1"
        echo "传给函数的第二个参数:$2"
        echo "传给函数的第六个参数:$6"
        # 位置参数由多于1个数字组成时,必须用{}括起来
        echo "传给函数的第十一个参数:${11}"
        # $11为$1与1拼接的字符串,并不是第十一个参数
        echo "\$11为\$1拼接1,不是第十一个参数:$11"
        echo "传给函数的参数个数:$#"
        echo "传给函数的所有参数:$*"
        echo "传给函数的所有参数:$@"
    }
    # 函数调用,传递参数
    func 0 1 2 3 4 5 6 7 8 9 10
    

    执行结果:
    4
    $*$@会将每个位置参数扩展为单独的单词,会进行单词拆分,"$*"会扩展为一个包含每个参数的单词,每个参数的值由特殊变量IFS的第一个字符分隔,"$@"会将每个位置参数扩展为单独的单词,不会进行单词拆分。示例如下:

    #!/bin/bash
    
    # 函数定义
    function func() {
        echo "传给函数的参数个数为$#"
        echo "从\$*打印传给函数的每个参数"
        for param in $*
        do
            echo "${param}"
        done
    
        echo "从\$@打印传给函数的每个参数"
        for param in $@
        do
            echo "${param}"
    	done
    
        echo "从\"\$*\"打印传给函数的每个参数"
        IFS_OLD=${IFS}
        # 将IFS的值修改为,,只以,作为分隔符
        IFS=,
        # "$*"扩展为一个包含每个参数的单词,每个参数的值由,分隔
        for param in "$*"
        do
            echo "${param}"
        done
        # 将IFS恢复为IFS的默认值
        IFS=${IFS_OLD}
    
        echo "从\"\$@\"打印传给函数的每个参数"
        for param in "$@"
        do
            echo "${param}"
        done
    }
    # 若位置参数为"Bourne Shell" "BourneAgain Shell" "C Shell"这三个
    # $*没在双引号内,每个位置参数都扩展为单独的单词,会进行单词拆分
    # 传给函数的参数个数为6
    func $*
    # 每个位置参数都扩展为单独的单词,$@没在双引号内,会进行单词拆分
    # 传给函数的参数个数为6
    func $@
    # $*在双引号内,会扩展为一个包含每个参数的单词,每个参数的值由特殊变量IFS的第一个字符分隔
    # 传给函数的参数个数为1
    func "$*"
    # 每个位置参数都扩展为单独的单词,$@在双引号内,不会进行单词拆分
    # 传给函数的参数个数为3
    func "$@"
    

    执行结果:
    5

    四、local变量

    函数的local变量可以用内置命令local声明,这些变量只对函数和它所调用的命令可见,这在shell函数调用其他函数时尤为重要。local命令语法如下:

    # local只能在函数中使用。它使得变量名的可见作用域仅限于该函数及其子函数。local命令的选项可以是declare所接受的任何选项
    local [option] name[=value]

    在函数中不用declare定义的变量默认具有全局属性。示例如下:

    #!/bin/bash
    
    function func() {
        # 不用declare定义的变量默认具有全局属性
        var="bash"
    }
    func
    declare -p var
    echo ${var}
    

    执行结果:
    6
    使用local声明的函数的local变量只对该函数和它所调用的命令可见。示例如下:

    #!/bin/bash
    
    function func1() {
        # 声明函数func1的local变量,该变量只对函数func1和它所调用的命令可见
        local var="bash"
        echo "在func1中输出的var的值为${var}"
        # 调用函数func2
        func2
    }
    function func2() {
        echo "在func2中输出的var的值为${var}"
    }
    # 调用函数func1
    func1
    declare -p var
    echo "在函数外输出的var的值为${var}"
    

    执行结果:
    7
    如果一个local变量与在前面的作用域中声明的变量同名,则该local变量就是前面的作用域中声明的变量的"影子"变量。在函数中声明的local变量会隐藏同名的global变量,在函数中引用(references)和赋值会引用(refer)local变量,而global变量不会被修改。当函数返回时,global变量再次可见。

    在函数外定义了一个变量,如果在函数中修改了该变量的值并且没有声明为它为local变量,则该变量在函数调用时会被修改。示例如下:

    #!/bin/bash
    
    var="hello"
    function func() {
        # 在函数中修改变量var的值
        var="world"
        echo ${var}
    }
    echo ${var}
    func
    echo ${var}
    

    执行结果:
    8
    函数中的local变量与前一个作用域中的变量同名时,在函数中声明的local变量会隐藏同名的global变量,修改local变量不会改变global变量的值,当函数返回时,global变量再次可见。示例如下:

    #!/bin/bash
    
    var="hello"
    function func() {
        # 声明函数func的local变量,该变量只对函数func和它所调用的命令可见
        # 该local变量与函数外的变量var同名,会隐藏同名的global变量
        # 在函数中对该local变量赋值不会改变函数外的global变量
        local var="world"
        echo ${var}
    }
    echo ${var}
    func
    echo ${var}
    

    执行结果:
    9
    shell使用动态作用域来控制变量在函数中的可见性。在动态作用域内,可见的变量及其值是导致执行到当前函数的一系列函数调用的结果。一个函数看到的变量的值取决于它在其调用者内的值,如果有的话,不管这个调用者是global作用域还是其他shell函数。这也是一个声明为"影子"的local变量的值,也是函数返回时恢复的值。

    变量var1在函数func1中声明为local变量,func1调用了函数func2,变量var2又在函数func2中声明为local变量,func2又调用了函数func3,那么在func2func3var1的值为func1中local变量var1的值,在func3var2的值为func2local变量var2的值,从而掩盖了任何同名的global变量的值。示例如下:

    #!/bin/bash
    
    function func1() {
        # 声明local变量var1
        local var1="bash"
        echo "在func1中输出的var1的值为${var1}"
        echo "在func1中输出的var2的值为${var2}"
        # 调用函数func2,func2中的var1的值为func1中local变量var1的值
        # 由于func2中声明了local变量var2,func2中的var2的值为其声明的local变量var2的值
        func2
    }
    function func2() {
        # 声明local变量var2
        local var2="python"
        echo "在func2中输出的var1的值为${var1}"
        echo "在func2中输出的var2的值为${var2}"
        # 调用函数func3,func3中的var1的值为func1中local变量var1的值
        # func3中的var2的值为func2中local变量var2的值
        func3
    }
    function func3() {
        echo "在func3中输出的var1的值为${var1}"
        echo "在func3中输出的var2的值为${var2}"
    }
    var1="csh"
    var2="java"
    echo "函数func1调用前输出的var1的值为${var1}"
    echo "函数func1调用前输出的var2的值为${var2}"
    # 调用函数func1,func1中的var2的值为global变量var2的值
    # 由于func1中声明了local变量var1,func1中的var1的值为其声明的local变量var1的值
    func1
    echo "函数func1调用后输出的var1的值为${var1}"