Bash One-Liners Explained 是一系列介绍 Bash 命令技巧的文章,由国外牛人 Peteris Krumins撰写。凭借扎实的功底和丰富的经验,作者总结了许多快速解决问题的技巧,并且每一条都只要用简洁的一行 Bash 命令就可以完成,同时每一行命令文中都给出了非常详尽的解释。
Peteris Krumins 是一位高产的博主,在他的博客上有很多非常精彩的文章,推荐大家有机会都可以去好好读一读。例如,大家耳熟能详的 Awk One-Liners Explained、Sed One-Liners Explained 等等。后者我也曾经在博客上分享过一篇笔记。
回到正题,虽然这一系列文章不难,但是还是可以从中学到很多细节的知识,相信这些肯定会对许多初学者有所帮助,所以我打算将这一系列翻译成中文,分享给大家。为了同原文保持一致,这一系列文章最终会分成以下五篇:
- Bash One-Liners Explained 译文(一): 文件处理;
- Bash One-Liners Explained 译文(二): 操作字符串;
- Bash One-Liners Explained 译文(三): 漫谈重定向;
- Bash One-Liners Explained 译文(四): 历史命令;
- 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`
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 系统上,GNU Coreutils 包中提供的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个数字加上文件名,文件名在最后:
$ cat file-with-5-lines
x 1
x 2
x 3
x 4
x 5
$ wc file-with-5-lines
5 10 20 file-with-5-lines
所以,这个文件包含5行,10个单词,以及20个字符。我们接下来,可以通过read命令将这些信息保存到变量中:
$ read lines words chars _ < <(wc file-with-5-lines)
$ echo $lines
5
$ echo $words
10
$ echo $chars
20
类似地,你也可以使用
here-strings
将字符串分隔并保存到变量中。假设你有一个字符串变量$info,内容为"20 packets in 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{,_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