bash cookbook 技巧
来自http://www.catonmat.net/blog
Part I: Working With Files 第一部分 文件处理
1.清空文件内容 Empty a file (truncate to 0 size)
$ > file
这一行命令用到了输出重定向操作符>
。输出重定向发生时,文件会被打开准备写入。如果此时文件不存在则先创建,存在则将其大小截取为0(truncate to 0)。这里我们并没有重定向写任何内容到文件中,所以文件依然保持为空。
如果你想替换文件的内容,或者创建一个包含指定内容的文件,可以运行下面的命令:
$ echo "some string" > file
如果你想往文件里面写入多行内容可以运行下面的命令:
cat << EOF > file
some string 1 line
stome string
.....
EOF
2.追加内容到文件
$ echo "foo bar baz" >> file
这个命令用到了另外一个输出重定向操作符>>
,该操作符将内容追加到文件。同样地,如果文件不存在则先创建它。追加的内容之后,紧跟着换行符。如果你不想要追加换行符,在执行echo命令时可以指定-n选项:
$ echo -n "foo bar baz" >> file
在使用输出重定向的时候,bash可以设置个 set -C
来禁止覆盖已经存在的文件
$ set -C
$ echo "foo bar baz" > file
bash: file: cannot overwrite existing file
此时如果确实想覆盖此文件可以使用>|
符号。
3.读取文件的首行并赋值给变量
$ read -r line < file
这一行命令用到了 Bash 的内置命令read,和输入重定向操作符<
。read命令从标准输入中读取一行,并将内容保存到变量line中。在这里,-r选项保证读入的内容是原始的内容(raw),意味着反斜杠转义的行为不会发生。输入重定向操作符< 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
对这里有个说明:对于循环读取标准输入的操作,很多程序内置有自己的标准输入链接到相同的输入源上面,和read命令一样。它偶尔会产生扭曲的结果。
另一种方法是从不同的文件描述符读取数据。
exec 3< input_file.txt # open input_file.txt on fd 3
while read -u 3 -r line ; do
# do stuff here
done
exec 3<&- # close fd 3
对于这个通过管道传给while循环的这个例子需要特别注意一点,这个while循环将会创建一个subshell子shell,之前的一些变量在这个while中是不存在的。
i=0
cat foo.txt | while read line; do ((i++)); done
echo "$i"
#will print 0.
i=0;cat foo.txt | (while read line; do ((i++)); done; echo "$i")
对于bash4.X可以使用mapfile内置命令来读取文件的一行
mapfile ARRAY < file
这个命令也有一些选项
-s count - skip the first count lines
-n count - read in at most count lines
-c quanta - set a quanta for the -C option
-C command - run command every quanta lines passing the index of the array about to be assigned
-0 index - start assigning at array[index] instead of 0
-t - strip trailing newline
-u fd - read from file descriptor fd instead of stdin
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。
这里如果直接执行wc -c file
的话结果会带个文件名称的,这里使用输入重定向就不会带文件名称了。
8.从文件路径中获取文件名
假设,你有一个文件,它的路径为/path/to/file.ext,然后你要从中获取文件名,在这里是file.ext。你要怎么做? 一个好的方法是通过参数展开(parameter expansion)功能:
$ filename=${path##*/}
这一行命令使用了参数展开的语法:${var##pattern}
,它从$var
字符串开始处开始匹配pattern。如果能够匹配成功,将最长匹配的内容删除后再返回。
在这个例子中,匹配的模式是*/,它尝试匹配/path/to/file.ext的开始部分,正如前面所说,这里是贪婪匹配,所以它能够匹配到最后一个斜杠为止,即匹配的内容是/path/to/。所以当把匹配的内容删除后,返回的内容就是文件名file.ext。
这里的井在键盘的左边,表示从左边开始匹配,2个井号表示尽可能多的匹配。
对此也可以使用basename命令
filename=$(basename $path)
9.从文件路径中获取目录名
和上面一样类似,这次你要从路径/path/to/file.txt中获取目录名/path/to。你可以继续通过参数展开功能来完成这个任务:
$ dirname=${path%/*}
这次的用法是${var%pattern}
,它从$var
的结尾处匹配/*
。如果能够成功匹配,将最短匹配的内容删除再返回。
在这个例子中,匹配的模式是/*,它能够匹配/file.ext部分,删除这部分内容后返回的就是目录名称。
通用的百分号在键盘的右边,表示从右边开始匹配,一个百分号表示尽可能少的匹配的。
对此也可以使用dirname命令
dirname=$(dirname $path)
注意:basename 和 dirname不是bash的内置命令,这个2个是个外部命令。
10.快速拷贝文件
假设你要将文件 /path/to/file
拷贝到/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
或者是文件带扩展名的
cp /long/path/to/file{,_bk}.ext
类似地,你可以执行下面的命令快速的移动文件:
$ mv /path/to/file{,_old}
这行命令展开后就变成了:
$ mv /path/to/file /path/to/file_old
function bk {
if [[ -z $1 ]]; then
echo "Usage: bk <file>"
return
fi
file=$(basename $1)
bk=_bk
while :; do
case $file in
*.*)
newfile=$(echo $file | sed 's/\(.*\)\.\(.*\)/\1'$bk'.\2/')
;;
*)
newfile=${file}$bk
;;
esac
if [[ ! -e $newfile ]]; then
b