Bash 变量

警告
本文最后更新于 2023-10-22,文中内容可能已过时。

Bash 变量

Bash 没有数据类型的概念,所有的变量值都是字符串。

bash 中还有数组,但是数组不是独立的数据类型。

我们在后面学习《Bash 函数》的时候就会发现,无论是往方法里传参还是获取方法返回值,都无法以数组类型实现。

简介

Bash 变量分成环境变量和自定义变量两类。

环境变量

环境变量是 Bash 环境自带的变量,进入 Shell 时已经定义好了,可以直接使用。它们通常是系统定义好的,也可以由用户从父 Shell 传入子 Shell。

当我们在 shell 脚本中通过.或者source调用另一个脚本的时候,在父脚本中定义的变量,在子脚本中是可以直接使用的

env命令或printenv命令,可以显示所有环境变量。

《Bash 脚本》的env小节中对此命令进行了深入解析

1
2
3
4
5
$ env
# 或者
$ printenv
# 快速查找
$ env | grep search_str

环境变量太多,就不一一列举了,我本地的环境是 Centos 7:

  • HOME:用户的主目录。
  • HOST:当前主机的名称。
  • LANG:字符集以及语言编码,比如zh_CN.UTF-8en_US.UTF-8
  • PATH:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。
  • PWD:当前工作目录。
  • OLDPWD:上一个工作目录。
  • SHELL:Shell 的名字。
  • TERM:终端类型名,即终端仿真器所用的协议。
  • USER:当前用户的用户名。

很多环境变量很少发生变化,而且是只读的,可以视为常量。由于它们的变量名全部都是大写,所以传统上,如果用户要自己定义一个常量,也会使用全部大写的变量名

注意,Bash 变量名区分大小写HOMEhome是两个不同的变量。

查看单个环境变量的值,可以使用printenv命令或echo命令。注意,printenv命令后面的变量名,不用加前缀$

1
2
3
$ printenv PATH
# 或者
$ echo $PATH

自定义变量

自定义变量是用户在当前 Shell 里面自己定义的变量,仅在当前 Shell 可用。一旦退出当前 Shell,该变量就不存在了。

set命令可以显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数。

1
2
3
$ set
# 快速查找
$ set | grep search_str

然后我们能发现很多在env命令中没有出现的变量

  • BASHOPTS:当前 Shell 的参数,可以用shopt命令修改。
  • IFS:词与词之间的分隔符,默认为空格。
  • PS1:Shell 提示符。
  • PS2:输入多行命令时,次要的 Shell 提示符。
  • SHELLOPTS:启动当前 Shell 的set命令的参数,参见《set 命令和 shopt 命令》。

当然还有我们手动在 shell 中创建的变量。

创建变量

用户创建变量的时候,变量名必须遵守下面的规则。

  • 字母、数字和下划线字符组成。
  • 第一个字符必须是一个字母或一个下划线,不能是数字。
  • 不允许出现空格和标点符号。

变量声明的语法如下。

1
variable=value

上面命令中,等号左边是变量名,右边是变量。注意,等号两边不能有空格。因为 Bash 会根据空格来分割命令参数,如果有空格的话,Bash 会把variable理解为一个命令,把=value理解为传递给它的参数。

如果变量的值确实包含空格,则必须将值放在引号中。

1
myvar="hello world"

在《转义和引号》中,我们研究过单引号字符串和双引号字符串,他们都可以赋值给变量

除此之外,我们经常通过子命令拓展来将一个命令的结果赋予一个变量

关于子命令拓展请看《Bash 的模式拓展》

1
files=$(ls)

或者将算数拓展的结果赋予一个变量

1
val=$((2+2))

变量可以重复赋值,后面的赋值会覆盖前面的赋值。

简单实践如下:

1
2
3
4
5
6
my_name='xiahsuo'
echo $my_name
# 需要加括号标识边界,不然 $my_nameaaa 会去找 my_nameaaa 变量
echo ${my_name}aaa bbb
# my_nameaaa 变量不存在,不会报错,只是啥也不输出
echo $my_nameaaa bbb

输出:

1
2
3
4
测试开始
xiahsuo
xiahsuoaaa bbb
bbb

读取变量

读取变量的时候,直接在变量名前加上$就可以了。

1
2
3
$ foo=bar
$ echo $foo
bar

每当 Shell 看到以$开头的单词时,就会尝试读取这个变量名对应的值。

实际上这就是变量名拓展,具体请看《Bash 的模式拓展》

如果变量不存在,Bash 不会报错,而会输出空字符。

$是特殊字符,无法作为普通字符直接输出,如果想要输出$符号本身,需要在前面加上反斜杠进行转义,echo \$,具体请查看《转义和引号》

读取变量的时候,变量名也可以使用花括号{}包围,比如$a也可以写成${a}。这种写法可以用于变量名与其他字符连用的情况。

