目录
10.5. 命令行工具的 help 命令、--help 或 -h 参数
本教程分为两篇:
上篇《Shell 基础知识》:必看,每一个开发者都必须了解的命令行基础知识。
下篇《Shell 脚本编程》:建议看,了解 shell 脚本编程基础,能够阅读和编写简单脚本工具。
从 shell 这个概念开始。
Bash 手册这样介绍:
A Unix shell is both a command interpreter and a programming language.
Unix shell 既是命令解释器,也是编程语言。
下面也将从这两个方面进行介绍。
Shell 是系统上的一个应用程序,用于解析用户命令并交给操作系统执行。
在早期操作系统中,用户只能通过敲击命令的方式进行系统操作,shell 在结构上成为用户与操作系统进行交互的接口,所以它也视为操作系统的外壳(单词 shell 的本意)。
Shell 程序有两种工作模式:交互式和非交互式。简单理解,就是对应终端启动和脚本执行。
/etc/shells
文件记录了系统上可使用的 shell 软件,使用 cat
命令查看该文件:
cat /etc/shells # /bin/bash # /bin/csh # /bin/dash # /bin/ksh # /bin/sh # /bin/tcsh # /bin/zsh 复制代码
可以看到,一个操作系统上可能内置了多个 shell 软件,其中最常见的两个是:
/bin/sh
:一个老牌 shell,一般情况下,也是系统的默认 shell。
/bin/bash
: 在 sh 的基础上增加了一些实用特性,是使用最广泛的 shell。
💡 本文内容以 bash 为准。
你也能根据个人喜好安装第三方 shell,比较受欢迎的有 zsh 和 fish,这些也被认为是更现代化的 shell。MacOS 也使用 zsh 作为默认 shell。
💡 关于 shell 的类别及其发展,可以参考 Linux shell 的演进史 。
许多人容易混淆 shell 与终端(terminal)程序。当我们需要执行命令时,直接打开的程序是终端,终端再与 shell 建立会话连接。可以理解为,终端是命令行环境的外壳,负责高亮显示、窗口管理等;shell 则是核心,负责命令的解析执行。这种结构是历史原因导致的,早期的终端是一个连接到计算机主机上的物理设备,现在使用的终端软件,全名是终端模拟器(terminal emulator),它以软件形式模拟了早期的终端设备。如此,尽管硬件结构上完全不同,但 shell 也不用做出太大的变化。
💡 更多概念辨析,可以参考 What is the exact difference between a 'terminal', a 'shell', a 'tty' and a 'console'?
Shell 解析器是支持运行脚本文件的,shell 一词也被用于指代它所支持的语言。
Shell 只能说是脚本语言,严格上不能称为编程语言。与 JS、Python 等高级脚本语言相比,它具有以下特点:
简单,体积小。换个角度看,就是低级,不方便。
Unix / Linux 内置,几乎没有环境依赖。
擅长系统操作密集任务,无法胜任计算密集的任务。
Shell 最大的优势体现在无环境依赖,如果无法保证执行环境或者不想增加环境依赖,就必须使用 shell 脚本,比如编写部署脚本时,难以保证服务器上安装了 Node 或 Python 环境,用 shell 更合适。
另外,shell 对系统操作也比较友好,很适合一些涉及操作系统的任务自动化。许多系统工具也是使用 shell 编写。
不过,更多时候,编程语言会是一个更好的选择。因为 shell 没有方便的工具库,意味着需要写更多的代码。而且规模管理糟糕,不适合复杂的程序。
命令(command)是 shell 最重要的单元。
一般地,命令可以分为以下五种类型:
可执行文件。
别名。
shell 内部命令(built-in)。
shell 函数。
保留字,如 if。
type
命令可以查看命令属于什么类型,也可以用于查看本机上是否存在该命令:
type ls # ls is /bin/ls type if # if is a shell keyword type type # type is a shell builtin 复制代码
以一个简单命令为例:
echo *.txt 复制代码
(交互式情况下) 接收用户输入,直到检测到用户输入回车。
解析收到的命令。
以空格为分隔符,识别到 echo
和 *.txt
两个单词(word)。
解析第一个单词。这里,echo
不属于变量赋值和重定向符号,标记为命令。
命令之后的单词 *.txt
识别为参数。
对参数 *.txt
进行展开。没有引号包裹,可以执行各种展开(见后面章节)。这里,只有文件展开对 *.txt
生效,假设当前目录下有文件 foot.txt 和 bar.txt,那么展开结果为 foot.txt bar.txt
。
查找命令。 echo
不含有斜线/
,说明不是文件形式,进一步查找到内置命令echo
。查找顺序是由内到外的:运行环境中的函数或别名、shell 中的内置命令、PATH
变量路径集合下的外部命令。
执行命令。调用命令,传递展开后的参数,执行 echo(foot.txt, bar.txt)
。
获取命令执行结果。输出 foot.txt bar.txt
。
当然,实际解析规则和执行过程比这个要复杂得多,上面忽略了一些边缘情况。
语法上,命令之后都会被解析为参数,默认以空格为分隔符。可以简单表示成:
command [arg1 [arg2] ... [argN]] 复制代码
不过,我们习以为常的以 -
或 --
开头的选项,并不属于 shell 的语法,而是规范上的内容,常见的有 POSIX 规范 和 GNU 规范。
根据这些规范,大概可以总结成以下几点:
参数之间用空白隔开。
以 -
或 --
开头的参数称为选项(option),其它称为非选项(non-option)或操作数。
长短上,选项可以分为长选项和短选项。长选项以 --long-opt
的格式,短选项以 -o
的格式。
结构类型上,选项可以分为标志型和键-值对型。键-值对的写法有 --key value
,--key=value
短选项可以组合,-ab
等同 -a -b
。短选项与值直接的空白是可选的,-afoo
等同 -a foo
。
选项之间顺序无关,一般按字母排序。
选项一般放在非选项之前。--
用于表示所有选项结束,后面都是非选项,比如 --foo -- --bar
,--foo
是选项,--bar
是非选项。
不同的命令行工具采用的规范可能不同,上面的规则并不通用,只是传统规约。
命令执行退出时会带有一个状态码(exit status code),范围为 0-255,表示命令执行成功与否,0 表示执行成功,非零表示执行失败。
变量 $?
记录了上一条命令的状态码。
false # 命令 true 和 false 单纯返回状态码 0 和 1 echo $? # 1 复制代码
参考 git 手册的 git push
语法说明写法:
git push [--all | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=] [--repo= ] [-f | --force] [-d | --delete] [--prune] [-v | --verbose] [-u | --set-upstream] [-o | --push-option= ] [--[no-]signed|--signed=(true|false|if-asked)] [--force-with-lease[= [: ]] [--force-if-includes]] [--no-verify] [ [ …]] 复制代码
其中用到了许多具有特定意义的符号:
[]
表示可选的部分,可以嵌套。
|
表示左右两边互斥。
< >
表示需要被实际内容替换的部分。
...
表示可以存在多个值。
了解这些可以帮助我们快速看懂命令的语法表示。自己写文档时也可以使用,提升文档的规范性。
命令运算符可以把多个命令拼接成一个命令,形成组合命令。
;
顺序执行命令有两种结束标志: 换行和分号 ;
。 使用 ;
,可以做到在一行内编写多个命令,这也可以实现在终端一次性键入多个命令。
command1; command2; command3 复制代码
需要注意的是,命令的顺序执行,不会因为出错而终止。也就是说,即使上一条命令执行失败了(退出码非 0),后面的命令也会按序执行。
&&
逻辑与&&
把几个命令通过与逻辑组合在一起,只有前面的命令成功执行,才执行后面的命令,是最常用的命令组合方式。
# 只有当目录创建成功时,才切换到该目录 mkdir my-folder && cd my-folder 复制代码
运用 &&
运算符能方便地实现条件执行,类似 if ... then ...
。
||
逻辑或与 &&
相反,||
表示的是或逻辑,只有当前面的命令执行失败时,才执行后面的命令。
结合 &&
与 ||
可以写出 if...else
结构:
true && echo true || echo false # true 复制代码
|
流水线流水线是种 I/O 重定向功能,可以把上一个命令的输出作为下一个命令的输入。
多条命令就好像连接成一条流水线一样,数据像流水线上的产品,经过多次加工处理后最终输出。
流水线经常用于数据转换和处理,它的写法非常直观便捷,在许多其它语言上也有流水线的影子。
# history 返回数百条用户历史命令 # grep 匹配出只带有"echo"单词的历史 # less 会将过滤后的历史以滚动查看的方式展示 history | grep "echo" | less 复制代码
💡 更多流水线的内容,可以参看 Bash pipe tutorial。
&
后台执行在命令后添加运算符 &
表示启动一个子 shell 进程在后台异步执行这个命令,结果输出到当前 shell。
&
也可以拼接命令。
command1 & command2 & command3 # 命令 1,2 在后台运行,3 在前台运行 复制代码
这种形式可以用来同时启动多个任务。
{}
代码块{ command1; command2; command3 } 复制代码
代码块可以把几个代码放到一个相同的执行上下文中。不过,这个并不影响变量作用域,也就是没有块作用域。
代码块用得不多,一般见于函数声明处。还有一个场景是实现多条命令的重定向:
{ echo "file content: "; cat source_file } > target_file 复制代码
如果去掉大括号,重定向的优先级更高,只会影响 cat
命令。
在使用命令行时,有一些快捷技巧可以提高效率。
💡 这一节的内容只适用于终端环境,不要在脚本中使用。
所谓行编辑(command line editing),是指命令行支持的一些编辑快捷键。
Bash 的行编辑是借助 Readline 工具库实现的,支持 emacs (默认)和 vi 两种风格,这里只讨论前者。
这里列一些个人认为比较实用的快捷键:
Tab
: 自动补全,支持文件、命令、参数、用户名、主机名等。两次 Tab 可列出所有可选的自动补全项。
Ctrl + A/E
: 移动到行首/尾。
Ctrl + U/K
: 清除光标位置到行首/尾的字符。
Ctrl + C
: 中止正在执行的命令。
Ctrl + L
: 清空 shell 打印内容。同命令 clear。
Ctrl + D
: 关闭 shell 会话。
💡 更多快捷键可以参考 Bash 手册 Command Line Editing (Bash Reference Manual)。
Bash 会记录用户执行过的历史命令,保存在 ~/.bash_history
中,默认保存最近 500 条。
history
命令可以查看历史命令。
历史命令可以方便重复执行。最常用的是上下方向键浏览之前的命令。除此之外,利用 !
运算符的历史展开(history expansion)功能,可以快速选取特定命令执行。
!!
: 指代上一条命令。
!-n
: 指代前 n 条命令,比如 !-1
即表示 !!
。
!n
: 指代 history
列出的命令中行号为 n 的命令。
除了命令,还能指代上一条命令的参数:
!$
: 上一个命令的最后一个参数。
!*
: 上一个命令的所有参数。
mkdir long-dir-name cd !* # 回车后,展开为 cd long-dir-name 复制代码
还有一个非常实用的功能:根据关键字查找最近执行的命令,称为 reverse-i-search。按下快捷键 Ctrl + R
,出现提示后,输入关键字,会匹配出历史中最近的一个命令。此时,回车可以立即执行,再按 Ctrl + R
会继续向上搜索。
别名(alias)可以把一个命令(及其一部分参数)定义为一个新命令。利用别名,用户简化一些常用的命令,大大减少常用命令的键击。
alias
命令用于创建别名:
alias ll='ls -al' # ll 会被替换成 ls -al 执行 ll # ls 原来的参数也可以正常支持 ll -d my-dir 复制代码
alias
创建的别名只在当前会话有效,重启终端后,别名就不存在了。如果希望创建一个持久化的别名,可以在 shell 的配置文件中加入别名声明。bash 的配置文件是 ~/.bashrc。
~/.bashrc
# ... # Aliases # alias alias_name="command_to_run" # Long format list alias ll="ls -al" 复制代码
每次启动时,shell 都会读取该配置进行初始化,这些别名就可以使用了。
Shell 不存在数据类型(有数组),只有字符串一种值。
有多种方式可以表示字符串:
无引号:简单情况下,字符串内不含有空白时不需要引号,因为空白会被识别成分隔符。
双引号:除了 $
(变量展开), ``(命令替换) 和
`(转义)仍然有特殊功能,其它都被解析为普通字符。
单引号:纯字符串,各种字符都会变成普通字符。
普通变量无需声明,使用时直接赋值即可。
variable=value # 注意 = 左右没有空格 复制代码
使用命令替换语法能把命令的输出赋给变量:
# 把 ls 的输出结果赋给 files files=`ls` 复制代码
变量前加美元符号,${variable}
表示取对应的变量值,其中大括号在不导致歧义时是可省略的。
echo $files echo "${files}_end" # 这里大括号是必须的 复制代码
变量的作用域可以分成三类:
环境变量:能在当前 shell 及其子 shell 中使用,使用 declare -x
或 export
导出。
全局变量:只能在当前 shell 进程内使用,默认。
局部变量:只能在函数内使用,使用命令 local
声明。
declare
命令变量除了保存值以外,还可能绑定某些属性,比如 只读、只能存储数值、作用域。
declare
命令可以赋予变量一些特殊的属性。
declare -r CONST_INT=2 # 设置只读变量,同 readonly 命令声明的变量 declare -i a_int=3 # 数字类型变量 declare -x ENV_VAR=value # 设置为环境变量 复制代码
尽管这看起来像是变量声明,不过也可以作用于已有变量。
var=val declare -r var 复制代码
set
与 unset
命令当一个变量被赋值,就称为被 set 的。
set
命令在不接参数会输出所有的变量。使用 unset
命令可以删除变量。
temp_var=temp_val set|grep temp # temp_var=temp_val unset temp_var set|grep temp # nothing 复制代码
Shell 使用一些位置变量和特殊变量来表示命令及其参数相关的值。
变量 | 含义及说明 |
---|---|
$0 | 命令行下表示用户当前的 shell;脚本内表示执行的脚本名称。 |
$N (N>0) | 表示执行脚本或函数时的第 N 个参数。N>9 时用 ${N} 表示。 |
$# | 执行脚本或函数时的参数个数。 |
$@ | 执行脚本时的参数。"$@" 等效于 "$1" "$2" ... "$N" |
$* | 执行脚本时的参数。"$*" 等效于 "$1 $2 ... $N" ,是一个整体 |
$? | 上一命令的退出状态码 |
$@
和 $*
不被双引号包裹时,没有区别。只有在双引号内并且执行 分割 的上下文中才会有差别。
echo-arguments.sh
#!/bin/bash echo "Use \$*:" for arg in "$*" do echo "Hello $arg" done echo "Use \$@:" for arg in "$@" do echo "Hello $arg" done 复制代码
输出结果为:
Use $*: Hello JS shell Python Use $@: Hello JS Hello shell Hello Python 复制代码
变量语法 ${variable}
其实是变量展开的基本形式,还有一些特殊的展开形式,比如:
${#variable}
: 展开为变量的内容长度或数组的长度。
${variable:-default}
:为变量设置默认值,当变量内容为空时,展开为默认值。
${variable:offset:length}
: 字符串或数组切片。
这部分在脚本写作中使用较多,具体展开规则请查看 bash 手册。
Shell 在启动时,会读取系统中的配置文件,设置一系列的环境变量,程序在运行时可以通过环境变量获取一些运行时的配置信息。
💡 这篇文章 阐述了 shell 启动时都会加载哪些配置。
可以通过 printenv 命令查看环境变量:
printenv PATH # /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/puppetlabs/bin 复制代码
💡 PATH 变量记录了一组目录,当 shell 解析到一个外部命令时,会到这些目录下查找对应的可执行文件。
也可以直接 echo $name
查看。
有几种方式可以设置环境变量:
a) 修改启动时的配置文件,对系统/用户永久生效(不同的 shell 配置文件有所不同)。
# ~/.bashrc # ... USER_ENV="HELLO" 复制代码
b) 使用 export
命令,仅当前会话有效。
export MODE=production # 同 declare -x MODE=production 复制代码
c) 在执行命令前设置,仅对该命令有效。
VAR1=V1 VAR2=V2 command arguments 复制代码
执行脚本或者 bash 命令时,会创建一个子 shell,子 shell 会继承父 shell 的环境变量(不包括普通变量),子 shell 中设置的环境变量不会影响到父 shell。
💡 参看 bash 中的子 shell 机制
在命令执行前,shell 会先对命令进行展开,即把命令中的特殊模式替换成实际的内容。按顺序依次进行:
大括号展开:ab{c?, d*, ef}g
展开为 abc?g abd*g abefg
变量展开:${var}
展开为对应变量值
算术展开:$(( expression ))
展开为表达式计算后的值。
命令替换:$(command)
或者 command
展开为命令执行后的输出。
单词分割:把上面的结果根据环境变量 IFS 分割成多个单词,默认使用空白。
文件名展开:含有字符 * ? []
的部分会被认为是文件名模式,展开为匹配的文件名(见下)。
在双引号内,只有 $
和 ``` 还有特殊作用,所以只有变量展开、算术展开和命令替换还有效。
如果想把含特殊符号的参数传递给命令,可以转义或者使用引号。比如,git add 有这样一段说明:
Adds content from all *.txt files under Documentation directory and its subdirectories:
$ git add Documentation/\*.txt 复制代码Note that the asterisk * is quoted from the shell in this example; this lets the command include the files from subdirectories of Documentation/ directory.
大括号和文件名展开是一种很方便的文件匹配方法,它有一个名称叫 glob。
Glob 在很多语言和工具中都有应用,比如 gitignore 文件,ESLint 配置。
常见的通配符和模式有:
通配符或模式 | 含义和例子 |
---|---|
* | 匹配任意字符串(含空串),但是不能跨越目录层级。 |
** | 匹配任意层级目录。 |
? | 匹配一个字符。 |
[abc] | 匹配中括号内的字符集合中的一个。排除法用 [^abc] 或 [!abc] 。 |
a{b,c*}d | 先展开成模式 abd ,ac*d ,再分别匹配,只要能满足一个就算匹配。 |
glob 和正则表达式容易混淆,二者虽然都是模式匹配的工具,但通配符的含义却是完全不同的。 Glob 是专用于匹配文件名的,而正则是一种更通用的字符串匹配工具。
💡 阮一峰的 命令行通配符教程 进一步说明了这些通配符的使用。
Shell 的标准输入输出包括 stdin 、stdout、stderr,分别对应文件描述符 0,1,2。
使用 >
把命令的输出重定向到文件:
ls > files.txt 复制代码
如果文件不存在,会创建该文件,所以可以用来很方便地创建一个小文件:
echo "{}" > config.json 复制代码
如果文件存在,则会先清空再写入。如果希望保留文件原内容,从文件末添加(append),可以使用 >>
:
ls >> files.txt 复制代码
在 >
前加上文件描述符 2:
ls 2> ls-err.text 复制代码
如果希望同时重定向输出和错误输出,使用 &>:
ls &> files.txt # 同 ls > files 2>&1 复制代码
重定向输入用 <
。输入重定向用得比较少,大部分情况都是直接支持用文件做参数。 下面的 read-print.sh 从标准输入读取输入,并打印
#!/bin/bash read var; echo $var; 复制代码
重定向标准输入,把一个文件内容作为输入:
bash read-print.sh < files.txt 复制代码
Here 文档允许我们把一段字符串作为输入源。语法如下:
command << token # 中间这里是字符串的内容 text ... token 复制代码
其中 token 是一段标识,不固定,收尾一致即可,结束标识必须顶格。Here 文档内部支持变量展开 ($ 仍然具有特殊意义)。
适合用于引用一些带格式的长文本。比如,一段 html 字符串:
title="Simple HTML" content="Hello" # cat 命令默认从标准输入读取内容 cat << _EOF_The title of page:$title $content _EOF_ 复制代码
如果字符串内容较短,可以使用 here 文档的变体 here 字符串:
alias echo-hello="bash read-print.sh <<< 'Hello'" echo-hello # Hello 复制代码
当你遇到问题时,你不一定需要 google,可以先查看一下命令行上的帮助信息。
help
命令Bash 的内置命令 help
能够显示内置命令的用法。不过,只能对内置命令有效,无法查看其它类型的命令用法。
# type 是一个内置命令 help type # type: type [-afptP] name [name ...] # Display information about command type. # ... # ls 是一个可执行文件 /bin/ls help ls # bash: help: no help topics match `ls'. 复制代码
man
命令大部分命令会带有使用手册(manual page),使用手册比较详细地描述个该命令的语法和参数及其作用。
命令 man
可以查看命令的用户手册。
man ls # LS(1) General Commands Manual LS (1) # # NAME # ls – list directory contents # # SYNOPSIS # ls [-@ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...] # # DESCRIPTION # ... 复制代码
第一行展示了该词条所在的区块。手册分为 8 个区块:1) 一般命令;2) 系统调用;3)库函数 ...... 同一词条在不同区块可能有不同含义。
手册的主要内容包括 NAME(名称)、SYNOPSIS(语法)、DESCRIPTION(描述) 等。
💡 了解更多 man 命令细节,可以参看 Linux 命令 man 全知全会。
对内置命令,man
会返回所有的内置命令的说明,不如 help
命令有效。
info
命令man page 是一种过时(但仍然使用广泛)的文档格式,Unix 已经采用能支持超链接的 info 格式来提供帮助文档。如果 man
命令失效,你可以试试 info
,在大多数时候,它们两个都能生效。
查看 info
手册需要使用一些特殊的子命令:
空格键/PgDn
:向下翻页。
PgUp
: 向上翻页。
x
:关闭窗口。
Tab
:跳转到下个超文本链接。
q
:退出。
...
apropos
命令apropos
能够根据关键字搜索出相关的命令。
apropos rename file # ... # mv(1) - move files # ... 复制代码
不过,经常会匹配到太多内容导致难以找到想要的词条,实际体验不如 google。
help
命令、--help
或 -h
参数好的软件总是会提供便利完善的帮助信息。主流的命令行工具,几乎都会提供 help
命令,或者 --help/-h
参数,提供使用说明。困惑的时候,不妨试试这种方法。
# 查看全部帮助信息 git help # 查看子命令帮助信息 git help add git add --help git add -h 复制代码
Bash Reference Manual:Bash 官方手册。
the-art-of-command-line:一份很好的命令行指引,github 107k 星。
awesome-shell: awesome 系列。
explainshell.com:帮助你解读 shell 命令的在线工具。