Bash One-Liners Explained

Bash One-Liners Explained

Bash One-Liners Explained 是一系列介绍 Bash 命令技巧的文章,由国外牛人 Peteris Krumins撰写。凭借扎实的功底和丰富的经验,作者总结了许多快速解决问题的技巧,并且每一条都只要用简洁的一行 Bash 命令就可以完成,同时每一行命令文中都给出了非常详尽的解释。

原文地址:http://www.catonmat.net/series/bash-one-liners-explained

 

一、文件处理

1. 清空文件内容

$ > file

这一行命令用到了输出重定向操作符>。输出重定向发生时,文件会被打开准备写入。如果此时文件不存在则先创建,存在则将其大小截取为0。这里我们并没有重定向写任何内容到文件中,所以文件依然保持为空。

如果你想替换文件的内容,或者创建一个包含指定内容的文件,可以运行下面的命令:

$ echo "some string" > file

2. 追加内容到文件

$ echo "foo bar baz" >> file

这一行命令用到了另外一个输出重定向操作符>>,该操作符将内容追加到文件。同样地,如果文件不存在则先创建它。追加的内容之后,紧跟着换行符。如果你不想要追加换行符,在执行echo命令时可以指定-n选项:

$ echo -n "foo bar baz" >> file

3. 读取文件的首行并赋值给变量

$ read -r line < file

这一行命令用到了 Bash的内置命令read,和输入重定向操作符<。read命令从标准输入中读取一行,并将内容保存到变量line中。在这里,-r选项保证读入的内容是原始的内容,意味着反斜杠转义的行为不会发生。输入重定向操作符< file打开并读取文件file,然后将它作为read命令的标准输入。

记住,read命令会删除包含在IFS变量中出现的所有字符,IFS 的全称是 Internal Field Separator,Bash 根据 IFS 中定义的字符来分隔单词。在这里,read命令读入的行被分隔成多个单词。默认情况下,IFS包含空格,制表符和回车,这意味着开头和结尾的空格和制表符都会被删除。如果你想保留这些符号,可以通过设置IFS为空来完成:

$ IFS= read -r line < file

IFS 的变化仅会影响当前的命令,这行命令可以保证读入原始的首行内容到变量line中,同时行首与行尾的空白字符被保留。

另外一种读取文件首行内容,并赋值给变量的方法是:

$ line=$(head -1 file)

这里用到了命令替换操作符$(...),它运行括号里的命令并且将输出返回。这个例子中,命令是head -1 file,输出的内容是文件的首行。输入然后通过等号赋值给变量line。$(...)的等价写法是`...`,所以也可以换成下面这样:

$ line=`head -1 file`

不过,在 Bash 中$(...)用法更加推荐,因为它看起来更加整洁,并且容易嵌套使用。

4. 依次读入文件每一行

$ while read -r line; do

             #do something with $line

done < file

这是一种正确的读取文件内容的做法,read命令放在while循环中。当read命令遇到文件结尾时(EOF),它会返回一个正值,导致循环判断失败终止。

记住,read命令会删除首尾多余的空白字符,所以如果你想保留,请设置 IFS 为空值:

$ while IFS= read -r line; do

# do something with $line

done < file

如果你不想将<file放在最后,可以通过管道将文件的内容输入到while 循环中:

$ cat file | while IFS= read -r line; do

             #do something with $line

done

5. 随机读取一行并赋值给变量

$ read -r random_line < <(shuf file)

Bash 中并没有提供一种直接的方法来随机读取文件的某一行内容,所以这里需要利用外部程序。在最新的一些 Linux 系统上,GNUCoreutils 包中提供的shuf命令可以满足我们的需求。

这一行命令中用到了进程替换(process substitution)操作符<(...)。进程替换操作会创建一个匿名的管道文件,并将进程命令的标准输出连接到管道的写一端。然后 Bash 开始执行进程替换中的命令,然后将整个进程替换的表达式替换成匿名管道的文件名。

当 Bash 看到<(shuf file)时,它首先打开一个特殊的文件/dev/fd/n,这里的n是一个空闲的文件描述符,然后执行shuf file命令,将标准输出连接到/dev/fd/n,并且替换<(shuf file) 为/dev/fd/n,因此实际的命令会变成:

$ read -r random_line < /dev/fd/n

结果会读取洗牌后的文件的第一行内容。

另外一种做法是,使用GNU sort 命令,它提供的-R选项可以随机排序文件:

$ read -r random_line < <(sort -R file)

或者,同前面一样,将结果赋值给变量:

$ random_line=$(sort -R file | head -1)

这里,我们首先通过sort-R随机排序文件,然后通过head -1 读取文件的第一行。

6. 读取文件首行前三个字段并赋值给变量

$ while read -r field1 field2 field3 throwaway; do

             #do something with $field1, $field2, and $field3

done < file

如果在read命令中指定多个变量名,它会将读入的内容分隔成多个字段,然后依次赋值给对应的变量,第一个字段赋值给第一个变量,第二个字段赋值给第二个变量,等等,最后将剩余的所有字段赋值给最后一个变量。这也是为什么,在上面的例子中,我们加了一个throwaway变量,否则的话,当文件的一行大于三个字段时,第三个变量的内容会包含所有剩余的字段。

