您的位置:首页 > 运维架构 > Shell

Bash One-Liners Explained 译文(一)

2015-07-30 11:44 477 查看
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`

不过,在 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: