Bash 基础

Shell 的定义

Shell 是一个命令行解释器,其功能是提供用户操作系统的一个接口,因此 shell 需要可以调用其他软件,用户通过 shell 来操作这些应用程序。也就是只要能够操作应用程序的接口都能够称为 shell。

可以通过检查 /etc/shells 来得知当前系统可用的 shell:

[root@101c7 4]$ cat /etc/shells 
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash

文件 passwd 里记录了用户能使用的 shell:

[root@101c7 4]$ cat /etc/passwd | grep root
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin

bash 特点

Bash (Bourne Again Shell) 的主要特点有:

  • 命令记忆能力:在命令行中按上下键就能找到输入过的命令, 默认记录最大 1000 条。
  • 命令补全功能:通过 [Tab] 按键能自动补全命令或文件名。
  • 命令别名功能:可以使用 alias 命名一些自定义命令。
  • 作业后台控制:可以将工作丢到后台执行。
  • 使用程序脚本:可以将很多命令写成一个文件执行。
  • 可使用通配符:支持常用的通配符比如 *?

命令类型

bash 已经内置了很多命令,可以使用 type 命令来查询命令类型。内建命令(Builtin)和外部命令的区别在于前者不需要使用子程序来进行,它们已经和 shell 编译成一体,作为 shell 工具的组成部分存在。因此内建命令执行速度更快,效率更高。

有些命令两种实现都有,which 命令只显示出外部命令文件,这种情况下要使用外部命令文件直接使用绝对路径运行。

例如查询 mancd 命令:

[root@101c7 4]$ type ls
ls is aliased to `ls --color=auto'
[root@101c7 4]$ type man
man is /usr/bin/man
[root@101c7 4]$ type cd
cd is a shell builtin

结果显示 man 是外部命令,cd 是内部命令,ls 有别名。

对于别名命令可以用 -a 参数进一步查询:

[root@101c7 4]$ type -a ls
ls is aliased to `ls --color=auto'
ls is /usr/bin/ls

特殊符号

理论上文件名中不要使用以下已被特殊定义的字符:

符号 内容
# 批注符号,一般在脚本中拿来注释说明行。
** 转义符号,将特殊字符还原成一般字符。
| 管道(pipe),分隔两个管道命令的界定。
; 连续命令执行分隔符。
~ 用户的主文件夹。
$ 使用变量前导符,即是变量之前需要加的变量替代值。
& 作业控制(job control),将命令放置后台工作。
! 逻辑运算意义的非(not)。
/ 目录符号,路径分隔的符号。
>, >> 数据流重定向,输出导向,代表替换与追加。
<, << 数据流重定向,输入导向。
' ' 单引号,不具有变量置换功能。
" " 双引号,具有变量置换功能。
` ` 套在可以先执行的命令两边,等同于 $()
( ) 在中间为子 shell 的起始与结束。
{ } 在中间为命令块的组合。

命令回传码

可以通过命令回传码来判断后续命令是否执行,在两条命令间用 &&|| 连起来。具体意义如下:

执行的命令 说明
cmd1 && cmd2 若 cmd1 执行完毕且正确执行($?=0),继续执行 cmd2。否则($?≠b0)不执行 cmd2
cmd1 || cmd2 若 cmd1 执行完毕且正确执行($?=0),不执行 cmd2。否则($?≠0)继续执行 cmd2

例如判断目录 /tmp/abc 存在时执行 cd 命令:

[root@101c7 ~]$ ls /tmp/abc && cd /tmp/abc
ls: cannot access /tmp/abc: No such file or directory

结果只有 ls 报错,说明并没有执行后面的 cd 命令。

例如判断目录 /tmp/abc 不存在时自动新建目录并进入:

[root@101c7 ~]$ ls /tmp/abc || mkdir /tmp/abc && cd /tmp/abc ; pwd
ls: cannot access /tmp/abc: No such file or directory
/tmp/abc

假如目录 /tmp/abc 存在时,回传为 0,不执行 mkdir 命令,$?=0 继续向后传,顺利执行 cd 命令。

如果将 ||&& 后面的命令调转一下位置,先进行 && 判断,那么在文件夹不存在的情况下,cd 命令不会运行,但会执行 mkdir 命令,没有达到目标。因此命令执行的顺序很重要。

标准输入输出

