Shell 内建命令

所谓 Shell 内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。

例如,用于进入或者切换目录的 cd 命令,虽然我们一直在使用它,但如果不加以注意很难意识到它与普通命令的性质是不一样的:该命令并不是某个外部文件,只要在 Shell 中你就一定可以运行这个命令。

可以使用 type 来确定一个命令是否是内建命令:
[root@localhost ~]# type cd
cd is a Shell builtin
[root@localhost ~]# type ifconfig
ifconfig is /sbin/ifconfig
由此可见,cd 是一个 Shell 内建命令,而 ifconfig 是一个外部文件,它的位置是/sbin/ifconfig

还记得系统变量 $PATH 吗?$PATH 变量包含的目录中几乎聚集了系统中绝大多数的可执行命令,它们都是外部命令。

通常来说,内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘 I/O,还需要 fork 出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前 Shell 进程的一个函数。

下表列出了 Bash Shell 中直接可用的内建命令。

Bash Shell 内建命令

命令 说明
: 扩展参数列表,执行重定向操作
. 读取并执行指定文件中的命令(在当前 shell 环境中)
alias 为指定命令定义一个别名
bg 将作业以后台模式运行
bind 将键盘序列绑定到一个 readline 函数或宏
break 退出 for、while、select 或 until 循环
builtin 执行指定的 shell 内建命令
caller 返回活动子函数调用的上下文
cd 将当前目录切换为指定的目录
command 执行指定的命令,无需进行通常的 shell 查找
compgen 为指定单词生成可能的补全匹配
complete 显示指定的单词是如何补全的
compopt 修改指定单词的补全选项
continue 继续执行 for、while、select 或 until 循环的下一次迭代
declare 声明一个变量或变量类型。
dirs 显示当前存储目录的列表
disown 从进程作业表中刪除指定的作业
echo 将指定字符串输出到 STDOUT
enable 启用或禁用指定的内建shell命令
eval 将指定的参数拼接成一个命令,然后执行该命令
exec 用指定命令替换 shell 进程
exit 强制 shell 以指定的退出状态码退出
export 设置子 shell 进程可用的变量
fc 从历史记录中选择命令列表
fg 将作业以前台模式运行
getopts 分析指定的位置参数
hash 查找并记住指定命令的全路径名
help 显示帮助文件
history 显示命令历史记录
jobs 列出活动作业
kill 向指定的进程 ID(PID) 发送一个系统信号
let 计算一个数学表达式中的每个参数
local 在函数中创建一个作用域受限的变量
logout 退出登录 shell
mapfile 从 STDIN 读取数据行,并将其加入索引数组
popd 从目录栈中删除记录
printf 使用格式化字符串显示文本
pushd 向目录栈添加一个目录
pwd 显示当前工作目录的路径名
read 从 STDIN 读取一行数据并将其赋给一个变量
readarray 从 STDIN 读取数据行并将其放入索引数组
readonly 从 STDIN 读取一行数据并将其赋给一个不可修改的变量
return 强制函数以某个值退出,这个值可以被调用脚本提取
set 设置并显示环境变量的值和 shell 属性
shift 将位置参数依次向下降一个位置
shopt 打开/关闭控制 shell 可选行为的变量值
source 读取并执行指定文件中的命令(在当前 shell 环境中)
suspend 暂停 Shell 的执行,直到收到一个 SIGCONT 信号
test 基于指定条件返回退出状态码 0 或 1
times 显示累计的用户和系统时间
trap 如果收到了指定的系统信号,执行指定的命令
type 显示指定的单词如果作为命令将会如何被解释
typeset 声明一个变量或变量类型。
ulimit 为系统用户设置指定的资源的上限
umask 为新建的文件和目录设置默认权限
unalias 刪除指定的别名
unset 刪除指定的环境变量或 shell 属性
wait 等待指定的进程完成,并返回退出状态码

Shell alias:给命令创建别名

alisa 用来给命令创建一个别名。若直接输入该命令且不带任何参数,则列出当前 Shell 进程中使用了哪些别名。
现在你应该能理解类似ll这样的命令为什么与ls -l的效果是一样的吧。

下面让我们来看一下有哪些命令被默认创建了别名:
image.png

