本教程分为两篇:
- 上篇《Shell 基础知识》:必看,每一个开发者都必须了解的命令行基础知识。
- 下篇《Shell 脚本编程》:建议看,了解 shell 脚本编程基础,能够阅读和编写简单脚本工具。
1. 认识 shell
从 shell 这个概念开始。
Bash 手册这样介绍:
A Unix shell is both a command interpreter and a programming language.
Unix shell 既是命令解释器,也是编程语言。
下面也将从这两个方面进行介绍。
1.1. 作为命令解析器
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’?
1.2. 作为编程语言
Shell 解析器是支持运行脚本文件的,shell 一词也被用于指代它所支持的语言。
Shell 只能说是脚本语言,严格上不能称为编程语言。与 JS、Python 等高级脚本语言相比,它具有以下特点:
- 简单,体积小。换个角度看,就是低级,不方便。
- Unix / Linux 内置,几乎没有环境依赖。
- 擅长系统操作密集任务,无法胜任计算密集的任务。
Shell 最大的优势体现在无环境依赖,如果无法保证执行环境或者不想增加环境依赖,就必须使用 shell 脚本,比如编写部署脚本时,难以保证服务器上安装了 Node 或 Python 环境,用 shell 更合适。
另外,shell 对系统操作也比较友好,很适合一些涉及操作系统的任务自动化。许多系统工具也是使用 shell 编写。
不过,更多时候,编程语言会是一个更好的选择。因为 shell 没有方便的工具库,意味着需要写更多的代码。而且规模管理糟糕,不适合复杂的程序。
2. 命令
命令(command)是 shell 最重要的单元。
2.1. 什么是命令
一般地,命令可以分为以下五种类型:
- 可执行文件。
- 别名。
- 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
2.2. 命令的执行过程
以一个简单命令为例:
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
。
当然,实际解析规则和执行过程比这个要复杂得多,上面忽略了一些边缘情况。
2.3. 命令的参数
语法上,命令之后都会被解析为参数,默认以空格为分隔符。可以简单表示成:
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
是非选项。
不同的命令行工具采用的规范可能不同,上面的规则并不通用,只是传统规约。
2.4. 命令的退出状态码
命令执行退出时会带有一个状态码(exit status code),范围为 0-255,表示命令执行成功与否,0 表示执行成功,非零表示执行失败。
变量 $?
记录了上一条命令的状态码。
false # 命令 true 和 false 单纯返回状态码 0 和 1
echo $?
# 1
2.5. 命令的语法文档
参考 git 手册的 git push
语法说明写法:
git push [--all | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
[--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-v | --verbose]
[-u | --set-upstream] [-o <string> | --push-option=<string>]
[--[no-]signed|--signed=(true|false|if-asked)]
[--force-with-lease[=<refname>[:<expect>]] [--force-if-includes]]
[--no-verify] [<repository> [<refspec>…]]
其中用到了许多具有特定意义的符号:
[]
表示可选的部分,可以嵌套。|
表示左右两边互斥。< >
表示需要被实际内容替换的部分。...
表示可以存在多个值。
了解这些可以帮助我们快速看懂命令的语法表示。自己写文档时也可以使用,提升文档的规范性。
3. 组合命令
命令运算符可以把多个命令拼接成一个命令,形成组合命令。
3.1. ;
顺序执行
命令有两种结束标志: 换行和分号 ;
。 使用 ;
,可以做到在一行内编写多个命令,这也可以实现在终端一次性键入多个命令。
command1; command2; command3
需要注意的是,命令的顺序执行,不会因为出错而终止。也就是说,即使上一条命令执行失败了(退出码非 0),后面的命令也会按序执行。
3.2. &&
逻辑与
&&
把几个命令通过与逻辑组合在一起,只有前面的命令成功执行,才执行后面的命令,是最常用的命令组合方式。
# 只有当目录创建成功时,才切换到该目录
mkdir my-folder && cd my-folder
运用 &&
运算符能方便地实现条件执行,类似 if ... then ...
。
3.3. ||
逻辑或
与 &&
相反,||
表示的是或逻辑,只有当前面的命令执行失败时,才执行后面的命令。
结合 &&
与 ||
可以写出 if...else
结构:
true && echo true || echo false
# true
3.4. |
流水线
流水线是种 I/O 重定向功能,可以把上一个命令的输出作为下一个命令的输入。
多条命令就好像连接成一条流水线一样,数据像流水线上的产品,经过多次加工处理后最终输出。
流水线经常用于数据转换和处理,它的写法非常直观便捷,在许多其它语言上也有流水线的影子。
# history 返回数百条用户历史命令
# grep 匹配出只带有"echo"单词的历史
# less 会将过滤后的历史以滚动查看的方式展示
history | grep "echo" | less
💡 更多流水线的内容,可以参看 Bash pipe tutorial。
3.5. &
后台执行
在命令后添加运算符 &
表示启动一个子 shell 进程在后台异步执行这个命令,结果输出到当前 shell。
&
也可以拼接命令。
command1 & command2 & command3 # 命令 1,2 在后台运行,3 在前台运行
这种形式可以用来同时启动多个任务。
3.6. {}
代码块
{ command1; command2; command3 }
代码块可以把几个代码放到一个相同的执行上下文中。不过,这个并不影响变量作用域,也就是没有块作用域。
代码块用得不多,一般见于函数声明处。还有一个场景是实现多条命令的重定向:
{ echo "file content: "; cat source_file } > target_file
如果去掉大括号,重定向的优先级更高,只会影响 cat
命令。
4. 命令行编辑技巧
在使用命令行时,有一些快捷技巧可以提高效率。
💡 这一节的内容只适用于终端环境,不要在脚本中使用。
4.1. 行编辑
所谓行编辑(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)。
4.2. 命令历史
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
会继续向上搜索。
4.3. 命令别名
别名(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 都会读取该配置进行初始化,这些别名就可以使用了。
5. 引号
Shell 不存在数据类型(有数组),只有字符串一种值。
有多种方式可以表示字符串:
- 无引号:简单情况下,字符串内不含有空白时不需要引号,因为空白会被识别成分隔符。
- 双引号:除了
$
(变量展开),`
(命令替换) 和\
(转义)仍然有特殊功能,其它都被解析为普通字符。 - 单引号:纯字符串,各种字符都会变成普通字符。
6. 变量
6.1. 变量赋值
普通变量无需声明,使用时直接赋值即可。
variable=value # 注意 = 左右没有空格
使用命令替换语法能把命令的输出赋给变量:
# 把 ls 的输出结果赋给 files
files=`ls`
6.2. 使用变量
变量前加美元符号,${variable}
表示取对应的变量值,其中大括号在不导致歧义时是可省略的。
echo $files
echo "${files}_end" # 这里大括号是必须的
6.3. 变量的作用域
变量的作用域可以分成三类:
- 环境变量:能在当前 shell 及其子 shell 中使用,使用
declare -x
或export
导出。 - 全局变量:只能在当前 shell 进程内使用,默认。
- 局部变量:只能在函数内使用,使用命令
local
声明。
6.4. declare
命令
变量除了保存值以外,还可能绑定某些属性,比如 只读、只能存储数值、作用域。
declare
命令可以赋予变量一些特殊的属性。
declare -r CONST_INT=2 # 设置只读变量,同 readonly 命令声明的变量
declare -i a_int=3 # 数字类型变量
declare -x ENV_VAR=value # 设置为环境变量
尽管这看起来像是变量声明,不过也可以作用于已有变量。
var=val
declare -r var
6.5. set
与 unset
命令
当一个变量被赋值,就称为被 set 的。
set
命令在不接参数会输出所有的变量。使用 unset
命令可以删除变量。
temp_var=temp_val
set|grep temp
# temp_var=temp_val
unset temp_var
set|grep temp
# nothing
6.6. 位置变量和特殊变量
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
6.7. 变量展开语法
变量语法 ${variable}
其实是变量展开的基本形式,还有一些特殊的展开形式,比如:
${#variable}
: 展开为变量的内容长度或数组的长度。${variable:-default}
:为变量设置默认值,当变量内容为空时,展开为默认值。${variable:offset:length}
: 字符串或数组切片。
这部分在脚本写作中使用较多,具体展开规则请查看 bash 手册。
7. 环境
Shell 在启动时,会读取系统中的配置文件,设置一系列的环境变量,程序在运行时可以通过环境变量获取一些运行时的配置信息。
💡 这篇文章 阐述了 shell 启动时都会加载哪些配置。
7.1. 查看环境变量
可以通过 printenv 命令查看环境变量:
printenv PATH
# /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/puppetlabs/bin
💡 PATH 变量记录了一组目录,当 shell 解析到一个外部命令时,会到这些目录下查找对应的可执行文件。
也可以直接 echo $name
查看。
7.2. 设置环境变量
有几种方式可以设置环境变量:
a) 修改启动时的配置文件,对系统/用户永久生效(不同的 shell 配置文件有所不同)。
# ~/.bashrc
# ...
USER_ENV="HELLO"
b) 使用 export
命令,仅当前会话有效。
export MODE=production # 同 declare -x MODE=production
c) 在执行命令前设置,仅对该命令有效。
VAR1=V1 VAR2=V2 command arguments
7.3. 子 shell
执行脚本或者 bash 命令时,会创建一个子 shell,子 shell 会继承父 shell 的环境变量(不包括普通变量),子 shell 中设置的环境变量不会影响到父 shell。
💡 参看 bash 中的子 shell 机制
8. 展开
在命令执行前,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.
8.1. Glob 模式
大括号和文件名展开是一种很方便的文件匹配方法,它有一个名称叫 glob。
Glob 在很多语言和工具中都有应用,比如 gitignore 文件,ESLint 配置。
常见的通配符和模式有:
通配符或模式 | 含义和例子 |
---|---|
* |
匹配任意字符串(含空串),但是不能跨越目录层级。 |
** |
匹配任意层级目录。 |
? |
匹配一个字符。 |
[abc] |
匹配中括号内的字符集合中的一个。排除法用 [^abc] 或 [!abc] 。 |
a{b,c*}d |
先展开成模式 abd ,ac*d ,再分别匹配,只要能满足一个就算匹配。 |
glob 和正则表达式容易混淆,二者虽然都是模式匹配的工具,但通配符的含义却是完全不同的。 Glob 是专用于匹配文件名的,而正则是一种更通用的字符串匹配工具。
💡 阮一峰的 命令行通配符教程 进一步说明了这些通配符的使用。
9. I/O 重定向
Shell 的标准输入输出包括 stdin 、stdout、stderr,分别对应文件描述符 0,1,2。
9.1. 重定向输出
使用 >
把命令的输出重定向到文件:
ls > files.txt
如果文件不存在,会创建该文件,所以可以用来很方便地创建一个小文件:
echo "{}" > config.json
如果文件存在,则会先清空再写入。如果希望保留文件原内容,从文件末添加(append),可以使用 >>
:
ls >> files.txt
9.2. 重定向错误输出
在 >
前加上文件描述符 2:
ls 2> ls-err.text
如果希望同时重定向输出和错误输出,使用 &>:
ls &> files.txt # 同 ls > files 2>&1
9.3. 重定向输入
重定向输入用 <
。输入重定向用得比较少,大部分情况都是直接支持用文件做参数。 下面的 read-print.sh 从标准输入读取输入,并打印
#!/bin/bash
read var;
echo $var;
重定向标准输入,把一个文件内容作为输入:
bash read-print.sh < files.txt
9.4. Here 文档和 here 字符串
Here 文档允许我们把一段字符串作为输入源。语法如下:
command << token # 中间这里是字符串的内容 text ... token
其中 token 是一段标识,不固定,收尾一致即可,结束标识必须顶格。Here 文档内部支持变量展开 ($ 仍然具有特殊意义)。
适合用于引用一些带格式的长文本。比如,一段 html 字符串:
title="Simple HTML"
content="Hello"
# cat 命令默认从标准输入读取内容
cat << _EOF_ <html> <head> <title> The title of page:$title </title> </head> <body> $content </body> </html> _EOF_
如果字符串内容较短,可以使用 here 文档的变体 here 字符串:
alias echo-hello="bash read-print.sh <<< 'Hello'"
echo-hello
# Hello
10. 获取帮助
当你遇到问题时,你不一定需要 google,可以先查看一下命令行上的帮助信息。
10.1. 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'.
10.2. 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
命令有效。
10.3. info
命令
man page 是一种过时(但仍然使用广泛)的文档格式,Unix 已经采用能支持超链接的 info 格式来提供帮助文档。如果 man
命令失效,你可以试试 info
,在大多数时候,它们两个都能生效。
查看 info
手册需要使用一些特殊的子命令:
空格键/PgDn
:向下翻页。PgUp
: 向上翻页。x
:关闭窗口。Tab
:跳转到下个超文本链接。q
:退出。- …
10.4. apropos
命令
apropos
能够根据关键字搜索出相关的命令。
apropos rename file
# ...
# mv(1) - move files
# ...
不过,经常会匹配到太多内容导致难以找到想要的词条,实际体验不如 google。
10.5. 命令行工具的 help
命令、--help
或 -h
参数
好的软件总是会提供便利完善的帮助信息。主流的命令行工具,几乎都会提供 help
命令,或者 --help/-h
参数,提供使用说明。困惑的时候,不妨试试这种方法。
# 查看全部帮助信息
git help
# 查看子命令帮助信息
git help add
git add --help
git add -h
11. 更多资料
- Bash Reference Manual:Bash 官方手册。
- the-art-of-command-line:一份很好的命令行指引,github 107k 星。
- awesome-shell: awesome 系列。
- explainshell.com:帮助你解读 shell 命令的在线工具。
今天的文章Shell 基础知识分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20814.html