1
2
3
4
5
6
$ name=xiashuo
# namexxxx 会被当作变量名进行查找,没有找到,所以返回空字符串
$ echo "$namexxxx"

$ echo "${name}xxxx"
xiashuoxxxx

事实上,读取变量的语法$foo,可以看作是${foo}的简写形式。

如果变量的值本身也是变量,可以使用${!varname}的语法,读取最终的值。注意,这个只能多查找一次,如下图示例中,${!name_wrapper_wrapper}返回name,而不是xiahsuo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ name=xiashuo
$ name_wrapper=name
$ name_wrapper_wrapper=name_wrapper
$ echo ${name_wrapper}
name
$ echo ${!name_wrapper}
xiashuo
$ echo $name_wrapper_wrapper
name_wrapper
$ echo ${!name_wrapper_wrapper}
name

如果变量的值包含连续空格(或制表符和换行符),最好将$val放在双引号里面读取,例如echo "$val"

双引号可以保留变量格式,在《转义和引号》中学习过

1
2
3
4
5
$ name="hello      world"
$ echo $name
hello world
$ echo "$name"
hello      world

删除变量

unset命令用来删除一个变量。

也可以用来删除一个函数。例如unset -f functionName

1
unset NAME

这个命令不是很有用。因为不存在的 Bash 变量在查看的时候会输出空字符串,所以即使unset命令删除了变量,还是可以读取这个变量,值为空字符串。

所以,删除一个变量,也可以将这个变量设成空字符串。效果是一样的,😂

1
2
$ foo=''
$ foo=

上面两种写法,都是删除了变量foo。由于不存在的值默认为空字符串,所以后一种写法可以在等号右边不写任何值。一般选第二种,因为简短。

因此,我们可以通过以下脚本判断对象是否存在:

1
2
3
4
# -n  检测字符串长度是否不为 0,不为 0 返回 true。
if [ -n "$var" ];then
    echo "$var"
fi

在父子 shell 中传递变量

什么是子 shell?什么时候产生子 shell?

请查看《Bash 子 shell》

跨进程(shell)中传递变量

用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。为了把变量传递给子 Shell,需要使用export命令。这样输出的变量,对于子 Shell 来说就是环境变量。因此,export命令修饰的变量,变量名最好全大写。表明这是全局变量。

export命令用来向子 Shell 传递变量,因为export声明的是环境变量,不过是临时的环境变量,在当前 Shell 会话中,所有 Shell 实例(包括当前 shell 进程和所有的子 shell 进程)都可以访问由 export 声明的临时环境变量。因为当前 Shell 会话中的所有 Shell 进程,都是当前 Shell 会话的子进程,所以可以与父进程一同访问环境变量。但在关闭当前 shell 会话后 export 声明的环境变量就会失效,比如退出 SSH 登录。

在 shell 脚本中调用另一个 shell 脚本的三种方式

  • .source:这两种方式都不需要考虑变量传递的问题,因为他们没有创建子 shell,而是将指定的脚本内容拷贝至当前的脚本中,由一个 Shell 进程来执行。在 shell 脚本中,我们最常用的就是这种方式。

  • bash或者sh:这种方式实际上就是新开了一个 shell 进程来执行另一个脚本,这就需要考虑父 shell 到子 shell 的变量传递问题,因此需要使用export来传递变量,

export命令可以声明已经存在的变量,同时变量的赋值和export输出也可以在一个步骤中完成。

1
2
3
4
NAME=foo
export NAME
# 或者
export NAME=value

上面命令执行后,当前 Shell 及随后新建的子 Shell,都可以读取变量$NAME。而且子 Shell 如果修改继承的变量,不会影响父 Shell。

简单实践如下:

ts.sh

1
2
3
4
#!/bin/bash
echo $NAME
NAME=1111
echo $NAME

声明变量并测试:

1
2
3
4
5
6
7
8
9
$ NAME=xiashuo
$ export NAME
$ echo $NAME
xiashuo
$ ./ts.sh
xiashuo
1111
$ echo $NAME
xiashuo

可以看到,在执行脚本的时候,脚本中的$NAME变量已经更新了,但是父 shell 中的$NAME值没有变。

特殊变量

Bash 提供一些特殊变量。这些变量的值由 Shell 提供,用户不能进行赋值。

$?

$?为上一个命令的退出码,可用来判断上一个命令是否执行成功。返回值是0,表示上一个命令执行成功;如果不是零,表示上一个命令执行失败。

$?可以获取上一个脚本,上一个方法,上一个命令的退出码

1
2
3
4
5
6
7
8
9
$ cd /aa;echo $?
-bash: cd: /aa: No such file or directory
1
$ cd /opt/;echo $?
0
$ cp aa.txt;echo $?
cp: missing destination file operand after ‘aa.txt’
Try 'cp --help' for more information.
1