你看,为了让我们使用方便,Shell 会给某些命令默认创建别名。

使用 alias 命令自定义别名

使用 alias 命令自定义别名的语法格式为:

  1. alias new_name='command'

比如,一般的关机命令是shutdown-h now,写起来比较长,这时可以重新定义一个关机命令,以后就方便多了。

  1. alias myShutdown='shutdown -h now'

再如,通过 date 命令可以获得当前的 UNIX 时间戳,具体写法为date +%s,如果你嫌弃它太长或者不容易记住,那可以给它定义一个别名。

  1. alias timestamp='date +%s'

在《Shell命令替换》一节中,我们使用date +%s计算脚本的运行时间,现在学了 alias,就可以简化代码了。

  1. #!/bin/bash
  2. alias timestamp='date +%s'
  3. begin=`timestamp`
  4. sleep 20s
  5. finish=$(timestamp)
  6. difference=$((finish - begin))
  7. echo "run time: ${difference}s"

运行脚本,20 秒后看到输出结果:
run time: 20s

别名只是临时的
在代码中使用 alias 命令定义的别名只能在当前 Shell 进程中使用,在子进程和其它进程中都不能使用。当前 Shell 进程结束后,别名也随之消失。

要想让别名对所有的 Shell 进程都有效,就得把别名写入 Shell 配置文件。Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,将别名放在配置文件中,那么每次启动进程都会定义这个别名。不知道如何修改配置文件的读者请猛击《Shell配置文件的加载》《编写自己的Shell配置文件》。

使用 unalias 命令删除别名

使用 unalias 内建命令可以删除当前 Shell 进程中的别名。unalias 有两种使用方法:

  • 第一种用法是在命令后跟上某个命令的别名,用于删除指定的别名。
  • 第二种用法是在命令后接-a参数,删除当前 Shell 进程中所有的别名。

同样,这两种方法都是在当前 Shell 进程中生效的。
要想永久删除配置文件中定义的别名,只能进入该文件手动删除。
image.png


Shell echo命令:输出字符串

echo 是一个 Shell 内建命令,用来在终端输出字符串,并在最后默认加上换行符。请看下面的例子:

  1. #!/bin/bash
  2. name="Shell"
  3. url="http://www.yuque.net/shell/"
  4. echo "读者,你好!" #直接输出字符串
  5. echo $url #输出变量
  6. echo "${name}的网址是:${url}" #双引号包围的字符串中可以解析变量
  7. echo '${name}的网址是:${url}' #单引号包围的字符串中不能解析变量

运行结果:
读者,你好!
http://www.yuque.net/shell/
Shell教程的网址是:http://www.yuque.net/shell/
${name}的网址是:${url}

不换行

echo 命令输出结束后默认会换行,如果不希望换行,可以加上-n参数,如下所示:

#!/bin/bash
name="Tom"
age=20
height=175
weight=62
echo -n "${name} is ${age} years old, "
echo -n "${height}cm in height "
echo "and ${weight}kg in weight."
echo "Thank you!"

运行结果:
Tom is 20 years old, 175cm in height and 62kg in weight.
Thank you!

输出转义字符

默认情况下,echo 不会解析以反斜杠\开头的转义字符。比如,\n表示换行,echo 默认会将它作为普通字符对待。请看下面的例子:

[root@localhost ~]# echo "hello \nworld"
hello \nworld

我们可以添加-e参数来让 echo 命令解析转义字符。例如:

[root@localhost ~]# echo -e "hello \nworld"
hello
world

\c 转义字符
有了-e参数,我们也可以使用转义字符\c来强制 echo 命令不换行了。请看下面的例子:

#!/bin/bash
name="Tom"
age=20
height=175
weight=62
echo -e "${name} is ${age} years old, \c"
echo -e "${height}cm in height \c"
echo "and ${weight}kg in weight."
echo "Thank you!"

运行结果:
Tom is 20 years old, 175cm in height and 62kg in weight.
Thank you!


Shell declare和typeset命令:设置变量属性

declare 和 typeset 都是 Shell 内建命令,它们的用法相同,都用来设置变量的属性。不过 typeset 已经被弃用了,建议使用 declare 代替。

