简洁的 Bash Programming 技巧系列文章专门介绍Bash编程中一些简洁的技巧,帮助大家提高平时 Bash 编程的效率。继上一篇文章发布后,收到很多读者的反响,所以我决定继续将自己学到的一些新的技巧更新在这篇续篇中,当然也希望其它同学也能一起分享你们的技巧。续篇中有部分的内容已经偏离bash编程了,而是命令行下的技巧,题目我暂时不改,请见谅。
1. bash中alias的使用
alias其实是给常用的命令定一个别名,比如很多人会定义一下的一个别名:
alias ll='ls -l'
以后就可以使用ll,实际展开后执行的是ls -l。现在很多发行版都会带几个默认的别名,比如:
alias grep='grep --color=auto' # 带颜色显示
alias ls='ls --color=auto' # 同上
alias rm='rm -i' # 删除文件需要确认
alias在某些方面确实提高了很大的效率,但是也是有隐患的,这点可以看我以前的一篇文章终端下肉眼看不见的东西。那么如何不要展开alias,而是用本来的意思呢?答案是使用转义:
\ls
\grep
在命令前面加一个反斜杠后就可以了。
这里要插一段故事,前两天我在shell脚本中定义了下面的一个alias,假设位于文件util.sh:
#!/bin/bash
...
alias ssh='ssh -o StrictHostKeyChecking=no -o LogLevel=quiet -o BatchMode=yes'
...
后面这串ssh选项是为了去掉一些warning的信息,不提示输入密码等等。具体可以看ssh的文档说明。我自己测试的时候好好的,当时我同事跑得时候却依然有报Warning。我对比了下我们两个人的用法:
sh util.sh # 我的
./util.sh # 他的
大家应该知道,直接./util.sh执行,shell会去找脚本第一行的shebang中给定的解释器去执行改脚本,所以第二种用法相当于直接用bash来执行。那想必是bash/sh对alias是否默认展开这一点上是有区别的了。翻阅了下Bash的man手册,发现可以通过设置expand_aliases选项来打开alias展开的功能,默认在非交互式Shell下是关闭的(什么是交互式登录Shell)。
修改下util.sh,打开这个选项就Ok了:
#!/bin/bash
...
# Expand aliases in script
shopt -s expand_aliases
alias ssh='ssh -o StrictHostKeyChecking=no -o LogLevel=quiet -o BatchMode=yes'
...
2. awk打印除第一列之外的其他列
awk用来截取输入行中的某几列很有用,当时如果要排除某几列呢?
例如有如下的一个文件:
$ cat /tmp/test.txt
1 2 3 4 5
10 20 30 40 50
可以用下面的代码解决(来源):
$ awk '{$1="";print $0}' /tmp/test.txt
2 3 4 5
20 30 40 50
但是前面多了一个空格,可以用cut命令稍微调整下:
$ awk '{$1="";print $0}' /tmp/test.txt | cut -c2-
2 3 4 5
20 30 40 50
3. 巧用bash的命令展开功能备份文件
假设要备份文件/your/path/to/file.list为/your/path/to/file.list.20121106,常规的方法是:
cp /your/path/to/file.list /your/path/to/file.list.20121106
这样重复写上一长串的路径,是不是很麻烦,这里利用bash的展开特性可以这样做:
cp /your/path/to/file.list{,.20121106}
/your/path/to/file.list{,.20121106}这一部分会展开为/your/path/to/file.list /your/path/to/file.list.20121106,再将此传给cp命令,就达到了与前面同样的效果。(思路同ls *)。具体可以man bash中的Brace Expansion这一段。
4. 命令行下使用ctrl+x ctrl+e来编辑当前命令
这个技巧来自最牛B的 Linux Shell 命令系列连载(二)。使用方法是键入命令之后,再按ctrl+x ctrl+e可以打开一个编辑器来编辑命令,默认是使用emacs。你也可以通过在~/.bashrc中添加以下这一行,将编辑器换成vim:
export EDITOR='vim'
为什么推荐这一条呢?对于一般的命令(这里指的是长度很短的命令)其实这个技巧没什么用处,我用方向键移一下就OK了,但是有时候(尤其是运维的一些命令)有些命令长度特别长,一堆参数,如果直接在命令行修改其实风险很高的(可以通过在命令的开头加上一个#号来规避这个风险,Bash将当前的命令当成注释不执行),而且方向键一个一个迁移非常不方便(当然有类似ctrl+x ctrl+e这种预设的快捷键来操作,可以看bind -p)。
像使用ctrl+x,ctrl+e打开vim来编辑命令在这种场景有两种好处:
a. 可以方便的用熟悉的编辑器高效地修改命令;
b. 有一个确认的过程,无误后,退出vim才执行命令。
不过我不是很推荐最牛B的 Linux Shell 命令 系列连载中的一些对历史命令的技巧,虽然方便,但是风险很高,因为没有一个确认的过程,是执行将历史命令调出就执行了。
5. 你知道sed的这个特性吗?
假设一个文件的每一行为一个路径:
[Tue Nov 06 06:33 PM] [kodango@devops] ~
$ cat /tmp/test.txt
/home/kodango/hello
/home/kodango/hello/world
/home/kodango/good
/home/kodango/good/bye
现在要把/home/kodango/good替换成/home/kodango/bad,普通的作法是:
[Tue Nov 06 06:35 PM] [kodango@devops] ~
$ sed -n 's/\/home\/kodango\/good/\/home\/kodango\/bye/p' /tmp/test.txt
/home/kodango/bye
/home/kodango/bye/bye
因为路径中的分隔符与sed的替换命令的分隔符都是'/',所以需要转义,非常麻烦。幸运的是,sed可以更改分隔符,例如使用#:
[Tue Nov 06 06:34 PM] [kodango@devops] ~
$ sed -n 's#/home/kodango/good#/home/kodango/bad#p' /tmp/test.txt
/home/kodango/bad
/home/kodango/bad/bye
这样就清爽多了。
补充,如果是在地址对中使用,首个分隔符前面要加反斜杠:
$ sed -n '\#/home/kodango/#p' /tmp/test.txt
/home/kodango/hello
/home/kodango/hello/world
/home/kodango/good
/home/kodango/good/bye
参见:Using different delimiters in sed。
6. 合并连续重复的字符(即squeeze操作)
例如要合并一个字符串中连续的多个空格,假设字符串为'print hello, world'。
第一种方法,使用sed命令,扫描整个字符串,替换2个以上的空格为1格:
$ echo 'print hello, world ' | sed -r 's/ {2,}/ /g'
print hello, world
第二种方法,使用tr命令的-s选项,专门就是为了合并连续重复的字符:
$ echo 'print hello, world ' | tr -s ' '
print hello, world
第三种方法,使用awk的域赋值来完成该目的:
$ echo 'print hello, world ' | awk '$1=$1'
print hello, world
对已经存在的域例如$1,$2..进行赋值,会导致awk重新使用OFS输出分隔符重组$0,关于这一点的详细说明见sosodream同学的博文Awk里的域赋值操作和部分源码解析($1=$1,$0=$0,FS,OFS)
7. 将文本中某列相同的行输出到不同的文件中
标题有点绕口,我们以实际例子来讲解,假设我们有以下的一个文件:
$ cat /tmp/test.txt
a char
1 int
2 int
b char
abc string
我们的目标是将该文本中的行按第二列的值归类,并且输出到相应的文件中,文件名为第二列的名称。例如第2行、第3行会输出到int.txt文件中,而第1行、第4行则输出到char.txt,以此类推。
我没有找到其它简单的方法,只找到一种用awk来处理的方法:
[Wed Nov 07 07:31 PM] [kodango@devops] ~/workspace
$ awk '{print $1 > $2 ".txt"}' /tmp/test.txt
我们来检查结果:
[Wed Nov 07 07:34 PM] [kodango@devops] ~/workspace/output
$ grep -nH . *
char.txt:1:a
char.txt:2:b
int.txt:1:1
int.txt:2:2
string.txt:1:abc
8. 用exec命令来完成重定向
以一个简单的例子开始,现在需要一个脚本,它可以接受一个文件名作为参数,然后按行读取该文件的内容并打印到标准输出。如果不指定文件名,则默认从标准输入读。首先按上面的功能需求写出一个可以完成功能的脚本:
[Sat Nov 10 12:16 AM] [kodango@devops] ~/workspace
$ cat test.sh
filename=$1
if [ -z "$filename" ]; then
while read line; do
echo $line
done
else
while read line; do
echo $line
done < $filename
fi
如果换exec来实现重定向,可以把脚本写得更优雅:
$ cat test1.sh
filename=$1
if [ -n "$filename" ]; then
exec 0< $filename
fi
while read line; do
echo $line
done
这里的关键在第5行代码,exec命令不仅可以用于执行命令,还可以用于打开、关闭或者复制文件描述符,这里就是利用exec将指定的文件名打开重定向到标准输入。类似地可以用exec >$filename
将文件重定向到标准输出。我们可以在命令行上做一个试验:
[Sat Nov 10 12:26 AM] [kodango@devops] ~
$ exec 3>&1 # 首先将fd 3重定向到标准输出,作为标准输出的一个备份
$ ls /proc/629/fd/{1,3} -l # 现在fd 3和fd 1指向同一个设备文件
lrwx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/1 -> /dev/pts/1
lrwx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/3 -> /dev/pts/1
$ exec >stdout # 现在把标准输出重定向到stdout这个文件中
$ ls /proc/629/fd/1 -l # 如果你此刻在同一个终端下执行本命令是没有返回的
$ ls /proc/629/fd/1 -l # 现在重新打开一个终端看看,确实已经重定向到stdout这个文件
l-wx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/1 -> /home/kodango/stdout
$ exec 1>&3 # 现在重新把标准输出重定向到之前备份的fd 3上
$ ls /proc/629/fd/{1,3} -l # 现在屏幕可以看到输出了,但是fd 3这个描述符还打开,需要关闭
lrwx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/1 -> /dev/pts/1
lrwx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/3 -> /