本文主要对Shell中的变量进行简单总结,另外本文所使用的Linux环境为CentOS Linux release 8.1.1911
,所使用的Shell为bash 4.4.19(1)-release
。
Shell编程中的变量一般分为三类:
$n
、$#
、$*
、$@
、$?
、$$
。补充说明:
$n
(除了$0
)又叫位置参数,$0
、$#
、$*
、$@
、$?
、$$
这些又叫特殊参数。参数是存储值的实体。它可以是一个名字,一个数字,或者前面的#
、*
、@
等特殊字符之一。
Shell支持以下三种定义变量的方式:
# variable为变量名,value为变量值
variable=value
variable='value'
variable="value"
注意:在bash中,变量默认类型都是字符串类型,无法直接进行数值运算。
变量定义规则:
=
两侧不能有空格。使用一个定义过的变量,有以下两种语法:
$variable
${variable}
变量名外的花括号{}
是可选的,加花括号是为了帮助解释器识别变量的边界。推荐给所有变量都加上花括号{}
。使用变量示例:
skill变量不加{}
,解释器就会把$skillScript
当成一个变量,可能不是预期结果,加花括号适用于拼接字符串。
使用readonly
命令可以将变量定义为只读变量,只读变量的值不能被改变。语法:
readonly variable
使用unset
命令可以删除变量。变量被删除后不能再次使用,unset
命令不能删除只读变量。语法:
unset variable
有以下示例脚本:
#!/bin/bash
name="zhang san"
echo 'hello! ${name}'
echo "hello! ${name}"
执行结果:
可知单引号与双引号的区别:
命令替换用命令的输出取代命令本身,Shell中完成命令替换有以下两种方式:
# commands是要执行的命令,可以只有一个命令,也可以有多个命令,多个命令之间以分号;分隔
`command`
$(command)
命令替换支持将Shell命令的输出结果赋值给变量:
variable=`command`
variable=$(command)
将命令的输出结果赋值给变量的示例:
#!/bin/bash
mkdir testdir
cd testdir
test_path=`pwd`
touch test_path.txt
echo "test路径:${test_path}" >> test_path.txt
echo $(cat test_path.txt)
执行结果:
如果命令的输出结果包括多行(有换行符),或者含有多个连续的空白符,那么在命令的结果赋值给变量后输出变量时应该将变量用双引号包围,如果直接输出命令的结果,应该将反引号或$()
包围的命令用双引号包围,防止出现格式混乱的情况。示例如下:
反引号看起来像单引号,容易造成混乱,更推荐使用$()
。命令替换可以嵌套,使用反引号形式进行嵌套时,里面的反引号需要用反斜杠转义。以下是$()
嵌套的示例,计算ls
命令列出的第一个文件的行数:
注意$()
仅在Bash中有效,而反引号可在多种Shell中使用。
命令替换会创建一个子Shell来执行对应的命令。如果在Shell脚本中使用命令替换,那么运行该脚本的Shell会创建一个子Shell来执行对应的命令。
用于登录某个虚拟控制器终端或在GUI中运行终端仿真器时所启动的默认的交互Shell可以看成一个Shell父进程。在命令提示符后输入/bin/bash
命令或其他等效的bash
命令时,会创建一个新的Shell进程,这个shell进程被称为Shell子进程(child shell)。Shell子进程也有命令提示符,同样会等待命令输入。有如下示例:
第一个Shell程序是Shell父进程,进程ID为5715,进程ID5754的进程是在Shell父进程中执行的第一个ps -f
命令,执行bash
后创建了一个新的Shell进程,即Shell子进程,进程ID为5755,进程ID为5774的进程是在Shell子进程中执行的第二个ps -f
命令。Shell子进程的父进程ID(PPID)为5715,即Shell父进程的进程ID,说明这个Shell父进程就是该Shell子进程的父进程。
注意:这里的Shell子进程(child shell)与子Shell(subshell)是两个概念,很容易混淆。Shell子进程(child shell)本质上是当前Shell通过执行外部命令启动了新进程,而这个新进程正好是Shell进程罢了,这样的child shell进程是通过执行硬盘上的命令来生成的,只能访问其Shell父进程的环境变量;而真正的子Shell是不需要重新执行硬盘上的外部命令的,全部是内存中的操作,可以访问其父Shell的任何变量。假设当前Shell创建了一个子Shell,子Shell肯定是当前Shell的子进程,当前Shell也被称为该子Shell的父Shell,但当前Shell的子进程不一定都是子Shell,比如当前Shell创建的Shell子进程(child shell)就不是子Shell。
Shell父进程和Shell子进程的关系如下图:
在Shell子进程中也可以继续创建Shell子进程。有如下示例:
根据PPID可以看出Shell进程是层层嵌套。Shell子进程的嵌套关系如下图:
通过exit
命令可以退出Shell子进程,还能用来登出当前的虚拟控制台终端或终端仿真器软件。退出Shell子进程的示例如下:
环境变量对当前Shell进程和所有生成的Shell子进程都可用,而局部变量则只对创建它们的Shell进程可用。局部变量在Shell子进程中不可用。示例如下:
新开一个Shell窗口,局部变量在新的Shell进程中不可用。示例如下:
Shell子进程中定义的局部变量,在退出Shell子进程后,该局部变量就不可用。示例如下:
以上示例说明了局部变量在除定义它的Shell之外的其他Shell中都不可用,只有在定义它的Shell中可用。
环境变量在设定它的Shell进程及其所有Shell子进程中可用,通过export
命令可以将自定义变量导出为环境变量,语法:
# 可以同时导出多个变量
export variable1 variable2
# 可以在定义的同时导出为环境变量
export variable3="abc"
示例如下:
反过来在Shell子进程中设定的环境变量在Shell父进程中不可用。示例如下:
修改Shell子进程中的环境变量并不会影响到Shell父进程中该变量的值。示例如下:
Shell子进程甚至无法使用export
命令改变Shell父进程中环境变量的值。示例如下:
在Shell子进程中删除一个环境变量只对Shell子进程有效,该环境变量在Shell父进程中依然可用。示例如下:
以上可总结为环境变量只能向下传递而不能向上传递,在Shell子进程中对环境变量的改动无法反映到Shell父进程中。
环境变量在设定它的Shell进程及其所有Shell子进程中可用,并没有说它在所有的Shell进程中都可用,也就是说环境变量在与设定它的Shell进程完全没有关联的Shell进程中是不可用的。如果新开了一个Shell窗口,这个新开Shell显然不是当前Shell的Shell子进程,环境变量在这个新的Shell进程中不可用。示例如下:
可以使用env
或printenv
命令查看环境变量,以下是截取的部分环境变量:
查看个别环境变量的值可以使用printenv
命令(变量前不加$
),也可以使用echo
(变量前加$
),不要用env
命令。示例如下:
通过set
命令可以查看所有环境变量、自定义变量和函数。
这里只列出一部分常用系统环境变量:
变量 | 描述 |
---|---|
BASH | 当前Bash实例的全路径 |
BASHPID | 当前Bash进程的PID |
HOME | 当前用户的主目录 |
HOSTNAME | 主机名 |
HOSTTYPE | 主机的架构,如x86_64 |
HISTFILE | 保存Shell历史记录列表的文件名 |
HISTSIZE | 历史文件中的命令数 |
LANG | Shell的语言环境类别 |
OLDPWD | 之前的路径 |
PATH | Shell查找命令的目录列表,由冒号分隔 |
PWD | 当前的路径 |
PS1 | Shell命令行界面的主提示符 |
PS2 | Shell命令行界面的次提示符 |
SHELL | Shell的全路径名 |
SHLVL | Bash进程的嵌套层次,每启动一个新的Bash时该变量都加1 |
系统环境变量基本上都是使用全大写字母,以区别于普通用户的环境变量。自己创建的局部变量一般使用小写字母。变量名区分大小写,自定义变量时使用小写字母,能够避免重新定义系统环境变量可能带来的灾难。
Shell的启动方式有交互式、非交互式和登录、非登录两类:
-l|--login
参数的bash
命令启动的Shell。例如系统启动,远程登录,su -
切换用户,bash --login
命令启动bash。su
切换用户,bash
命令启动bash。判断Shell是否是交互式Shell
方法1,查看变量-
的值值,如果值中包含了字母i
,则表示交互式。在控制台输出-
的值:
包含i
,为交互式。在以下脚本中输出-
的值:
不包含i
,执行脚本的Shell为非交互式。
方法2,查看变量PS1
的值,如果非空,则为交互式,否则为非交互式。在控制台输出PS1
的值:
不为空,为交互式。在以下脚本中输出PS1
的值:
为空,执行脚本的Shell为非交互式。
判断Shell是否是登录Shell
方法1,执行shopt login_shell
,值为on
为登录Shell,off
为非登录Shell。登录控制台后查看,为on
,是登录Shell,bash
命令启动一个新的Shell后再查看,值为off
,这个新启动的Shell为非登录Shell:
方法2,通过exit
退出Shell,输出logout
,表示之前退出的为登录Shell,输出exit
,表示之前退出的为非登录Shell。先在控制台执行bash -l
启动一个Shell,退出后输出logout
,之前启动的为登录Shell,再执行bash
启动一个Shell,退出后输出exit
,之前启动的为非登录Shell:
将命令组合起来,使用
echo $PS1; shopt login_shell
或echo $-; shopt login_shell
同时判断是否交互和登录。
使用由()
包围的组命令或者命令替换进入子Shell时,子Shell会继承父Shell的交互和登录属性。
ssh执行远程命令,但不登录时为非交互式非登录Shell。
执行Shell脚本时(在新启动的Shell进程中执行),为非交互式非登录Shell,指定了--login
时,为非交互式登录Shell。
Bash Shell启动时相关的配置文件有:
/etc/profile
/etc/bashrc
/etc/profile.d/*.sh
~/.bash_profile
~/.bash_login
~/.profile
~/.bashrc
Bash Shell启动时配置文件怎么加载取决于Bash Shell的启动方式。
登录Shell加载配置文件
/etc/profile
文件,/etc/profiles
文件中有加载/etc/profile.d/*.sh
的语句,会加载/etc/profile.d/
下所有可执行的sh
后缀的脚本文件。~/.bash_profile > ~/.bash_login > ~/.profile
这三个文件,仅加载第一个找到的文件,然后该文件会加载~/.bashrc
,~/.bashrc
中又有加载/etc/bashrc
的命令,最后加载/etc/bashrc
。不同的Linux发行版附带的个人配置文件不同,有的可能只有
~/.bash_profile
、~/.bash_login
、~/.profile
这三个中的一个,有的可能三者都有。
交互式非登录Shell加载配置文件
交互式非登录Shell启动时配置文件的加载流程如下图:
不读取/etc/profile
和~/.bash_profile
、~/.bash_login
和~/.profile
,读取~/.bashrc
,~/.bashrc
文件还会加载 /etc/bashrc
,最后再加载/etc/profile.d/*.sh
。
非交互式非登录Shell加载配置文件
执行Shell脚本时一般用的就是这种Shell。Shell启动一个非交互式非登录Shell进程时,会检查环境变量BASH_ENV
来查看要执行的启动文件。如果有指定的文件,Shell会加载该文件,如果没有设置BASH_ENV
,Shell脚本是通过启动一个Shell子进程来执行的,Shell子进程可以继承Shell父进程中的环境变量。对于在当前Shell进程中执行的Shell脚本,因为并没有启动一个新的Shell进程,所以执行脚本时不会加载配置文件,可以直接使用当前Shell中的变量。
通过export
命令导出的环境变量只对当前Shell进程以及所有的子进程可用,如果最顶层的父进程被关闭了,那么环境变量也就失效了。只有将环境变量写入Shell配置文件中才能使该环境变量在所有Shell进程中都可用并且永久性存在,因为每次启动Shell进程都会定义这个变量。
对于普通用户,可以将环境变量写入~/.bashrc
文件,因为除了非交互式非登陆Shell设置了BASH_ENV
并且没有指向~/.bashrc
时,都会加载该文件。也可以将环境变量写入在/etc/profile.d
目录中创建的一个以sh
为后缀的文件里。不建议将新的环境变量写入/etc/profile
,因为升级Linux发行版会使该文件更新,那自定义的环境变量也就没有了。这里以~/.bashrc
为例,执行以下命令持久化保存一个环境变量:
echo "export VAR1=env1" >> ~/.bashrc
# 重载配置文件
source ~/.bashrc
在将环境变量写入~/.bashrc
文件持久化保存后,该环境变量在所有Shell进程中都可用,如下图所示:
$0
代表shell或shell脚本的名称,通常为shell脚本文件名,n≥1时,$n
代表传递给脚本或函数的参数。n是几,表示第几个参数。注意n≥10时,需要写成${n}
,例如${10}
如果写成$10
,则效果会变成$1
的值拼接一个0
。下面创建一个名为test1.sh
的示例Shell脚本,代码如下:
#!/bin/bash
echo "执行的脚本文件名:$0"
echo "传给脚本的第一个参数:$1"
echo "传给脚本的第二个参数:$2"
echo "传给脚本的第六个参数:$6"
echo "传给脚本的第十个参数(不带{}):$10"
echo "传给脚本的第十个参数(带{}):${10}"
# 定义函数
function func1() {
echo "传给函数的第一个参数:$1"
echo "传给函数的第二个参数:$2"
echo "传给函数的第三个参数:$3"
}
# 调用函数
func1 a b c
执行脚本并附带参数:
bash test1.sh aaa bbb ccc ddd eee fff ggg hhh iii jjj
$#
用于获取传递给脚本或函数的参数个数。下面创建一个名为test2.sh
的示例Shell脚本,代码如下:
#!/bin/bash
echo "执行的脚本文件名:$0"
echo "传给脚本的第一个参数:$1"
echo "传给脚本的第二个参数:$2"
echo "传给脚本的第三个参数:$3"
echo "传给脚本的参数个数:$#"
# 定义函数
function func1() {
echo "传给函数的第一个参数:$1"
echo "传给函数的第二个参数:$2"
echo "传给函数的参数个数:$#"
}
# 调用函数
func1 a b c
执行脚本并附带参数:
bash test2.sh aaa bbb ccc ddd
$*
和$@
都表示传递给函数或脚本的所有参数,当$*
和$@
不被双引号""
包围时,都是将接收到的每个参数看做一份数据,一般以空格分隔(以"$1"
"$2"
… "$n"
的形式输出所有参数),当$*
和$@
被双引号""
包围时,"$*"
会将所有的参数从整体上看做一份数据(以"$1 $2 … $n"
的形式输出所有参数),而不是把每个参数都看做一份数据,"$@"
仍然将每个参数都看作一份数据(以"$1"
"$2"
… "$n"
的形式输出所有参数)。下面创建一个名为test3.sh
的示例Shell脚本,代码如下:
#!/bin/bash
echo "执行的脚本文件名:$0"
echo "传给脚本的所有参数(\$*):"$*
echo "传给脚本的所有参数(\$@):"$@
echo "传给脚本的所有参数(\"\$*\"):""$*"
echo "传给脚本的所有参数(\"\$@\"):""$@"
echo "从\$*打印传给脚本的每个参数"
for var in $*
do
echo "${var}"
done
echo "从\$@打印传给脚本的每个参数"
for var in $@
do
echo "${var}"
done
echo "从\"\$*\"打印传给脚本的每个参数"
for var in "$*"
do
echo "${var}"
done
echo "从\"\$@\"打印传给脚本的每个参数"
for var in "$@"
do
echo "${var}"
done
# 定义函数
function func1() {
echo "传给函数的所有参数(\$*):"$*
echo "传给函数的所有参数(\$@):"$@
echo "传给函数的所有参数(\"\$*\"):""$*"
echo "传给函数的所有参数(\"\$@\"):""$@"
echo "从\$*打印传给函数的每个参数"
for var in $*
do
echo "${var}"
done
echo "从\$@打印传给函数的每个参数"
for var in $@
do
echo "${var}"
done
echo "从\"\$*\"打印传给函数的每个参数"
for var in "$*"
do
echo "${var}"
done
echo "从\"\$@\"打印传给函数的每个参数"
for var in "$@"
do
echo "${var}"
done
}
# 调用函数
func1 a b c d e
执行脚本并附带参数:
bash test3.sh 1 3 5 7 9
$?
用于获取上个命令的退出状态,或上一个函数的返回值,一般0代表命令执行成功,非0代表命令执行失败。下面创建一个名为test4.sh
的示例Shell脚本,代码如下:
#!/bin/bash
echo "执行的脚本文件名:$0"
# 判断命令是否执行成功
if [ $? == 0 ]
then
echo "退出状态为0,命令执行成功!"
else
echo "退出状态非0,命令执行失败!"
fi
abc
if [ $? == 0 ]
then
echo "退出状态为0,命令执行成功!"
else
echo "退出状态非0,命令执行失败!"
fi
# 定义函数
function add() {
return $(expr $1 + $2)
}
# 调用函数
add 2 3
echo "函数的返回值:$?"
执行脚本:
bash test4.sh
注意:严格来说,Shell函数中的
return
关键字用来表示函数的退出状态,而不是函数的返回值;Shell没有专门处理返回值的关键字。
$$
用于获取当前Shell进程ID,对于Shell脚本就是脚本所在的进程ID。在子Shell(subshell)中(如命令组合()
),获取的是启动子Shell的Shell进程ID,而不是子Shell的进程ID。下面创建一个名为test5.sh
的示例Shell脚本,代码如下:
#!/bin/bash
echo "执行的脚本文件名:$0"
echo "执行的脚本所在的进程ID:$$"
调用
$()
时会创建一个子Shell来执行对应的命令。按理说在$()
中的命令用$$
获取到的是子Shell的进程ID,但是这种情况获取到的进程ID为启动子Shell进程的Shell进程ID,而不是子Shell的进程ID。