标准输出(stdout,Standard Output)与标准错误输出(stderr,Standard Error Output)指的是命令执行所回传的正常与报错信息。默认情况下都是输出到屏幕上。

标准输入(stdin,Standard Input)一般接受的是用户键盘输入数据。

匿名管道

匿名管道(Anonymous Pipe)命令使用 | 符号界定用途时,将前一个命令的标准输出(stdout,不能处理 stderr)转给后一命令做标准输入,管道将自动创建。

每个管道后面接的第一个数据必须是命令,并且这个命令必须能接受 stdin 的数据。例如 lesstailgrep 等。

命名管道

命名管道(FIFO,First-in First-out)和匿名管道相似,不同的是命名管道必须显式创建,并且可以重用。

通常命名管道用来促进两个进程之间的数据交换,这一操作也叫做进程间通信(IPC,Interprocess Communication),程序将在需要时创建,使用然后删除命名管道。

命名管道使用 mkfifo 命令来创建。例如创建一个叫做 fifotest 的管道并输送一些数据:

[root@server1 ~]$ mkfifo fifotest
[root@server1 ~]$ ll > fifotest

再另开一个终端通过 cat 来捕获 fifotest 中的内容:

[root@server1 ~]$ cat < fifotest 
total 16
-rw-------. 1 root root 1818 Sep 22 21:13 anaconda-ks.cfg
drwxr-xr-x. 4 root root  157 Sep 29 18:17 bin
prw-r--r--. 1 root root    0 Oct  4 23:14 fifotest

管道本身没有储存数据,一旦管道中有数据进入,cat 会立即将其打印出来。

减号 - 用途

在管道命令中,经常会使用到前一个命令的 stdout 作为这次的 stdin,某些命令需要用到文件名(例如 tar)来进行处理时,该 stdin 与 stdout 可以利用减号 - 来代替。

子 shell

把命令用括号括起来(可以是连续命令)后,命令列表就成为了进程列表,这些命令被称为一个编组(Grouping)。其作用表示使用一个子 shell 来执行对应命令。

使用 $BASH_SUBSHELL 变量可以检测是否在子 shell 中运行:

[root@server1 ~]$ pwd ; echo $BASH_SUBSHELL 
/root
0
[root@server1 ~]$ (pwd ; echo $BASH_SUBSHELL)
/root
1
[root@server1 ~]$ (pwd ; (echo $BASH_SUBSHELL))
/root
2

创建子 shell 可以嵌套运行,一般用来进行多进程处理,例如将程序置入后台运行。但是采用子 shell 的代价太高,必须为子 shell 创建出一个全新的环境,会明显拖慢处理速度。

另一个可以调用子 shell 的方式是使用协程,它能在后台生成一个子 shell 并在这个子 shell 中执行命令:

[root@server1 ~]$ coproc my_sleep { sleep 100; }
[1] 41326
[root@server1 ~]$ jobs
[1]+  Running                 coproc my_sleep { sleep 100; } &

数据流重定向

数据流重定向可以将 stdout 和 stderr 分别传送到其他的文件或设备中去,将由 stdin 的数据改为文件内容来替代输入:

类型 表示
标准输入(stdin) 代码 0,使用 < 或 <<;
标准输出(stdout) 代码 1,使用 > 或 >>;
标准错误输出(stderr) 代码 2,使用 2> 或 2>>

例如将根目录文件列表输出到 root.txt 文件中:

[root@101c7 ~]$ ls -la / > root.txt
[root@101c7 ~]$ tail root.txt 
drwxr-xr-x.   2 root root    6 Apr 11  2018 opt
dr-xr-xr-x. 227 root root    0 Sep  9 15:08 proc

/root 目录信息追加到 root.txt 文件中:

[root@101c7 ~]$ ls -la ~ 1>> root.txt
[root@101c7 ~]$ ll | grep root.txt 
-rw-r--r--. 1 root root      2640 Sep 11 10:05 root.txt

也可以使用 1> 来定义标准输出,因为默认就是代码 1,要输出标准错误,使用 2>

[root@101c7 ~]$ cat xx 2>> root.txt ; tail -3 root.txt
cat: xx: No such file or directory
-rw-r--r--.  1 root root       129 Dec 28  2013 .tcshrc
-rw-r--r--.  1 root root  20971520 Jul  7 04:07 TinyCore-current.iso

