Table of Contents
概述
首先,咱们来了解一下,什么是Shell
。操作系统内核给我们提供了各种接口,同时也提供了各种用户层的库,理论上我们基于这些可以编写程序实现各种我们想要的功能,不过问题是,咱们不可能做什么事情都要重新编写程序,这样使用起来也太困难了。因此,操作系统(包括Linux)通常都会引入一个Shell
这样的特殊程序,这个程序会接受输入的命令然后执行,并可能将执行结果呈现出来。总结来说,Shell
是一个从输入设备或者文件读取命令,并且解释、执行的用户态程序。
在Linux系统中,通常使用的Shell
程序包括有:
* Sh (Bourne Shell)
* Bash (Bourne Again Shell)
* Csh (C Shell)
* Ksh (Korn Shell)
一般来说,Bash
应该是使用最多的Shell
程序了,本文也主要基于Bash
来展开。
Shell展开(Shell Expansion)
Shell
程序是一个命令解释器,因此在终端输入命令之后,Shell
将扫描命令并做适当的修改,这个过程称为Shell展开。Shell展开是Shell解释执行之前极为重要的一步,了解它将有利于你对Shell命令或者脚本的理解,本章节将逐步带大家来了解这个过程。
命令参数解析
这里的空格包括了制表符(Tab)。当Shell程序扫描输入的命令时,会以*连续*的空格为界,将命令切分成一组参数,因此你输入多个空格为界跟输入一个空格的效果是一样的。通常来讲,第一个参数就是要执行的命令,而后面的参数则是改命令的参数。一下几个命令其实是等效的:
|
|
引号
当然,有时候你需要在一个参数中包括空格,这样的话你就需要将这个参数以引号引起来,引号包括了单引号'
跟双引号"
,两者都可以。shell
会将引号中的字符串视为一个参数,不论里面有没有空格。当然,特别指出的是,不要用反引号`
,反引号将在后面详细讲述。
如命令echo 'Hello World!'
在shell
解析之后会有两个参数,分别为echo
跟Hello World!
。而如果不用引号echo Hello World!
,则将解析为三个参数。
特别提一下,对于
echo
命令,如果需要输出需要转义的字符,如回车等,则需要执行echo -e "Hello World! "
,如果不加-e
,则会被直接显示出来。
123456789101112131415161718192021222324252627282930313233
> # echo "hello "> hello> # echo -e "hello "> hello>> ```## 命令对于`shell`来说,命令有内部命令(Builtin Commands)跟外部命令(External Commands)之分,所谓内部命令指的是包含在`shell`解析器中的命令。内部命令一般有[4种类型](http://www.gnu.org/software/bash/manual/bashref.html#Shell-Builtin-Commands):* `sh`内部命令这些内部命令来源于`Bourne Shell`,通常包括了以下命令:`: . break cd continue eval exec exit export getopts hash pwd readonly return shift test/[ times trap umask unset`。* `bash`内部命令这些内部命令来源于`Bourne Again Shell`,通常包括了以下命令:`alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias`。* 修改`shell`行为的内部命令这些内部命令用来修改`shell`的默认行为。包括了`set shopt`命令。* 特殊内部命令由于历史原因,POSIX标准将一些内部命令划分为特殊内部命令,特殊的之处在于这些命令的查找时间以及命令运行后的状态等方面,只有当Bash以[POSIX模式](http://www.gnu.org/software/bash/manual/bashref.html#Bash-POSIX-Mode)运行时,这些命令才是特殊命令,否则它们跟其它内部命令没啥区别。特殊内部命令包括了`break : . continue eval exec exit export readonly return set shift trap unset`。**内部命令可能会被提前至于内存中,因此运行起来会比外部命令要快。**对于外部命令,可以认为除了内部命令之后就可以认为是外部命令了,通常来讲,`/bin`跟`/sbin`下的都是外部命令,当然,应用有关的通常也是外部命令。我们可以通过`type`命令来查看一个命令是否是内部命令:type cd
cd is a shell builtin
type awk
awk is /usr/bin/awk
123
另外,对于很多内部命令,它们可能对应的会有外部命令版本,可以通过`type`命令来查看:type -a echo
echo is a shell builtin echo is /usr/bin/echo
type -a cd
cd is a shell builtin cd is /usr/bin/cd
123
反过来,我们一般可以通过命令`which`来查询一个命令是否是外部命令:which awk
/usr/bin/awk
which .
/usr/bin/which: no . in (/opt/rh/rh-python34/root/usr/bin:/usr/java/default/bin/:/usr/local/git/bin:/opt/ActiveTcl-8.5/bin:/root/perl5/bin:/root/env/maven/apache-maven-3.3.3/bin:/root/soft/wrk/wrk-4.0.1:/root/usr/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin)
123
总结一下,通过`which`查询出来的是其外部命令版本,通过`type`默认查询出来的是内部命令:which echo
/usr/bin/echo
type echo
echo is a shell builtin
1234567
对于内部命令的详细说明,可以查看[GNU的文档](http://www.gnu.org/software/bash/manual/bashref.html#Shell-Builtin-Commands)。## 别名可以用`alias`命令给一个命令取一个别名:alias print=echo
print “hello”
hello
type print
print is aliased to `echo’
123 别名一个常用的用法是用来缩写已知的命令:type ls
ls is aliased to `ls –color=auto’
123
可见`ls`命令实际上是命令`ls --color=auto`的别名,这样就相当于改变了`ls`命令的默认行为了。在这种情况下,如果仍然想用原先的命令,可以在别名前加反斜杠`` ``:type ls
ls is aliased to `ls –color=auto’
ls
Test1 test2 test.cpp test.log time.log
1234
前面咱们通过`type`命令来查看命令的别名,实际上更加推荐采用`alias`或者`which`来查看:alias ls
alias ls=‘ls –color=auto’
which ls
alias ls=‘ls –color=auto’ /usr/bin/ls
123
如果要取消别名,则可以采用`unalias`命令:which ls
alias ls=‘ls –color=auto’ /usr/bin/ls
unalias ls
which ls
/usr/bin/ls
123456
## 显示shell展开的结果由于`shell`展开的存在,你输入的命令被展开之后可能会发生变化,如果需要知道`shell`展开之后的命令,可以使用内部命令`set`来修改`shell`的默认参数来显示:set -x
++ printf ‘ 33]0;%s@%s:%s 07’ root traffic-base1 ‘~’
echo hello world
- echo hello world hello world ++ printf ‘ 33]0;%s@%s:%s 07’ root traffic-base1 ‘~’ ```
其中,以
+
开头的就是展开之后的命令,可见展开之后,shell
将多余的空格去掉了。如果不要再显示了,可以输入命令set +x
。shell控制操作符 (Control Operators)
$?
操作符每个命令执行完后都会有个退出码(Exit Code),其值为0时表示命令成功,否则命令失败。这个退出码可以通过
$?
来访问,执行完命令后立马访问$?
可以获取该命令的退出码,并以此来判断命令是否成功。每个命令的执行都会产生新的退出码,所以请务必在命令执行完,立刻访问$?
来获取退出码。初看起来,
$?
似乎是一个shell
变量,但实际上并非如此,因为你无法对$?
赋值。$?
准确来说是shell
的一个内部参数。分号
;
shell
命令输入时,你可以将多个命令输入在一行,只要在不同命令之间以分号;
隔开,当然分号不能是在引号中。 > 必须注意的是,如果将多个命令以;
连接在一起,执行的结果通过$?
查询出来将只是最后一个命令的结果
&
符号通常情况下,
shell
会在前台执行命令,并等待命令结束才返回。如果需要将命令放到后台去执行,可以使用&
符号放在命令最后面,这样的话命令会被放在后台执行,shell
会立刻返回而不用等待命令结束。 > 注意的是,即便放在后台执行,但是如果不处理好命令的输入,则命令的输出可能会继续在当前的终端输出,后面会讲述如何处理命令的输出。
&&
操作符此操作符表示逻辑与,你可以将两个命令用此操作符连接起来,如
cmd1 && cmd2
,只有当cmd1
执行成功之后,cmd2
才会被执行。这里的成功指的是cmd1
的退出码是0。
12345
# hello && echo world-bash: hello: command not found# echo hello && echo worldhelloworld当然,
&&
也可以将多个命令连接起来,其执行类似,只有当前面的命令成功,后面的才会执行。因此,将多个命令写在一行用&&
可以实现,只不过&&
必须按照逻辑与的关系执行,而;
号的话会执行所有的命令。
||
操作符很显然,与
&&
相对,||
操作符表示逻辑或的关系,同样可以连接两个命令,如cmd1 || cmd2
,只有当cmd1
失败了,才会执行cmd2
,这里的失败指的是cmd1
的退出码非0。
&&
与||
混合这两个操作符是可以混合使用的,其遵循的原则保持一致,且是从左向右依次判断,结合这两种操作符,可以实现类似于
if then else
的逻辑结构。如cmd1 && cmd2 || cmd3
意思就是如果cmd1
成功,则执行cmd2
,否则执行cmd3
。但务必注意的是,此处并非真正意思上的if then else
逻辑,因为如果cmd2
也执行失败,cmd3
其实也会被执行。如下例:
1234567
# echo hello && echo ok || echo worldhellook# echo hello && rm dfsdf || echo worldhellorm: cannot remove ‘dfsdf’: No such file or directoryworld
&&
相当于将两条命令逻辑上连成了一条命令,这样就变成了cmd1-2 || cmd3
,其中cmd1-2
就是cmd1 && cmd2
,因此,cmd3
只要在cmd1-2
失败的情况下都会被执行,而cmd1-2
失败的情况有两种,一种是cmd1
失败,一种是cmd1
成功但是cmd2
失败。同样的,||
也会将两条命令连成一条命令,如cmd1-2 || cmd3 && cmd4
就相当于cmd1-2_3 && cmd4
,cmd4
是否会执行,决定于cmd1-2_3
是否失败,以具体例子说明:
12345
# echo hello && echo ok || echo world && rm dsdfsf || echo endhellookrm: cannot remove ‘dsdfsf’: No such file or directoryend这行命令相当于
cmd1 && cmd2 || cmd3 && cmd4 || cmd5
,可以看出cmd1
,cmd2
,cmd4
还是有cmd5
被执行了,而cmd3
没有执行。咱们来解析一下,为何是如此的执行结果。首先,shell
从左往右扫描执行:
- 发现
cmd1 && cmd2
,由&&
连成一个命令cmd1-2
,因为两个命令都是成功的,所以都被执行了,这样可以认为cmd1-2
成功- 执行成功之后,接下来是
||
操作符,这里并不会因为前面的命令是成功的,而不再执行后面所有的命令,而是||
操作符相当于将cmd1-2
与cmd3
连接成了cmd1-2_3
,因为cmd1-2
成功了,所以cmd3
不再执行,但是cmd1-2_3
相当于执行成功了- 继续执行,发现是
&&
操作符,同样将cmd1-2_3
与cmd4
连接起来,记为cmd1-2_3-4
,因为cmd1-2_3
执行成功了,所以cmd4
也被执行,但是cmd4
执行失败了,所以cmd1-2_3-4
相当于执行失败- 继续执行,发现是
||
操作符,同样将cmd1-2_3-4
与cmd5
连成cmd1-2_3-4_5
,因为cmd1-2_3-4
执行失败,所以cmd5
被执行可见,
shell
永远都是从左往右扫描执行,&&
跟||
会将前后两个命令连接起来,根据两种操作符的规则就可以知道多个连起来的命令是如何执行的了。
#
符号跟其它很多语言一样,
#
在shell
里面用来注释。
转义符号
符号可以用来转义一些特殊符号,如
$
,#
等。特别指出的是,如果转义符号放在行末单独使用,则用来连接下一行。
shell变量
基本概念
定义跟引用
shell
中也可以使用变量,变量不需要像其它语言一样需要预先申明。shell
中赋值给一个不存在的变量就相当于定义了变量,如name="Mr. Hao"
,就定义了name
变量,后续如果再对name
赋值,就相当于改变改变量的值。与很多语言不同的是,shell
中变量引用以$
符号开头,后面跟变量的名字。如前面的变量,引用如下echo "$name"
。需要注意的是,在shell
中,变量名是大小写敏感的。在
shell
展开中会自动展开变量的引用,即便该变量处在双引号中。但是,如果变量引用在单引号中,shell
不会对其进行解析。
1234567
# name="Mr. Hao"# echo "$name"Mr. Hao# set -x# echo '$name'+ echo 'Mr. Hao'$name查找变量
可以使用
set
命令来查找所定义的变量:
12
# set | grep -E '^name='name='Mr. Hao'删除变量
与很多语言不同的是,在
shell
中定义的变量是可以删除的,使用unset
命令删除定义的变量。
1234
# set | grep -E '^name='name='Mr. Hao'# unset name# set | grep -E '^name='
export
声明通常情况下,
shell
在执行命令的时候会为该命令创建子进程。如果希望将当前的变量作用到子进程,则需要将变量export
声明,这种变量称之为环境变量,如:
12345
# var1="hello"# export var2="world"# bash# echo "var1=$var1, var2=$var2"var1=, var2=world其中,
bash
命令开启了一个新的shell
,可见只有export
声明的变量在新的shell
中才是可见的。环境变量可以通过env
命令列举出来,在后面一节会详细讲述。此外,如果需要将非export
变量重新声明为export
变量,则只需要用export
重新声明一下即可:
12345
# var1=hello# env | grep var1# export var1# env | grep var1var1=hello
env
命令如果需要查看当前
shell
中有哪些export
声明的变量,可以使用env
命令,该命令会列出当前所有export
声明的变量。请注意与set
命令的区别,set
命令会列出所有的变量,包括哪些不是export
声明的变量。通常,我们把env
命令输出的变量称之为环境变量
。此外,
env
也常用来为子shell
预先定义一些临时变量,如:
12345
# var1="hello"# env var1="tmp" bash -c 'echo "$var1"'tmp# echo $var1hello其中,用
env
命令定义了临时变量var1
,然后bash
命令开启了一个子shell
,并在子shell
中执行了echo "$var1"
命令。可见,输出了定义的临时变量,在命令结束后,又回到之前的shell
,输出的也是之前shell
中定义的值。当然,在使用env
定义临时变量的时候,为了方便,通常我们可以省略env
命令,如:
12345
# var1="hello"# var1="tmp" bash -c 'echo "$var1"'tmp# echo $var1hello另外,
env
命令还有一种常用的用法,就是用来开启一个干净的子shell
,即在子shell
中不继承所有的变量,即便这些变量在之前的shell
中采用export
声明,此时env
命令需要加入-i
的参数,如:
12345
# export var1="hello world"# bash -c 'echo "var1=$var1"'var1=hello world# env -i bash -c 'echo "var1=$var1"'var1=可见,使用
env -i
之后,即便var1
被export
声明,但是在子shell
中也没有被继承。变量解释
在前面章节,我们知道
shell
采用$
符号引用变量,在$
符号后紧跟变量的名字。而shell
在提取变量名字的时候一般以非字母数字(non-alphanumeric)为边界,这有时候就会产生问题,如:
123
# prefix=Super# echo Hello $prefixman and $prefixgirlHello and可见,
shell
并不能提取我们定义的变量prefix
,因为其后并没有非字母数字的字符为界。这种情况下,我们可以使用{}
将变量名保护起来。
123
# prefix=Super# echo Hello ${prefix}man and ${prefix}girlHello Superman and Supergirl非绑定(unbound)变量
所谓非绑定(unbound)变量其实指的是没有预先定义的变量,或者说不存在的变量。默认情况下,
shell
在解释这种变量的时候会以空字符串替代:```
echo $unbound_var
|
|
echo $unbound_var
bash: unbound_var: unbound variable
set +u
echo $unbound_var
|
|
mrhao:~$
|
|
mrhao:~$ PS1=“hello > “ hello > echo “PS1 value is ‘$PS1’” PS1 value is ‘hello > ‘ hello >
|
|
RED=’[ 33[01;31m]‘
WHITE=’[ 33[01;00m]‘
GREEN=’[ 33[01;32m]‘
BLUE=’[ 33[01;34m]‘
PS1=“$GREENu$WHITE@$BLUEh$WHITEw$ “
mrhao@mrhao-host~$ echo “$PS1” [ 33[01;32m]u[ 33[01;00m]@[ 33[01;34m]h[ 33[01;00m]w$
|
|
echo $PATH
/opt/rh/rh-python34/root/usr/bin:/usr/java/default/bin/:/usr/local/git/bin:/opt/ActiveTcl-8.5/bin:/root/perl5/bin:/root/env/maven/apache-maven-3.3.3/bin:/root/soft/wrk/wrk-4.0.1:/root/usr/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
|
|
PATH=$PATH:/opt/local/bin
echo $PATH
/opt/rh/rh-python34/root/usr/bin:/usr/java/default/bin/:/usr/local/git/bin:/opt/ActiveTcl-8.5/bin:/root/perl5/bin:/root/env/maven/apache-maven-3.3.3/bin:/root/soft/wrk/wrk-4.0.1:/root/usr/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/opt/local/bin
|
|
var1=hello
echo $(var1=world; echo $var1)
world
echo $var1
hello
|
|
var1=hello
var2=“$(echo $var1 world)”
echo $var2
hello world
|
|
A=shell
echo $C$B$A $(B=sub;echo $C$B$A; echo $(C=sub;echo $C$B$A))
shell subshell subsubshell
|
|
var1=hello
echo var1=world; echo $var1
world
echo $var1
hello
|
|
history 10
1000 history 1001 history 10 1002 echo “hello world” 1003 ls -l 1004 ps -ef | grep named 1005 env | grep http 1006 grep hello /var/log/messages 1007 tmux ls 1008 find . -name “hello” 1009 history 10
|
|
echo “test1”
test1
!ec:s/1/2/
echo “test2” test2
|
|
ctt /etc/passwd | cut -d: -f1
-bash: ctt: command not found
cat !*
cat /etc/passwd | cut -d: -f1 root bin daemon adm …
|
|
例如:
|
|
可见,在设置$HISTIGNORE
变量之后,在前面加了空格的命令将不再记录。这在保护敏感信息的时候非常有用。
文件匹配(File Globbing)
文件匹配(File Globbing)又成为动态文件名生成,用它可以非常方便的在shell
中输入文件名。
*
星号
*
星号在shell
中用来匹配任意数量的字符,比如文件名File*.mp4
,将匹配以File
开头,.mp4
结尾的任何文件名。shell
在扫描解释命令的时候会自动去查找符合该匹配的所有文件或目录。当然,你也可以只用*
来匹配所有的文件及目录,但请注意,只使用*
跟不带*
还是有所区别的,
|
|
可见,带上*
后不仅把当前目录的所有文件及目录显示出来,而且还把目录下的内容显示出来了。
?
问号
问号用来匹配一个字符,如File?.mp4
可以匹配File1.mp4
。
[]
方括号
[]
方括号也用来匹配一个字符,但是在括号里面可以指定一个字符集用来限定匹配的字符必须在该字符集内,字符集里面的字符顺序没有关系。
|
|
如果需要匹配不在某个字符集里面的字符,可以在[]
第一个字符加入!
:
|
|
特别的,为了方便,[]
中可以使用-
来定义一些连续的字符集(Range匹配),常用的这类字符集包括:
字符集 | 说明 |
---|---|
0-9 | 表示数字字符集 |
a-z | 表示小写字母字符集 |
A-Z | 表示大写字母字符集 |
当然,你也不必要把所有范围都包括在内,如[a-d]
可以用来限定从a
到d
的小写字母集。另外,用-
连起来的字符集还可以跟其它字符集一起使用,如[a-d_]
表示a
到d
的小写字母加上_
所组成的字符集。
Range匹配的大小写问题
对于
[]
的Range匹配,还有一点很重要。在很多发行版本中,默认情况下,[]
的Range匹配是忽略大小写的12345678910# lsTest1 test2# ls [a-z]*Test1 test2# ls [A-Z]*Test1 test2# ls [t]*test2# ls [T]*Test1注意,是
[]
的Range匹配会忽略大小写,而如果不是Range匹配还是大小写敏感的:12345678910> # ls> Test1 test2> # ls [T]*> Test1> # ls [t]*> test2> ```如果需要大小写敏感,可以设置环境变量`LC_ALL`:LC_ALL=C
ls [a-z]*
test2
ls [A-Z]*
Test1 ```
当然,请务必注意,
LC_ALL
的会改变当前的语言环境,还请慎重使用,建议只在临时的子shell
中使用。
阻止文件匹配(File Globbing)
有时候我们就是需要输出*
等匹配符号,这个时候就需要阻止shell
做相应的匹配。可以使用转义符号来做到这点,或者将匹配符号放在引号中:
|
|
shell快捷键
shell
中支持非常多的快捷键,可以非常方便我们输入命令:
快捷键 | 说明 |
---|---|
Ctrl-d | 表示EOF 的意思,在shell终端中输入该快捷键会退出该终端 |
Ctrl-z | 该快捷键用来暂停一个在shell终端中正在执行的进程,暂停后可以用fg 命令恢复 |
Ctrl-a | 输入命令时跳到行首 |
Ctrl-e | 跳到行尾 |
Ctrl-k | 删除从光标到行尾的部分 |
Ctrl-y | 粘贴刚刚删除的部分 |
Ctrl-w | 删除从光标至其左边第一个空格处 |
</div>