有时候,为了书写方便,可以简单地用_来替换throwaway变量:

$ while read -r field1 field2 field3 _; do

             #do something with $field1, $field2, and $field3

done < file

又或者,如果你的文件确实只有三个字段,那可以忽略它:

$ while read -r field1 field2 field3; do

             #do something with $field1, $field2, and $field3

done < file

下面是一个例子,假如你想知道一个文件到底包含多少行,多少个单词以及多少个字节。当你执行wc命令时,你会得到3个数字加上文件名,文件名在最后:

$ catfile-with-5-lines

x 1

x 2

x 3

x 4

x 5

 

$ wcfile-with-5-lines

 5 10 20 file-with-5-lines

所以,这个文件包含5行,10个单词,以及20个字符。我们接下来,可以通过read命令将这些信息保存到变量中:

$ read lines wordschars _ < <(wc file-with-5-lines)

 

$ echo $lines

5

$ echo $words

10

$ echo $chars

20

类似地,你也可以使用 here-strings将字符串分隔并保存到变量中。假设你有一个字符串变量$info,内容为"20 packetsin 10 seconds",然后你想要将从中获取20和10。在不久之前,我是这样来完成的:

$ packets=$(echo $info | awk '{ print $1 }')

$ time=$(echo $info | awk '{ print $4 }')

然而,得益于read命令的强大和对 Bash 的了解,我们可以这样做:

$ read packets _ _ time _ <<< "$info"

这里,<<<就是 here-string 的语法,它允许你直接传递字符串给标准输入。

7. 保存文件的大小到变量

$ size=$(wc -c < file)

这一行命令中用到了第3点中介绍的命令替换操作$(...),它运行里面的命令并将结果获取回来。在这个例子中,命令是wc -c < file,它输出文件的字节数。这个结果最终会赋值给变量size。

8. 从文件路径中获取文件名

假设,你有一个文件,它的路径为/path/to/file.ext,然后你要从中获取文件名,在这里是file.ext。你要怎么做? 一个好的方法是通过参数展开(parameter expansion)功能:

$filename=${path##*/}

这一行命令使用了参数展开的语法:${var##pattern},它从$var字符串开始处开始匹配pattern。如果能够匹配成功,将最长匹配的内容删除后再返回。

在这个例子中,匹配的模式是*/,它尝试匹配/path/to/file.ext的开始部分,正如前面所说,这里是贪婪匹配,所以它能够匹配到最后一个斜杠为止,即匹配的内容是/path/to/。所以当把匹配的内容删除后,返回的内容就是文件名file.ext。

9. 从文件路径中获取目录名

和上面一样类似,这次你要从路径/path/to/file.txt中获取目录名/path/to。你可以继续通过参数展开功能来完成这个任务:

$ dirname=${path%/*}

这次的用法是${var%pattern},它从$var的结尾处匹配/*。如果能够成功匹配,将最短匹配的内容删除再返回。

在这个例子中,匹配的模式是/*,它能够匹配/file.ext部分,删除这部分内容后返回的就是目录名称。

10. 快速拷贝文件

假设你要将文件/path/to/fil拷贝到/path/to/file_copy,一般情况下,大多数人会这么来写:

$ cp /path/to/file /path/to/file_copy

不过,你可以利用括号展开(braceexpansion){...}功能:

$ cp /path/to/file{,_copy}

括号展开可以生成任意字符串的组合,在这个例子中,/path/to/file{,_copy}最终生成/path/to/file/path/to/file_copy。所以上面这行命令最终发型成:

$ cp /path/to/file /path/to/file_copy

类似地,你可以执行下面的命令快速的移动文件:

$ mv /path/to/file{,_old}

这行命令展开后就变成了:

$ mv /path/to/file /path/to/file_old

 

二、操作字符串

1. 生成从 a 到 z 的字母表

$ echo {a..z}

这一行命令用到了括号展开(Braceexpansion)功能,它可以用于生成任意的字符串。{x..y}是一个序列表达式,其中 x 和 y 都是单个字符,这个表达式展开后包含 x 与 y 之间的所有字符。

运行上面的命令会生成从 a到 z 的所有字母:

$ echo {a..z}

a b c d e f g h i j k l m n o p q r s t u v w x y z

2. 生成从 a 到 z 的字母表,字母之间不包含空格

$ printf "%c" {a..z}

这是一个 99.99% 的人都不知道的非常棒的技巧。如果你在printf命令之后指定一个列表,最终它会循环依次打印每个元素,直到完成为止。

在这一行命令中,printf的格式为"%c",代表一个字符(character),后面的参数是从 a 到 z 的字符列表,字符之间以空格分隔。所以,当printf执行时,它依次输出每个字符直到所有字符全被处理完成为止。

下面是执行的结果:

abcdefghijklmnopqrstuvwxyz

输出的结果最后不包含换行符,因为printf的输出格式是"%c",其中并没有包含\n。如果你想输出完整的一行,可以简单地在字符列表后面增加一个$'\n':

$ printf "%c" {a..z} $'\n'

$'\n'代表换行符,这是一个常用的技巧。

另外一种方式是,通过echo 来输出 printf 的结果:

$ echo $(printf "%c" {a..z})

这一行命令用到了命令替换功能:执行printf "%c" {a..z}命令然后用执行的输出替换命令。然后,echo 打印输出结果时会带上换行符。

如果你想要每一行仅输出一个字母,在字符后面增加一个换行符:

$ printf "%c\n" {a..z}

输出:

a

b

...

z

如果想要快速地将printf 的结果保存到变量中,可以使用-v选项:

$ printf -v alphabet "%c" {a..z}

结果会将abcdefghijklmnopqrstuvwxyz保存到变量alphabet中。

类似地,你也可以利用同样的语法生成一个数字列表,例如从1到100:

$ echo {1..100}

输出:

1 2 3 ... 100

或者,如果你忘记这种方法,可以使用 seq 命令来做这个事情:

$ seq 1 100

3. 输出从 00 到 09 的数字

$ printf "%02d " {0..9}

这里我们又用到了printf的循环输出功能,这一次的输出格式为"%02d ",意思是在输出数字的时候,如果不满两位就用0补齐。同时,输出的元素是 0 到 9的列表(括号展开后的结果)。

输出结果:

00 01 02 03 04 05 06 07 08 09

如果你使用的是最新的Bash 4 版本,你可以使用加强的括号展开功能:

$ echo {00..09}

老版本不包含该特性。

4. 生成 30 个英文单词

$ echo {w,t,}h{e{n{,ce{,forth}},re{,in,fore,with{,al}}},ither,at}

这是一个滥用括号展开的例子,看看最终输出的结果是什么:

when whence whenceforth where wherein wherefore wherewithwherewithal whither what then thence thenceforth there therein thereforetherewith therewithal thither that hen hence henceforth here herein hereforeherewith herewithal hither hat

是不是很棒?

你可以通过括号展开生成一组单词或者符号的排列。例如:

$ echo {a,b,c}{1,2,3}

上面的命令会生成以下结果:a1a2 a3 b1 b2 b3 c1 c2 c3。首先,它取出第一个括号中的第一个元素a,然后依次与第二个括号{1,2,3}的所有元素组合,生成a1 a2 a3,依此类推。

5. 重复输出 10 次字符串

$ echo foo{,,,,,,,,,,}

这一行命令两次利用了括号展开功能,字符串foo与10个空字符串组合,最终生成10分拷贝:

foo foo foo foo foo foo foo foo foo foo foo

6. 拼接字符串

$ echo "$x$y"

这一行命令简单地将两个变量的值连接在一起,所以如果x变量的值为foo,而y的值为bar,则结果为foobar。

注意,这里的"$x$y"是加了双引号的,如果忘记加了,echo会将$x$y当成常规的命令行参数去解析。所以,如果$x在开头包含-,它就变成一个命令行参数,而不是被 echo 输出的内容。

x=-n

y=" foo"

echo $x$y

执行后的输出:

foo

相反,正确书写的方式执行后的结果如下所示:

x=-n

$ y=" foo"

$echo "$x$y"

-n foo

不过,如果你要将两个字符串相连的结果赋值给变量,是可以将双引号省略的:

var=$x$y

7. 分割字符串

假设变量$str的值为foo-bar-baz,如果你想按-分割成多个段并遍历它,可以使用read命令,并且设置 IFS 的值为-:

$ IFS=- read -r x y z <<< "$str"

这里我们使用read x命令从标准输入读取内容,分割后并依次保存到x y z变量中。其中,$x 为 foo, $y 为 bar, $z 为 baz。

另外要留意的一处是here-string操作符<<<,可以很方便地将字符串传递给命令的标准输入。在这个例子中,$str的内容传给 read 命令的标准输入。

你也可以将分割后的几个字段保存到数组类型的变量中:

$ IFS=- read -ra parts <<< "foo-bar-baz"

在这里,-a 选项告诉read命令将分割后的元素保存到数组parts中。随后,你可以通过${parts[0]}, ${parts[1]}和${parts[0]}来访问数组的各个元素,或者通过${parts[@]}来访问所有元素。

8. 逐个字符方式处理字符串

$ while IFS= read -rn1 c; do

    # do something with $c

done <<< "$str"

这里我们通过指定-n1参数,让read命令依次读入一个字符,类似地,-n2说明每次读入两个字符。

9. 将字符串中的 foo 替换成 bar

$ echo ${str/foo/bar}

这一行命令用到了参数展开的另外一种形式:${var/find/replace},找到$var变量中的find字符串,并将它替换成bar。

要想替换所有出现的字符串"foo",请使用${var//find/replace}的形式:

$ echo ${str//foo/bar}

10. 检查字符串是否匹配模式

$ if [[ $file = *.zip ]]; then

    # do something

fi

这一行命令是说,如果$file的值匹配*.zip,则执行if语句里的命令。这种语法下的模式是最简单的通配符(globpattern)匹配,通配符包括* ? [...]。其中,*可以匹配一个或者多个字符, ?只能匹配单个字符,[...]能够匹配任意出现在中括号里面的字符或者一类字符集。

下面是另外一个例子,用来判断回答是否匹配 Y 或者 y:er is Y or y:

$ if [[ $answer = [Yy]* ]]; then

    # do something

fi

11. 检查字符串是否匹配某个正则表达式

$ if [[ $str =~ [0-9]+\.[0-9]+ ]]; then

    # do something

fi

这一行命令检查$str是否能够匹配正则表达式[0-9]+\.[0-9]+,即两个数字中间包含一个点号。正则表达式的规范可以通过 man 手册查询: man 3 regex

12.计算字符串的长度

$ echo ${#str}

这里我们又用到了参数展开(也可以叫参数替换)的语法: ${#str},它返回$str变量值的长度。

13. 从字符串中提取子串

$ str="hello world"

$ echo ${str:6}

这一行命令通过子串提取操作,从字符串hello world中取到了子串world。子串提取操作的语法格式为${var:offset:length},它的意思是说从变量var中,提取第offset个位置(下标从0开始计算)开始的总共length个数的字符。在我们这个例子中,忽略了length,默认会返回所有剩余的字符。

下面是另外一个例子,返回$str变量中第7、8位置的两个字符:

$ echo ${str:7:2}

输出结果为or。

14. 转换成大写

$ declare -u var

$ var="foo bar"

Bash 中的内置命令 declare 可以用于声明一个变量,或者设置变量的属性。在这个例子中,通过指定-u选项,使得变量$var在赋值时,就会自动地将内容转换成大写的格式。现在你 echo 它,可以看到所有内容已经变成大写了:

$ echo $var

FOO BAR

注意,-u选项也是在 Bash 4 新版本中引入的功能,在低版本下是没有的。类似地,你还可以使用 Bash 4 提供的另外一种参数展开语法${str^^},也可以将字符串转换成太写的格式:

$ str="zoo raw"

$ echo ${str^^}

15. 转换成小写

$ declare -l var

$ var="FOO BAR"

同上面一条类似,-l选项声明变量的小写属性,使得其值转换成小写的格式:

$ echo $var

foo bar

同样,只有 Bash 4 以及以上的版本才支持-l选项。另外一种方式是使用参数展开语法:

$ str="ZOO RAW"

$ echo ${str,,}

我补充一句,如果是Bash 4 以下,还是老老实实地用tr命令就可以了。

 

三、漫谈重定向

       重定向其实是通过操作文件描述符来完成的,这样会更容易理解。当 Bash 启动时,会自动创建三个标准的文件描述符,它们分别是 stdin(标准输入,文件描述符为0),stdout(标准输出,文件描述符为1)和 stderr(标准错误输出,文件描述符为2)。你也可以创建更多的文件描述符,例如3,4,5等等,或者关闭它们,又或者拷贝它们。你可以从对应的文件中读取或者写入内容。

       文件描述符指向某个文件(除非它们被关闭)。通常情况下,Bash 启动的三个文件描述符 —— stdin,stdout 和 stderr 都是指向你的终端,从终端输入中读取内容,并且把标准输出和标准错误都送到终端上。

假设你的终端对应的设备文件是 /dev/tty0,下面的截图解释了Bash 启动时文件描述符表的样子:

       当 Bash 执行一个命令时,他会 fork 一个子进程(查看man 2 fork)。子进程会从父进程继承所有的文件描述符,设置好指定的重定向,最后执行该命令(查看man 3 exec)。

可以尝试用图表来可视化展现,重定向发生时文件描述符表的变化过程,这种方法可以帮助你更好的理解重定向功能。

1. 重定向命令的 stdout 到文件

$command >file

>是输出重定向操作符。Bash 首先会打开文件准备写入,如果文件打开成功,则将命令command stdout 指向之前打开的文件。如果打开失败,则不会继续执行命令。command >file的写法和command 1>file的写法是一样的,1 stdout 对应的文件描述符。

下面的图片描述了上述命令执行时文件描述符表的变化过程:


2. 重定向命令的 stderr 到文件

$command 2> file

Bash将这行命令的 stderr(文件描述符为2)重定向到文件file中。

下面是重定向后的文件描述符表:


3. 重定向命令的 stdout stder 到同一个文件中

$command &>file

这一行命令使用了&>操作符,它将命令command stdout stderr 都重定向到文件file中。

除此之外,还有几种方法可以将stdout stderr 同时重定向到同一个文件中。你可以依次重定向每个输出。

$command >file 2>&1

上面是一种更加常见的方法,首先重定向stdout 到文件file,然后将 stderr 重定向到和stdout 同样的文件中。

Bash 在命令中遇到多个重定向操作时,它会从左到右依次处理。我们通过图表来依次推导这整个过程。初始时文件描述符表的样子:

现在 Bash 处理第一组重定向>file,我们之前已经解释过,它将使得stdout 指向文件file

接下来,Bash开始处理第二组重定向2>&1,它会把stderr 重定向到stdout 所指向的文件:

这里要注意不要错误的写成:

$command 2>&1 >file

重定向的顺序是很重要的,这行命令只会把stdout 重定向到文件,而stderr 会继续输出到终端屏幕上。为了理解原因,我们同样来推导依次整个处理过程。

Bash 遇到2>&1时,它会把stderr 指向 stdout 对应的文件(这里是终端):

紧接着,Bash看到>file,按照之前我们解释的,它会把stdout 重定向到文件file

从上面的图片中可以看出,stdout指向了文件file,但是stderr 依然指向终端。所以,一定要注意重定向的书写顺序。

4. 丢弃命令的 stdout 输出

$command > /dev/null

/dev/null是一个特殊的文件,任何写入到该文件的内容都会被丢弃。所以,我们需要做的就是把 stdout 重定向到文件/dev/null

类似的,基于前一条命令,我们可以做到把输出到 stdout stderr 的内容都丢弃:

$command >/dev/null 2>&1

或者简单的写成:

$command &>/dev/null

此时的文件描述符表为:

5. 重定向文件到命令的 stdin

$command <file

Bash在执行命令之前,打开文件file准备读入。如果打开文件出错,Bash会直接返错,不会继续执行命令。相反如果打开成功,Bash 会使用打开的文件的文件描述符作为命令的标准输入。此时,文件描述符表的样子为:

下面是一个例子,假如你想把文件的第一行读入到变量中:

$read -r line < file

6. 重定向一堆字符串到命令的 stdin

$command <<EOL

your

multi-line

text

goes

here

EOL

这里用到了 here document 的语法<<MARKER。当 Bash 遇到该操作符是,它会从标准输入读取每一行,直到遇到一行以MARKER开头为止。这个例子中,Bash读取到所有内容并传给command stdin

假设你想去除一堆URL 地址中的http://部分,可以用下面的一行命令:

$sed 's|http://||' <<EOF

http://url1.com

http://url2.com

http://url3.com

EOF

输出结果为:

url1.com

url2.com

url3.com

7. 重定向一行文本到命令的 stdin

$command <<< "foo bar baz"

等价于:

$echo "foo bar baz" | command

8. 重定向所有命令的 stderr 到文件中

$ exec2>file

$command1

$command2

$ ...

这一行命令中使用了Bash 的内置命令exec。如果你在它之后指定重定向操作,重定向的效果为一直持续到显示改变或者脚本退出为止。

在这个例子中,2>file处理之后,随后所有命令的 stderr 都会重定向到文件file中。通过这种方法,你可以很方便的把脚本中所有命令的 stderr 都汇总到一个文件,同时又不用每一个命令之后都指定2>file

9. 打开文件并通过特定文件描述符读

$exec 3<file

上面我们再次用到了exec命令,3<file告诉它以只读方式打开文件file,并将文件描述符 3指向打开的文件:

随后你可以通过描述符 3来读取文件内容:

$read -u 3 line

一些常规的命令,例如grep,还可以这么用:

$grep "foo" <&3

执行了上面的命令后,grep命令的 stdin 指向了之前打开的文件,看起来好像将文件描述符 3 复制成了 0

当你使用完成后,通过下面的方法关闭该文件:

$ exec3>&-

这里文件描述符 3指向&-,就意味着关闭改文件描述符。

10. 打开文件并通过特定文件描述符写

$ exec4>file

同上面一条类似,这里我们将文件描述符 4指向以写方式打开的文件:

你可以看到,你并不需要按顺序使用文件描述符,可以任意挑选从 0 255 之内的所有未被使用的描述符。

接下来,我们可以很方便的通过描述符 4来写文件:

$ echo"foo" >&4

或者关闭描述符:

$ exec4>&-

11. 打开文件并通过特定文件描述符读写

$ exec3<>file

这里我们用到了菱形操作符(diamondoperator) <>,该操作符表示打开的文件既可以用于读也可以用于写。例如:

$echo "foo bar" > file   #write string "foo bar" to file "file".

$ exec5<> file           # open"file" for rw and assign it fd 5.

$read -n 3 var <&5       # read thefirst 3 characters from fd 5.

$echo $var

结果会输出foo。然后,我们可以往里写些内容:

$echo -n + >&5           # write"+" at 4th position.

$ exec5>&-               # close fd 5.

$cat file

结果输出foo+bar

12. 重定向一组命令的 stdout 到文件中

$ (command1;command2) >file

这一行命令使用(commands)语法,commands 会在一个子shell 中执行。所以在这里,command1command2会在子shell 中运行,然后Bash 将子 shell stdout 重定向到文件中。

13. Shell 中通过文件中转执行的命令

打开两个shell,在第一个中执行以下命令:

mkfifofifo

exec< fifo

而在第二个中,执行:

exec3> fifo;

echo'echo test' >&3

回头你会发现在第一个shell 中会输出test,你可以继续不断地往文件fifo 中输入命令,第一个shell 会一直执行这些命令。

我们来解释下这里的原理。

在第一个shell 中,我们使用 mkfifo 命令创建了一个命名管道fifo。命名管道(也可以叫做 FIFO)类似之前提到的管道(匿名管道),除了前者是以文件系统上的文件的方式存在(标识一条特殊的进程通信的内核通道)。命名管道可以被多个进程打开同时读写,当多个进程通过 FIFO 交换数据时,内核并没有写到文件系统中,而是自己私下里传递了这些数据。所以,FIFO 这中特殊的文件,它在文件系统中是没有存放数据块的。文件系统只是通过文件名的形式提供标识,以便进程间可以利用这个标识来访问管道。

接下来,我们通过exec< fifo命令,使用fifo 作为当前shell 的标准输入。

现在,我们在第二个shell 中以写的方式打开命名管道,并将文件描述符 3 指向改它。接下来,我们只要简单地把echotest 文件描述描述符 3,最总会写到管道 fifo 中。因为第一个shell 的标准输入连接到管道的读的一段,它会接受到传递过来的内容并执行。

14. 通过 Bash 访问Web 站点

$exec 3<>/dev/tcp/www.google.com/80

$echo -e "GET / HTTP/1.1\n\n" >&3

$cat <&3

Bash/dev/tcp/host/port当作一种特殊的文件,它并不需要实际存在于系统中,这种类型的特殊文件是给 Bash 建立 tcp 连接用的。

在这个例子中,我们首先以读写的方式打开文件描述符 3,并把它指向/dev/tcp/www.google.com/80,后者是一个连接,表示连接到 www.google.com 80端口。

接下来,我们往文件描述符 3GET / HTTP/1.1\n\n。完成之后,我们使用cat命令从同样的地方读取返回内容。

类似的,你也可以通过/dev/udp/host/port来创建一个 UDP 连接。

使用/dev/tcp/host/port,你甚至可以使用 Bash 写一个端口扫描程序。

15. 重定向输出时防止覆盖已有的文件

$ set-o noclobber

这行命令将当前shell noclobber选项打开,这个选项的作用是,防止>重定向操作符覆盖已有的文件内容。

这时如果你重定向写入到一个文件,会返回一个错误:

$program > file

bash:file: cannot overwrite existing file

如果你100%确定你要覆盖一个文件,可以使用>|重定向操作符:

$program >| file

上面的命令会正确的执行,因为它覆盖了noclobber选项。

16. 重定向标准输入到文件,同时打印到标准输出

$command | tee file

tee是一个很方便的命令,它并不是 Bash 的一部分,但是你会经常用到这个命令。它将接收到的输入,同时打印到标准输出和一个文件中。

下面的图片描述了上面命令执行的过程:

17. 重定向进程的标准输出到另外一个进程的标准输入

$command1 | command2

这是一个大家非常熟悉的管道用法:

18. 重定向进程的标准输出和标准错误到另外一个进程的标准输入

$command1 |& command2

以上用法只在Bash 4.0 以后的版本才能使用,对于老的版本,比较通用的做法是:

$command1 2>&1 | command2

下面的图片描述了上面命令执行的过程:

19. Give file descriptors names

略,Bash 4以上新增功能。

20. 重定向顺序

你可以将重定向放到命令的任意位置,一下三个命令的执行结果都是一样的:

$echo hello >/tmp/example

$echo >/tmp/example hello

$>/tmp/example echo hello

21. 交换标准输出与标准错误输出

$command 3>&1 1>&2 2>&3

在这里,我们首先让文件描述符3指向 stdout,然后将stdout(文件描述符1)指向 stderr(文件描述符2)。最后有把 stderr(文件描述符2)指向文件描述符3,即 stdout。最终,我们交换了 stdout stderr

下面我们通过图来展示以上过程,初始的时候是这样的:

首先,执行了3>&1之后,文件描述符3指向 stdout

接下来,执行1>&2,文件描述符1指向了stderr

最后,执行2>&3,文件描述符2执向了stdout

如果你是一个追求完美的人,可以将文件描述符3关闭:

$command 3>&1 1>&2 2>&3 3>&-

最终的文件描述符图会是这样的:

22. 重定向标准输出和标注错误输出给不同的进程

$command > >(stdout_cmd) 2> >(stderr_cmd)

这一行命令用到了进程替换(ProcessSubstitution)语法。>(...)操作符的执行过程是,运行里面的命令,同时将命令的标准输入连接到一个命名管道的读段。Bash 随后会用命名管道的实际文件名替换这个操作符。

例如,假设第一个替换操作>(stdout_cmd)返回/dev/fd/60,而后一个返回/dev/fd/61。替换后,最初的命令变成以下形式:

$command > /dev/fd/60 2> /dev/fd/61

从上面可以看出,标准输出重定向到了/dev/fd/60,而标准错误输出则重定向到了/dev/fd/61

当命令执行是输出内容到stdout,则管道/dev/fd/60后面的进程(stdout_cmd)会从另外一侧读取到数据。同样的,进程stderr_cmd也能从命令的 stderr 输出中读取。

23. 获取管道流中的所有命令执行退出码

假设你用管道流执行多个命令:

$cmd1 | cmd2 | cmd3 | cmd4

然后你想获取所有命令的退出码,但是这里并没有一种简单的做法可以实现,因为 Bash 只会返回最后一个命令的退出码。

Bash的开发者同样思考了这个问题,他们添加了PIPESTATUS数组,这个数组中存放了管道流中所有命令的退出码。

下面是一个简单的例子:

$echo 'pants are cool' | grep 'moo' | sed 's/o/x/' | awk '{ print $1 }'

$echo ${PIPESTATUS[@]}

01 0 0

 

四、历史命令

1. 清除命令行历史

$rm ~/.bash_history

Bash将历史执行的命令都保存在文件.bash_history中,该文件位于你的家目录下。为了清除命令行历史,只要把这个文件删除即可。

注意,当你执行完退出后,最后一个rm~/.bash_history命令依然会被记录下来。如果你想隐藏清除的操作命令,请看下一条。

2. 当前会话下停止记录命令行历史

$unset HISTFILE

环境变量HISTFILE指向命令行执行历史保存的目标文件路径,如果你重置了该变量,Bash 就不会保存历史。

另外一种方法是将它指向/dev/null

$HISTFILE=/dev/null

3. 不要记录当前执行的命令

很简单,只要在命令之前加空格就行:

command

注意,以上正常工作的前提是,HISTIGNORE变量被正确的设置,它的值是冒号分隔的匹配表达式列表,如果一个命令匹配其中的任意一个表达式则不会被保存到记录中。

例如,忽略空格开头的命令:

HISTIGNORE="[\t]*"

我(原文作者)的配置如下所示:

HISTIGNORE="&:[\t]*"

这里的&符号是有特殊含义的,它表示上一次执行的命令。所以,这里除了忽略空格开头的命令之外,重复执行的命令也只会被记录一次。

4. 更改保存命令行历史的目标文件

$HISTFILE=~/docs/shell_history.txt

之后执行的命令会被记录到文件~/docs/shell_history.txt中。

5. 命令行历史记录中增加时间戳

$HISTTIMEFORMAT="%Y-%m-%d %H:%M:%S"

如果你将环境变量HISTTIMEFORMAT设置成一个合法的日期格式(具体请参考man 3 strftime),Bash 会在历史记录中同时保存命令执行的时间,而且在你执行history命令时,它也会将时间显示出来。

6. 显示历史

$history

history命令可以按行显示执行的历史命令,如果设置了HISTTIMEFORMAT,在结果中还会显示命令执行的时间。

7. 显示最近执行的50个命令

$history 50

如果你在执行history命令时,指定一个数字参数,例如 50,那么它只会显示最近50个命令。

8. 显示执行最多的10个命令

$history |

    sed 's/^ \+//;s/  / /' |

    cut -d' ' -f2- |

    awk '{ count[$0]++ } END { for (i in count)print count[i], i }' |

    sort -rn |

    head -10

这一行命令中结合了sed,cut, awk, sorthead等多个命令。让我们来回顾下整个过程,理解发生了什么。假设history命令的输出是这样的:

$history

    1  rm.bash_history

    2 dmesg

    3  su-

    4 man cryptsetup

    5  dmesg

首先,我们通过sed命令删除开头的空格,同时将行号后的连续两个空格替换成一个:

$history | sed 's/^ \+//;s/  / /'

1rm .bash_history

2dmesg

3su -

4man cryptsetup

5dmesg

接下来,我们使用cut命令将第一列删除:

$history |

    sed 's/^ \+//;s/  / /' |

    cut -d' ' -f2-

 

rm.bash_history

dmesg

su-

mancryptsetup

dmesg

然后,我们再用awk命令统计命令在历史记录中出现的次数:

$history |

    sed 's/^ \+//;s/  / /' |

    cut -d' ' -f2- |

    awk '{ count[$0]++ } END { for (i in count)print count[i], i }'

 

1rm .bash_history

2dmesg

1su -

1man cryptsetup

紧接着,使用sort命令逆序排列结果:

$history |

    sed 's/^ \+//;s/  / /' |

    cut -d' ' -f2- |

    awk '{ count[$0]++ } END { for (i in count)print count[i], i }' |

    sort -rn

 

2dmesg

1rm .bash_history

1su -

1man cryptsetup

最后,我们截取最开始的10行,即10个最频繁实用的命令。

$history |

    sed 's/^ \+//;s/  / /' |

    cut -d' ' -f2- |

    awk '{ count[$0]++ } END { for (i in count)print count[i], i }' |

    sort -rn |

    head -10

以下是我的使用最多的10个命令:

2172ls

1610gs

252cd ..

215gp

213ls -las

197cd projects

155gpu

151cd

119gl

119cd tests/

9. 快速执行上一个命令

$ !!

输入两个感叹号,第一个感叹号表示开始历史命令替换,而第二个感叹号表示上一次执行的命令。例如:

$echo foo

foo

$ !!

foo

这里echo foo被重复执行了一次。

这个用法在你忘记通过sudo执行命令时尤其有用,例如:

$rm /var/log/something

rm:cannot remove `/var/log/something': Permission denied

$

$sudo !!   # executes `sudo rm /var/log/something

10. 快速执行最近一个以特定字符串开头的命令

$ !foo

上一个命令中,第一个感叹号表示开始历史命令替换,后面的内容表示最近一次执行的以foo开头的命令。例如:

$echo foo

foo

$ls /

/bin/boot /home /dev /proc /root /tmp

$awk -F: '{print $2}' /etc/passwd

...

$ !ls

/bin/boot /home /dev /proc /root /tmp

11. 使用文本编辑器打开上一次执行的命令

$fc

fc命令执行后,会用文本编辑器打开上一个命令。当你想要编辑一个很长并且复杂的命令式,这个功能会帮你省下不少功夫。

例如,你输入了下面一行错误的命令:

$ forwav in wav/*; do mp3=$(sed 's/\.wav/\.mp3/' <<< "$wav");ffmpeg -i "$wav" "$m3p"; done

当你输完命令后,因为内容过长,你找不出错误的地方。这中情况下,你可以使用fc命令加载该命令到文本编辑器中,然后错误的地方(最后的 mp3 单词拼错)就一目了然了。

 

五、命令行跳转

0. 行编辑模式介绍

Bash使用 GNU readline 来提供行编辑特性。readline 库同时支持Emacs 风格和Vi 风格的快捷键绑定,也支持用户去做自定义绑定。默认情况下,readline 会使用Emacs 风格的键绑定,不过你可以很方便的切换到Vi 风格,或者自定义设置。

执行set -o emacs命令切换到 Emacs 风格,set-o vi则会切换到Vi 风格。

除此之外,你仍可以通过~/.inputrc或者bind命令来自定义快捷键绑定。例如,bind'"\C-f": "ls\n"'CTRL+F绑定为执行ls命令。你可以通过查阅Bash 手册中的readline 一节来更多地了解readline 的快捷键绑定语法。

1. 移动光标到行首

CTRL+ a

2. 移动光标到行尾

CTRL+ e

3. 光标往后(向左)移动一个单词

ESC+ b 或者 ALT + b

4. 光标往前(向右)移动一个单词

ESC+ f 或者 ALT + f

5. 删除上一个单词

CTRL+ w

删除一个单词也被称为"killinga word",每个被删除的单词都被保存在缓存中,可以按下CTRL + y将其粘贴回来,这个操作被称为"yanking"

6. 粘贴上一次被删除的内容

CTRL+ y

7. 光标往后(向左)移动一个字符

CTRL+ b

8. 光标往前(向右)移动一个字符

CTRL+ f

9. 删除光标前的字符

CTRL+ u

删除光标前的字符,删除的内容被保存到缓存中,同样可以用CTRL + y粘贴回来。

10. 反向历史搜索

CTRL+ r

这可能是Bash 中最常用的快捷键,当你按下CTRL+ r时,会开始反向搜索命令行执行历史。你只要输入之前执行的命令中的少许字符就可以很快地从历史记录中找到该命令。

11. 正向历史搜索

CTRL+ s

如果你按下CTRL+ s,终端会停止屏幕刷新,因为默认情况下,你的终端将它解释成停止输出流的信号。当我是新手时,这种情况快把我逼疯了。每次我不小心按下CTRL + s后,屏幕就冻结了,然后我就不知道发生了什么。之后,我才学会用CTRL + q键来恢复终端。

正确的方式应该是通过stty命令来更改终端对于CTRL + s按下后采取的行为:

$stty stop 'undef'

这样会取消默认的停止信号的快捷键绑定,然后你可以开始使用 Bash CTRL + s功能。

CTRL+ s Bash 中的作用和CTRL + r相反,是执行正向历史搜索。

12. 交换相邻两个字符的位置

CTRL+ t

13. 交换相邻两个单词的位置

ESC+ t 或者 ALT + t

14. 将光标开始到单词结尾的字符转换成大写

ESC+ u 或者 ALT + u

15. 将光标开始到单词结尾的字符转换成小写

ESC+ l 或者 ALT + l

16. 单词首字符大写

ESC+ c 或者 ALT + c

在单词的首字符下按下,可以将首字符转换成大写的形式。

17. 输入特殊字符

CTRL+ v

按下CTRL + v之后,会取消下一个输入字符的特殊含义,例如CTRL + v后按下TAB键,可以在命令行下输入一个制表符,或者之后按下CTRL + m会输入一个Windows 下的回车符(注: ^M)。

18. 注释当前输入的命令(在开头添加#)

ESC+ # 或者 ALT + #

19. 在文本编辑器中快速打开当前命令

CTRL+ x CTRL + e

按下以上快捷键可以将当前输入的命令用你最喜欢的文本编辑器打开,当退出编辑器后,该命令会被自动执行。

注:设置默认的编辑器方法,例如vim

exportEDITOR='vim'

20. 删除光标左侧的字符

CTRL+ h

21. 删除光标所在处的字符

CTRL+ d

注:相当于delete 键。

22. 撤销上一次编辑操作(undo

CTRL+ x CTRL + u

23. 插入上一个命令的最后一个参数

ESC+ . 或者 ALT + .

在当前位置下,按下该建后可以快速插入上一个命令中的最后一个参数。

24. 撤销对当前行的所有编辑操作

ESC+ r 或者 ALT + r

25. 清除屏幕内容

CTRL+ l

26. 切换成 vi 编辑风格

$set -o vi

 

注:作者在文章最后提供了一份更加全面的快捷键 Cheatsheet下载地址

全文完。

 

转自:团子的小窝

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值