declare 命令的用法如下所示:

declare [+/-] [aAfFgilprtux] [变量名=变量值]

其中,-表示设置属性,+表示取消属性,aAfFgilprtux都是具体的选项,它们的含义如下表所示:

选项 含义
-f [name] 列出之前由用户在脚本中定义的函数名称和函数体。
-F [name] 仅列出自定义函数名称。
-g name 在 Shell 函数内部创建全局变量。
-p [name] 显示指定变量的属性和值。
-a name 声明变量为普通数组。
-A name 声明变量为关联数组(支持索引下标为字符串)。
-i name 将变量定义为整数型。
-r name[=value] 将变量定义为只读(不可修改和删除),等价于 readonly name。
-x name[=value] 将变量设置为环境变量,等价于 export name[=value]。

【实例1】将变量声明为整数并进行计算。

#!/bin/bash
declare -i m n ret  #将多个变量声明为整数
m=10
n=30
ret=$m+$n
echo $ret

运行结果:
40


Shell数学计算

如果要执行算术运算(数学计算),就离不开各种运算符号,和其他编程语言类似,Shell 也有很多算术运算符,下面就给大家介绍一下常见的 Shell 算术运算符,如下表所示。

Shell 算术运算符一览表

算术运算符 说明/含义
+、- 加法(或正号)、减法(或负号)
*、/、% 乘法、除法、取余(取模)
** 幂运算
++、— 自增和自减,可以放在变量的前面也可以放在变量的后面
!、&&、|| 逻辑非(取反)、逻辑与(and)、逻辑或(or)
<、<=、>、>= 比较符号(小于、小于等于、大于、大于等于)
==、!=、= 比较符号(相等、不相等;对于字符串,= 也可以表示相当于)
<<、>> 向左移位、向右移位
~、|、 &、^ 按位取反、按位或、按位与、按位异或
=、+=、-=、*=、/=、%= 赋值运算符,例如 a+=1 相当于 a=a+1,a-=1 相当于 a=a-1

但是,Shell 和其它编程语言不同,Shell 不能直接进行算数运算,必须使用数学计算命令,这让初学者感觉很困惑,也让有经验的程序员感觉很奇葩。

下面我们先来看一个反面的例子:

[yuanzi]$ echo 2+8
2+8
[yuanzi]$ a=23
[yuanzi]$ b=$a+55
[yuanzi]$ echo $b
23+55
[yuanzi]$ b=90
[yuanzi]$ c=$a+$b
[yuanzi]$ echo $c
23+90

从上面的运算结果可以看出,默认情况下,Shell 不会直接进行算术运算,而是把+两边的数据(数值或者变量)当做字符串,把+当做字符串连接符,最终的结果是把两个字符串拼接在一起形成一个新的字符串。

这是因为,在 Bash Shell 中,如果不特别指明,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。

换句话说,Bash shell 在默认情况下不会区分变量类型,即使你将整数和小数赋值给变量,它们也会被视为字符串,这一点和大部分的编程语言不同。

这一点我们已在《Shell变量》中提到,读者可以猛击链接回忆。

数学计算命令

要想让数学计算发挥作用,必须使用数学计算命令,Shell 中常用的数学计算命令如下表所示。

Shell 中常用的六种数学计算方式

运算操作符/运算命令 说明
(( )) 用于整数运算,效率很高,推荐使用
let 用于整数运算,和 (()) 类似。
$[] 用于整数运算,不如 (()) 灵活。
expr 可用于整数运算,也可以处理字符串。比较麻烦,需要注意各种细节,不推荐使用。
bc Linux下的一个计算器程序,可以处理整数和小数。Shell 本身只支持整数运算,想计算小数就得使用 bc 这个外部的计算器。
declare -i 将变量定义为整数,然后再进行数学运算时就不会被当做字符串了。功能有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算、自增自减等,所以在实际开发中很少使用。

Shell (( )) 的用法

双小括号 (( )) 的语法格式为:
((表达式))
通俗地讲,就是将数学运算表达式放在(())之间。

表达式可以只有一个,也可以有多个,多个表达式之间以逗号,分隔。对于多个表达式的情况,以最后一个表达式的值作为整个 (( )) 命令的执行结果。

