Bash-note-2
Bash-note-2
参考文献:Bash 脚本教程
Bash 的基本语法
最基本语法
echo 命令
由于后面的例子会大量用到echo
命令,这里先介绍这个命令。
echo
命令的作用是在屏幕输出一行文本,可以将该命令的参数原样输出。
1 | $ echo hello world |
上面例子中,echo
的参数是hello world
,可以原样输出。
如果想要输出的是多行文本,即包括换行符。这时就需要把多行文本放在引号里面。
1 | $ echo "<HTML> |
上面例子中,echo
可以原样输出多行文本。
-n
参数
默认情况下,echo
输出的文本末尾会有一个回车符。-n
参数可以取消末尾的回车符,使得下一个提示符紧跟在输出内容的后面。
1 | $ echo -n hello world |
上面例子中,world
后面直接就是下一行的提示符$
。
1 | $ echo a;echo b |
上面例子中,-n
参数可以让两个echo
命令的输出连在一起,出现在同一行。
-e
参数
-e
参数会解释引号(双引号和单引号)里面的特殊字符(比如换行符\n
)。如果不使用-e
参数,即默认情况下,引号会让特殊字符变成普通字符,echo
不解释它们,原样输出。
1 | $ echo "Hello\nWorld" |
上面代码中,-e
参数使得\n
解释为换行符,导致输出内容里面出现换行。
命令格式
命令行环境中,主要通过使用 Shell 命令,进行各种操作。Shell 命令基本都是下面的格式。
1 | $ command [ arg1 ... [ argN ]] |
上面代码中,command
是具体的命令或者一个可执行文件,arg1 ... argN
是传递给命令的参数,它们是可选的。
1 | $ ls -l |
上面这个命令中,ls
是命令,-l
是参数。
有些参数是命令的配置项,这些配置项一般都以一个连词线开头,比如上面的-l
。同一个配置项往往有长和短两种形式,比如-l
是短形式,--list
是长形式,它们的作用完全相同。短形式便于手动输入,长形式一般用在脚本之中,可读性更好,利于解释自身的含义。
1 | # 短形式 |
上面命令中,-r
是短形式,--reverse
是长形式,作用完全一样。前者便于输入,后者便于理解。
Bash 单个命令一般都是一行,用户按下回车键,就开始执行。有些命令比较长,写成多行会有利于阅读和编辑,这时可以在每一行的结尾加上反斜杠,Bash 就会将下一行跟当前行放在一起解释。
1 | $ echo foo bar |
空格
Bash 使用空格(或 Tab 键)区分不同的参数。
1 | $ command foo bar |
上面命令中,foo
和bar
之间有一个空格,所以 Bash 认为它们是两个参数。
如果参数之间有多个空格,Bash 会自动忽略多余的空格。
1 | $ echo this is a test |
上面命令中,a
和test
之间有多个空格,Bash 会忽略多余的空格。
分号
分号(;
)是命令的结束符,使得一行可以放置多个命令,上一个命令执行结束后,再执行第二个命令。
1 | $ clear; ls |
上面例子中,Bash 先执行clear
命令,执行完成后,再执行ls
命令。
注意,使用分号时,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。
命令的组合符&&
和||
除了分号,Bash 还提供两个命令组合符&&
和||
,允许更好地控制多个命令之间的继发关系。
1 | Command1 && Command2 |
上面命令的意思是,如果Command1
命令运行成功,则继续运行Command2
命令。
1 | Command1 || Command2 |
上面命令的意思是,如果Command1
命令运行失败,则继续运行Command2
命令。
下面是一些例子。
1 | $ cat filelist.txt ; ls -l filelist.txt |
上面例子中,只要cat
命令执行结束,不管成功或失败,都会继续执行ls
命令。
1 | $ cat filelist.txt && ls -l filelist.txt |
上面例子中,只有cat
命令执行成功,才会继续执行ls
命令。如果cat
执行失败(比如不存在文件flielist.txt
),那么ls
命令就不会执行。
1 | $ mkdir foo || mkdir bar |
上面例子中,只有mkdir foo
命令执行失败(比如foo
目录已经存在),才会继续执行mkdir bar
命令。如果mkdir foo
命令执行成功,就不会创建bar
目录了。
type 命令
Bash 本身内置了很多命令,同时也可以执行外部程序。怎么知道一个命令是内置命令,还是外部程序呢?
type
命令用来判断命令的来源。
1 | $ type echo |
上面代码中,type
命令告诉我们,echo
是内部命令,ls
是外部程序(/bin/ls
)。
type
命令本身也是内置命令。
1 | $ type type |
如果要查看一个命令的所有定义,可以使用type
命令的-a
参数。
1 | $ type -a echo |
上面代码表示,echo
命令既是内置命令,也有对应的外部程序。
type
命令的-t
参数,可以返回一个命令的类型:别名(alias),关键词(keyword),函数(function),内置命令(builtin)和文件(file)。
1 | $ type -t bash |
上面例子中,bash
是文件,if
是关键词。
快捷键
Bash 提供很多快捷键,可以大大方便操作。下面是一些最常用的快捷键,完整的介绍参见《行操作》一章。
Ctrl + L
:清除屏幕并将当前行移到页面顶部。Ctrl + C
:中止当前正在执行的命令。Shift + PageUp
:向上滚动。Shift + PageDown
:向下滚动。Ctrl + U
:从光标位置删除到行首。Ctrl + K
:从光标位置删除到行尾。Ctrl + W
:删除光标位置前一个单词。Ctrl + D
:关闭 Shell 会话。↑
,↓
:浏览已执行命令的历史记录。
除了上面的快捷键,Bash 还具有自动补全功能。命令输入到一半的时候,可以按下 Tab 键,Bash 会自动完成剩下的部分。比如,输入tou
,然后按一下 Tab 键,Bash 会自动补上ch
。
除了命令的自动补全,Bash 还支持路径的自动补全。有时,需要输入很长的路径,这时只需要输入前面的部分,然后按下 Tab 键,就会自动补全后面的部分。如果有多个可能的选择,按两次 Tab 键,Bash 会显示所有选项,让你选择。
Bash 的模式扩展
简介
Shell 接收到用户输入的命令以后,会根据空格将用户的输入,拆分成一个个词元(token)。然后,Shell 会扩展词元里面的特殊字符,扩展完成后才会调用相应的命令。
这种特殊字符的扩展,称为模式扩展(globbing)。其中有些用到通配符,又称为通配符扩展(wildcard expansion)。Bash 一共提供八种扩展。
- 波浪线扩展
?
字符扩展*
字符扩展- 方括号扩展
- 大括号扩展
- 变量扩展
- 子命令扩展
- 算术扩展
本章介绍这八种扩展。
Bash 是先进行扩展,再执行命令。因此,扩展的结果是由 Bash 负责的,与所要执行的命令无关。命令本身并不存在参数扩展,收到什么参数就原样执行。这一点务必需要记住。
模块扩展的英文单词是globbing
,这个词来自于早期的 Unix 系统有一个/etc/glob
文件,保存扩展的模板。后来 Bash 内置了这个功能,但是这个名字就保留了下来。
模式扩展与正则表达式的关系是,模式扩展早于正则表达式出现,可以看作是原始的正则表达式。它的功能没有正则那么强大灵活,但是优点是简单和方便。
Bash 允许用户关闭扩展。
1 | $ set -o noglob |
下面的命令可以重新打开扩展。
1 | $ set +o noglob |
波浪线扩展
波浪线~
会自动扩展成当前用户的主目录。
1 | $ echo ~ |
~/dir
表示扩展成主目录的某个子目录,dir
是主目录里面的一个子目录名。
1 | # 进入 /home/me/foo 目录 |
~user
表示扩展成用户user
的主目录。
1 | $ echo ~foo |
上面例子中,Bash 会根据波浪号后面的用户名,返回该用户的主目录。
如果~user
的user
是不存在的用户名,则波浪号扩展不起作用。
1 | $ echo ~nonExistedUser |
~+
会扩展成当前所在的目录,等同于pwd
命令。
1 | $ cd ~/foo |
?
字符扩展
?
字符代表文件路径里面的任意单个字符,不包括空字符。比如,Data???
匹配所有Data
后面跟着三个字符的文件名。
1 | # 存在文件 a.txt 和 b.txt |
上面命令中,?
表示单个字符,所以会同时匹配a.txt
和b.txt
。
如果匹配多个字符,就需要多个?
连用。
1 | # 存在文件 a.txt、b.txt 和 ab.txt |
上面命令中,??
匹配了两个字符。
?
字符扩展属于文件名扩展,只有文件确实存在的前提下,才会发生扩展。如果文件不存在,扩展就不会发生。
1 | # 当前目录有 a.txt 文件 |
上面例子中,如果?.txt
可以扩展成文件名,echo
命令会输出扩展后的结果;如果不能扩展成文件名,echo
就会原样输出?.txt
。
*
字符扩展
*
字符代表文件路径里面的任意数量的任意字符,包括零个字符。
1 | # 存在文件 a.txt、b.txt 和 ab.txt |
上面例子中,*.txt
代表后缀名为.txt
的所有文件。
如果想输出当前目录的所有文件,直接用*
即可。
1 | $ ls * |
*
可以匹配空字符,下面是一个例子。
1 | # 存在文件 a.txt、b.txt 和 ab.txt |
注意,*
不会匹配隐藏文件(以.
开头的文件),即ls *
不会输出隐藏文件。
如果要匹配隐藏文件,需要写成.*
。
1 | # 显示所有隐藏文件 |
如果要匹配隐藏文件,同时要排除.
和..
这两个特殊的隐藏文件,可以与方括号扩展结合使用,写成.[!.]*
。
1 | $ echo .[!.]* |
注意,*
字符扩展属于文件名扩展,只有文件确实存在的前提下才会扩展。如果文件不存在,就会原样输出。
1 | # 当前目录不存在 c 开头的文件 |
上面例子中,当前目录里面没有c
开头的文件,导致c*.txt
会原样输出。
*
只匹配当前目录,不会匹配子目录。
1 | # 子目录有一个 a.txt |
上面的例子,文本文件在子目录,*.txt
不会产生匹配,必须写成*/*.txt
。有几层子目录,就必须写几层星号。
Bash 4.0 引入了一个参数globstar
,当该参数打开时,允许**
匹配零个或多个子目录。因此,**/*.txt
可以匹配顶层的文本文件和任意深度子目录的文本文件。详细介绍请看后面shopt
命令的介绍。
方括号扩展
方括号扩展的形式是[...]
,只有文件确实存在的前提下才会扩展。如果文件不存在,就会原样输出。括号之中的任意一个字符。比如,[aeiou]
可以匹配五个元音字母中的任意一个。
1 | # 存在文件 a.txt 和 b.txt |
上面例子中,[ab]
可以匹配a
或b
,前提是确实存在相应的文件。
方括号扩展属于文件名匹配,即扩展后的结果必须符合现有的文件路径。如果不存在匹配,就会保持原样,不进行扩展。
1 | # 不存在文件 a.txt 和 b.txt |
上面例子中,由于扩展后的文件不存在,[ab].txt
就原样输出了,导致ls
命名报错。
方括号扩展还有两种变体:[^...]
和[!...]
。它们表示匹配不在方括号里面的字符,这两种写法是等价的。比如,[^abc]
或[!abc]
表示匹配除了a
、b
、c
以外的字符。
1 | # 存在 aaa、bbb、aba 三个文件 |
上面命令中,[!a]
表示文件名第二个字符不是a
的文件名,所以返回了aba
和bbb
两个文件。
注意,如果需要匹配[
字符,可以放在方括号内,比如[[aeiou]
。如果需要匹配连字号-
,只能放在方括号内部的开头或结尾,比如[-aeiou]
或[aeiou-]
。
[start-end] 扩展
方括号扩展有一个简写形式[start-end]
,表示匹配一个连续的范围。比如,[a-c]
等同于[abc]
,[0-9]
匹配[0123456789]
。
1 | # 存在文件 a.txt、b.txt 和 c.txt |
下面是一些常用简写的例子。
[a-z]
:所有小写字母。[a-zA-Z]
:所有小写字母与大写字母。[a-zA-Z0-9]
:所有小写字母、大写字母与数字。[abc]*
:所有以a
、b
、c
字符之一开头的文件名。program.[co]
:文件program.c
与文件program.o
。BACKUP.[0-9][0-9][0-9]
:所有以BACKUP.
开头,后面是三个数字的文件名。
这种简写形式有一个否定形式[!start-end]
,表示匹配不属于这个范围的字符。比如,[!a-zA-Z]
表示匹配非英文字母的字符。
1 | $ ls report[!1–3].txt |
上面代码中,[!1-3]
表示排除1、2和3。
大括号扩展
大括号扩展{...}
表示分别扩展成大括号里面的所有值,各个值之间使用逗号分隔。比如,{1,2,3}
扩展成1 2 3
。
1 | $ echo {1,2,3} |
注意,大括号扩展不是文件名扩展。它会扩展成所有给定的值,而不管是否有对应的文件存在。
1 | $ ls {a,b,c}.txt |
上面例子中,即使不存在对应的文件,{a,b,c}
依然扩展成三个文件名,导致ls
命令报了三个错误。
另一个需要注意的地方是,大括号内部的逗号前后不能有空格。否则,大括号扩展会失效。
1 | $ echo {1 , 2} |
上面例子中,逗号前后有空格,Bash 就会认为这不是大括号扩展,而是三个独立的参数。
逗号前面可以没有值,表示扩展的第一项为空。
1 | $ cp a.log{,.bak} |
大括号可以嵌套。
1 | $ echo {j{p,pe}g,png} |
大括号也可以与其他模式联用,并且总是先于其他模式进行扩展。
1 | $ echo /bin/{cat,b*} |
上面例子中,会先进行大括号扩展,然后进行*
扩展,等同于执行两条echo
命令。
大括号可以用于多字符的模式,方括号不行(只能匹配单字符)。
1 | $ echo {cat,dog} |
由于大括号扩展{...}
不是文件名扩展,所以它总是会扩展的。这与方括号扩展[...]
完全不同,如果匹配的文件不存在,方括号就不会扩展。这一点要注意区分。
1 | # 不存在 a.txt 和 b.txt |
上面例子中,如果不存在a.txt
和b.txt
,那么[ab].txt
就会变成一个普通的文件名,而{a,b}.txt
可以照样扩展。
{start…end} 扩展
大括号扩展有一个简写形式{start..end}
,表示扩展成一个连续序列。比如,{a..z}
可以扩展成26个小写英文字母。
1 | $ echo {a..c} |
这种简写形式支持逆序。
1 | $ echo {c..a} |
注意,如果遇到无法理解的简写,大括号模式就会原样输出,不会扩展。
1 | $ echo {a1..3c} |
这种简写形式可以嵌套使用,形成复杂的扩展。
1 | $ echo .{mp{3..4},m4{a,b,p,v}} |
大括号扩展的常见用途为新建一系列目录。
1 | $ mkdir {2007..2009}-{01..12} |
上面命令会新建36个子目录,每个子目录的名字都是”年份-月份“。
这个写法的另一个常见用途,是直接用于for
循环。
1 | for i in {1..4} |
上面例子会循环4次。
如果整数前面有前导0
,扩展输出的每一项都有前导0
。
1 | $ echo {01..5} |
这种简写形式还可以使用第二个双点号(start..end..step
),用来指定扩展的步长。
1 | $ echo {0..8..2} |
上面代码将0
扩展到8
,每次递增的长度为2
,所以一共输出5个数字。
多个简写形式连用,会有循环处理的效果。
1 | $ echo {a..c}{1..3} |
变量扩展
Bash 将美元符号$
开头的词元视为变量,将其扩展成变量值,详见《Bash 变量》一章。
1 | $ echo $SHELL |
变量名除了放在美元符号后面,也可以放在${}
里面。
1 | $ echo ${SHELL} |
${!string*}
或${!string@}
返回所有匹配给定字符串string
的变量名。
1 | $ echo ${!S*} |
上面例子中,${!S*}
扩展成所有以S
开头的变量名。
子命令扩展
$(...)
可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。
1 | $ echo $(date) |
上面例子中,$(date)
返回date
命令的运行结果。
还有另一种较老的语法,子命令放在反引号之中,也可以扩展成命令的运行结果。
1 | $ echo `date` |
$(...)
可以嵌套,比如$(ls $(pwd))
。
算术扩展
$((...))
可以扩展成整数运算的结果,详见《Bash 的算术运算》一章。
1 | $ echo $((2 + 2)) |
字符类
[[:class:]]
表示一个字符类,扩展成某一类特定字符之中的一个。常用的字符类如下。
[[:alnum:]]
:匹配任意英文字母与数字[[:alpha:]]
:匹配任意英文字母[[:blank:]]
:空格和 Tab 键。[[:cntrl:]]
:ASCII 码 0-31 的不可打印字符。[[:digit:]]
:匹配任意数字 0-9。[[:graph:]]
:A-Z、a-z、0-9 和标点符号。[[:lower:]]
:匹配任意小写字母 a-z。[[:print:]]
:ASCII 码 32-127 的可打印字符。[[:punct:]]
:标点符号(除了 A-Z、a-z、0-9 的可打印字符)。[[:space:]]
:空格、Tab、LF(10)、VT(11)、FF(12)、CR(13)。[[:upper:]]
:匹配任意大写字母 A-Z。[[:xdigit:]]
:16进制字符(A-F、a-f、0-9)。
请看下面的例子。
1 | $ echo [[:upper:]]* |
上面命令输出所有大写字母开头的文件名。
字符类的第一个方括号后面,可以加上感叹号!
,表示否定。比如,[![:digit:]]
匹配所有非数字。
1 | $ echo [![:digit:]]* |
上面命令输出所有不以数字开头的文件名。
字符类也属于文件名扩展,如果没有匹配的文件名,字符类就会原样输出。
1 | # 不存在以大写字母开头的文件 |
上面例子中,由于没有可匹配的文件,字符类就原样输出了。
使用注意点
通配符有一些使用注意点,不可不知。
(1)通配符是先解释,再执行。
Bash 接收到命令以后,发现里面有通配符,会进行通配符扩展,然后再执行命令。
1 | $ ls a*.txt |
上面命令的执行过程是,Bash 先将a*.txt
扩展成ab.txt
,然后再执行ls ab.txt
。
(2)文件名扩展在不匹配时,会原样输出。
文件名扩展在没有可匹配的文件时,会原样输出。
1 | # 不存在 r 开头的文件名 |
上面代码中,由于不存在r
开头的文件名,r*
会原样输出。
下面是另一个例子。
1 | $ ls *.csv |
另外,前面已经说过,大括号扩展{...}
不是文件名扩展。
(3)只适用于单层路径。
所有文件名扩展只匹配单层路径,不能跨目录匹配,即无法匹配子目录里面的文件。或者说,?
或*
这样的通配符,不能匹配路径分隔符(/
)。
如果要匹配子目录里面的文件,可以写成下面这样。
1 | $ ls */*.txt |
Bash 4.0 新增了一个globstar
参数,允许**
匹配零个或多个子目录,详见后面shopt
命令的介绍。
(4)文件名可以使用通配符。
Bash 允许文件名使用通配符,即文件名包括特殊字符。这时引用文件名,需要把文件名放在单引号或双引号里面。
1 | $ touch 'fo*' |
上面代码创建了一个fo*
文件,这时*
就是文件名的一部分。
量词语法
量词语法用来控制模式匹配的次数。它只有在 Bash 的extglob
参数打开的情况下才能使用,不过一般是默认打开的。下面的命令可以查询。
1 | $ shopt extglob |
如果extglob
参数是关闭的,可以用下面的命令打开。
1 | $ shopt -s extglob |
量词语法有下面几个。
?(pattern-list)
:模式匹配零次或一次。*(pattern-list)
:模式匹配零次或多次。+(pattern-list)
:模式匹配一次或多次。@(pattern-list)
:只匹配一次模式。!(pattern-list)
:匹配给定模式以外的任何内容。
1 | $ ls abc?(.)txt |
上面例子中,?(.)
匹配零个或一个点。
1 | $ ls abc?(def) |
上面例子中,?(def)
匹配零个或一个def
。
1 | $ ls abc@(.txt|.php) |
上面例子中,@(.txt|.php)
匹配文件有且只有一个.txt
或.php
后缀名。
1 | $ ls abc+(.txt) |
上面例子中,+(.txt)
匹配文件有一个或多个.txt
后缀名。
1 | $ ls a!(b).txt |
上面例子中,!(b)
表示匹配单个字母b
以外的任意内容,所以除了ab.txt
以外,其他文件名都能匹配。
量词语法也属于文件名扩展,如果不存在可匹配的文件,就会原样输出。
1 | # 没有 abc 开头的文件名 |
上面例子中,由于没有可匹配的文件,abc?(def)
就原样输出,导致ls
命令报错。
shopt 命令
shopt
命令可以调整 Bash 的行为。它有好几个参数跟通配符扩展有关。
shopt
命令的使用方法如下。
1 | # 打开某个参数 |
(1)dotglob 参数
dotglob
参数可以让扩展结果包括隐藏文件(即点开头的文件)。
正常情况下,扩展结果不包括隐藏文件。
1 | $ ls * |
打开dotglob
,就会包括隐藏文件。
1 | $ shopt -s dotglob |
(2)nullglob 参数
nullglob
参数可以让通配符不匹配任何文件名时,返回空字符。
默认情况下,通配符不匹配任何文件名时,会保持不变。
1 | $ rm b* |
上面例子中,由于当前目录不包括b
开头的文件名,导致b*
不会发生文件名扩展,保持原样不变,所以rm
命令报错没有b*
这个文件。
打开nullglob
参数,就可以让不匹配的通配符返回空字符串。
1 | $ shopt -s nullglob |
上面例子中,由于没有b*
匹配的文件名,所以rm b*
扩展成了rm
,导致报错变成了”缺少操作数“。
(3)failglob 参数
failglob
参数使得通配符不匹配任何文件名时,Bash 会直接报错,而不是让各个命令去处理。
1 | $ shopt -s failglob |
上面例子中,打开failglob
以后,由于b*
不匹配任何文件名,Bash 直接报错了,不再让rm
命令去处理。
(4)extglob 参数
extglob
参数使得 Bash 支持 ksh 的一些扩展语法。它默认应该是打开的。
1 | $ shopt extglob |
它的主要应用是支持量词语法。如果不希望支持量词语法,可以用下面的命令关闭。
1 | $ shopt -u extglob |
(5)nocaseglob 参数
nocaseglob
参数可以让通配符扩展不区分大小写。
1 | $ shopt -s nocaseglob |
上面例子中,打开nocaseglob
以后,program*
就不区分大小写了,可以匹配ProgramData
等。
(6)globstar 参数
globstar
参数可以使得**
匹配零个或多个子目录。该参数默认是关闭的。
假设有下面的文件结构。
1 | a.txt |
上面的文件结构中,顶层目录、第一级子目录sub1
、第二级子目录sub1\sub2
里面各有一个文本文件。请问怎样才能使用通配符,将它们显示出来?
默认情况下,只能写成下面这样。
1 | $ ls *.txt */*.txt */*/*.txt |
这是因为*
只匹配当前目录,如果要匹配子目录,只能一层层写出来。
打开globstar
参数以后,**
匹配零个或多个子目录。因此,**/*.txt
就可以得到想要的结果。
1 | $ shopt -s globstar |
引号和转义
Bash 只有一种数据类型,就是字符串。不管用户输入什么数据,Bash 都视为字符串。因此,字符串相关的引号和转义,对 Bash 来说就非常重要。
转义
某些字符在 Bash 里面有特殊含义(比如$
、&
、*
)。
1 | $ echo $date |
上面例子中,输出$date
不会有任何结果,因为$
是一个特殊字符。
如果想要原样输出这些特殊字符,就必须在它们前面加上反斜杠,使其变成普通字符。这就叫做“转义”(escape)。
1 | $ echo \$date |
上面命令中,只有在特殊字符$
前面加反斜杠,才能原样输出。
反斜杠本身也是特殊字符,如果想要原样输出反斜杠,就需要对它自身转义,连续使用两个反斜线(\\
)。
1 | $ echo \\ |
上面例子输出了反斜杠本身。
反斜杠除了用于转义,还可以表示一些不可打印的字符。
\a
:响铃\b
:退格\n
:换行\r
:回车\t
:制表符
如果想要在命令行使用这些不可打印的字符,可以把它们放在引号里面,然后使用echo
命令的-e
参数。
1 | $ echo a\tb |
上面例子中,命令行直接输出不可打印字符\t
,Bash 不能正确解释。必须把它们放在引号之中,然后使用echo
命令的-e
参数。
换行符是一个特殊字符,表示命令的结束,Bash 收到这个字符以后,就会对输入的命令进行解释执行。换行符前面加上反斜杠转义,就使得换行符变成一个普通字符,Bash 会将其当作长度为0
的空字符处理,从而可以将一行命令写成多行。
1 | $ mv \ |
上面例子中,如果一条命令过长,就可以在行尾使用反斜杠,将其改写成多行。这是常见的多行命令的写法。
单引号
Bash 允许字符串放在单引号或双引号之中,加以引用。
单引号用于保留字符的字面含义,各种特殊字符在单引号里面,都会变为普通字符,比如星号(*
)、美元符号($
)、反斜杠(\
)等。
1 | $ echo '*' |
上面命令中,单引号使得 Bash 扩展、变量引用、算术运算和子命令,都失效了。如果不使用单引号,它们都会被 Bash 自动扩展。
由于反斜杠在单引号里面变成了普通字符,所以如果单引号之中,还要使用单引号,不能使用转义,需要在外层的单引号前面加上一个美元符号($
),然后再对里层的单引号转义。
1 | # 不正确 |
不过,更合理的方法是改在双引号之中使用单引号。
1 | $ echo "it's" |
双引号
双引号比单引号宽松,大部分特殊字符在双引号里面,都会失去特殊含义,变成普通字符。
1 | $ echo "*" |
上面例子中,通配符*
是一个特殊字符,放在双引号之中,就变成了普通字符,会原样输出。这一点需要特别留意,这意味着,双引号里面不会进行文件名扩展。
但是,三个特殊字符除外:美元符号($
)、反引号(```)和反斜杠(\
)。这三个字符在双引号之中,依然有特殊含义,会被 Bash 自动扩展。
1 | $ echo "$SHELL" |
上面例子中,美元符号($
)和反引号(```)在双引号中,都保持特殊含义。美元符号用来引用变量,反引号则是执行子命令。
1 | $ echo "I'd say: \"hello!\"" |
上面例子中,反斜杠在双引号之中保持特殊含义,用来转义。所以,可以使用反斜杠,在双引号之中插入双引号,或者插入反斜杠本身。
换行符在双引号之中,会失去特殊含义,Bash 不再将其解释为命令的结束,只是作为普通的换行符。所以可以利用双引号,在命令行输入多行文本。
1 | $ echo "hello |
上面命令中,Bash 正常情况下会将换行符解释为命令结束,但是换行符在双引号之中就失去了这种特殊作用,只用来换行,所以可以输入多行。echo
命令会将换行符原样输出,显示的时候正常解释为换行。
双引号的另一个常见的使用场合是,文件名包含空格。这时就必须使用双引号(或单引号),将文件名放在里面。
1 | $ ls "two words.txt" |
上面命令中,two words.txt
是一个包含空格的文件名,如果不放在双引号里面,就会被 Bash 当作两个文件。
双引号会原样保存多余的空格。
1 | $ echo "this is a test" |
双引号还有一个作用,就是保存原始命令的输出格式。
1 | # 单行输出 |
上面例子中,如果$(cal)
不放在双引号之中,echo
就会将所有结果以单行输出,丢弃了所有原始的格式。
Here 文档
Here 文档(here document)是一种输入多行字符串的方法,格式如下。
1 | << token |
它的格式分成开始标记(<< token
)和结束标记(token
)。开始标记是两个小于号 + Here 文档的名称,名称可以随意取,后面必须是一个换行符;结束标记是单独一行顶格写的 Here 文档名称,如果不是顶格,结束标记不起作用。两者之间就是多行字符串的内容。
下面是一个通过 Here 文档输出 HTML 代码的例子。
1 | $ cat << _EOF_ |
Here 文档内部会发生变量替换,同时支持反斜杠转义,但是不支持通配符扩展,双引号和单引号也失去语法作用,变成了普通字符。
1 | $ foo='hello world' |
上面例子中,变量$foo
发生了替换,但是双引号和单引号都原样输出了,表明它们已经失去了引用的功能。
如果不希望发生变量替换,可以把 Here 文档的开始标记放在单引号之中。
1 | $ foo='hello world' |
上面例子中,Here 文档的开始标记(_example_
)放在单引号之中,导致变量替换失效了。
Here 文档的本质是重定向,它将字符串重定向输出给某个命令,相当于包含了echo
命令。
1 | $ command << token |
上面代码中,Here 文档相当于echo
命令的重定向。
所以,Here 字符串只适合那些可以接受标准输入作为参数的命令,对于其他命令无效,比如echo
命令就不能用 Here 文档作为参数。
1 | $ echo << _example_ |
上面例子不会有任何输出,因为 Here 文档对于echo
命令无效。
此外,Here 文档也不能作为变量的值,只能用于命令的参数。
Here 字符串
Here 文档还有一个变体,叫做 Here 字符串(Here string),使用三个小于号(<<<
)表示。
1 | <<< string |
它的作用是将字符串通过标准输入,传递给命令。
有些命令直接接受给定的参数,与通过标准输入接受参数,结果是不一样的。所以才有了这个语法,使得将字符串通过标准输入传递给命令更方便,比如cat
命令只接受标准输入传入的字符串。
1 | $ cat <<< 'hi there' |
上面的第一种语法使用了 Here 字符串,要比第二种语法看上去语义更好,也更简洁。
1 | $ md5sum <<< 'ddd' |
上面例子中,md5sum
命令只能接受标准输入作为参数,不能直接将字符串放在命令后面,会被当作文件名,即md5sum ddd
里面的ddd
会被解释成文件名。这时就可以用 Here 字符串,将字符串传给md5sum
命令。
Bash 变量
简介
Bash 变量分成环境变量和自定义变量两类。
环境变量
环境变量是 Bash 环境自带的变量,进入 Shell 时已经定义好了,可以直接使用。它们通常是系统定义好的,也可以由用户从父 Shell 传入子 Shell。
env
命令或printenv
命令,可以显示所有环境变量。
1 | $ env |
下面是一些常见的环境变量。
BASHPID
:Bash 进程的进程 ID。BASHOPTS
:当前 Shell 的参数,可以用shopt
命令修改。DISPLAY
:图形环境的显示器名字,通常是:0
,表示 X Server 的第一个显示器。EDITOR
:默认的文本编辑器。HOME
:用户的主目录。HOST
:当前主机的名称。IFS
:词与词之间的分隔符,默认为空格。LANG
:字符集以及语言编码,比如zh_CN.UTF-8
。PATH
:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。PS1
:Shell 提示符。PS2
: 输入多行命令时,次要的 Shell 提示符。PWD
:当前工作目录。RANDOM
:返回一个0到32767之间的随机数。SHELL
:Shell 的名字。SHELLOPTS
:启动当前 Shell 的set
命令的参数,参见《set 命令》一章。TERM
:终端类型名,即终端仿真器所用的协议。UID
:当前用户的 ID 编号。USER
:当前用户的用户名。
很多环境变量很少发生变化,而且是只读的,可以视为常量。由于它们的变量名全部都是大写,所以传统上,如果用户要自己定义一个常量,也会使用全部大写的变量名。
注意,Bash 变量名区分大小写,HOME
和home
是两个不同的变量。
查看单个环境变量的值,可以使用printenv
命令或echo
命令。
1 | $ printenv PATH |
注意,printenv
命令后面的变量名,不用加前缀$
。
自定义变量
自定义变量是用户在当前 Shell 里面自己定义的变量,仅在当前 Shell 可用。一旦退出当前 Shell,该变量就不存在了。
set
命令可以显示所有变量(包括环境变量和自定义变量),以及所有的 Bash 函数。
1 | $ set |
创建变量
用户创建变量的时候,变量名必须遵守下面的规则。
- 字母、数字和下划线字符组成。
- 第一个字符必须是一个字母或一个下划线,不能是数字。
- 不允许出现空格和标点符号。
变量声明的语法如下。
1 | variable=value |
上面命令中,等号左边是变量名,右边是变量。注意,等号两边不能有空格。
如果变量的值包含空格,则必须将值放在引号中。
1 | myvar="hello world" |
Bash 没有数据类型的概念,所有的变量值都是字符串。
下面是一些自定义变量的例子。
1 | a=z # 变量 a 赋值为字符串 z |
变量可以重复赋值,后面的赋值会覆盖前面的赋值。
1 | $ foo=1 |
上面例子中,变量foo
的第二次赋值会覆盖第一次赋值。
如果同一行定义多个变量,必须使用分号(;
)分隔。
1 | $ foo=1;bar=2 |
上面例子中,同一行定义了foo
和bar
两个变量。
读取变量
读取变量的时候,直接在变量名前加上$
就可以了。
1 | $ foo=bar |
每当 Shell 看到以$
开头的单词时,就会尝试读取这个变量名对应的值。
如果变量不存在,Bash 不会报错,而会输出空字符。
由于$
在 Bash 中有特殊含义,把它当作美元符号使用时,一定要非常小心,
1 | $ echo The total is $100.00 |
上面命令的原意是输入$100
,但是 Bash 将$1
解释成了变量,该变量为空,因此输入就变成了00.00
。所以,如果要使用$
的原义,需要在$
前面放上反斜杠,进行转义。
1 | $ echo The total is \$100.00 |
读取变量的时候,变量名也可以使用花括号{}
包围,比如$a
也可以写成${a}
。这种写法可以用于变量名与其他字符连用的情况。
1 | $ a=foo |
上面代码中,变量名a_file
不会有任何输出,因为 Bash 将其整个解释为变量,而这个变量是不存在的。只有用花括号区分$a
,Bash 才能正确解读。
事实上,读取变量的语法$foo
,可以看作是${foo}
的简写形式。
如果变量的值本身也是变量,可以使用${!varname}
的语法,读取最终的值。
1 | $ myvar=USER |
上面的例子中,变量myvar
的值是USER
,${!myvar}
的写法将其展开成最终的值。
如果变量值包含连续空格(或制表符和换行符),最好放在双引号里面读取。
1 | $ a="1 2 3" |
上面示例中,变量a
的值包含两个连续空格。如果直接读取,Shell 会将连续空格合并成一个。只有放在双引号里面读取,才能保持原来的格式。
删除变量
unset
命令用来删除一个变量。
1 | unset NAME |
这个命令不是很有用。因为不存在的 Bash 变量一律等于空字符串,所以即使unset
命令删除了变量,还是可以读取这个变量,值为空字符串。
所以,删除一个变量,也可以将这个变量设成空字符串。
1 | $ foo='' |
上面两种写法,都是删除了变量foo
。由于不存在的值默认为空字符串,所以后一种写法可以在等号右边不写任何值。
输出变量,export 命令
用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。为了把变量传递给子 Shell,需要使用export
命令。这样输出的变量,对于子 Shell 来说就是环境变量。
export
命令用来向子 Shell 输出变量。
1 | NAME=foo |
上面命令输出了变量NAME
。变量的赋值和输出也可以在一个步骤中完成。
1 | export NAME=value |
上面命令执行后,当前 Shell 及随后新建的子 Shell,都可以读取变量$NAME
。
子 Shell 如果修改继承的变量,不会影响父 Shell。
1 | # 输出变量 $foo |
上面例子中,子 Shell 修改了继承的变量$foo
,对父 Shell 没有影响。
特殊变量
Bash 提供一些特殊变量。这些变量的值由 Shell 提供,用户不能进行赋值。
(1)$?
$?
为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0
,表示上一个命令执行成功;如果不是零,表示上一个命令执行失败。
1 | $ ls doesnotexist |
上面例子中,ls
命令查看一个不存在的文件,导致报错。$?
为1,表示上一个命令执行失败。
(2)$$
$$
为当前 Shell 的进程 ID。
1 | $ echo $$ |
这个特殊变量可以用来命名临时文件。
1 | LOGFILE=/tmp/output_log.$$ |
(3)$_
$_
为上一个命令的最后一个参数。
1 | $ grep dictionary /usr/share/dict/words |
(4)$!
$!
为最近一个后台执行的异步命令的进程 ID。
1 | $ firefox & |
上面例子中,firefox
是后台运行的命令,$!
返回该命令的进程 ID。
(5)$0
$0
为当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时)。
1 | $ echo $0 |
上面例子中,$0
返回当前运行的是 Bash。
(6)$-
$-
为当前 Shell 的启动参数。
1 | $ echo $- |
(7)$@
和$#
$#
表示脚本的参数数量,$@
表示脚本的参数值,参见脚本一章。
变量的默认值
Bash 提供四个特殊语法,跟变量的默认值有关,目的是保证变量不为空。
1 | ${varname:-word} |
上面语法的含义是,如果变量varname
存在且不为空,则返回它的值,否则返回word
。它的目的是返回一个默认值,比如${count:-0}
表示变量count
不存在时返回0
。
1 | ${varname:=word} |
上面语法的含义是,如果变量varname
存在且不为空,则返回它的值,否则将它设为word
,并且返回word
。它的目的是设置变量的默认值,比如${count:=0}
表示变量count
不存在时返回0
,且将count
设为0
。
1 | ${varname:+word} |
上面语法的含义是,如果变量名存在且不为空,则返回word
,否则返回空值。它的目的是测试变量是否存在,比如${count:+1}
表示变量count
存在时返回1
(表示true
),否则返回空值。
1 | ${varname:?message} |
上面语法的含义是,如果变量varname
存在且不为空,则返回它的值,否则打印出varname: message
,并中断脚本的执行。如果省略了message
,则输出默认的信息“parameter null or not set.”。它的目的是防止变量未定义,比如${count:?"undefined!"}
表示变量count
未定义时就中断执行,抛出错误,返回给定的报错信息undefined!
。
上面四种语法如果用在脚本中,变量名的部分可以用数字1
到9
,表示脚本的参数。
1 | filename=${1:?"filename missing."} |
上面代码出现在脚本中,1
表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。
declare 命令
declare
命令可以声明一些特殊类型的变量,为变量设置一些限制,比如声明只读类型的变量和整数类型的变量。
它的语法形式如下。
1 | declare OPTION VARIABLE=value |
declare
命令的主要参数(OPTION)如下。
-a
:声明数组变量。-f
:输出所有函数定义。-F
:输出所有函数名。-i
:声明整数变量。-l
:声明变量为小写字母。-p
:查看变量信息。-r
:声明只读变量。-u
:声明变量为大写字母。-x
:该变量输出为环境变量。
declare
命令如果用在函数中,声明的变量只在函数内部有效,等同于local
命令。
不带任何参数时,declare
命令输出当前环境的所有变量,包括函数在内,等同于不带有任何参数的set
命令。
1 | $ declare |
(1)-i
参数
-i
参数声明整数变量以后,可以直接进行数学运算。
1 | $ declare -i val1=12 val2=5 |
上面例子中,如果变量result
不声明为整数,val1*val2
会被当作字面量,不会进行整数运算。另外,val1
和val2
其实不需要声明为整数,因为只要result
声明为整数,它的赋值就会自动解释为整数运算。
注意,一个变量声明为整数以后,依然可以被改写为字符串。
1 | $ declare -i var=12 |
上面例子中,变量var
声明为整数,覆盖以后,Bash 不会报错,但会赋以不确定的值,上面的例子中可能输出0,也可能输出的是3。
(2)-x
参数
-x
参数等同于export
命令,可以输出一个变量为子 Shell 的环境变量。
1 | $ declare -x foo |
(3)-r
参数
-r
参数可以声明只读变量,无法改变变量值,也不能unset
变量。
1 | $ declare -r bar=1 |
上面例子中,后两个赋值语句都会报错,命令执行失败。
(4)-u
参数
-u
参数声明变量为大写字母,可以自动把变量值转成大写字母。
1 | $ declare -u foo |
(5)-l
参数
-l
参数声明变量为小写字母,可以自动把变量值转成小写字母。
1 | $ declare -l bar |
(6)-p
参数
-p
参数输出变量信息。
1 | $ foo=hello |
上面例子中,declare -p
可以输出已定义变量的值,对于未定义的变量,会提示找不到。
如果不提供变量名,declare -p
输出所有变量的信息。
1 | $ declare -p |
(7)-f
参数
-f
参数输出当前环境的所有函数,包括它的定义。
1 | $ declare -f |
(8)-F
参数
-F
参数输出当前环境的所有函数名,不包含函数定义。
1 | $ declare -F |
readonly 命令
readonly
命令等同于declare -r
,用来声明只读变量,不能改变变量值,也不能unset
变量。
1 | $ readonly foo=1 |
上面例子中,更改只读变量foo
会报错,命令执行失败。
readonly
命令有三个参数。
-f
:声明的变量为函数名。-p
:打印出所有的只读变量。-a
:声明的变量为数组。
let 命令
let
命令声明变量时,可以直接执行算术表达式。
1 | $ let foo=1+2 |
上面例子中,let
命令可以直接计算1 + 2
。
let
命令的参数表达式如果包含空格,就需要使用引号。
1 | $ let "foo = 1 + 2" |
let
可以同时对多个变量赋值,赋值表达式之间使用空格分隔。
1 | $ let "v1 = 1" "v2 = v1++" |
上面例子中,let
声明了两个变量v1
和v2
,其中v2
等于v1++
,表示先返回v1
的值,然后v1
自增。
字符串操作
本章介绍 Bash 字符串操作的语法。
字符串的长度
获取字符串长度的语法如下。
1 | ${#varname} |
下面是一个例子。
1 | $ myPath=/home/cam/book/long.file.name |
大括号{}
是必需的,否则 Bash 会将$#
理解成脚本的参数个数,将变量名理解成文本。
1 | $ echo $#myvar |
上面例子中,Bash 将$#
和myvar
分开解释了。
子字符串
字符串提取子串的语法如下。
1 | ${varname:offset:length} |
上面语法的含义是返回变量$varname
的子字符串,从位置offset
开始(从0
开始计算),长度为length
。
1 | $ count=frogfootman |
上面例子返回字符串frogfootman
从4号位置开始的长度为4的子字符串foot
。
这种语法不能直接操作字符串,只能通过变量来读取字符串,并且不会改变原始字符串。
1 | # 报错 |
上面例子中,"hello"
不是变量名,导致 Bash 报错。
如果省略length
,则从位置offset
开始,一直返回到字符串的结尾。
1 | $ count=frogfootman |
上面例子是返回变量count
从4号位置一直到结尾的子字符串。
如果offset
为负值,表示从字符串的末尾开始算起。注意,负数前面必须有一个空格, 以防止与${variable:-word}
的变量的设置默认值语法混淆。这时还可以指定length
,length
可以是正值,也可以是负值(负值不能超过offset
的长度)。
1 | $ foo="This string is long." |
上面例子中,offset
为-5
,表示从倒数第5个字符开始截取,所以返回long.
。如果指定长度length
为2
,则返回lo
;如果length
为-2
,表示要排除从字符串末尾开始的2个字符,所以返回lon
。
搜索和替换
Bash 提供字符串搜索和替换的多种方法。
(1)字符串头部的模式匹配。
以下两种语法可以检查字符串开头,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。
1 | # 如果 pattern 匹配变量 variable 的开头, |
上面两种语法会删除变量字符串开头的匹配部分(将其替换为空),返回剩下的部分。区别是一个是最短匹配(又称非贪婪匹配),另一个是最长匹配(又称贪婪匹配)。
匹配模式pattern
可以使用*
、?
、[]
等通配符。
1 | $ myPath=/home/cam/book/long.file.name |
上面例子中,匹配的模式是/*/
,其中*
可以匹配任意数量的字符,所以最短匹配是/home/
,最长匹配是/home/cam/book/
。
下面写法可以删除文件路径的目录部分,只留下文件名。
1 | $ path=/home/cam/book/long.file.name |
上面例子中,模式*/
匹配目录部分,所以只返回文件名。
下面再看一个例子。
1 | $ phone="555-456-1414" |
如果匹配不成功,则返回原始字符串。
1 | $ phone="555-456-1414" |
上面例子中,原始字符串里面无法匹配模式444
,所以原样返回。
如果要将头部匹配的部分,替换成其他内容,采用下面的写法。
1 | # 模式必须出现在字符串的开头 |
上面例子中,被替换的JPG
必须出现在字符串头部,所以返回jpg.JPG
。
(2)字符串尾部的模式匹配。
以下两种语法可以检查字符串结尾,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,返回剩下的部分。原始变量不会发生变化。
1 | # 如果 pattern 匹配变量 variable 的结尾, |
上面两种语法会删除变量字符串结尾的匹配部分(将其替换为空),返回剩下的部分。区别是一个是最短匹配(又称非贪婪匹配),另一个是最长匹配(又称贪婪匹配)。
1 | $ path=/home/cam/book/long.file.name |
上面例子中,匹配模式是.*
,其中*
可以匹配任意数量的字符,所以最短匹配是.name
,最长匹配是.file.name
。
下面写法可以删除路径的文件名部分,只留下目录部分。
1 | $ path=/home/cam/book/long.file.name |
上面例子中,模式/*
匹配文件名部分,所以只返回目录部分。
下面的写法可以替换文件的后缀名。
1 | $ file=foo.png |
上面的例子将文件的后缀名,从.png
改成了.jpg
。
下面再看一个例子。
1 | $ phone="555-456-1414" |
如果匹配不成功,则返回原始字符串。
如果要将尾部匹配的部分,替换成其他内容,采用下面的写法。
1 | # 模式必须出现在字符串的结尾 |
上面例子中,被替换的JPG
必须出现在字符串尾部,所以返回JPG.jpg
。
(3)任意位置的模式匹配。
以下两种语法可以检查字符串内部,是否匹配给定的模式。如果匹配成功,就删除匹配的部分,换成其他的字符串返回。原始变量不会发生变化。
1 | # 如果 pattern 匹配变量 variable 的一部分, |
上面两种语法都是最长匹配(贪婪匹配)下的替换,区别是前一个语法仅仅替换第一个匹配,后一个语法替换所有匹配。
1 | $ path=/home/cam/foo/foo.name |
上面例子中,前一个命令只替换了第一个foo
,后一个命令将两个foo
都替换了。
下面的例子将分隔符从:
换成换行符。
1 | $ echo -e ${PATH//:/'\n'} |
上面例子中,echo
命令的-e
参数,表示将替换后的字符串的\n
字符,解释为换行符。
模式部分可以使用通配符。
1 | $ phone="555-456-1414" |
上面的例子将5-4
替换成-
。
如果省略了string
部分,那么就相当于匹配的部分替换成空字符串,即删除匹配的部分。
1 | $ path=/home/cam/foo/foo.name |
上面例子中,第二个斜杠后面的string
部分省略了,所以模式.*
匹配的部分.name
被删除后返回。
前面提到过,这个语法还有两种扩展形式。
1 | # 模式必须出现在字符串的开头 |
改变大小写
下面的语法可以改变变量的大小写。
1 | # 转为大写 |
下面是一个例子。
1 | $ foo=heLLo |
Bash 的算术运算
算术表达式
((...))
语法可以进行整数的算术运算。
1 | $ ((foo = 5 + 5)) |
((...))
会自动忽略内部的空格,所以下面的写法都正确,得到同样的结果。
1 | $ ((2+2)) |
这个语法不返回值,命令执行的结果根据算术运算的结果而定。只要算术结果不是0
,命令就算执行成功。
1 | $ (( 3 + 2 )) |
上面例子中,3 + 2
的结果是5,命令就算执行成功,环境变量$?
为0
。
如果算术结果为0
,命令就算执行失败。
1 | $ (( 3 - 3 )) |
上面例子中,3 - 3
的结果是0
,环境变量$?
为1
,表示命令执行失败。
如果要读取算术运算的结果,需要在((...))
前面加上美元符号$((...))
,使其变成算术表达式,返回算术运算的值。
1 | $ echo $((2 + 2)) |
((...))
语法支持的算术运算符如下。
+
:加法-
:减法*
:乘法/
:除法(整除)%
:余数**
:指数++
:自增运算(前缀或后缀)--
:自减运算(前缀或后缀)
注意,除法运算符的返回结果总是整数,比如5
除以2
,得到的结果是2
,而不是2.5
。
1 | $ echo $((5 / 2)) |
++
和--
这两个运算符有前缀和后缀的区别。作为前缀是先运算后返回值,作为后缀是先返回值后运算。
1 | $ i=0 |
上面例子中,++
作为后缀是先返回值,执行echo
命令,再进行自增运算;作为前缀则是先进行自增运算,再返回值执行echo
命令。
$((...))
内部可以用圆括号改变运算顺序。
1 | $ echo $(( (2 + 3) * 4 )) |
上面例子中,内部的圆括号让加法先于乘法执行。
$((...))
结构可以嵌套。
1 | $ echo $(((5**2) * 3)) |
这个语法只能计算整数,否则会报错。
1 | # 报错 |
$((...))
的圆括号之中,不需要在变量名之前加上$
,不过加上也不报错。
1 | $ number=2 |
上面例子中,变量number
前面有没有美元符号,结果都是一样的。
如果在$((...))
里面使用字符串,Bash 会认为那是一个变量名。如果不存在同名变量,Bash 就会将其作为空值,因此不会报错。
1 | $ echo $(( "hello" + 2)) |
上面例子中,"hello"
会被当作变量名,返回空值,而$((...))
会将空值当作0
,所以乘法的运算结果就是0
。同理,如果$((...))
里面使用不存在的变量,也会当作0
处理。
如果一个变量的值为字符串,跟上面的处理逻辑是一样的。即该字符串如果不对应已存在的变量,在$((...))
里面会被当作空值。
1 | $ foo=hello |
上面例子中,变量foo
的值是hello
,而hello
也会被看作变量名。这使得有可能写出动态替换的代码。
1 | $ foo=hello |
上面代码中,foo + 2
取决于变量hello
的值。
最后,$[...]
是以前的语法,也可以做整数运算,不建议使用。
1 | $ echo $[2+2] |
数值的进制
Bash 的数值默认都是十进制,但是在算术表达式中,也可以使用其他进制。
number
:没有任何特殊表示法的数字是十进制数(以10为底)。0number
:八进制数。0xnumber
:十六进制数。base#number
:base
进制的数。
下面是一些例子。
1 | $ echo $((0xff)) |
上面例子中,0xff
是十六进制数,2#11111111
是二进制数。
位运算
$((...))
支持以下的二进制位运算符。
<<
:位左移运算,把一个数字的所有位向左移动指定的位。>>
:位右移运算,把一个数字的所有位向右移动指定的位。&
:位的“与”运算,对两个数字的所有位执行一个AND
操作。|
:位的“或”运算,对两个数字的所有位执行一个OR
操作。~
:位的“否”运算,对一个数字的所有位取反。^
:位的异或运算(exclusive or),对两个数字的所有位执行一个异或操作。
下面是右移运算符>>
的例子。
1 | $ echo $((16>>2)) |
下面是左移运算符<<
的例子。
1 | $ echo $((16<<2)) |
下面是17
(二进制10001
)和3
(二进制11
)的各种二进制运算的结果。
1 | $ echo $((17&3)) |
逻辑运算
$((...))
支持以下的逻辑运算符。
<
:小于>
:大于<=
:小于或相等>=
:大于或相等==
:相等!=
:不相等&&
:逻辑与||
:逻辑或!
:逻辑否expr1?expr2:expr3
:三元条件运算符。若表达式expr1
的计算结果为非零值(算术真),则执行表达式expr2
,否则执行表达式expr3
。
如果逻辑表达式为真,返回1
,否则返回0
。
1 | $ echo $((3 > 2)) |
三元运算符执行一个单独的逻辑测试。它用起来类似于if/then/else
语句。
1 | $ a=0 |
上面例子中,第一个表达式为真时,返回第二个表达式的值,否则返回第三个表达式的值。
赋值运算
算术表达式$((...))
可以执行赋值运算。
1 | $ echo $((a=1)) |
上面例子中,a=1
对变量a
进行赋值。这个式子本身也是一个表达式,返回值就是等号右边的值。
$((...))
支持的赋值运算符,有以下这些。
parameter = value
:简单赋值。parameter += value
:等价于parameter = parameter + value
。parameter -= value
:等价于parameter = parameter – value
。parameter *= value
:等价于parameter = parameter * value
。parameter /= value
:等价于parameter = parameter / value
。parameter %= value
:等价于parameter = parameter % value
。parameter <<= value
:等价于parameter = parameter << value
。parameter >>= value
:等价于parameter = parameter >> value
。parameter &= value
:等价于parameter = parameter & value
。parameter |= value
:等价于parameter = parameter | value
。parameter ^= value
:等价于parameter = parameter ^ value
。
下面是一个例子。
1 | $ foo=5 |
如果在表达式内部赋值,可以放在圆括号中,否则会报错。
1 | $ echo $(( a<1 ? (a+=1) : (a-=1) )) |
求值运算
逗号,
在$((...))
内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值。
1 | $ echo $((foo = 1 + 2, 3 * 4)) |
上面例子中,逗号前后两个表达式都会执行,然后返回后一个表达式的值12
。
expr 命令
expr
命令支持算术运算,可以不使用((...))
语法。
1 | $ expr 3 + 2 |
expr
命令支持变量替换。
1 | $ foo=3 |
expr
命令也不支持非整数参数。
1 | $ expr 3.5 + 2 |
上面例子中,如果有非整数的运算,expr
命令就报错了。
let 命令
let
命令用于将算术运算的结果,赋予一个变量。
1 | $ let x=2+3 |
上面例子中,变量x
等于2+3
的运算结果。
注意,x=2+3
这个式子里面不能有空格,否则会报错。
Bash 行操作
简介
Bash 内置了 Readline 库,具有这个库提供的很多“行操作”功能,比如命令的自动补全,可以大大加快操作速度。
这个库默认采用 Emacs 快捷键,也可以改成 Vi 快捷键。
1 | $ set -o vi |
下面的命令可以改回 Emacs 快捷键。
1 | $ set -o emacs |
如果想永久性更改编辑模式(Emacs / Vi),可以将命令写在~/.inputrc
文件,这个文件是 Readline 的配置文件。
1 | set editing-mode vi |
本章介绍的快捷键都属于 Emacs 模式。Vi 模式的快捷键,读者可以参考 Vi 编辑器的教程。
Bash 默认开启这个库,但是允许关闭。
1 | $ bash --noediting |
上面命令中,--noediting
参数关闭了 Readline 库,启动的 Bash 就不带有行操作功能。
光标移动
Readline 提供快速移动光标的快捷键。
Ctrl + a
:移到行首。Ctrl + b
:向行首移动一个字符,与左箭头作用相同。Ctrl + e
:移到行尾。Ctrl + f
:向行尾移动一个字符,与右箭头作用相同。Alt + f
:移动到当前单词的词尾。Alt + b
:移动到当前单词的词首。
上面快捷键的 Alt 键,也可以用 ESC 键代替。
清除屏幕
Ctrl + l
快捷键可以清除屏幕,即将当前行移到屏幕的第一行,与clear
命令作用相同。
编辑操作
下面的快捷键可以编辑命令行内容。
Ctrl + d
:删除光标位置的字符(delete)。Ctrl + w
:删除光标前面的单词。Ctrl + t
:光标位置的字符与它前面一位的字符交换位置(transpose)。Alt + t
:光标位置的词与它前面一位的词交换位置(transpose)。Alt + l
:将光标位置至词尾转为小写(lowercase)。Alt + u
:将光标位置至词尾转为大写(uppercase)。
使用Ctrl + d
的时候,如果当前行没有任何字符,会导致退出当前 Shell,所以要小心。
剪切和粘贴快捷键如下。
Ctrl + k
:剪切光标位置到行尾的文本。Ctrl + u
:剪切光标位置到行首的文本。Alt + d
:剪切光标位置到词尾的文本。Alt + Backspace
:剪切光标位置到词首的文本。Ctrl + y
:在光标位置粘贴文本。
同样地,Alt 键可以用 Esc 键代替。
自动补全
命令输入到一半的时候,可以按一下 Tab 键,Readline 会自动补全命令或路径。比如,输入cle
,再按下 Tab 键,Bash 会自动将这个命令补全为clear
。
如果符合条件的命令或路径有多个,就需要连续按两次 Tab 键,Bash 会提示所有符合条件的命令或路径。
除了命令或路径,Tab 还可以补全其他值。如果一个值以$
开头,则按下 Tab 键会补全变量;如果以~
开头,则补全用户名;如果以@
开头,则补全主机名(hostname),主机名以列在/etc/hosts
文件里面的主机为准。
自动补全相关的快捷键如下。
- Tab:完成自动补全。
Alt + ?
:列出可能的补全,与连按两次 Tab 键作用相同。Alt + /
:尝试文件路径补全。Ctrl + x /
:先按Ctrl + x
,再按/
,等同于Alt + ?
,列出可能的文件路径补全。Alt + !
:命令补全。Ctrl + x !
:先按Ctrl + x
,再按!
,等同于Alt + !
,命令补全。Alt + ~
:用户名补全。Ctrl + x ~
:先按Ctrl + x
,再按~
,等同于Alt + ~
,用户名补全。Alt + $
:变量名补全。Ctrl + x $
:先按Ctrl + x
,再按$
,等同于Alt + $
,变量名补全。Alt + @
:主机名补全。Ctrl + x @
:先按Ctrl + x
,再按@
,等同于Alt + @
,主机名补全。Alt + *
:在命令行一次性插入所有可能的补全。Alt + Tab
:尝试用.bash_history
里面以前执行命令,进行补全。
上面的Alt
键也可以用 ESC 键代替。
操作历史
基本用法
Bash 会保留用户的操作历史,即用户输入的每一条命令都会记录。有了操作历史以后,就可以使用方向键的↑
和↓
,快速浏览上一条和下一条命令。
退出当前 Shell 的时候,Bash 会将用户在当前 Shell 的操作历史写入~/.bash_history
文件,该文件默认储存500个操作。
环境变量HISTFILE
总是指向这个文件。
1 | $ echo $HISTFILE |
history
命令会输出这个文件的全部内容。用户可以看到最近执行过的所有命令,每条命令之前都有行号。越近的命令,排在越后面。
1 | $ history |
输入命令时,按下Ctrl + r
快捷键,就可以搜索操作历史,选择以前执行过的命令。这时键入命令的开头部分,Shell 就会自动在历史文件中,查询并显示最近一条匹配的结果,这时按下回车键,就会执行那条命令。
下面的方法可以快速执行以前执行过的命令。
1 | $ echo Hello World |
上面例子中,!e
表示找出操作历史之中,最近的那一条以e
开头的命令并执行。Bash 会先输出那一条命令echo Goodbye
,然后直接执行。
同理,!echo
也会执行最近一条以echo
开头的命令。
1 | $ !echo |
注意,!string
语法只会匹配命令,不会匹配参数。所以!echo H
不会执行echo Hello World
,而是会执行echo Goodbye
,并把参数H
附加在这条命令之后。同理,!echo H G
也是等同于echo Goodbye
命令之后附加H G
。
由于!string
语法会扩展成以前执行过的命令,所以含有!
的字符串放在双引号里面,必须非常小心,如果它后面有非空格的字符,就很有可能报错。
1 | $ echo "I say:\"hello!\"" |
上面的命令会报错,原因是感叹号后面是一个反斜杠,Bash 会尝试寻找,以前是否执行过反斜杠开头的命令,一旦找不到就会报错。解决方法就是在感叹号前面,也加上反斜杠。
1 | $ echo "I say:\"hello\!\"" |
history 命令
前面说过,history
命令能显示操作历史,即.bash_history
文件的内容。
1 | $ history |
使用该命令,而不是直接读取.bash_history
文件的好处是,它会在所有的操作前加上行号,最近的操作在最后面,行号最大。
通过定制环境变量HISTTIMEFORMAT
,可以显示每个操作的时间。
1 | $ export HISTTIMEFORMAT='%F %T ' |
上面代码中,%F
相当于%Y - %m - %d
,%T
相当于%H : %M : %S
。
只要设置HISTTIMEFORMAT
这个环境变量,就会在.bash_history
文件保存命令的执行时间戳。如果不设置,就不会保存时间戳。
环境变量HISTSIZE
设置保存历史操作的数量。
1 | $ export HISTSIZE=10000 |
上面命令设置保存过去10000条操作历史。
如果不希望保存本次操作的历史,可以设置HISTSIZE
等于0。
1 | export HISTSIZE=0 |
如果HISTSIZE=0
写入用户主目录的~/.bashrc
文件,那么就不会保留该用户的操作历史。如果写入/etc/profile
,整个系统都不会保留操作历史。
环境变量HISTIGNORE
可以设置哪些命令不写入操作历史。
1 | export HISTIGNORE='pwd:ls:exit' |
上面示例设置,pwd
、ls
、exit
这三个命令不写入操作历史。
如果想搜索某个以前执行的命令,可以配合grep
命令搜索操作历史。
1 | $ history | grep /usr/bin |
上面命令返回.bash_history
文件里面,那些包含/usr/bin
的命令。
操作历史的每一条记录都有编号。知道了命令的编号以后,可以用感叹号 + 编号
执行该命令。如果想要执行.bash_history
里面的第8条命令,可以像下面这样操作。
1 | $ !8 |
history
命令的-c
参数可以清除操作历史。
1 | $ history -c |
相关快捷键
下面是一些与操作历史相关的快捷键。
Ctrl + p
:显示上一个命令,与向上箭头效果相同(previous)。Ctrl + n
:显示下一个命令,与向下箭头效果相同(next)。Alt + <
:显示第一个命令。Alt + >
:显示最后一个命令,即当前的命令。Ctrl + o
:执行历史文件里面的当前条目,并自动显示下一条命令。这对重复执行某个序列的命令很有帮助。
感叹号!
的快捷键如下。
!!
:执行上一个命令。!n
:n
为数字,执行历史文件里面行号为n
的命令。!-n
:执行当前命令之前n
条的命令。!string
:执行最近一个以指定字符串string
开头的命令。!?string
:执行最近一条包含字符串string
的命令。!$
:代表上一个命令的最后一个参数。!*
:代表上一个命令的所有参数,即除了命令以外的所有部分。^string1^string2
:执行最近一条包含string1
的命令,将其替换成string2
。
下面是!$
和!*
的例子。
1 | $ cp a.txt b.txt |
上面示例中,!$
代表上一个命令的最后一个参数(b.txt
),!*
代表上一个命令的所有参数(a.txt b.txt
)。
下面是^string1^string2
的例子。
1 | $ rm /var/log/httpd/error.log |
上面示例中,^error^access
将最近一条含有error
的命令里面的error
,替换成access
。
如果希望确定是什么命令,然后再执行,可以打开histverify
选项。这样的话,使用!
快捷键所产生的命令,会先打印出来,等到用户按下回车键后再执行。
1 | $ shopt -s histverify |
其他快捷键
Ctrl + j
:等同于回车键(LINEFEED)。Ctrl + m
:等同于回车键(CARRIAGE RETURN)。Ctrl + o
:等同于回车键,并展示操作历史的下一个命令。Ctrl + v
:将下一个输入的特殊字符变成字面量,比如回车变成^M
。Ctrl + [
:等同于 ESC。Alt + .
:插入上一个命令的最后一个词。Alt + _
:等同于Alt + .
。
上面的Alt + .
快捷键,对于很长的文件路径,有时会非常方便。因为 Unix 命令的最后一个参数通常是文件路径。
1 | $ mkdir foo_bar |
上面例子中,在cd
命令后按下Alt + .
,就会自动插入foo_bar
。
目录堆栈
为了方便用户在不同目录之间切换,Bash 提供了目录堆栈功能。
cd -
Bash 可以记忆用户进入过的目录。默认情况下,只记忆前一次所在的目录,cd -
命令可以返回前一次的目录。
1 | # 当前目录是 /path/to/foo |
上面例子中,用户原来所在的目录是/path/to/foo
,进入子目录bar
以后,使用cd -
可以回到原来的目录。
pushd,popd
如果希望记忆多重目录,可以使用pushd
命令和popd
命令。它们用来操作目录堆栈。
pushd
命令的用法类似cd
命令,可以进入指定的目录。
1 | $ pushd dirname |
上面命令会进入目录dirname
,并将该目录放入堆栈。
第一次使用pushd
命令时,会将当前目录先放入堆栈,然后将所要进入的目录也放入堆栈,位置在前一个记录的上方。以后每次使用pushd
命令,都会将所要进入的目录,放在堆栈的顶部。
popd
命令不带有参数时,会移除堆栈的顶部记录,并进入新的栈顶目录(即原来的第二条目录)。
下面是一个例子。
1 | # 当前处在主目录,堆栈为空 |
这两个命令的参数如下。
(1)-n 参数
-n
的参数表示仅操作堆栈,不改变目录。
1 | $ popd -n |
上面的命令仅删除堆栈顶部的记录,不改变目录,执行完成后还停留在当前目录。
(2)整数参数
这两个命令还可以接受一个整数作为参数,该整数表示堆栈中指定位置的记录(从0开始)。pushd
命令会把这条记录移动到栈顶,同时切换到该目录;popd
则从堆栈中删除这条记录,不会切换目录。
1 | # 将从栈顶算起的3号目录(从0开始)移动到栈顶,同时切换到该目录 |
上面例子的整数编号都是从0开始计算,popd +0
是删除第一个目录,popd +1
是删除第二个,popd -0
是删除最后一个目录,popd -1
是删除倒数第二个。
(3)目录参数
pushd
可以接受一个目录作为参数,表示将该目录放到堆栈顶部,并进入该目录。
1 | $ pushd dir |
popd
没有这个参数。
dirs 命令
dirs
命令可以显示目录堆栈的内容,一般用来查看pushd
和popd
操作后的结果。
1 | $ dirs |
该命令会输出一行文本,列出目录堆栈,目录之间使用空格分隔。栈顶(最晚入栈的目录)在最左边,栈底(最早入栈的目录)在最右边。
它有以下参数。
-c
:清空目录栈。-l
:用户主目录不显示波浪号前缀,而打印完整的目录。-p
:每行一个条目打印目录栈,默认是打印在一行。-v
:每行一个条目,每个条目之前显示位置编号(从0开始)。+N
:N
为整数,表示显示堆顶算起的第 N 个目录,从零开始。-N
:N
为整数,表示显示堆底算起的第 N 个目录,从零开始。
- Title: Bash-note-2
- Author: Charles
- Created at : 2023-02-18 08:04:29
- Updated at : 2023-11-05 21:36:19
- Link: https://charles2530.github.io/2023/02/18/bash-note-2/
- License: This work is licensed under CC BY-NC-SA 4.0.