单行bash编程技巧(一)
Bash One-Liners Explained 是一系列介绍 Bash 命令技巧的文章,由国外牛人 Peteris Krumins 撰写。凭借扎实的功底和丰富的经验,作者总结了许多快速解决问题的技巧,并且每一条都只要用简洁的一行 Bash 命令就可以完成,同时每一行命令文中都给出了非常详尽的解释。Peteris Krumins 是一位高产的博主,在他的博客上有很多非常精彩的文章,推荐大家有机会都可以去好好读一读。例如,大家耳熟能详的 Awk One-Liners Explained、Sed 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 系统上,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 /path/to/file_copy
不过,你可以利用括号展开(brace expansion){...}
功能:
$ 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
本文文字及图片出自 kodango.com
你也许感兴趣的:
- 浅析Bash中的 {花括号}
- 10 个 Linux 中方便的 Bash 别名
- 译 | Bash编程中43种易犯的错误
- Linux bash 数组用法技巧
- 译 | 关于bash函数你不知道的一些事情
- 写好shell脚本的13个技巧
- Linux Bash 中字符串操作
- Linux程序员应该知道的10个bash小技巧
- 面试你必须懂的bash shell之25问
- Linux中10个关于命令行自动补全的技巧
你对本文的反应是: