当前位置 博文首页 > RtxTitanV的博客:Shell编程之函数
本文主要对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
、:
、.
、continue
、eval
、exec
、exit
、export
、readonly
、return
、set
、shift
、trap
、unset
。- 重定向:在执行命令之前,它的输入和输出可以使用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
除了
DEBUG
和RETURN
陷阱(trap)不会被继承外,shell执行环境的所有其他方面在函数和其调用者之间都是相同的,除非函数已经使用内置命令declare
赋予了它trace
属性,或者通过内置命令set
启用了-o functrace
选项(在这种情况下,所有函数都会继承DEBUG
和RETURN
陷阱),ERR
陷阱不会被继承,除非启用了shell的-o errtrace
选项。
不传递参数的函数调用的示例如下:
#!/bin/bash
# 函数定义
function func() {
echo "这是一个函数"
}
# 函数调用
func
执行结果:
不能把函数调用放到函数调用之前。示例如下:
#!/bin/bash
# 函数调用
func
# 函数定义
function func() {
echo "这是一个函数"
}
执行结果:
由于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
执行结果:
调用函数时给它传递参数,多个参数之间以空格分隔:
# 函数调用,并向它传递参数
fname param1 param2 param3 ...
当函数被执行时,函数的参数在其执行过程中成为位置参数。扩展为位置参数数量的特殊参数#
会被更新以反映变化。特殊参数0
不变。在函数执行时,变量FUNCNAME
的第一个元素被设置为函数的名称。
位置参数和特殊参数见下表:
位置参数 | 说明 |
---|---|
n(n>=1) | 用除了单个0 以外一个或多个数字表示的参数。位置参数是在shell启动时由其参数赋值的,也可以使用内置命令set 来重新赋值,位置参数不能用赋值语句进行赋值,可以用内置命令set 和shift 来设置和取消它们。当执行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
执行结果:
$*
和$@
会将每个位置参数扩展为单独的单词,会进行单词拆分,"$*"
会扩展为一个包含每个参数的单词,每个参数的值由特殊变量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 "$@"
执行结果:
函数的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}
执行结果:
使用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}"
执行结果:
如果一个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}
执行结果:
函数中的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}
执行结果:
shell使用动态作用域来控制变量在函数中的可见性。在动态作用域内,可见的变量及其值是导致执行到当前函数的一系列函数调用的结果。一个函数看到的变量的值取决于它在其调用者内的值,如果有的话,不管这个调用者是global作用域还是其他shell函数。这也是一个声明为"影子"的local变量的值,也是函数返回时恢复的值。
变量var1
在函数func1
中声明为local变量,func1
调用了函数func2
,变量var2
又在函数func2
中声明为local变量,func2
又调用了函数func3
,那么在func2
和func3
中var1
的值为func1
中local变量var1
的值,在func3
中var2
的值为func2
中local
变量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}"