15. 内建命令
内建命令 builtin 是包含在 Bash 工具集中的命令,又写作 built in。使用内建命令通常出于性能原因或是需要直接存取 shell 内部变量的考量。内建命令执行速度比外部命令的执行速度快,因为外部命令通常需要派生[^1]出一个单独的进程执行。
命令或 shell 本身启动或生成一个子进程用于执行任务的操作被称为派生 forking。新生成的进程被称作子进程,而派生出子进程的进程被称作父进程。当子进程在执行任务时,父进程也仍在运行。
需要注意的是,父进程可以获取子进程的进程ID,并能传递参数给子进程,但反之则不行。该机制会产生一些难以捉摸的问题。
样例 15-1. 脚本生成多个自身实例
#!/bin/bash
# spawn.sh
PIDS=$(pidof sh $0) # 脚本的多个实例的进程ID。
P_array=( $PIDS ) # 将进程ID放置到数组中(为什么?)。
echo $PIDS # 显示父进程和子进程的进程ID。
let "instances = $(#P_array[*]} - 1" # 元素数量减1。
# 为什么要减1?
echo "$instances instance(s) of this script running."
echo "[Hit Ctl-C to exit.]"; echo
sleep 1 # 闲置等待。
sh $0 # 再运行一次。
exit 0 # 这不是必须的;脚本永远不会执行到这里。
# 为什么不是必须的?
# 在键入 Ctl-C 退出脚本后,
#+ 是否所有生成的脚本实例都会终止?
# 如果是,为什么?
# 注意:
# ----
# 注意不要长时间运行这个脚本。
# 它最终会占用大量的系统资源。
# 你认为让脚本生成大量的自身实例
#+ 是否是一个可取的编写脚本的技巧?
# 为什么?通常来说,Bash 的内建命令不会在脚本中通过派生子进程来执行。而在脚本中调用外部系统命令或筛选器通常需要派生子进程。
一些内建命令可能与系统命令重名,但是这些都是在 Bash 内部重新实现后的命令。例如 Bash 中的 echo 命令与系统命令 /bin/echo 的功能基本一致,但是它们本质上并不一样。
关键词 keyword 是保留使用的词汇、标记或运算符。关键词在 shell 中具有特殊意义,是 shell 语法的组成部分。例如 for,while,do 和 ! 都是关键词。关键词与 内建命令 相似,它们都是硬编码到 Bash 中的。但是关键词本身不是命令,而是构成命令的子单位[2]。
输入与输出 I/O
echo
打印一个表达式或变量到标准输出 stdout(参考 样例 4-1)。
echo 命令需要 -e 选项来打印转义字符。参考 样例 5-2。
通常情况下,每一个 echo 命令都会在最后打印一个终端换行符,但使用 -n 选项可以禁止这个行为。
需要注意的是 ``echo command``` 会删除 command` 中生成的所有换行符。
$IFS(内部字段分隔符)变量通常包含 \n(换行符) 作为其空白字符集之一。因此,Bash将换行符把command的输出分割为echo命令参数。然后echo以空格分割,输出这些参数。
那么,我们如何在echo输出的字符串中嵌入换行符呢?
该命令是一个shell内建程序,尽管二者行为类似,但是与
/bin/echo不同。
printf
printf,即格式化打印,该命令是增强版的echo。它是C语言printf()库函数的有限变体,并且语法有些不同。
printf format-string... parameter...
这是/bin/printf或/usr/bin/printf命令的Bash内置版本。请参见(系统命令的)printf man手册以获得更深入的内容。
老版本的Bash可能不支持printf。
样例 15-2. printf在起作用
printf的一个实用场景是格式化错误信息。
另请参阅样例 36-17。
read
从标准输入(stdin)“读取”变量值,即以交互式获取键盘的输入。-a参数可以使read可以获取数组变量(参见样例 27-6)。
样例 15-3. 变量赋值,使用read
当read命令没有相关联的变量接收输入时,它将把输入传递给指定的变量$REPLY。
样例 15-4. 当read没有变量时会发生什么
通常,输入 \ 会忽略输入read的换行符。-r选项会使输入的 \ 进行字面转义。
样例 15-5. 多行输入至read
read命令有一些有趣的选项,允许在不敲击ENTER键的情况下输出提示甚至读取击键。
read的-n选项同样支持检测箭头键以及其他某些不常用的键。
样例 15-6. 检测箭头键
read的
-n选项会不检测ENTER(新行)键。
read的-t选项允许定时输入(参阅样例 9-4和样例 A-41)。
-u选项采用目标文件的文件描述符。
read命令还可以从重定向到标准输入(stdin)的文件中 “读取” 其变量值。如果文件包含多行,则仅将第一行分配给变量。如果read具有多个参数,则每个变量都会被分配一个连续的空格描述字符串。请注意!
样例 15-7. 使用文件重定向的read
然而,正如Bjön Eriksson展示的:
样例 15-8. 从管道中读取发生的问题
gendiff脚本(通常可以在许多Linux发行版上的/usr/bin目录下找到)将find的输出通过管道传输到while read结构中。
文件系统命令
cd
熟悉的cd跳转目录命令可以在脚本中使用,命令执行后将跳转至指定的目录中。
[来自Alan Cox所写先前引用的样例]
cd的-P(物理)选项用于忽略符号链接。
cd -将跳转至$OLDPWD,即之前的工作目录。
当带有两个正斜杠时,cd命令将不能正常工作。
当然,输出应该是 / 。这是命令行和脚本都存在的问题。
pwd
打印工作目录。该命令给出了用户(或者脚本)当前的目录(参阅样例 15-9)。其效果等同于读取内置变量$PWD的值。
pushd, popd, dirs
这个命令集是一种为工作目录添加书签的机制,通过一种有序的方式在目录间来回移动。将目录名推入堆栈用于跟踪。该命令选项允许对目录堆栈进行各种操作。
pushd dir-name将路径dir-name推送到目录堆栈上(堆栈的顶部),同时将当前工作目录更改为dir-name。
popd从目录堆栈中删除(弹出)顶层目录路径名,同时将当前工作目录更改为现在位于堆栈顶层的目录。
dirs列出目录堆栈的内容(与$DIRSTACK变量进行比较)。成功执行的pushd或popd将自动调用dirs。
需要对当前工作目录进行各种更改而不硬编码目录名更改的脚本可以很好地利用这些命令。请注意,隐式$DIRSTACK数组变量(可从脚本中访问)保存目录堆栈的内容。
样例 15-9. 更改当前的工作目录
变量命令
let
let命令用于对变量执行算术运算。[3]在许多情况下,它充当expr的一个简洁版本。
样例 15-10. 让let来做算术运算
在特定的上下文中,let命令可以返回一个惊人的退出状态。
eval
eval arg1 [arg2] ... [argN]
组合表达式或表达式列表中的参数并对其求值。表达式中的任何变量都将被引申含义。最终结果是将字符串转换成命令。
每次调用eval都会强制对其参数重新转义。
样例 15-11. 展示eval的效果
样例 15-12. 使用eval从变量中进行筛选
样例 15-13. 输出命令行参数
样例 15-14. 强制注销
样例 15-15. rot13的一个版本
这是另一个使用eval来计算复杂表达式的例子,这个例子来自于早期YongYe俄罗斯方块游戏脚本。
eval命令出现在间接引用的旧版本中。
使用eval命令可能具有风险,如果存在合理的替代方案,通常应该避免使用。eval $COMMANDS会执行COMMANDS的内容,其中可能包含诸如rm -rf *这样令人不快的“惊喜”。对陌生人编写的陌生代码进行eval是非常危险的。
set
set命令用于更改内部脚本变量/选项的值。其中一个用途是切换选项标志,来帮助确定脚本的行为。另一个应用是重置脚本认为是命令结果的位置参数(set `command `)。之后,脚本可以解析命令输出的字段。
样例 15-6. 使用带有位置参数的set
还有更多位置参数的故事,等你去探索!
样例 15-17. 反转位置参数
没有任何选项或参数来调用set,,仅会列出了所有已初始化的环境变量和其他变量。
使用带有 -- 参数的set会显式地将变量值赋给位置参数。如果 -- 后没有跟任何变量,则会取消设置位置参数。
样例 15-18. 重新赋值位置参数
unset
unset命令会删除一个shell变量,有效地将其设为空值。请注意该条命令不会影响位置参数。
样例 15-19. “Unset”一个变量
在大多数情况下,未声明的变量和被unset的变量是等效的。但是,${parameter:-default}参数替换构造可以区分两者。
export
export [4] 命令用于为正在运行的脚本或shell的所有子进程提供可用的变量。export命令的一个重要用途是在启动文件中,来进行初始化并使后续用户进程可访问环境变量。
不幸的是,没有办法将导出的变量重新导回父进程中,即调用该命令的脚本或shell中。
样例 15-20. 使用export来将变量传递给嵌入的awk脚本
导出的变量可能需要进行特殊处理。请参见样例 M-2。
declare, typeset
declare和typeset命令指定并且/或限制变量的特性。
readonly
与declare -r相同,将变量设置为只读,或者实际上设置为常量。如果尝试修改该变量将失败,并显示错误信息。这是shell针对C语言const类型限定符的模拟。
getopts
这个强大的工具会解析传递给脚本的命令行参数。这是C程序员所熟悉的getopt外部命令和getopt库函数的Bash模拟。它允许多个选项[[5]](https://tldp.org/LDP/abs/html/internal.html#FTN. AEN9289)和关联参数传递和联结到脚本(例如scriptname -abc -e /usr/local)。
getopts构造使用了两个隐式变量。其中$OPTIND是参数指针(选项索引),$OPTARG (选项参数)是附加到选项的(可选)参数。在声明中,选项名后面的冒号会将该选项标记为关联参数。
getopts构造通常打包在while循环中,该循环一次仅处理一个选项和参数,然后递增隐式$ OPTIND变量来指向下一个。
样例 15-21. 使用getopts来读取传递给脚本的选项 / 参数
脚本行为控制命令
source, . (点命令)
从命令行调用时,此命令将执行脚本。在脚本中,source file-name会读取文件file-name。source一个文件(点命令)会将代码导入脚本,并附加到脚本中(与C程序中的#include指令相同的效果)。最终结果与脚本中实际存在的 “source” 代码行相同。这在多个脚本中使用通用数据文件或函数库的情况下很有用。
样例 15-22. "Include"一个数据文件
上面的样例 15-22中的文件data-file。必须位于相同目录下。
如果被source的文件本身是可执行脚本,则它将运行,然后将控制权返回给调用它的脚本。为此目的,被source的可执行脚本可以使用return来返回。
参数可以 (可选) 作为位置参数传递给被source的文件。
脚本甚至有可能自己source自己,尽管这似乎没有任何实际应用。
样例 15-23. 一个source自己的(无用)脚本
exit
无条件终止脚本。[6]exit命令可以选择接受一个整数作为参数,该参数会作为脚本的退出状态返回给shell。除了最简单的脚本之外,最好用exit 0来结束脚本,这表示运行成功。
如果脚本由于不携带参数的exit终止,则脚本的退出状态是脚本中执行的最后一个命令的退出状态,不取决于exit。等效于exit。
exit命令也可以用于终止子shell。
exec
这个shell内置命令用指定的命令替换当前进程。通常,当shell遇到命令时,它会派生出一个子进程来实际执行该命令。当使用exec内置命令时,shell不会fork,并且exec所携带的命令会替换shell。因此,当在脚本中使用时,它会在exec所携带的命令终止时强制退出脚本。[7]
样例 15-24. exec的效果
样例 15-25. 一个exec自己的脚本
exec还用于重新分配文件描述符。例如,exec <zzz-file会使用文件zzz-file替换标准输入(stdin)。
find命令的
-exec选项和shell内置命令exec不是一个东西。
shopt
此命令允许动态更改shell选项 (参见示例 25-1和示例 25-2)。它经常出现在Bash启动文件中,但在脚本中也有其用途。需要Bash 2.0及以上版本。
caller
将caller命令放在一个函数中会将该函数中caller的状态输出到标准输出(stdout)。
caller命令还可以从另一个被source的脚本中获取caller信息。类似于函数,这是一个“子例程调用(suroutine call)”。
在调试的时候你会发现这个命令很有用。
命令
true
一个返回成功(0)退出状态码的命令,别无他用。
false
一个返回失败退出状态码的命令,别无他用。
type [cmd]
类似于外部命令which,type cmd会标识“cmd”。与which不同,type是Bash内置命令。type的-a选项会标识关键字和内置程序,并定位具有相同名称的系统命令,非常有用。
type命令在用来测试特定命令是否存在这方面非常有用。
hash [cmds]
在shell哈希表[8]中记录指定命令的路径名,这样shell或脚本在后续调用这些命令时就不需要搜索$PATH。当不带参数调用hash时,它将仅仅列出已经被散列的命令。-r选项重置哈希表。
bind
bind内建命令会显示或修改readline[9]绑定键。
help
获取shell内置命令的简短摘要。与whatis相对应,但用于内置命令。在Bash第4版中,help信息获得了极大的扩充。
注记
[1]正如Nathan Coulter所指出的,“虽然分叉一个进程是一个低成本的操作,但在新分叉的子进程中执行一个新程序会增加额外的开销。”
[2]但是,time命令是一个例外,它在Bash官方文档中作为关键字(“保留字”)列出。
[3]请注意let不能用于设置字符串型变量。
[4]Export信息是为了使其在可用于更广的上下文中。另请参见作用域。
[5]选项是充当标志的参数,用于打开或关闭脚本行为。与特定选项相关联的参数说明该选项(标志)所对应的脚本行为发生或不发生。
[6]从技术上讲,exit仅仅终止正在运行的进程(或者shell),而不是父进程。
哈希是一种为存储在表中的数据创建查找关键字的方法。数据本身被“打乱”以创建密钥,这属于许多简单的数学算法(方式或解决办法)中的一种。
哈希的一个优点是速度快。缺点是可能会发生冲突——一个键映射到多个数据项。
关于哈希的其他样例,你可以参阅样例 A-20和样例 A-21。
[9]Bash使用readline库在交互式shell中读取输入。
最后更新于
这有帮助吗?