关于$?的具体使用,请看《Bash 脚本》的命令 / 脚本执行结果小节

$$

$$为当前 Shell 的进程 ID。

1
2
$ echo $$
10662

经常用此变量作为后缀来命名临时文件。比如日志文件。

1
LOGFILE=/tmp/output_log.$$

大部分人都会直接通过$$来判断是否进入子 shell,其实这样是不准确的,实际上,除了直接执行 bash 命令和 shell 脚本这两种子 shell,其他进入子 shell 的情况都会继承父 shell 中$$变量的值。

关于什么时候会创建子 shell,请看《Bash 子 shell》

$_

$_为上一个命令的最后一个参数。

1
2
3
$ echo xiashuo hello world;echo $_
xiashuo hello world
world

$!

$!为最近一个后台执行的异步命令的进程 ID。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ echo 111 &
[1] 25468
111
$ echo 222 &
[2] 25509
222
[1]   Done                    echo 111
$ echo $!
25509
[2]+  Done                    echo 222

在命令的最后添加&就是将其放到后台执行。

$0

$0为当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时)。

1
2
$ echo $0
-bash

$-

$-为当前 Shell 的启动参数。

1
2
$ echo $-
himBH

$@$#

$#表示脚本的参数数量,$@表示脚本的参数值,具体使用请看《Bash 脚本》

检查变量的值是否为空

Bash 提供四个特殊语法,跟变量的值有关,在变量的值为空或者不为空的时候执行不同的动作。

  • ${varname:-word}:如果变量varname存在且不为空,则返回它的值,否则返回word。它的目的是返回一个默认值,比如echo ${count:-0}表示变量count不存在时返回0

  • ${varname:=word}:如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word。它的目的是设置变量的默认值,比如echo ${count:=0}表示变量count不存在时返回0,且将count设为0

  • ${varname:+word}:如果varname存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在,比如echo ${count:+1}表示变量count存在时返回1(表示true),否则返回空值。

  • ${varname:?message}:如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。如果省略了message,则输出默认的信息“parameter null or not set.”。它的目的是防止变量未定义,这也是我们使用最多的场景,比如${count:?"undefined!"}表示变量count未定义时就中断执行,抛出错误,返回给定的报错信息undefined!

上面四种语法如果用在脚本中,变量名的部分可以用数字19,表示脚本的参数。

1
2
3
filename=${1:?"filename missing."}
# 或者
filename=${1:=default_file}

上面代码出现在脚本中,1表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。

简单实践一下:

主要实践一下${varname:?message},我们一般也就是用这个来在方法中或者子脚本中进行参数的判断,不存在就退出执行。

ts.sh

1
2
3
4
#!/bin/bash
name=
echo name is ${name:?name not exist,shell exit}
echo shell execute

执行:

1
2
$ ./ts.sh
./ts.sh: line 3: name: name not exist,shell exit

会发现脚本中途退出了,在脚本中设置了 name 的值之后

1
2
3
4
#!/bin/bash
name=111
echo name is ${name:?name not exist,shell exit}
echo shell execute

脚本正常执行完了

1
2
3
$ ./ts.sh
name is 111
shell execute

修改ts.sh,检查参数

1
2
3
4
#!/bin/bash
name=${1:?name not exist,shell exit}
age=${2:?age not exist,shell exit}
echo shell execute

可以看到,只有参数填全了,才能正常执行脚本

1
2
3
4
5
6
7
8
9
$ ./ts.sh
./ts.sh: line 2: 1: name not exist,shell exit
# 检查执行结果,不是 0 
$ echo $?
1
$ ./ts.sh xiashuo
./ts.sh: line 3: 2: age not exist,shell exit
$ ./ts.sh xiashuo 12
shell execute

declare 命令

普通变量在使用之前不需要专门声明,直接写varname=value就可以了,declare命令主要用于声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量。

它的语法形式如下。

1
declare OPTION VARIABLE=value

declare命令的主要参数(OPTION)如下。

  • -a:声明数组变量。
  • -A:声明关联数组。
  • -f:输出所有函数定义。
  • -F:输出所有函数名。
  • -i:声明整数变量。
  • -l:声明变量为小写字母。
  • -p:查看变量信息。
  • -r:声明只读变量。
  • -u:声明变量为大写字母。
  • -x:该变量输出为环境变量。

declare命令如果用在函数中,声明的变量只在函数内部有效,等同于local命令。

Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。local命令声明的变量,只在函数体内有效,函数体外没有定义

更多细节,请看《Bash 函数》

不带任何参数时,declare命令输出当前环境的所有变量,包括函数在内,等同于不带有任何参数的set命令。

-a参数

-a参数用于命令声明一个数组

