Shell 变量的作用域可以分为三种:
Shell 也支持自定义函数,但是 Shell 函数和 C++、Java、C# 等其他编程语言函数的一个不同点就是:在 Shell 函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果。请看下面的代码:
#!/bin/bash
#定义函数
function func(){
a=99
}
#调用函数
func
#输出函数内部的变量
echo $a
输出结果:
99
a 是在函数内部定义的,但是在函数外部也可以得到它的值,证明它的作用域是全局的,而不是仅限于函数内部。
要想变量的作用域仅限于函数内部,可以在定义时加上local命令,此时该变量就成了局部变量。请看下面的代码:
#!/bin/bash
#定义函数
function func(){
local a=99
}
#调用函数
func
#输出函数内部的变量
echo $a
输出结果为空,表明变量 a 在函数外部无效,是一个局部变量。
Shell 变量的这个特性和 JavaScript 中的变量是类似的。在 JavaScript 函数内部定义的变量,默认也是全局变量,只有加上var关键字,它才会变成局部变量。
所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响。在 Shell 中定义的变量,默认就是全局变量。
想要实际演示全局变量在不同 Shell 进程中的互不相关性,可在图形界面下同时打开两个 Shell,或使用两个终端远程连接到服务器(SSH)。
首先打开一个 Shell 窗口,定义一个变量 a 并赋值为 99,然后打印,这时在同一个 Shell 窗口中是可正确打印变量 a 的值的。然后再打开一个新的 Shell 窗口,同样打印变量 a 的值,但结果却为空,如图 1 所示。
这说明全局变量 a 仅仅在定义它的第一个 Shell 进程中有效,对新的 Shell 进程没有影响。这很好理解,就像小王家和小徐家都有一部电视机(变量名相同),但是同一时刻小王家和小徐家的电视中播放的节目可以是不同的(变量值不同)。
需要强调的是,全局变量的作用范围是当前的 Shell 进程
,而不是当前的 Shell 脚本文件
,它们是不同的概念。打开一个 Shell 窗口就创建了一个 Shell 进程,打开多个 Shell 窗口就创建了多个 Shell 进程,每个 Shell 进程都是独立的,拥有不同的进程 ID。在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效。
例如,现在有两个 Shell 脚本文件,分别是 a.sh 和 b.sh。a.sh 的代码如下:
#!/bin/bash
echo $a
b=200
b.sh 的代码如下:
#!/bin/bash
echo $b
打开一个 Shell 窗口,输入以下命令:
[root@VM-4-17-centos ~]# vim a.sh
[root@VM-4-17-centos ~]# vim b.sh
[root@VM-4-17-centos ~]# a=99
[root@VM-4-17-centos ~]# . ./a.sh
99
[root@VM-4-17-centos ~]# . ./b.sh
200
这三条命令都是在一个进程中执行的,从输出结果可以发现,在 Shell 窗口中以命令行的形式定义的变量 a,在 a.sh 中有效;在 a.sh 中定义的变量 b,在 b.sh 中也有效,变量 b 的作用范围已经超越了 a.sh
。
全局变量只在当前 Shell 进程中有效,对其它 Shell 进程和子进程都无效。如果使用export命令将全局变量导出,那么它就在所有的子进程中也有效了,这称为“环境变量”。
环境变量被创建时所处的 Shell 进程称为父进程,如果在父进程中再创建一个新的进程来执行 Shell 命令,那么这个新的进程被称作 Shell 子进程。当 Shell 子进程产生时,它会继承父进程的环境变量为自己所用,所以说环境变量可从父进程传给子进程。不难理解,环境变量还可以传递给孙进程。
注意,两个没有父子关系的 Shell 进程是不能传递环境变量的,并且环境变量只能向下传递而不能向上传递,即“传子不传父”。
创建 Shell 子进程最简单的方式是运行 bash 命令,如图 2 所示。
下面演示一下环境变量的使用:
[c.biancheng.net]$ a=22 #定义一个全局变量
[c.biancheng.net]$ echo $a #在当前Shell中输出a,成功
22
[c.biancheng.net]$ bash #进入Shell子进程
[c.biancheng.net]$ echo $a #在子进程中输出a,失败
[c.biancheng.net]$ exit #退出Shell子进程,返回上一级Shell
exit
[c.biancheng.net]$ export a #将a导出为环境变量
[c.biancheng.net]$ bash #重新进入Shell子进程
[c.biancheng.net]$ echo $a #在子进程中再次输出a,成功
22
[c.biancheng.net]$ exit #退出Shell子进程
exit
[c.biancheng.net]$ exit #退出父进程,结束整个Shell会话
可以发现,默认情况下,a 在 Shell 子进程中是无效的;使用 export 将 a 导出为环境变量后,在子进程中就可以使用了。
export a这种形式是在定义变量 a 以后再将它导出为环境变量,如果想在定义的同时导出为环境变量,可以写作export a=22。
我们一直强调的是环境变量在 Shell 子进程中有效,并没有说它在所有的 Shell 进程中都有效;如果你通过终端创建了一个新的 Shell 窗口,那它就不是当前 Shell 的子进程,环境变量对这个新的 Shell 进程仍然是无效的。请看下图:
环境变量也是临时的
通过 export 导出的环境变量只对当前 Shell 进程以及所有的子进程有效,如果最顶层的父进程被关闭了,那么环境变量也就随之消失了,其它的进程也就无法使用了,所以说环境变量也是临时的。(通过export导出的环境变量是临时的
)
有读者可能会问,如果我想让一个变量在所有 Shell 进程中都有效,不管它们之间是否存在父子关系,该怎么办呢?
只有将变量写入 Shell 配置文件中才能达到这个目的!Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,如果将变量放在配置文件
中,那么每次启动进程都会定义这个变量。
$ env
# 或者
$ printenv
[root@VM-4-17-centos ~]# set | less
ABRT_DEBUG_LOG=/dev/null
BASH=/bin/bash
BASHOPTS=checkwinsize:cmdhist:expand_aliases:extglob:extquote:force_fignore:histappend:interactive_comments:login_shell:progcomp:promptvars:sourcepath
BASH_ALIASES=()
BASH_ARGC=()
BASH_ARGV=()
BASH_CMDS=()
BASH_COMPLETION_COMPAT_DIR=/etc/bash_completion.d
BASH_LINENO=()
BASH_SOURCE=()
BASH_VERSINFO=([0]="4" [1]="2" [2]="46" [3]="2" [4]="release" [5]="x86_64-redhat-linux-gnu")
BASH_VERSION='4.2.46(2)-release'
COLUMNS=210
DIRSTACK=()
EUID=0
....... 略
Shell 支持以下三种定义变量的方式:
Bash 没有数据类型的概念,所有的变量值都是字符串
。variable 是变量名,value 是赋给变量的值。如果 value 不包含任何空白符(例如空格、Tab 缩进等),那么可以不使用引号;如果 value 包含了空白符,那么就必须使用引号包围起来。使用单引号和使用双引号也是有区别的,稍后我们会详细说明。
注意,赋值号=
的周围不能有空格
,这可能和你熟悉的大部分编程语言都不一样。
Shell 变量的命名规范和大部分编程语言都一样:
下面是一些自定义变量的例子。
a=z # 变量 a 赋值为字符串 z
b="a string" # 变量值包含空格,就必须放在引号里面
c="a string and $b" # 变量值可以引用其他变量的值
d="\t\ta string\n" # 变量值可以使用转义字符
e=$(ls -l foo.txt) # 变量值可以是命令的执行结果
f=$((5 * 7)) # 变量值可以是数学运算的结果
变量可以重复赋值,后面的赋值会覆盖前面的赋值。
$ foo=1
$ foo=2
$ echo $foo
2
上面例子中,变量foo的第二次赋值会覆盖第一次赋值。
读取变量的时候,直接在变量名前加上$就可以了。
$ foo=bar
$ echo $foo
bar
每当 Shell
看到以$
开头的单词时,就会尝试读取这个变量名对应的值。
如果变量不存在,Bash
不会报错,而会输出空字符
。
读取变量的时候,变量名也可以使用花括号{}
包围,比如$a
也可以写成${a}
。这种写法可以用于变量名与其他字符连用的情况。
$ a=foo
$ echo $a_file
$ echo ${a}_file
foo_file
上面代码中,变量名a_file
不会有任何输出,因为 Bash 将其整个解释为变量,而这个变量是不存在的。只有用花括号区分$a
,Bash 才能正确解读。
事实上,读取变量的语法$foo
,可以看作是${foo}
的简写形式。
如果变量的值本身也是变量,可以使用${!varname}
的语法,读取最终的值。
[root@VM-4-17-centos ~]# myvar=USER
[root@VM-4-17-centos ~]# echo ${!myvar}
root
上面的例子中,变量myvar
的值是USER
,${!myvar}
的写法将其展开成最终的值。
如果变量值包含连续空格(或制表符和换行符),最好放在双引号
里面读取。
$ a="1 2 3"
$ echo $a
1 2 3
$ echo "$a"
1 2 3
上面示例中,变量a的值包含两个连续空格。如果直接读取,Shell 会将连续空格合并成一个。只有放在双引号里面读取,才能保持原来的格式。
unset
命令用来删除一个变量。
unset NAME
这个命令不是很有用。因为不存在的 Bash 变量一律等于空字符串,所以即使unset
命令删除了变量,还是可以读取这个变量,值为空字符串。
所以,删除一个变量,也可以将这个变量设成空字符串。
$ foo=''
$ foo=
上面两种写法,都是删除了变量foo
。由于不存在的值默认为空字符串,所以后一种写法可以在等号右边不写任何值。
$?
(功能描述:最后一次执行的命令的返回状态。如果这个变量的值为0,证明上一个命令正确执行;如果这个变量的值为非0(具体是哪个数,由命令自己来决定),则证明上一个命令执行不正确了。)
测试:
[root@VM-4-17-centos shell_program]# echo $?
0
$$
表示当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。
[root@VM-4-17-centos shell-program]# echo $$
10606
$_
为上一个命令的最后一个参数。
$ grep dictionary /usr/share/dict/words
dictionary
$ echo $_
/usr/share/dict/words
$!
为最近一个后台执行的异步命令的进程 ID。
$ firefox &
[1] 11064
$ echo $!
11064
上面例子中,firefox是后台运行的命令,$!返回该命令的进程 ID。
$-
为当前 Shell 的启动参数。
$ echo $-
himBHs
$n
(功能描述: n为数字,$0代表该脚本名称,$1-$9代表第一到第九个参数,十以上的参数,十以上的参数需要用大括号包含,如${10}
)
parameter.sh
如下:
#! /bin/bash
echo '===========$n============' #注意:如果使用的是双引号,$n会被识别为变量
echo script name: $0
echo 1st parameter: $1
echo 2nd parameter: $2
测试:
[root@VM-4-17-centos shell_program]# ./parameter.sh
===========$n============
script name: ./parameter.sh
1st parameter:
2nd parameter:
[root@VM-4-17-centos shell_program]# ./parameter.sh zhx yxy
===========$n============
script name: ./parameter.sh
1st parameter: zhx
2nd parameter: yxy
$#
(功能描述:获取所有输入参数个数,常用于循环,判断参数的个数是否正确以及加强脚本的健壮性)。
parameter.sh
如下:
#! /bin/bash
echo '===========$n============' #注意:如果使用的是双引号,$n会被识别为变量
echo script name: $0
echo 1st parameter: $1
echo 2nd parameter: $2
echo '===========$#============'
echo parameter numbers: $#
测试:
[root@VM-4-17-centos shell_program]# ./parameter.sh zhx yxy
===========$n============
script name: ./parameter.sh
1st parameter: zhx
2nd parameter: yxy
===========$#============
parameter numbers: 2
$*
(功能描述:这个变量代表命令行中所有的参数,$*
把所有的参数看成一个整体
)
$@
(功能描述:这个变量也代表命令行中所有的参数,不过$@
把每个参数区分对待
);被双引号(" ")包含时,与 $* 稍有不同;
parameter.sh
如下:
#! /bin/bash
echo '===========$n============' #注意:如果使用的是双引号,$n会被识别为变量
echo script name: $0
echo 1st parameter: $1
echo 2nd parameter: $2
echo '===========$#============'
echo parameter numbers: $#
echo '===========$*============'
echo $*
echo '===========for i in $*============'
for i in $*
do
echo $i
done
echo '===========for i in "$*"============'
for i in "$*"
do
echo $i
done
echo '===========$@============'
echo $@
echo '===========for i in $@============'
for i in $@
do
echo $i
done
echo '===========for i in "$@"============'
for i in "$@"
do
echo $i
done
测试:
[root@VM-4-17-centos parameter]# ./parameter.sh zhx yxy
===========$n============
script name: ./parameter.sh
1st parameter: zhx
2nd parameter: yxy
===========$#============
parameter numbers: 2
===========$*============
zhx yxy
===========for i in $*============
zhx
yxy
===========for i in "$*"============
zhx yxy
===========$@============
zhx yxy
===========for i in $@============
zhx
yxy
===========for i in "$@"============
zhx
yxy
Bash 提供四个特殊语法,跟变量的默认值有关,目的是保证变量不为空。
${varname:-word}
上面语法的含义是,如果变量varname
存在且不为空,则返回它的值,否则返回word
。它的目的是返回一个默认值,比如**${count:-0}
表示变量count不存在时返回0
。**
${varname:=word}
上面语法的含义是,如果变量varname
存在且不为空,则返回它的值,否则将它设为word
,并且返回word。它的目的是设置变量的默认值,比如${count:=0}
表示变量count不存在时返回0,且将count设为0。
${varname:+word}
上面语法的含义是,如果varname存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在,比如${count:+1}表示变量count存在时返回1(表示true),否则返回空值。
${varname:?message}
上面语法的含义是,**如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。**如果省略了message,则输出默认的信息“parameter null or not set.”。它的目的是防止变量未定义,比如${count:?"undefined!"}
表示变量count
未定义时就中断执行,抛出错误,返回给定的报错信息undefined!
。
上面四种语法如果用在脚本中,变量名的部分可以用数字1到9,表示脚本的参数。
filename=${1:?"filename missing."}
上面代码出现在脚本中,1表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。
declare
命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量。
它的语法形式如下:
declare OPTION VARIABLE=value
declare
命令的主要参数(OPTION)如下。
-a
:声明数组变量。-f
:输出所有函数定义。-F
:输出所有函数名。-i
:声明整数变量。-l
:声明变量为小写字母。-p
:查看变量信息。-r
:声明只读变量。-u
:声明变量为大写字母。-x
:该变量输出为环境变量。declare
命令如果用在函数中,声明的变量只在函数内部
有效,等同于local
命令。
不带任何参数时,declare命令输出当前环境的所有变量,包括函数在内,等同于不带有任何参数的set
命令。
$ declare
readonly
命令等同于declare -r
,用来声明只读变量,不能改变变量值,也不能unset
变量。
$ readonly foo=1
$ foo=2
bash: foo:只读变量
$ echo $?
1
上面例子中,更改只读变量foo会报错,命令执行失败。
readonly命令有三个参数。
-f
:声明的变量为函数名。-p
:打印出所有的只读变量。-a
:声明的变量为数组。let
命令声明变量时,可以直接执行算术表达式。
$ let foo=1+2
$ echo $foo
3
上面例子中,let
命令可以直接计算1 + 2
。
let
命令的参数表达式如果包含空格
,就需要使用引号
。
$ let "foo = 1 + 2"
let
可以同时对多个变量赋值
,赋值表达式之间使用空格分隔
。
$ let "v1 = 1" "v2 = v1++"
$ echo $v1,$v2
2,1
上面例子中,let声明了两个变量v1和v2,其中v2等于v1++,表示先返回v1的值,然后v1自增。