将标准输出和标准错误输出分别输出到不同文件:

[root@101c7 ~]$ find / -name .bashrc > list.txt 2> list_error.txt

将标准输出和标准错误输出到同一个文件可以用 2>&1,也可以用 &> 来操作:

[root@101c7 ~]$ find / -name .bashrc > list.txt 2>&1
[root@101c7 ~]$ find / -name .bashrc &> list.txt 

也可以将标准错误输出直接丢弃:

[root@101c7 ~]$ find / -name .bashrc > list.txt 2> /dev/null

list.txt 文件的内容作为 cat 命令的输入来生成文件 catfile

[root@101c7 ~]$ cat > catfile < /root/list.txt ; head catfile
/etc/skel/.bashrc
/root/.bashrc

<< 称为内联输入重定向(Inline Input Redirection),代表的含义是从标准输入接收数据,遇到了关键词则结束输入。例如下面设的结束关键词 “end”,输入 end 再敲击回车后,输入立马结束,不同于 Ctrl+d 结束输入,end 这个关键词不会被记录到文件中,仅仅作为结束标记使用:

[root@101c7 ~]$ cat > catfile << "end"
> yes it/'s begin
> line 2
> end
[root@101c7 ~]$ cat catfile 
yes it/'s begin
line 2

操作符 > 有几个修饰符标记,这些标记可以改变它的行为。例如:

  1. 操作符 >& 表示将标准输出和错误同时进行重定向;
  2. 操作符 >! 将迫使文件以 append 模式创建,或者以 normal 模式覆盖一个已存在的文件;
  3. 操作符 >@ 将以二进制模式而不是文件模式打开一个文件。

双向重定向

tee 命令可以将单向传输的数据流半路截取同时移作他用。

例如将 ls 命令的结果追加到 root.txt 文件末尾并排序打印出来:

[root@101c7 ~]$ ls -la | tee -a root.txt | sort | head -3; tail -3 root.txt 
drwx------.  2 root root        52 Sep 11 04:07 audit
drwxr-----.  3 root root        19 Sep  7 05:51 .pki
drwxr-xr-x.  2 root root        23 Sep 10 13:03 etc
-rw-r--r--.  1 root root       129 Dec 28  2013 .tcshrc
-rw-r--r--.  1 root root  20971520 Jul  7 04:07 TinyCore-current.iso
-rw-------.  1 root root      3358 Sep 11 04:33 .viminfo

参数代换

xargs 用来产生某个命令的参数。xargs 可以读入 stdin 的数据,并以空格符或断行字符进行分辨,将 stdin 的数据分隔成为参数。主要是因为很多命令并不支持管道命令,才需要使用 xargs

基本用法:xargs [-0epn] command

参数:

参数 说明
-0 如果输入的 stdin 含有特殊字符比如 ,\ 等,这个参数可以将它还原成一般字符;
-e 这个是 EOF 的意思,后面可以接一个字符串,当解析到这个字符串时就会停止继续工作;
-p 在执行每个命令参数时,都会询问用户;
-n 后面接次数,每次 command 命令执行时,要使用几个参数。
xargs 后面没有接任何的命令时,默认以 echo 来进行输出。

例如将 /etc/passwd 第 1 列取出,只取 3 行,使用 finger 命令将账号内容显示出来:

[root@101c7 sdb4m]$ cut -d ':' -f 1 /etc/passwd | head -3 | xargs finger
Login: root                             Name: root
Directory: /root                        Shell: /bin/bash
On since Sat Sep 11 09:35 (EDT) on tty1    3 hours 14 minutes idle
On since Sat Sep 11 07:08 (EDT) on pts/0 from 192.168.2.101

finger 命令用来查询账户说明信息,xargs 将 3 个账号名称传给 finger 作为参数。

passwd 中所有账号都用 finger 查询,但一次只查询 5 个:

[root@101c7 sdb4m]$ cut -d ':' -f 1 /etc/passwd | xargs -p -n 5 finger
finger root bin daemon adm lp ?...
finger sync shutdown halt mail operator ?...

同上,当参数分析到 lp 就结束命令:

[root@101c7 sdb4m]$ cut -d ':' -f 1 /etc/passwd | xargs -p -e'lp' finger
finger root bin daemon adm ?...

注意 -e'lp' 之间没有空格。