1
2
3
4
5
$ declare -a arr
$ arr[0]=11
$ arr[1]=22
$ echo ${arr[@]}
11 22

关于数组的更多知识,请看《Bash 数组》

-A参数

-A选项就是用于声明一个关联数组。

什么是关联数组?关联数组可以使用任意的字符串、或者整数作为下标来访问数组元素。有点像Map这种数据结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ declare -A asso_arr
$ asso_arr['a']=aaa
$ asso_arr['b']=bbb
$ echo ${asso_arr[@]}
aaa bbb
$ echo ${asso_arr['b']}
bbb
$ echo ${asso_arr['a']}
aaa
$ declare -A site=(["google"]="www.google.com" ["runoob"]="www.runoob.com" ["taobao"]="www.taobao.com")
$ echo ${site['google']}
www.google.com
$ echo ${site['runoob']}
www.runoob.com
$ echo ${site[@]}
www.google.com www.runoob.com www.taobao.com

关于数组的更多知识,请看《Bash 数组》

-i参数

-i参数声明整数变量以后,则会将赋予此变量的表达式的结果理解为数字,如果表达式为数学运算,则会进行数学运算算出一个结果。如果表达式中包含的表达式中包含字符串,则会将字符串理解为数字 0,参与数学计算。    

关于整数的运算,请看《Bash 的算术运算》

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 直接将数学表达式赋值给变量,不会进行数学运行,会将表达式其理解为字符串
$ val=10+20
$ echo $val
10+20
# 如果变量为整形,表达式将会解析为数学表达式进行运算
$ declare -i result
$ result=10+20
$ echo $result
30
$ name=10
$ age=20
$ result=name*age
$ echo $result
200
$ result=name/age
$ echo $result
0
$ result=age/name
$ echo $result
2
# 如果数学表达式中的变量的类型不是数字是字符串,则会被当成0处理,
$ aaa=xiashuo
$ result=age+aaa
$ echo $result
20
$ result=age/aaa
-bash: age/aaa: division by 0 (error token is "aaa")

-x参数

-x参数等同于export命令,可以输出一个变量为子 Shell 的环境变量。

1
2
3
$ declare -x foo
# 等同于
$ export foo

-r参数

-r参数可以声明只读变量,无法改变变量值,也不能unset变量。

效果跟readonly一样

1
2
3
4
5
6
7
8
9
$ declare -r name=12
$ name=13
-bash: name: readonly variable
$ name=
-bash: name: readonly variable
$ ^C
$ unset name
-bash: unset: name: cannot unset: readonly variable
$

-u参数

-u参数声明变量的值全都是大写字母,可以自动把变量值转成大写字母。

1
2
3
$ declare -u nation=china
$ echo $nation
CHINA

-l参数

`-l``参数声明变量的值全都是小写字母,可以自动把变量值转成小写字母。

1
2
3
$ declare -l title="My Name Is XiaShuo " 
$ echo $title
my name is xiashuo

-p参数

-p参数输出变量信息。

declare -p可以输出已定义变量的值,对于未定义的变量,会提示找不到。

如果不提供变量名,declare -p输出所有变量的信息,因为declare命令默认就是输出所有的变量和方法。

1
2
3
4
5
6
$ declare -p name
declare -r name="12"
$ declare -p title
declare -l title="my name is xiashuo "
$ declare -p xx
-bash: declare: xx: not found

-f参数和-F参数

-f参数输出当前环境的所有函数,包括它的定义。-F参数输出当前环境的所有函数名,不包含函数定义。

1
$ declare -f
1
$ declare -F

readonly 命令

readonly命令等同于declare -r,用来声明只读变量,不能改变变量值,也不能unset变量。

readonly命令有三个参数。

  • -f:声明的变量为函数名。
  • -p:打印出所有的只读变量。
    • 这个很好用
  • -a:声明的变量为数组。

简单实践如下

1
2
3
gender="male"
readonly gender
gender="female"

输出:

1
./test.sh:行15: gender: 只读变量

你也可以把readonly和变量的初始化写在一行

1
2
# 你也可以写在一行
readonly age=10

let 命令

let命令声明变量时,可以直接执行算术表达式。let命令的参数表达式如果包含空格,就需要使用引号。

1
2
3
4
5
6
$ let val=1+2
$ echo $val
3
$ let "val = 1 + 5"
$ echo $val
6

let可以同时对多个变量赋值,赋值表达式之间使用空格分隔。

1
2
3
4
5
$ let v1=1 v2=v1++
$ echo v1 v2
v1 v2
$ echo $v1 $v2
2 1

上面例子中,let声明了两个变量v1v2,其中v2等于v1++,表示先返回v1的值,然后v1自增。

这种语法支持的运算符,参考《Bash 的算术运算》一章。

0%