变量是任何一种编程语言都必不可少的组成部分,变量用来存放各种数据。
脚本语言在定义变量时通常不需要指明类型,直接赋值就可以,Shell 变量也遵循这个规则。
在Bash shell中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。
这意味着,Bash shell 在默认情况下不会区分变量类型,即使你将整数和小数赋值给变量,它们也会被视为字符串,这一点和大部分的编程语言不同。例如在C语言或者 C++ 中,变量分为整数、小数、字符串、布尔等多种类型。
当然,如果有必要,你也可以使用 Shell declare 关键字显式定义变量的类型,但在一般情况下没有这个需求,Shell 开发者在编写代码时自行注意值的类型即可。
变量的使用
定义变量
Shell 支持以下三种定义变量的方式:
variable=valuevariable='value'variable="value"
variable 是变量名,value 是赋给变量的值。如果 value 不包含任何空白符(例如空格、Tab 缩进等),那么可以不使用引号;如果 value 包含了空白符,那么就必须使用引号包围起来。使用单引号和使用双引号也是有区别的,稍后我们会详细说明。
注意,赋值号=的周围不能有空格,这可能和你熟悉的大部分编程语言都不一样。
Shell 变量的命名规范和大部分编程语言都一样:
- 变量名由数字、字母、下划线组成;
- 必须以字母或者下划线开头;
- 不能使用 Shell 里的关键字(通过 help 命令可以查看保留关键字)。
变量定义举例:
url=https://www.yuque.com/echo $urlname='yuque'echo $nameauthor="yuanzi"echo $author
使用变量
使用一个定义过的变量,只要在变量名前面加美元符号$即可,如:
author="yuanzi"echo $authorecho ${author}
变量名外面的花括号{ }是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,
比如下面这种情况:
skill="Java"echo "I am good at ${skill}Script"
如果不给 skill 变量加花括号,写成echo "I am good at $skillScript",解释器就会把 $skillScript 当成一个变量(其值为空),代码执行结果就不是我们期望的样子了。
修改变量的值
已定义的变量,可以被重新赋值,如:
url="https://www.yuque.com/"echo ${url}url="https://www.yuque.com/shell/"echo ${url}
第二次对变量赋值时不能在变量名前加$,只有在使用变量时才能加$。
单引号和双引号的区别
前面我们还留下一个疑问,定义变量时,变量的值可以由单引号' '包围,也可以由双引号" "包围,它们到底有什么区别呢?不妨以下面的代码为例来说明:
#!/bin/bashurl="https://www.yuque.com/"website1='yuque:${url}'website2="yuque:${url}"echo $website1echo $website2
运行结果:
yuque:${url}
yuque:https://www.yuque.com/
以单引号' '包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出。这种方式比较适合定义显示纯字符串的情况,即不希望解析变量、命令等的场景。
以双引号" "包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。这种方式比较适合字符串中附带有变量和命令并且想将其解析后再输出的变量定义。
建议:如果变量的内容是数字,那么可以不加引号;如果真的需要原样输出就加单引号;其他没有特别要求的字符串等最好都加上双引号,定义变量时加双引号是最常见的使用场景。
将命令的结果赋值给变量
Shell 也支持将命令的执行结果赋值给变量,常见的有以下两种方式:
variable=`command`variable=$(command)
第一种方式把命令用反引号 (位于 Esc 键的下方)包围起来,反引号和单引号非常相似,容易产生混淆,
所以不推荐使用这种方式;第二种方式把命令用$()包围起来,区分更加明显,所以推荐使用这种方式。
例如,我在 demo 目录中创建了一个名为 log.txt 的文本文件,用来记录我的日常工作。下面的代码中,使用 cat 命令将 log.txt 的内容读取出来,并赋值给一个变量,然后使用 echo 命令输出。
只读变量
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。
下面的例子尝试更改只读变量,结果报错:
#!/bin/bashmyUrl="http://www.yuque.com/"readonly myUrlmyUrl="http://www.yuque.com/shell/"
运行脚本,结果如下:
bash: myUrl: This variable is read only.
删除变量
使用 unset 命令可以删除变量。语法:
unset variable_name
变量被删除后不能再次使用;unset 命令不能删除只读变量。
举个例子:
#!/bin/shmyUrl="http://www.yuque.com/shell/"unset myUrlecho $myUrl
上面的脚本没有任何输出, 而如果是只读变量, 会报错-bash: unset: myUrl: cannot unset: readonly variable
变量的作用域
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 进程中的互不相关性,可在图形界面下同时打开两个 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/bashecho $ab=200
b.sh 的代码如下:
#!/bin/bashecho $b
打开一个 Shell 窗口,输入以下命令:
这三条命令都是在一个进程中执行的,从输出结果可以发现,在 Shell 窗口中以命令行的形式定义的变量 a,
在 a.sh 中有效;在 a.sh 中定义的变量 b,在 b.sh 中也有效,变量 b 的作用范围已经超越了 a.sh。
注意,必须在当前进程中运行 Shell 脚本,不能在新进程中运行 Shell 脚本
Shell 环境变量
全局变量只在当前 Shell 进程中有效,对其它 Shell 进程和子进程都无效。如果使用export命令将全局变量导出,那么它就在所有的子进程中也有效了,这称为环境变量;。
环境变量被创建时所处的 Shell 进程称为父进程,如果在父进程中再创建一个新的进程来执行 Shell 命令,那么这个新的进程被称作 Shell 子进程。当 Shell 子进程产生时,它会继承父进程的环境变量为自己所用,所以说环境变量可从父进程传给子进程。不难理解,环境变量还可以传递给子孙进程。
注意,两个没有父子关系的 Shell 进程是不能传递环境变量的,并且环境变量只能向下传递而不能向上传递,即传子不传父。
创建 Shell 子进程最简单的方式是运行 bash 命令,如图所示。
通过exit命令可以一层一层地退出 Shell。
下面演示一下环境变量的使用:
可以发现,默认情况下,a 在 Shell 子进程中是无效的;使用 export 将 a 导出为环境变量后,在子进程中就可以使用了。
export a这种形式是在定义变量 a 以后再将它导出为环境变量,如果想在定义的同时导出为环境变量,可以写作export a=11。
环境变量也是临时的
通过 export 导出的环境变量只对当前 Shell 进程以及所有的子进程有效,如果最顶层的父进程被关闭了,那么环境变量也就随之消失了,其它的进程也就无法使用了,所以说环境变量也是临时的。
如果我想让一个变量在所有 Shell 进程中都有效,不管它们之间是否存在父子关系,该怎么办呢?
只有将变量写入 Shell 配置文件中才能达到这个目的!Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,如果将变量放在配置文件中,那么每次启动进程都会定义这个变量。
命令赋值给变量
Shell 命令替换是指将命令的输出结果赋值给某个变量。比如,在某个目录中输入 ls 命令可查看当前目录中所有的文件,但如何将输出内容存入某个变量中呢?这就需要使用命令替换了,这也是 Shell 编程中使用非常频繁的功能。
Shell 中有两种方式可以完成命令替换,一种是反引号 ,一种是$(),使用方法如下:
variable=`commands`variable=$(commands)
其中,variable 是变量名,commands 是要执行的命令。
commands 可以只有一个命令,也可以有多个命令,多个命令之间以分号;分隔。
例如,date 命令用来获得当前的系统时间,使用命令替换可以将它的结果赋值给一个变量。
#!/bin/bashbegin_time=`date` #开始时间,使用``替换sleep 20s #休眠20秒finish_time=$(date) #结束时间,使用$()替换echo "Begin time: $begin_time"echo "Finish time: $finish_time"
运行脚本,20 秒后可以看到输出结果:
Begin time: Mon Feb 8 10:23:59 CST 2021
Finish time: Mon Feb 8 10:24:19 CST 2021:
使用 data 命令的%s格式控制符可以得到当前的 UNIX 时间戳,这样就可以直接计算脚本的运行时间了。UNIX 时间戳是指从 1970 年 1 月 1 日 00:00:00 到目前为止的秒数
#!/bin/bashbegin_time=`date +%s` #开始时间,使用``替换sleep 20s #休眠20秒finish_time=$(date +%s) #结束时间,使用$()替换run_time=$((finish_time - begin_time)) #时间差echo "begin time: $begin_time"echo "finish time: $finish_time"echo "run time: ${run_time}s"
运行脚本,20 秒后可以看到输出结果:
begin time: 1555639864
finish time: 1555639884
run time: 20s
注意,如果被替换的命令的输出内容包括多行(也即有换行符),或者含有多个连续的空白符,那么在输出变量时应该将变量用双引号包围,否则系统会使用默认的空白符来填充,这会导致换行无效,以及连续的空白符被压缩成一个。请看下面的代码:
#!/bin/bashLSL=`ls -l`echo $LSL #不使用双引号包围echo "--------------------------" #输出分隔符echo "$LSL" #使用引号包围
运行结果:
再谈反引号和 $()
原则上讲,上面提到的两种变量替换的形式是等价的,可以随意使用;但是,反引号毕竟看起来像单引号,有时候会对查看代码造成困扰,而使用 $() 就相对清晰,能有效避免这种混乱。而且有些情况必须使用 $():$() 支持嵌套,反引号不行。
下面的例子演示了使用计算 ls 命令列出的第一个文件的行数,这里使用了两层嵌套。
[yuanzi.yuque]$ Fir_File_Lines=$(wc -l $(ls | sed -n '1p'))[yuanzi.yuque]$ echo "$Fir_File_Lines"3 a.sh
要注意的是,$() 仅在 Bash Shell 中有效,而反引号可在多种 Shell 中使用。所以这两种命令替换的方式各有特点,究竟选用哪种方式全看个人需求。