可以使用$获取 (( )) 命令的结果,这和使用$获得变量值是类似的。

表1:(( )) 的用法

运算操作符/运算命令 说明
((a=10+66)
((b=a-15))
((c=a+b))
这种写法可以在计算完成后给变量赋值。以 ((b=a-15)) 为例,即将 a-15 的运算结果赋值给变量 c。

注意,使用变量时不用加$前缀,(( )) 会自动解析变量名。
a=$((10+66)
b=$((a-15))
c=$((a+b))
可以在 (( )) 前面加上$符号获取 (( )) 命令的执行结果,也即获取整个表达式的值。以 c=$((a+b)) 为例,即将 a+b 这个表达式的运算结果赋值给变量 c。

注意,类似 c=((a+b)) 这样的写法是错误的,不加$就不能取得表达式的结果。
((a>7 && b==c)) (( )) 也可以进行逻辑运算,在 if 语句中常会使用逻辑运算。
echo $((a+10)) 需要立即输出表达式的运算结果时,可以在 (( )) 前面加$符号。
((a=3+5, b=a+10)) 对多个表达式同时进行计算。

在 (( )) 中使用变量无需加上$前缀,(( )) 会自动解析变量名,这使得代码更加简洁,也符合程序员的书写习惯


Shell let命令:对整数进行数学运算

et 命令和双小括号 (( )) 的用法是类似的,它们都是用来对整数进行运算,读者已经学习了《Shell (())》,再学习 let 命令就相当简单了。

注意:和双小括号 (( )) 一样,let 命令也只能进行整数运算,不能对小数(浮点数)或者字符串进行运算。

Shell let 命令的语法格式为:
let 表达式
或者
let “表达式”
或者
let ‘表达式’
它们都等价于((表达式))

当表达式中含有 Shell 特殊字符(例如 |)时,需要用双引号" "或者单引号' '将表达式包围起来。

和 (( )) 类似,let 命令也支持一次性计算多个表达式,并且以最后一个表达式的值作为整个 let 命令的执行结果。但是,对于多个表达式之间的分隔符,let 和 (( )) 是有区别的:

  • let 命令以空格来分隔多个表达式;
  • (( )) 以逗号,来分隔多个表达式。

另外还要注意,对于类似let x+y这样的写法,Shell 虽然计算了 x+y 的值,但却将结果丢弃;若不想这样,可以使用let sum=x+y将 x+y 的结果保存在变量 sum 中。

这种情况下 (( )) 显然更加灵活,可以使用$((x+y))来获取 x+y 的结果。请看下面的例子:

[yuanzi]$ a=10 b=20
[yuanzi]$ echo $((a+b))
30
[yuanzi]$ echo let a+b  #错误,echo会把 let a+b作为一个字符串输出
let a+b

Shell let 命令实例演示
【实例1】给变量 i 加 8:

[yuanzi]$ i=2
[yuanzi]$ let i+=8
[yuanzi]$ echo $i
10

let i+=8 等同于 ((i+=8)),但后者效率更高。

【实例2】let 后面可以跟多个表达式。

[yuanzi]$ a=10 b=35
[yuanzi]$ let a+=6 c=a+b  #多个表达式以空格为分隔
[yuanzi]$ echo $a $c
16 51

Shell $[]:对整数进行数学运算

和 (())、let 命令类似,$[] 也只能进行整数运算。

shell $[] 的用法如下:
$[表达式]
$[] 会对表达式进行计算,并取得计算结果。如果表达式中包含了变量,那么你可以加$,也可以不加。

Shell $[] 举例:

yuanliangdeMacBook-Air:~ yuanlang$ echo $[3*5]  #直接输出结算结果
15
yuanliangdeMacBook-Air:~ yuanlang$ echo $[(3+4)*5]  #使用()
35
yuanliangdeMacBook-Air:~ yuanlang$ n=6
yuanliangdeMacBook-Air:~ yuanlang$ m=$[n*2]        #将计算结果赋值给变量
yuanliangdeMacBook-Air:~ yuanlang$ echo $[m+n]
18
yuanliangdeMacBook-Air:~ yuanlang$ echo $[$m&$n]
4
yuanliangdeMacBook-Air:~ yuanlang$ echo $[$m*$n]    #在变量前边加$也是可以的
72
yuanliangdeMacBook-Air:~ yuanlang$ echo $[4*(m+n)]
72

需要注意的是,不能单独使用 $[],必须能够接收 $[] 的计算结果。例如,下面的用法是错误的:

yuanliangdeMacBook-Air:~ yuanlang$ $[3+4]
-bash: 7: command not found
yuanliangdeMacBook-Air:~ yuanlang$ $[m+3]
-bash: 15: command not found
yuanliangdeMacBook-Air:~ yuanlang$

Shell expr命令:对整数进行运算

expr 是 evaluate expressions 的缩写,译为“表达式求值”。shell expr 是一个功能强大,并且比较复杂的命令,它除了可以实现整数计算,还可以结合一些选项对字符串进行处理,例如计算字符串长度、字符串比较、字符串匹配、字符串提取等。

本节只讲解 expr 在整数计算方面的应用,并不涉及字符串处理,有兴趣的读者请自行研究。

Shell expr 对于整数计算的用法为:
**expr 表达式**
expr 对表达式的格式有几点特殊的要求:

  • 出现在表达式中的运算符、数字、变量和小括号的左右两边至少要有一个空格,否则会报错。
  • 有些特殊符号必须用反斜杠\进行转义(屏蔽其特殊含义),比如乘号*和小括号(),如果不用\转义,那么 Shell 会把它们误解为正则表达式中的符号(*对应通配符,()对应分组)。
  • 使用变量时要加$前缀。

【实例1】expr 整数计算简单举例:

[]$ expr 2 +3  #错误:加号和 3 之前没有空格
expr: 语法错误
[]$ expr 2 + 3  #这样才是正确的
5
[]$ expr 4 * 5  #错误:乘号没有转义
expr: 语法错误
[]$ expr 4 \* 5  #使用 \ 转义后才是正确的
20
[]$ expr ( 2 + 3 ) \* 4  #小括号也需要转义
bash: 未预期的符号 `2` 附近有语法错误

[]$ expr \( 2 + 3 \) \* 4  #使用 \ 转义后才是正确的
20
[]$ n=3
[]$ expr n + 2  #使用变量时要加 $
expr: 非整数参数
[]$ expr $n + 2  #加上 $ 才是正确的
5
[]$ m=7
[]$ expr $m \* \( $n + 5 \)
56

以上是直接使用 expr 命令,计算结果会直接输出,如果你希望将计算结果赋值给变量,那么需要将整个表达式用反引号````(位于 Tab 键的上方)包围起来,请看下面的例子。

【实例2】将 expr 的计算结果赋值给变量:

[]$ m=5
[]$ n=`expr $m + 10`
[]$ echo $n
15

Shell declare -i:将变量声明为整数类型

在《shell declare命令》一节中,我们已经讲解了 declare 命令的各种选项,为了让 Shell 进行整数运算,本节我们重点讲解-i选项。

默认情况下,Shell 中每一个变量的值都是字符串(不了解的读者请猛击《Shell变量》),即使你给变量赋值一个数字,它其实也是字符串,所以在进行数学计算时会出错。

使用 declare 命令的-i选项可以将一个变量声明为整数类型,这样在进行数学计算时就不会作为字符串处理了,请看下面的例子:

#!/bin/bash
declare -i m n ret
m=10
n=30
ret=$m+$n
echo $ret
ret=$n/$m
echo $ret

运行结果:
40
3

除了将 m、n 定义为整数,还必须将 ret 定义为整数,如果不这样做,在执行ret=$m+$nret=$n/$m时,Shell 依然会将 m、n 视为字符串。

此外,你也不能写类似echo $m+$n这样的语句,这种情况下 m、n 也会被视为字符串。

总之,除了将参与运算的变量定义为整数,还得将承载结果的变量定义为整数,而且只能用整数类型的变量来承载运算结果,不能直接使用 echo 输出。

和 (())、let、$[] 不同,declare -i的功能非常有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算(比较运算、与运算、或运算、非运算),所以在实际开发中很少使用。