BASH的保护性编程技巧

BASH的保护性编程技巧

 
shell常用逻辑判断
-b file 若文件存在且是一个块特殊文件,则为真
-c file 若文件存在且是一个字符特殊文件,则为真
-d file 若文件存在且是一个目录,则为真
-e file 若文件存在,则为真
-f file 若文件存在且是一个规则文件,则为真
-g file 若文件存在且设置了SGID位的值,则为真
-h file 若文件存在且为一个符合链接,则为真
-k file 若文件存在且设置了"sticky"位的值
-p file 若文件存在且为一已命名管道,则为真
-r file 若文件存在且可读,则为真
-s file 若文件存在且其大小大于零,则为真
-u file 若文件存在且设置了SUID位,则为真
-w file 若文件存在且可写,则为真
-x file 若文件存在且可执行,则为真
-o file 若文件存在且被有效用户ID所拥有,则为真
 
-z string 若string长度为0,则为真
-n string 若string长度不为0,则为真
string1 = string2 若两个字符串相等,则为真
string1 != string2 若两个字符串不相等,则为真
 
int1 -eq int2 若int1等于int2,则为真
int1 -ne int2 若int1不等于int2,则为真
int1 -lt int2 若int1小于int2,则为真
int1 -le int2 若int1小于等于int2,则为真
int1 -gt int2 若int1大于int2,则为真
int1 -ge int2 若int1大于等于int2,则为真
 
!expr 若expr为假则复合表达式为真。expr可以是任何有效的测试表达式
expr1 -a expr2 若expr1和expr2都为真则整式为真
expr1 -o expr2 若expr1和expr2有一个为真则整式为真
 
特殊变量:
0正在被执行命令的名字。对于shell脚本而言,这是被激活命令的路径n 该变量与脚本被激活时所带的参数相对应。n是正整数,与参数位置相对应(1,2…)
#                 提供脚本的参数号* 所有这些参数都被双引号引住。若一个脚本接收两个参数,∗等于12@ 所有这些参数都分别被双引号引住。若一个脚本接收到两个参数,@等价于12? 前一个命令执行后的退出状态
$$ 当前shell的进程号。对于shell脚本,这是其正在执行时的进程ID
$! 前一个后台命令的进程号
 
其中下面两种判断:
expr1 -a expr2 若expr1和expr2都为真则整式为真
expr1 -o expr2 若expr1和expr2有一个为真则整式为真
 
本人亲测过不行,不知道是不是格式出了问题,待解答
 
例子:
if [ ! -z err−o!−eapk ]; then 会报出-e无法找到的错误;
而if [ ! -z err]||[!−eapk ]; then 没问题;
 
 
整数比较 :
-eq 等于,如:if [ "a"−eq"b" ] 
-ne 不等于,如:if [ "a"−ne"b" ] 
-gt 大于,如:if [ "a"−gt"b" ] 
-ge 大于等于,如:if [ "a"−ge"b" ] 
-lt 小于,如:if [ "a"−lt"b" ] 
-le 小于等于,如:if [ "a"−le"b" ] 
< 小于(需要双括号),如:(("a"<"b")) 
<= 小于等于(需要双括号),如:(("a"<="b")) 
> 大于(需要双括号),如:(("a">"b")) 
>= 大于等于(需要双括号),如:(("a">="b"))
 
 
字符串比较 :
= 等于,如:if [ "a"="b" ] 
== 等于,如:if [ "a"=="b" ],与=等价 
注意:==的功能在[[]]和[]中的行为是不同的,如下: 
[[ a == z* ]] # 如果a以"z"开头(模式匹配)那么将为true 
[[ a == "z*" ]] # 如果a等于z*(字符匹配),那么结果为true
 
[ a == z* ] # File globbing 和word splitting将会发生          [ "a" == "z*" ] # 如果$a等于z*(字符匹配),那么结果为true 
#一点解释,关于File globbing是一种关于文件的速记法,比如"*.c"就是,再如~也是. 但是file globbing并不是严格的正则表达式,虽然绝大多数情况下结构比较像.
 
!= 不等于,如:if [ "a"!="b" ] ;这个操作符将在[[]]结构中使用模式匹配.
 
< 小于,在ASCII字母顺序下.如: 
if [[ "a"<"b" ]] 
if [ "a"<"b" ] 
注意:在[]结构中"<"需要被转义.
 
> 大于,在ASCII字母顺序下.如: 
if [[ "a">"b" ]] 
if [ "a""b" ] 
注意:在[]结构中">"需要被转义.
 
-z 字符串为"null".就是长度为0. 
-n 字符串不为"null" 
注意: 
使用-n在[]结构中测试必须要用""把变量引起来.使用一个未被""的字符串来使用! -z 或者就是未用""引用的字符串本身,放到[]结构中。虽然一般情况下可 以工作,但这是不安全的.习惯于使用""来测试字符串是一种好习惯. 
awk '{print $2}' class.txt | grep '^[0-9.]' > res
 
 
SHELL下的数字比较及计算
 
比较: 
方法一: if [ A−lt{B} ]; then ... 
这是最基本的比较方法,使用lt(小于),gt(大于),le(小于等于),ge(大于等于),优点:还没发现;缺点:只能比较整数,使用lt,gt等不直观 ;
方法二: if ((A<{B})) then ... 
这是CShell风格比较,优点:不用使用lt,gt等难记的字符串;缺点:还是只能比较整数 
方法三: if (echo A{B} | awk '!(1>2){exit 1}') then ... 
这是使用awk比较,优点:可以比较小数;缺点:表达式太复杂,难记 
方法四: if (echo A−{B} | bc -q | grep -q "^-"); then ... 
这是使用bc计算比较,优点:可以比较小数;缺点:表达式更复杂,难记
 
计算: 
方法一:typeset C=(expr{A} + B);SHELL中的基本工具,优点:方便检测变量是否为数字;缺点:只能计算整数,且只能计算加减法,不能计算乘除法方法二:let"C={A}+B";或let"C=A+B"内嵌命令计算,优点:能计算乘除法及位运算等;缺点:只能计算整数方法三:typesetC=((A+B)) 
CShell风格的计算,优点:能计算乘除法及位运算等,简介,编写方便;缺点:不能计算小数 
方法四:typeset C={echo{A} B|awk′print$1+$2′)使用awk计算,优点:能计算小数,可以实现多种计算方式,计算灵活;缺点:表达式太复杂方法五:typesetC={echo A+{B} | bc -q) 
使用awk计算,优点:能计算小数,计算方式比awk还多,计算灵活;
缺点:表达式太复杂,小数点后面的位数必须使用scale=N来设置,否则可能会将结果截断为整数
 
 
特殊字符 :
符号 使用 
; 一般情况我们输出完一个命令需要按一个回车,如果你想在一行执行多个命令,中间可以用;号分割 cd /home ; ls 
* 表示任意字符(正则) 
? 任一个字符 
[abc] 列表项之一 
[^abc] 对于列表取非 也可以使用范围 [a-z] [0-9] [A-Z](所有字符和数字) 
{} 循环列表时用 touch_{1,2,3}时就会建立touch_1,touch_2,touch_3循环出这三个文件,也会用 echo abc home目录cd (普通通话进入的是/home目录下用户自己的家目录) 提取变量值 
`` ()命令替换touch‘date+(date +%F_(date+[] 整数计算 echo [2+3]−∗/() 同上,但它弥补了``的嵌套缺陷 
@ 无特殊含义 
# 注释(一般编程都需要加注释,让其他团队队员对自己写的程序功能了解) 
变量取值() 命令替换 
${} 变量名的范围 
% 杀后台经常jobs号,取模运算(大家对取模应该并不陌生) 
^ 取非 和 !雷同 
& 用进程后台处理, &&用于逻辑与 
* 匹配任意字符串;计算乘法 
() 子进程执行 
- 减号,区间,cd - 回到上层目录,杀掉当前jobs
_ (下划线)无特殊含义 
+ 加号; 杀掉当前jobs(进程) 
= 赋值 
| 管道,|| 逻辑或 
转义 当一些特殊符号如$是一个变量需要转义才不被bash解析 
{} 命令列表 {ls;cd /;} 
[] 字符通配符,[]也是用于测试命令 
空命令 真值 
; 命令结束符 
"" 软引 '' 硬引 
< 输入重定向 
> 输出重定向 
>& 合并2和1输出 
, 枚举分隔符 
. 当前目录 
/ 目录分隔符 
? 单个字符 
回车 命令执行 
 
${COLUMN:-} 
如果COLUMN是空变量,或者变量不存在时,返回-后面的内容。
如果变量有值,那么返回这个值。
 
 

简单shell脚本

!/bin/bash

这一行表明,不管用户选择的是那种交互式shell,该脚本需要使用bash shell来运行。由于每种shell的语法大不相同,所以这句非常重要。

简单实例

下面是一个非常简单的shell脚本。它只是运行了几条简单的命令

1
2
3
4
#!/bin/bash
echo "hello, $USER. I wish to list some files of yours"
echo "listing files in the current directory, $PWD"
ls  # 列出当前目录所有文件

首先,请注意第四行。在bash脚本中,跟在#符号之后的内容都被认为是注释(除了第一行)。Shell会忽略注释。这样有助于用户阅读理解脚本。 ?$USER和 $PWD都是变量。它们是bash脚本自定义的标准变量,无需在脚本中定义即可使用。请注意,在双引号中引用的变量会被展开(expanded)。“expanded”是一个非常合适的形容词:基本上,当shell执行命令并遇到$USER变量时,会将其替换为该变量对应的值。

变量

任何编程语言都会用到变量。你可以使用下面的语句来定义一个变量:

1
X="hello"

并按下面的格式来引用这个变量:

$X

更具体的说,$X表示变量X的值。关于语义方面有如下几点需要注意:

  • 等于号两边不可以有空格!例如,下面的变量声明是错误的 :
1
X = hello
  • 在我所展示的例子中,引号并不都是必须的。只有当变量值包含空格时才需要加上引号。例如:
1
2
X = hello world # 错误
X = "hello world" # 正确

这是由于shell将每一行命令视为命令及其参数的集合,以空格分隔。 foo=bar就被视为一条命令。foo = bar 的问题就在于shell将空格分开的foo视为命令。同样,X=hello world的问题就在于shell将X=hello视为一条完整的命令,而”world”则被彻底无视(因为赋值命令不需其他参数)。

单引号 VS 双引号

基本上来说,变量名会在双引号中展开,单引号中则不会。如果你不需要引用变量值,那么使用单引号可以很直观的输出你期望的结果。 An example 示例

1
2
3
4
#!/bin/bash
echo -n '$USER=' # -n选项表示阻止echo换行
echo "$USER"
echo "$USER=$USER"  # 该命令等价于上面的两行命令

输出如下(假设你的用户名为elflord)) $USER=elflord $USER=elflord

1
2
3
$USER=elflord
 
$USER=elflord

从例子中可以看出,在双引号中使用转义字符也是一种解决方案。虽然双引号的使用更灵活,但是其结果不可预见。如果要在单引号和双引号之间做出选择,最好选择单引号。

使用引号封装变量

有时候,使用双引号来保护变量名是个很好的点子。如果你的变量值存在空格或者变量值为空字符串,这点就显得尤其重要。看下面这个例子:

1
2
3
4
5
#!/bin/bash
X=""
if [ -n $X ]; then  # -n 用来检查变量是否非空
    echo "the variable X is not the empty string"
fi

运行这个脚本,输出如下:

the variable X is not the empty string

为何?这是因为shell将$X展开为空字符串,表达式[-n]返回真值(因为改表达式没有提供参数)。再看这个脚本:

1
2
3
4
5
#!/bin/bash
X=""
if [ -n "$X" ]; then    # -n 用来检查变量是否非空
         echo "the variable X is not the empty string"
fi

在这个例子中,表达式展开为[ -n ""],由于引号中内容为空,因此该表达式返回false值。

在执行时展开变量

为了证实shell就像我上面说的那样直接展开变量,请看下面的例子:

1
2
3
4
5
#!/bin/bash
LS="ls"
LS_FLAGS="-al"
 
$LS $LS_FLAGS $HOME

乍一看可能有点不好理解。其实最后一行就是执行这样一条命令:

Ls -al /home/elflord

(假设当前用户home目录为/home/elflord)。这就说明了shell仅仅只是将变量替换为对应的值再执行命令而已。

使用大括号保护变量

这里有一个潜在的问题。假设你想打印变量X的值,并在值后面紧跟着打印”abc”。那么问题来了:你该怎么做呢? 先试一试:

1
2
3
#!/bin/bash
X=ABC
echo "$Xabc"

这个脚本没有任何输出。究竟哪里出了问题?这是由于shell以为我们想要打印变量Xabc的值,实际上却没有这个变量。为了解决这种问题可以用大括号将变量名包围起来,从而避免其他字符的影响。下面这个脚本可以正常工作:

!/bin/bashX=ABCecho “${X}abc”

1
2
3
#!/bin/bash
X=ABC
echo "${X}abc"

条件语句, if/then/elif

在某些情况下,我们需要做条件判断。比如判断字符串长度是否为0?判断文件foo是否存在?它是一个链接文件还是实际文件?首先,我们需要if命令来执行检查。语法如下:

1
2
3
4
5
6
if condition
then
    statement1
    statement2
    ..........
fi

当指定条件不满足时,可以通过else来指定其他执行动作。

1
2
3
4
5
6
7
8
if condition
then
    statement1
    statement2
    ..........
else
    statement3
fi

当if条件不满足时,可以添加多个elif来检查其他条件是否满足。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if condition1
then
    statement1
    statement2
    ..........
elif condition2
then
    statement3
    statement4
    ........   
elif condition3
then
    statement5
    statement6
    ........   
 
fi

当相关条件满足时,shell会执行在相应的if/elif与下个elif或fi之间的语句。事实上,判断条件可以是任意命令,当且只当命令返回并且退出状态为0时,才会执行该条件块中的语句(换句话说,就是当命令成功返回时)。不过在本文的学习中,我们只会关注“test”或“[]”形式的条件判断。

Test命令与操作符

条件判断中的命令几乎都是test命令。test根据测试条件通过或失败来返回true或false(更准确的说是返回0或非0值)。如下所示:

1
test operand1 operator operand2

对某些测试来说,只需要一个操作数(operand2)通常是下面这种情况的简写:

1
[ operand1 operator operand2 ]

为了让我们的讨论更接地气一点,给出下面一些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
X=3
Y=4
empty_string=""
if [ $X -lt $Y ]    # is $X less than $Y ?
then
    echo "$X=${X}, which is smaller than $Y=${Y}"
fi
 
if [ -n "$empty_string" ]; then
    echo "empty string is non_empty"
fi
 
if [ -e "${HOME}/.fvwmrc" ]; then           # test to see if ~/.fvwmrc exists
    echo "you have a .fvwmrc file"
    if [ -L "${HOME}/.fvwmrc" ]; then       # is it a symlink ? 
        echo "it's a symbolic link
    elif [ -f "${HOME}/.fvwmrc" ]; then     # is it a regular file ?
        echo "it's a regular file"
    fi
else
    echo "you have no .fvwmrc file"
fi

需要注意的细节

Test命令的格式为“操作数< 空格 >操作符< 空格 >操作数”或者“操作符< 空格 >操作数”,这里特别说明必须要有这些空格,因为shell将没有空格的第一串字符视为一个操作符(以-开头)或者操作数。比如下面这个:

if [ 1=2 ]; then echo “hello”fi

它会打印出hello,这明显与预期结果是不一致的(因为shell只看到操作数1=2,没看到操作符)。

还有一种隐藏陷阱是未加引号的变量。像我们之前例子说的-n测试时变量须加引号的情形。其实,不管在什么情况下,加上引号总是没有坏处的,还有可能规避一些很奇葩的错误。因为有时候不加引号的变量扩展开的测试结果会让人非常困惑。例如:

1
2
3
4
5
6
#!/bin/bash
X="-n"
Y=""
if [ $X = $Y ] ; then
    echo "X=Y"
fi

这个脚本打印出来的结果是错误的,因为shell将判断展开为 [ -n = ],但是”=”的长度不为0,所以条件判断通过从而导致输出结果为“X=Y”。

Test操作符简介

下图是test操作符的快速查询列表。当然这个列表并不全面,但记下这些就足够平常使用了(如果还需要了解其他操作符,可以查看man手册)。

operator produces true if… number of operands
-n operand non zero length 1
-z operand has zero length 1
-d there exists a directory whose name is operand 1
-f there exists a file whose name is operand 1
-eq the operands are integers and they are equal 2
-neq the opposite of -eq 2
= the operands are equal (as strings) 2
!= opposite of = 2
-lt operand1 is strictly less than operand2 (both operands should be integers) 2
-gt operand1 is strictly greater than operand2 (both operands should be integers) 2
-ge operand1 is greater than or equal to operand2 (both operands should be integers) 2
-le operand1 is less than or equal to operand2 (both operands should be integers) 2

循环

循环结构允许我们执行重复的步骤或者在若干个不同条目上执行相同的程序。Bash中有下面两种循环

  • for 循环
  • while 循环

For 循环

直接来个例子,来直观地感受for循环的语法。

1
2
3
4
5
#!/bin/bash
for X in red green blue
do
    echo $X
done

For循环会遍历空格分开的条目。注意,如果某一项含有空格,必须要用引号引起来,例子如下:

1
2
3
4
5
6
7
8
#!/bin/bash
colour1="red"
colour2="light blue"
colour3="dark green"
for X in "$colour1" $colour2" $colour3"
do
    echo $X
done

如果我们漏掉for循环中的引号,你能猜想出会发生什么吗?这个例子说明,除非你确认变量中不会包含空格,否则最好都用引号将变量保护起来。

在for循环中使用通配符

如果shell解析字符串时遇到*号,会将它展开为所有匹配的文件名。当且仅当目标文件与号展开后的字符串一致才会匹配成功。例如,单独的*号展开为当前目录的所有文件,中间以空格分开(包含隐藏文件)。

所以:

echo *

列出当前目录下的所有文件和目录。

echo *.jpg

列出所有的jpeg图片格式的文件。

echo ${HOME}/public_html/*.jpg

列出home目录中public_html目录下的所有jpeg文件。

正是由于这种特性,使得我们可以很方便的来操作目录和文件,尤其是和for循环结合使用时,更是便利。例子如下:

1
2
3
4
5
#!/bin/bash
for X in *.html
do
        grep -L '<UL>' "$X"
done

打印出当前目录下所有不包含<UL>字段的html文件。

While 循环

当给定条件为真值时,while循环会重复执行。例如:

1
2
3
4
5
6
7
#!/bin/bash
X=0
while [ $X -le 20 ]
do
    echo $X
    X=$((X+1))
done

这样导致这样的疑问: 为什么bash不能使用C风格的for循环呢?

for (X=1,X<10; X++)

这也跟bash自身的特性有关,之所以不允许这种for循环是由于:bash是一种解释性语言,因此其运行效率比较低。也正是由于这个原因,高负荷迭代是不允许的。

命令替换

Bash shell有个非常好用的特性叫做命令替换。允许我们将一个命令的输出当做另一个命令的输入。比如你想要将命令的输出赋值给变量X,你可以通过变量替换来实现。

有两种命令替换的方式:大括号扩展和反撇号扩展。

大括号扩展: $(commands) 会展开为命令commands的输出结果。并且允许嵌套使用,所以commands中允许包含子大括号扩展。

反撇好扩展:将commands扩展为命令commands的输出结果。不允许嵌套。

这里有一个例子:

1
2
3
4
5
6
7
#!/bin/bash
files="$(ls)"
web_files=`ls public_html`
echo "$files"      # we need the quotes to preserve embedded newlines in $files
echo "$web_files"  # we need the quotes to preserve newlines
X=`expr 3 * 2 + 4` # expr evaluate arithmatic expressions. man expr for details.
echo "$X"

$()替换方式的优点不言自明:非常易于嵌套。并且大多数bourne shell的衍生版本都支持(POSIX shell 或者更好的都支持)。不过,反撇号替换更简单明了,即使是最基本的shell它也提供了支持(任意版本的#!/bin/sh都可以)。

 

下面这几条是我自己在写shell代码的时候,比较喜欢的几种写法,抛砖引玉。

1) 检查命令执行是否成功

第一种写法,比较常见:

1
2
3
4
5
6
7
echo abcdee | grep -q abcd
 
if [ $? -eq 0 ]; then
    echo "Found"
else
    echo "Not found"
fi

简洁的写法:

1
2
3
4
5
if echo abcdee | grep -q abc; then
    echo "Found"
else
    echo "Not found"
fi

当然你也可以不要if/else,不过这样可读性比较差:

1
2
3
[Sun Nov 04 05:58 AM] [kodango@devops] ~/workspace
$ echo abcdee | grep -q abc && echo "Found" || echo "Not found"
Found

2) 将标准输出与标准错误输出重定向到/dev/null 第一种写法,比较常见:

1
grep "abc" test.txt 1>/dev/null 2>&1

常见的错误写法:

1
grep "abc" test.txt 2>&1 1>/dev/null

简洁的写法:

1
grep "abc" test.txt &> /dev/null

3) awk的使用

举一个实际的例子,获取Xen DomU的id。

常见的写法:

1
sudo xm li | grep vm_name | awk '{print $2}'

简洁的写法:

1
sudo xm li | awk '/vm_name/{print $2}'

4) 将一个文本的所有行用逗号连接起来

假设文件内容如下所示:

1
2
3
4
5
[Sat Nov 03 10:04 PM] [kodango@devops] ~/workspace
$ cat /tmp/test.txt
1
2
3

使用Sed命令:

1
2
3
[Sat Nov 03 10:14 PM] [kodango@devops] ~/workspace
$ sed ':a;$!N;s/ /,/;ta' /tmp/test.txt
1,2,3

简洁的写法:

1
2
3
[Sat Nov 03 10:04 PM] [kodango@devops] ~/workspace
$ paste -sd, /tmp/test.txt
1,2,3

5) 过滤重复行

假设文件内容如下所示:

1
2
3
4
5
6
[Sat Nov 03 10:16 PM] [kodango@devops] ~/workspace
$ sort /tmp/test.txt
1
1
2
3

常用的方法:

1
2
3
4
5
[Sat Nov 03 10:16 PM] [kodango@devops] ~/workspace
$ sort /tmp/test.txt | uniq
1
2
3

简单的写法:

1
2
3
4
5
[Sat Nov 03 10:16 PM] [kodango@devops] ~/workspace
$ sort /tmp/test.txt -u
1
2
3

6) grep查找单词

假设一个文本的每一行是一个ip地址,例如

1
2
3
4
5
[Sat Nov 03 10:20 PM] [kodango@devops] ~/workspace
$ cat /tmp/ip.list
10.0.0.1
10.0.0.12
10.0.0.123

使用grep查找是否包括10.0.0.1这个ip地址。常见的写法:

1
2
3
[Sat Nov 03 10:22 PM] [kodango@devops] ~/workspace
$ grep '10.0.0.1>' /tmp/ip.list
10.0.0.1

简单的方法(其实这方法不见得简单,只是为了说明-w这个参数还是很有用的)

1
2
3
[Sat Nov 03 10:23 PM] [kodango@devops] ~/workspace
$ grep -w '10.0.0.1' /tmp/ip.list
10.0.0.1

顺便grep的-n/-H/-v/-f/-c这几参数都很有用。

7) 临时设置环境变量

常见的写法:

1
2
3
4
5
6
[Sat Nov 03 10:26 PM] [kodango@devops] ~/workspace
$ export LC_ALL=zh_CN.UTF-8
 
[六 11月 03 10:26 下午] [kodango@devops] ~/workspace
$ date
2012年 11月 03日 星期六 22:26:55 CST

简洁的写法:

1
2
3
4
5
6
[六 11月 03 10:26 下午] [kodango@devops] ~/workspace
$ unset LC_ALL
 
[Sat Nov 03 10:27 PM] [kodango@devops] ~/workspace
$ LC_ALL=zh_CN.UTF-8 date
2012年 11月 03日 星期六 22:27:43 CST

在命令之前加上环境变更的设置,只是临时改变当前执行命令的环境。

8) $1,$2…等位置参数的使用

假设只想使用$2,$3..这几个参数,常见的做法是:

1
2
shift
echo "$@"

为什么不这样写呢?

1
echo "${@:2}"

9)退而求其次的写法

相信大家会有这种需求,当一个参数值没有提供时,可以使用默认值。常见的写法是:

1
2
3
4
5
arg=$1
 
if [ -z "$arg" ]; then
   arg=0
fi

简洁的写法是这样的:

1
arg=${1:-0}

10)bash特殊参数–的用法

假设要用grep查找字符串中是否包含-i,我们会这样尝试:

1
2
3
4
5
6
7
8
[Sat Nov 03 10:45 PM] [kodango@devops] ~/workspace
$ echo 'abc-i' | grep "-i"
Usage: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.
 
[Sat Nov 03 10:45 PM] [kodango@devops] ~/workspace
$ echo 'abc-i' | grep "-i"
abc-i

简洁的方法是:

1
2
3
[Sat Nov 03 10:45 PM] [kodango@devops] ~/workspace
$ echo 'abc-i' | grep -- -i
abc-i

bash中–后面的参数不会被当作选项解析。

11)函数的返回值默认是最后一行语句的返回值

1
2
3
4
5
6
7
8
# Check whether an item is a function
# $1: the function name
# Return: 0(yes) or 1(no)
function is_function()
{
    local func_name=$1
    test "`type -t $1 2>/dev/null`" = "function"
}

不要画蛇添足再在后面加一行return $?了。

12) 将printf格式化的结果赋值给变量

例如将数字转换成其十六进制形式,常见的写法是:

1
2
[Sat Nov 03 10:55 PM] [kodango@devops] ~/workspace
$ var=$(printf '%%%02x' 111)

简单的写法是:

1
2
[Sat Nov 03 10:54 PM] [kodango@devops] ~/workspace
$ printf -v var '%%%02x' 111

看看printf的help

1
2
3
4
5
6
7
8
[Sat Nov 03 10:53 PM] [kodango@devops] ~/workspace
$ help printf | grep -A 1 -B 1 -- -v
printf: printf [-v var] format [arguments]
    Formats and prints ARGUMENTS under control of the FORMAT.
--
    Options:
      -v var    assign the output to shell variable VAR rather than
            display it on the standard output

13)打印文件行

打印文件的第一行:

1
head -1 test.txt

打印文件的第2行:

1
sed -n '2p' test.txt

打印文件的第2到5行:

1
sed -n '2,5p' test.txt

打印文件的第2行始(包括第2行在内)5行的内容:

1
sed -n '2,+4p' test.txt

打印倒数第二行:

1
2
$ tail -2 test.txt | head -1
$ tac test.txt | sed -n '2p'

14)善用let或者(())命令做算术运算

如何对一个数字做++运算,可能你会这样用:

1
2
a=1
a=`expr a + 1`

为何不用你熟悉的:

1
2
3
a=1
let a++
let a+=2

15)获取软连接指定的真实文件名

如果你不知道,你可能会这样获取:

1
2
3
[Sat Nov 03 11:12 PM] [kodango@devops] ~/workspace
$ ls -l /usr/bin/python | awk -F'->' '{print $2}' | tr -d ' '
/usr/bin/python2

如果你知道有一个叫readlink的命令,那么:

1
2
3
[Sat Nov 03 11:13 PM] [kodango@devops] ~/workspace
$ readlink /usr/bin/python
/usr/bin/python2

16)获取一个字符的ASCII码

1
2
3
4
5
6
[Sat Nov 03 11:14 PM] [kodango@devops] ~/workspace
$ printf '%02x' "'+"
2b
[Sat Nov 03 11:30 PM] [kodango@devops] ~/workspace
$ echo -n '+' | od -tx1 -An | tr -d ' '
2b

17)清空一个文件

常见的用法:

1
echo "" > test.txt

简单的写法:

1
> test.txt

18) 不要忘记有here document

下面一段代码:

1
2
3
4
5
6
grep -v 1 /tmp/test.txt | while read line; do
    let a++
    echo --$line--
done
 
echo a:$a

执行后有什么问题吗?

1
2
3
4
5
[Sun Nov 04 05:35 AM] [kodango@devops] ~/workspace
$ sh test.sh
--2--
--3--
a:

发现a这个变量没有被赋值,为什么呢?因为管道后面的代码是在在一个子shell中执行的,所做的任何更改都不会对当前shell有影响,自然a这个变量就不会有赋值了。

换一种思路,可以这样做:

1
2
3
4
5
6
7
8
9
grep -v 1 /tmp/test.txt > /tmp/test.tmp
 
while read line; do
    let a++
    echo --$line--
done < /tmp/test.tmp
 
echo a:$a
rm -f /tmp/test.tmp

不过多了一个临时文件,最后还要删除。这里其实可以用到here document:

1
2
3
4
5
6
7
8
9
b=1
while read line2; do
    let b++
    echo ??$line2??
done < < EOF
`grep -v 1 /tmp/test.txt`
EOF
 
echo b: $b

here document往往用于需要输出一大段文本的地方,例如脚本的help函数。

19)删除字符串中的第一个或者最后一个字符

假设字符串为:

1
2
[Sun Nov 04 10:21 AM] [kodango@devops] ~/workspace
$ str="aremoveb"

可能你第一个想法是通过sed或者其它命令来完成这个功能,但是其实有很简单的方法:

1
2
3
4
5
6
7
[Sun Nov 04 10:24 AM] [kodango@devops] ~/workspace
$ echo "${str#?}"
removeb
 
[Sun Nov 04 10:24 AM] [kodango@devops] ~/workspace
$ echo "${str%?}"
aremove

类似地,你也可以删除2个、3个、4个……

有没有一次性删除第一个和最后一个字符的方法呢?答案当然是肯定的:

1
2
3
[Sun Nov 04 10:26 AM] [kodango@devops] ~/workspace
$ echo "${str:1:-1}"
remove

关于这些变量替换的内容在bash的man手册中都有说明。

20)使用逗号join数组元素

假设数组元素没有空格,可以用这种方法:

1
2
3
4
5
6
7
[Sun Nov 04 10:14 AM] [kodango@devops] ~/workspace
$ a=(1 2 3)
$ b="${a[*]}"
 
[Sun Nov 04 10:15 AM] [kodango@devops] ~/workspace
$ echo ${b// /,}
1,2,3

假设数组元素包含有空格,可以借用printf命令来达到:

1
2
3
4
5
6
[Sun Nov 04 10:15 AM] [kodango@devops] ~/workspace
$ a=(1 "2 3" 4)
 
[Sun Nov 04 10:15 AM] [kodango@devops] ~/workspace
$ printf ",%s" "${a[@]}" | cut -c2-  
1,2 3,4

21) Shell中的多进程

在命令行下,我们会在命令行后面加上&符号来让该命令在后台执行,在shell脚本中,使用”(cmd)”可以让fork一个子shell来执行该命令。利用这两点,可以实现shell的多线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
job_num=10
 
function do_work()
{
    echo "Do work.."
}
 
for ((i=0; i<job_num ;i++)); do
    echo "Fork job $i"
    (do_work) &
done
 
wait   # wait for all job done
echo "All job have been done!"

注意最后的wait命令,作用是等待所有子进程结束。

22) bash中alias的使用

alias其实是给常用的命令定一个别名,比如很多人会定义一下的一个别名:

1
alias ll='ls -l'

以后就可以使用ll,实际展开后执行的是ls -l。

现在很多发行版都会带几个默认的别名,比如:

1
2
3
alias grep='grep --color=auto'  # 带颜色显示
alias ls='ls --color=auto' # 同上
alias rm='rm -i'  # 删除文件需要确认

alias在某些方面确实提高了很大的效率,但是也是有隐患的,这点可以看我以前的一篇文章终端下肉眼看不见的东西。那么如何不要展开alias,而是用本来的意思呢?答案是使用转义:

1
2
ls
grep

在命令前面加一个反斜杠后就可以了。

这里要插一段故事,前两天我在shell脚本中定义了下面的一个alias,假设位于文件util.sh:

1
2
3
4
#!/bin/bash
...
alias ssh='ssh -o StrictHostKeyChecking=no -o LogLevel=quiet -o BatchMode=yes'
...

后面这串ssh选项是为了去掉一些warning的信息,不提示输入密码等等。具体可以看ssh的文档说明。我自己测试的时候好好的,当时我同事跑得时候却依然有报Warning。我对比了下我们两个人的用法:

1
2
sh util.sh  # 我的
./util.sh   # 他的

大家应该知道,直接./util.sh执行,shell会去找脚本第一行的shebang中给定的解释器去执行改脚本,所以第二种用法相当于直接用bash来执行。那想必是bash/sh对alias是否默认展开这一点上是有区别的了(可能是bash版本的问题,RHEL 5U4)。翻阅了下Bash的man手册,发现可以通过设置expand_aliases选项来打开alias展开的功能,默认在非交互式Shell下是关闭的(什么是交互式登录Shell)。

修改下util.sh,打开这个选项就Ok了:

1
2
3
4
5
6
#!/bin/bash
...
# Expand aliases in script
shopt -s expand_aliases
alias ssh='ssh -o StrictHostKeyChecking=no -o LogLevel=quiet -o BatchMode=yes'
...

23)awk打印除第一列之外的其他列

awk用来截取输入行中的某几列很有用,当时如果要排除某几列呢?

例如有如下的一个文件:

1
2
3
$ cat /tmp/test.txt
1 2 3 4 5
10 20 30 40 50

可以用下面的代码解决(来源):

1
2
3
$ awk '{$1="";print $0}' /tmp/test.txt
 2 3 4 5
 20 30 40 50

但是前面多了一个空格,可以用cut命令稍微调整下:

1
2
3
$ awk '{$1="";print $0}' /tmp/test.txt | cut -c2-
2 3 4 5
20 30 40 50

附几则小技巧:

1)sudo iptables -L -n | vim -

2)grep -v xxx | vim -

3)echo $’”

4)set — 1 2 3; echo “$@”

5)搜索stackoverflow/superuser等站点

 
 

最好的 Bash 脚本不仅能正常工作,而且编写得易于理解和修改。这得益于采用一致的变量名和编码风格。验证用户提供参数的合法性并检查命令是否成功运行也能保证脚本长时间可用。下面是一些我个人行之有效的建议。

采用一致缩进

缩进使代码更具可读性,也因此更具可维护性,尤其在代码逻辑嵌套超过三层。缩进使得脚本逻辑的基本结构非常直观。至于缩进多少空格无关紧要,尽管大部分人都倾向于使用4个或8个空格。只要确保采用缩进并进行对齐,这样就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
 
if [ $# -ge 1 ] && [ -d $1 ]; then
    for file in `ls $1`
    do
        if [ $debug == "on" ]; then
            echo working on $file
        fi
        wc -l $1/$file
    done
else
    echo "USAGE: $0 directory"
    exit 1
fi

提供有效信息

有效信息可以帮助运行脚本的人了解他们需要提供哪些参数,甚至是对两年后你自己。

1
2
3
4
if [ $# == 0 ]; then
    echo "Usage: $0 filename"
    exit 1
fi

合理使用注释

提供注释可以解释你的代码,特别是当代码比较复杂时,但不需要解释显而易见的代码行,只需要解释使用的每一条命令或者在代码段容易弄混的重要代码行。

1
2
3
4
5
6
7
8
username=$1
 
# make sure the account exists on the system
grep ^$username: /etc/passwd
if [ $? != 0 ]; then
    echo "No such user: $username"
    exit 1
fi

在出错退出时返回错误码

即使你不会查看错误码,但在代码出错时返回非零值是个不错的主意。有一天,也许你想找一种简单的方法来检查脚本哪里出错,那么返回值1或4或11可以帮你很快弄明白。

1
2
3
4
5
6
7
echo -n "In what year were you born?> "
read year
 
if [ $year -gt `date +%Y` ]; then
    echo "Sorry, but that's just not possible."
    exit 2
fi

使用函数替换重复命令集

函数也能让你的代码更具可读性和可维护性。如果重复使用的命令只有一条就不必麻烦,但如果很容易分离出一小撮共用命令行,就很有必要这样做。如果以后需要进行改动,只需要在一处进行即可。

1
2
3
4
5
6
7
function lower()
{
    local str="$@"
    local output
    output=$(tr '[A-Z]' '[a-z]'<<<"${str}")
    echo $output
}

为变量取有实际意义的名称

Unix管理员通常尽量避免输入一些额外字符,但不要在脚本中这样做。花些额外时间给变量一个有意义的命名并注意命名的一致性。

1
2
3
4
5
6
7
8
#!/bin/bash
 
if [ $# != 1 ]; then
    echo "Usage: $0 address"
    exit 1
else
    ip=$1
fi

检查参数类型是否正确

如果在使用参数前,对提供给脚本的输入参数进行类型检查,可以避免很多麻烦。下面是一种简单的方法,用于检查参数是否是数字。

1
2
3
4
5
if ! [ "$1" -eq "$1" 2> /dev/null ]
then
  echo "ERROR: $1 is not a number!"
  exit 1
fi

检查参数缺失或提供参数顺序的错误信息

不要以为使用者知道自己在做什么。如果他应该提供多个参数,请确保他提供了正确的参数。

1
2
3
if [ $# != 3 ]; then
    echo "What part of THREE ARGUMENTS don't you understand?"
fi

检查必要文件是否真实存在

在使用一个文件前检查它是否存在非常简单。下面的简单例子用于检查第一个输入参数指定的文件在系统上是否真实存在。

1
2
3
if [ ! -f $1 ]; then
    echo "$1 -- no such file"
fi

输出发送至/dev/null

将命令输出发送到 /dev/null 并以一种更加友好的方式告诉用户哪里出错,可以让你的脚本对使用者而言更加简单。

1
2
3
4
5
6
7
8
9
10
11
12
if [ $1 == "help" ]; then
    echo "Sorry -- No help available for $0"
else
    CMD=`which $1 >/dev/null 2>&1`
    if [ $? != 0 ]; then
        echo "$1: No such command -- maybe misspelled or not on your search path"
        exit 2
    else
        cmd=`basename $1`
        whatis $cmd
    fi
fi

使用错误码

你可以在脚本中使用返回码来判定一条命令的执行结果是否符合预期。

1
2
3
4
5
6
# check if the person is still logged in or has running processes
ps -U $username 2> /dev/null
if [ $? == 0 ]; then
    echo "processes:" >> /home/oldaccts/$username
    ps -U $username >> /home/oldaccts/$username
fi

信息提示

不要忘记告诉运行脚本的人他们应该知道的内容。他们不必在阅读代码后才知道你为他们创建了一个文件,特别是创建的文件不在当前文件夹中。

1
2
3
4
...
date >> /tmp/report<span class="MathJax_Preview">(
echo "Your report is /tmp/report)</span><script type="math/tex">
echo "Your report is /tmp/report</script>"

引用所有参数扩展

如果在脚本中使用字符扩展,别忘了使用引号,这样就不会得到一个意料之外的结果。

1
2
3
4
5
6
7
#!/bin/bash
 
msg="Be careful to name your files *.txt"
# this will expand *.txt
echo $msg
# this will not
echo "$msg"

引用所有参数时使用$@

$@变量会列出所有提供给脚本的参数,并且非常容易使用,正如下面一段脚本摘录所示。

1
2
3
4
5
6
#!/bin/bash
 
for i in "$@"
do
    echo "$i"
done

一些额外的注意和一致性可能意味着你现在编写的脚本将在之后很多年都易于使用。

 

脚本安全

我的所有bash脚本都以下面几句为开场白:

1
2
3
#!/bin/bash
set -o nounset
set -o errexit

这样做会避免两种常见的问题:

  1. 引用未定义的变量(缺省值为“”)
  2. 执行失败的命令被忽略

需要注意的是,有些Linux命令的某些参数可以强制忽略发生的错误,例如“mkdir -p” 和 “rm -f”。

还要注意的是,在“errexit”模式下,虽然能有效的捕捉错误,但并不能捕捉全部失败的命令,在某些情况下,一些失败的命令是无法检测到的。(更多细节请参考这个帖子。)

脚本函数

在bash里你可以定义函数,它们就跟其它命令一样,可以随意的使用;它们能让你的脚本更具可读性:

1
2
3
4
5
6
7
ExtractBashComments() {
    egrep "^#"
}
 
cat myscript.sh | ExtractBashComments | wc
 
comments=$(ExtractBashComments < myscript.sh)

还有一些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SumLines() {  # iterating over stdin - similar to awk     
    local sum=0
    local line=””
    while read line ; do
        sum=$((${sum} + ${line}))
    done
    echo ${sum}
}
 
SumLines < data_one_number_per_line.txt
 
log() {  # classic logger
   local prefix="[$(date +%Y/%m/%d %H:%M:%S)]: "
   echo "${prefix} $@" >&2
}
 
log "INFO" "a message"

尽可能的把你的bash代码移入到函数里,仅把全局变量、常量和对“main”调用的语句放在最外层。

变量注解

Bash里可以对变量进行有限的注解。最重要的两个注解是:

  1. local(函数内部变量)
  2. readonly(只读变量)
1
2
3
4
5
6
7
8
9
# a useful idiom: DEFAULT_VAL can be overwritten
#       with an environment variable of the same name
readonly DEFAULT_VAL=${DEFAULT_VAL:-7}
 
myfunc() {
   # initialize a local variable with the global default
   local some_var=${DEFAULT_VAL}
   ...
}

这样,你可以将一个以前不是只读变量的变量声明成只读变量:

1
2
3
4
x=5
x=6
readonly x
x=7   # failure

尽量对你bash脚本里的所有变量使用localreadonly进行注解。

$()代替反单引号(`)

反单引号很难看,在有些字体里跟正单引号很相似。$()能够内嵌使用,而且避免了转义符的麻烦。

1
2
3
# both commands below print out: A-B-C-D
echo "A-`echo B-\`echo C-\\`echo D\\`\``"
echo "A-$(echo B-$(echo C-$(echo D)))"

[[]](双层中括号)替代[]

使用[[]]能避免像异常的文件扩展名之类的问题,而且能带来很多语法上的改进,而且还增加了很多新功能:

操作符功能说明
|| 逻辑or(仅双中括号里使用)
&& 逻辑and(仅双中括号里使用)
< 字符串比较(双中括号里不需要转移)
-lt 数字比较
= 字符串相等
== 以Globbing方式进行字符串比较(仅双中括号里使用,参考下文)
=~ 用正则表达式进行字符串比较(仅双中括号里使用,参考下文)
-n 非空字符串
-z 空字符串
-eq 数字相等
-ne 数字不等

单中括号:

1
[ "${name}" > "a" -o ${name} < "m" ]

双中括号

1
[[ "${name}" > "a" && "${name}" < "m"  ]]

正则表达式/Globbing

使用双中括号带来的好处用下面几个例子最能表现:

1
2
3
4
5
t="abc123"
[[ "$t" == abc* ]]         # true (globbing比较)
[[ "$t" == "abc*" ]]       # false (字面比较)
[[ "$t" =~ [abc]+[123]+ ]] # true (正则表达式比较)
[[ "$t" =~ "abc*" ]]       # false (字面比较)

注意,从bash 3.2版开始,正则表达式和globbing表达式都不能用引号包裹。如果你的表达式里有空格,你可以把它存储到一个变量里:

1
2
r="a b+"
[[ "a bbb" =~ $r ]]        # true

按Globbing方式的字符串比较也可以用到case语句中:

1
2
3
case $t in
abc*)  <action> ;;
esac

字符串操作

Bash里有各种各样操作字符串的方式,很多都是不可取的。

基本用户

1
2
3
4
5
6
7
8
9
10
11
f="path1/path2/file.ext" 
 
len="${#f}" # = 20 (字符串长度)
 
# 切片操作: ${<var>:<start>} or ${<var>:<start>:<length>}
slice1="${f:6}" # = "path2/file.ext"
slice2="${f:6:5}" # = "path2"
slice3="${f: -8}" # = "file.ext"(注意:"-"前有空格)
pos=6
len=5
slice4="${f:${pos}:${len}}" # = "path2"

替换操作(使用globbing)

1
2
3
4
5
6
7
8
9
f="path1/path2/file.ext" 
 
single_subst="${f/path?/x}"   # = "x/path2/file.ext"
global_subst="${f//path?/x}"  # = "x/x/file.ext"
 
# 字符串拆分
readonly DIR_SEP="/"
array=(${f//${DIR_SEP}/ })
second_dir="${arrray[1]}"     # = path2

删除头部或尾部(使用globbing)

1
2
3
4
5
6
7
8
9
10
11
12
13
f="path1/path2/file.ext"
 
# 删除字符串头部
extension="${f#*.}"  # = "ext"
 
# 以贪婪匹配方式删除字符串头部
filename="${f##*/}"  # = "file.ext"
 
# 删除字符串尾部
dirname="${f%/*}"    # = "path1/path2"
 
# 以贪婪匹配方式删除字符串尾部
root="${f%%/*}"      # = "path1"

避免使用临时文件

有些命令需要以文件名为参数,这样一来就不能使用管道。这个时候?<()?就显出用处了,它可以接受一个命令,并把它转换成可以当成文件名之类的什么东西:

1
2
# 下载并比较两个网页
diff <(wget -O - url1) <(wget -O - url2)

还有一个非常有用处的是”here documents”,它能让你在标准输入上输入多行字符串。下面的’MARKER’可以替换成任何字词。

1
2
3
4
5
6
7
# 任何字词都可以当作分界符
command  << MARKER
...
${var}
$(cmd)
...
MARKER

如果文本里没有内嵌变量替换操作,你可以把第一个MARKER用单引号包起来:

1
2
3
4
5
6
command << 'MARKER'
...
no substitution is happening here.
$ (dollar sign) is passed through verbatim.
...
MARKER

内置变量

变量说明
$0 脚本名称
$n 传给脚本/函数的第n个参数
$$ 脚本的PID
$! 上一个被执行的命令的PID(后台运行的进程)
$? 上一个命令的退出状态(管道命令使用${PIPESTATUS})
$# 传递给脚本/函数的参数个数
$@ 传递给脚本/函数的所有参数(识别每个参数)
$* 传递给脚本/函数的所有参数(把所有参数当成一个字符串)
提示
使用$*很少是正确的选择。
$@能够处理空格参数,而且参数间的空格也能正确的处理。
使用$@时应该用双引号括起来,像”$@”这样。

调试

对脚本进行语法检查:

1
bash -n myscript.sh

跟踪脚本里每个命令的执行:

1
bash -v myscripts.sh

跟踪脚本里每个命令的执行并附加扩充信息:

1
bash -x myscript.sh

你可以在脚本头部使用set -o verboseset -o xtrace来永久指定-v-o。当在远程机器上执行脚本时,这样做非常有用,用它来输出远程信息。

什么时候不应该使用bash脚本

  • 你的脚本太长,多达几百行
  • 你需要比数组更复杂的数据结构
  • 出现了复杂的转义问题
  • 有太多的字符串操作
  • 不太需要调用其它程序和跟其它程序管道交互
  • 担心性能

这个时候,你应该考虑一种脚本语言,比如Python或Ruby。

 
 
BASH的保护性编程技巧
 

这是我写BASH程序的招式。这里本没有什么新的内容,但是从我的经验来看,人们爱滥用BASH。他们忽略了计算机科学,而从他们的程序中创造的是“大泥球”(译注:指架构不清晰的软件系统)。

在此我告诉你方法,以保护你的程序免于障碍,并保持代码的整洁。

不可改变的全局变量

  • 尽量少用全局变量
  • 以大写命名
  • 只读声明
  • 用全局变量来代替隐晦的$0,$1等
  • 在我的程序中常使用的全局变量:
1
2
3
readonly PROGNAME=$(basename $0)
readonly PROGDIR=$(readlink -m $(dirname $0))
readonly ARGS="$@"

一切皆是局部的

所有变量都应为局部的。

1
2
3
4
5
6
7
change_owner_of_file() {
    local filename=$1
    local user=$2
    local group=$3
 
    chown $user:$group $filename
}
1
2
3
4
5
6
7
8
9
10
11
change_owner_of_files() {
    local user=$1; shift
    local group=$1; shift
    local files=$@
    local i
 
    for i in $files
    do
        chown $user:$group $i
    done
}
  • 自注释(self documenting)的参数
  • 通常作为循环用的变量i,把它声明为局部变量是很重要的。
  • 局部变量不作用于全局域。
1
2
kfir@goofy ~ $ local a
bash: local: can only be used in a function

 

main()

  • 有助于保持所有变量的局部性
  • 直观的函数式编程
  • 代码中唯一的全局命令是:main
1
2
3
4
5
6
7
8
9
10
main() {
    local files="/tmp/a /tmp/b"
    local i
 
    for i in $files
    do
        change_owner_of_file kfir users $i
    done
}
main

 

一切皆是函数

  • 唯一全局性运行的代码是:

- 不可变的全局变量声明

- main()函数

  • 保持代码整洁
  • 过程变得清晰
1
2
3
main() {
    local files=$(ls /tmp | grep pid | grep -v daemon)
}
1
2
3
4
5
6
7
8
9
10
11
temporary_files() {
    local dir=$1
 
    ls $dir
        | grep pid
        | grep -v daemon
}
 
main() {
    local files=$(temporary_files /tmp)
}
  • 第二个例子好得多。查找文件是temporary_files()的问题而非main()的。这段代码用temporary_files()的单元测试也是可测试的。
  • 如果你一定要尝试第一个例子,你会得到查找临时文件以和main算法的大杂烩。
1
2
3
4
5
6
7
8
9
10
11
12
test_temporary_files() {
    local dir=/tmp
 
    touch $dir/a-pid1232.tmp
    touch $dir/a-pid1232-daemon.tmp
 
    returns "$dir/a-pid1232.tmp" temporary_files $dir
 
    touch $dir/b-pid1534.tmp
 
    returns "$dir/a-pid1232.tmp $dir/b-pid1534.tmp" temporary_files $dir
}
如你所见,这个测试不关心main()。

 

调试函数

  • 带-x标志运行程序:
1
bash -x my_prog.sh
只调试一小段代码,使用set-x和set+x,会只对被set -x和set +x包含的当前代码打印调试信息。
1
2
3
4
5
6
7
8
9
temporary_files() {
    local dir=$1
 
    set -x
    ls $dir
        | grep pid
        | grep -v daemon
    set +x
}
打印函数名和它的参数:
1
2
3
4
5
6
7
8
temporary_files() {
    echo $FUNCNAME $@
    local dir=$1
 
    ls $dir
        | grep pid
        | grep -v daemon
}

调用函数:

1
temporary_files /tmp

会打印到标准输出:

1
temporary_files /tmp

 

代码的清晰度

这段代码做了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main() {
    local dir=/tmp
 
    [[ -z $dir ]]
        && do_something...
 
    [[ -n $dir ]]
        && do_something...
 
    [[ -f $dir ]]
        && do_something...
 
    [[ -d $dir ]]
        && do_something...
}
main

让你的代码说话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
is_empty() {
    local var=$1
 
    [[ -z $var ]]
}
 
is_not_empty() {
    local var=$1
 
    [[ -n $var ]]
}
 
is_file() {
    local file=$1
 
    [[ -f $file ]]
}
 
is_dir() {
    local dir=$1
 
    [[ -d $dir ]]
}
 
main() {
    local dir=/tmp
 
    is_empty $dir
        && do_something...
 
    is_not_empty $dir
        && do_something...
 
    is_file $dir
        && do_something...
 
    is_dir $dir
        && do_something...
}
main

 

每一行只做一件事

  • 用反斜杠来作分隔符。例如:
1
2
3
4
5
temporary_files() {
    local dir=$1
 
    ls $dir | grep pid | grep -v daemon
}

可以写得简洁得多:

1
2
3
4
5
6
7
temporary_files() {
    local dir=$1
 
    ls $dir
        | grep pid
        | grep -v daemon
}
  • 符号在缩进行的开始

符号在行末的坏例子:(译注:原文在此例中用了temporary_files()代码段,疑似是贴错了。结合上下文,应为print_dir_if_not_empty())

1
2
3
4
5
6
7
print_dir_if_not_empty() {
    local dir=$1
 
    is_empty $dir &&
        echo "dir is empty" ||
        echo "dir=$dir"
}

好的例子:我们可以清晰看到行和连接符号之间的联系。

1
2
3
4
5
6
7
print_dir_if_not_empty() {
    local dir=$1
 
    is_empty $dir
        && echo "dir is empty"
        || echo "dir=$dir"
}

 

打印用法

不要这样做:

1
2
3
echo "this prog does:..."
echo "flags:"
echo "-h print help"

它应该是个函数:

1
2
3
4
5
usage() {
    echo "this prog does:..."
    echo "flags:"
    echo "-h print help"
}

echo在每一行重复。因此我们得到了这个文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
usage() {
    cat <<- EOF
    usage: $PROGNAME options
 
    Program deletes files from filesystems to release space.
    It gets config file that define fileystem paths to work on, and whitelist rules to
    keep certain files.
 
    OPTIONS:
       -c --config              configuration file containing the rules. use --help-config to see the syntax.
       -n --pretend             do not really delete, just how what you are going to do.
       -t --test                run unit test to check the program
       -v --verbose             Verbose. You can specify more then one -v to have more verbose
       -x --debug               debug
       -h --help                show this help
          --help-config         configuration help
 
    Examples:
       Run all tests:
       $PROGNAME --test all
 
       Run specific test:
       $PROGNAME --test test_string.sh
 
       Run:
       $PROGNAME --config /path/to/config/$PROGNAME.conf
 
       Just show what you are going to do:
       $PROGNAME -vn -c /path/to/config/$PROGNAME.conf
    EOF
}

注意在每一行的行首应该有一个真正的制表符‘ ’。

在vim里,如果你的tab是4个空格,你可以用这个替换命令:

1
:s/^    / /

 

命令行参数

这里是一个例子,完成了上面usage函数的用法。我从Kirk’s blog post – bash shell script to use getopts with gnu style long positional parameters得到这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
cmdline() {
    # got this idea from here:
    # http://kirk.webfinish.com/2009/10/bash-shell-script-to-use-getopts-with-gnu-style-long-positional-parameters/
    local arg=
    for arg
    do
        local delim=""
        case "$arg" in
            #translate --gnu-long-options to -g (short options)
            --config)         args="${args}-c ";;
            --pretend)        args="${args}-n ";;
            --test)           args="${args}-t ";;
            --help-config)    usage_config &amp;&amp; exit 0;;
            --help)           args="${args}-h ";;
            --verbose)        args="${args}-v ";;
            --debug)          args="${args}-x ";;
            #pass through anything else
            *) [[ "${arg:0:1}" == "-" ]] || delim="""
                args="${args}${delim}${arg}${delim} ";;
        esac
    done
 
    #Reset the positional parameters to the short options
    eval set -- $args
 
    while getopts "nvhxt:c:" OPTION
    do
         case $OPTION in
         v)
             readonly VERBOSE=1
             ;;
         h)
             usage
             exit 0
             ;;
         x)
             readonly DEBUG='-x'
             set -x
             ;;
         t)
             RUN_TESTS=$OPTARG
             verbose VINFO "Running tests"
             ;;
         c)
             readonly CONFIG_FILE=$OPTARG
             ;;
         n)
             readonly PRETEND=1
             ;;
        esac
    done
 
    if [[ $recursive_testing || -z $RUN_TESTS ]]; then
        [[ ! -f $CONFIG_FILE ]]
            &amp;&amp; eexit "You must provide --config file"
    fi
    return 0
}

你像这样,使用我们在头上定义的不可变的ARGS变量:

1
2
3
4
main() {
    cmdline $ARGS
}
main

 

单元测试

  • 在更高级的语言中很重要。
  • 使用shunit2做单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
test_config_line_paths() {
    local s='partition cpm-all, 80-90,'
 
    returns "/a" "config_line_paths '$s /a, '"
    returns "/a /b/c" "config_line_paths '$s /a:/b/c, '"
    returns "/a /b /c" "config_line_paths '$s   /a  :    /b : /c, '"
}
 
config_line_paths() {
    local partition_line="$@"
 
    echo $partition_line
        | csv_column 3
        | delete_spaces
        | column 1
        | colons_to_spaces
}
 
source /usr/bin/shunit2

这里是另一个使用df命令的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
DF=df
 
mock_df_with_eols() {
    cat &lt;&lt;- EOF
    Filesystem           1K-blocks      Used Available Use% Mounted on
    /very/long/device/path
                         124628916  23063572 100299192  19% /
    EOF
}
 
test_disk_size() {
    returns 1000 "disk_size /dev/sda1"
 
    DF=mock_df_with_eols
    returns 124628916 "disk_size /very/long/device/path"
}
 
df_column() {
    local disk_device=$1
    local column=$2
 
    $DF $disk_device
        | grep -v 'Use%'
        | tr ' ' ' '
        | awk "{print $$column}"
}
 
disk_size() {
    local disk_device=$1
 
    df_column $disk_device 2
}

这里我有个例外,为了测试,我在全局域中声明了DF为非只读。这是因为shunit2不允许改变全局域函数。

 
 
有用的 bash 别名和函数

作为一个命令行探索者,你或许发现你自己一遍又一遍重复同样的命令。如果你总是用ssh进入到同一台电脑,如果你总是将一连串命令连接起来,如果你总是用同样的参数运行一个程序,你也许希望在这种不断的重复中为你的生命节约下几秒钟。

解决方案是使用一个别名(alias)。正如你可能知道的,别名用一种让你的shell记住一个特定的命令并且给它一个新的名字的方式。不管怎么样,别名有一些限制,它只是shell命令的快捷方式,不能传递或者控制其中的参数。所以作为补充,bash 也允许你创建你自己的函数,这可能更长一些和复杂一点,它允许任意数量的参数。

当然,当你有美食时,比如某种汤,你要分享它给大家。我这里有一个列表,列出了一些最有用bash别名和函数的。注意“最有用的”只是个说法,别名的是否有用要看你是否每天都需要在 shell 里面用它。

在你开始你的别名体验之旅前,这里有一个便于使用的小技巧:如果你的别名和原本的命令名字相同,你可以用如下技巧来访问原本的命令(LCTT 译注:你也可以直接原本命令的完整路径来访问它。)

command

例如,如果有一个替换了ls命令的别名 ls。如果你想使用原本的ls命令而不是别名,通过调用它:

ls

提升生产力

这些别名真的很简单并且真的很短,但他们大多数是为了给你的生命节省几秒钟,最终也许为你这一辈子节省出来几年,也许呢。

alias ls="ls --color=auto"

简单但非常重要。使ls命令带着彩色输出。

alias ll="ls --color -al"

以彩色的列表方式列出目录里面的全部文件。

alias grep='grep --color=auto'

类似,只是在grep里输出带上颜色。

mcd() { mkdir -p "$1"; cd "$1";}

我的最爱之一。创建一个目录并进入该目录里: mcd [目录名]。

cls() { cd "$1"; ls;}

类似上一个函数,进入一个目录并列出它的的内容:cls[目录名]。

backup() { cp "$1"{,.bak};}

简单的给文件创建一个备份: backup [文件] 将会在同一个目录下创建 [文件].bak。

md5check() { md5sum "$1" | grep "$2";}

因为我讨厌通过手工比较文件的md5校验值,这个函数会计算它并进行比较:md5check[文件][校验值]。

alias makescript="fc -rnl | head -1 >"

很容易用你上一个运行的命令创建一个脚本:makescript [脚本名字.sh]

alias genpasswd="strings /dev/urandom | grep -o '[[:alnum:]]' | head -n 30 | tr -d ' '; echo"

只是瞬间产生一个强壮的密码。

alias c="clear"

清除你终端屏幕不能更简单了吧?

alias histg="history | grep"

快速搜索你的命令输入历史:histg [关键字]

alias ..='cd ..'

回到上层目录还需要输入 cd 吗?

alias ...='cd ../..'

自然,去到上两层目录。

extract() {
if [ -f $1 ] ; then
case $1 in
*.tar.bz2) tar xjf $1 ;;
*.tar.gz) tar xzf $1 ;;
*.bz2) bunzip2 $1 ;;
*.rar) unrar e $1 ;;
*.gz) gunzip $1 ;;
*.tar) tar xf $1 ;;
*.tbz2) tar xjf $1 ;;
*.tgz) tar xzf $1 ;;
*.zip) unzip $1 ;;
*.Z) uncompress $1 ;;
*.7z) 7z x $1 ;;
*) echo "'$1' cannot be extracted via extract()" ;;
esac
else
echo "'$1' is not a valid file"
fi
}

很长,但是也是最有用的。解压任何的文档类型:extract: [压缩文件]

系统信息

想尽快地知道关于你的系统一切信息?

alias cmount="mount | column -t"

按列格式化输出mount信息。

alias tree="ls -R | grep ":$" | sed -e 's/:$//' -e 's/[^-][^/]*//--/g' -e 's/^/ /' -e 's/-/|/'"

以树形结构递归地显示目录结构。

sbs() { du -b --max-depth 1 | sort -nr | perl -pe 's{([0-9]+)}{sprintf "%.1f%s", $1>=2**30? ($1/2**30, "G"): $1>=2**20? ($1/2**20, "M"): $1>=2**10? ($1/2**10, "K"): ($1, "")}e';}

安装文件在磁盘存储的大小排序,显示当前目录的文件列表。

alias intercept="sudo strace -ff -e trace=write -e write=1,2 -p"

接管某个进程的标准输出和标准错误。注意你需要安装了 strace。

alias meminfo='free -m -l -t'

查看你还有剩下多少内存。

alias ps? = "ps aux | grep"

可以很容易地找到某个进程的PID:ps? [名字]。

alias volume="amixer get Master | sed '1,4 d' | cut -d [ -f 2 | cut -d ] -f 1"

显示当前音量设置。

网络

对于所有用在互联网和本地网络的命令,也有一些神奇的别名给它们。

alias websiteget="wget --random-wait -r -p -e robots=off -U mozilla"

下载整个网站:websiteget [URL]。

alias listen="lsof -P -i -n"

显示出哪个应用程序连接到网络。

alias port='netstat -tulanp'

显示出活动的端口。

gmail() { curl -u "$1" --silent "https://mail.google.com/mail/feed/atom" | sed -e 's/</fullcount.*/ /' | sed -e 's/.*fullcount>//'}

大概的显示你的谷歌邮件里未读邮件的数量:gmail [用户名]

alias ipinfo="curl ifconfig.me && curl ifconfig.me/host"

获得你的公网IP地址和主机名。

getlocation() { lynx -dump http://www.ip-adress.com/ip_tracer/?QRY=$1|grep address|egrep 'city|state|country'|awk '{print $3,$4,$5,$6,$7,$8}'|sed 'sip address flag \'|sed 'sMy\';}

返回你的当前IP地址的地理位置。

也许无用

所以呢,如果一些别名并不是全都具有使用价值?它们可能仍然有趣。

kernelgraph() { lsmod | perl -e 'print "digraph "lsmod" {";<>;while(<>){@_=split/s+/; print ""$_[0]" -> "$_" " for split/,/,$_[3]}print "}"' | dot -Tpng | display -;}

绘制内核模块依赖曲线图。需要可以查看图片。

alias busy="cat /dev/urandom | hexdump -C | grep 'ca fe'"

在那些非技术人员的眼里你看起来是总是那么忙和神秘。

最后,这些别名和函数的很大一部分来自于我个人的.bashrc。而那些令人点赞的网站 alias.shcommandlinefu.com我早已在我的帖子best online tools for Linux 里面介绍过。你可以去看看,如果你愿意,也可以分享下你的。也欢迎你在这里评论,分享一下你的智慧。

做为奖励,这里有我提到的全部别名和函数的纯文本版本,随时可以复制粘贴到你的.bashrc。(如果你已经一行一行的复制到这里了,哈哈,你发现你又浪费了生命的几秒钟~)

#Productivity
alias ls="ls --color=auto"
alias ll="ls --color -al"
alias grep='grep --color=auto'
mcd() { mkdir -p "$1"; cd "$1";}
cls() { cd "$1"; ls;}
backup() { cp "$1"{,.bak};}
md5check() { md5sum "$1" | grep "$2";}
alias makescript="fc -rnl | head -1 >"
alias genpasswd="strings /dev/urandom | grep -o '[[:alnum:]]' | head -n 30 | tr -d ' '; echo"
alias c="clear"
alias histg="history | grep"
alias ..='cd ..'
alias ...='cd ../..'
extract() {
if [ -f $1 ] ; then
case $1 in
*.tar.bz2) tar xjf $1 ;;
*.tar.gz) tar xzf $1 ;;
*.bz2) bunzip2 $1 ;;
*.rar) unrar e $1 ;;
*.gz) gunzip $1 ;;
*.tar) tar xf $1 ;;
*.tbz2) tar xjf $1 ;;
*.tgz) tar xzf $1 ;;
*.zip) unzip $1 ;;
*.Z) uncompress $1 ;;
*.7z) 7z x $1 ;;
*) echo "'$1' cannot be extracted via extract()" ;;
esac
else
echo "'$1' is not a valid file"
fi
}
 
#System info
alias cmount="mount | column -t"
alias tree="ls -R | grep ":$" | sed -e 's/:$//' -e 's/[^-][^/]*//--/g' -e 's/^/ /' -e 's/-/|/'"
sbs(){ du -b --max-depth 1 | sort -nr | perl -pe 's{([0-9]+)}{sprintf "%.1f%s", $1>=2**30? ($1/2**30, "G"): $1>=2**20? ($1/2**20, "M"): $1>=2**10? ($1/2**10, "K"): ($1, "")}e';}
alias intercept="sudo strace -ff -e trace=write -e write=1,2 -p"
alias meminfo='free -m -l -t'
alias ps?="ps aux | grep"
alias volume="amixer get Master | sed '1,4 d' | cut -d [ -f 2 | cut -d ] -f 1"
 
#Network
alias websiteget="wget --random-wait -r -p -e robots=off -U mozilla"
alias listen="lsof -P -i -n"
alias port='netstat -tulanp'
gmail() { curl -u "$1" --silent "https://mail.google.com/mail/feed/atom" | sed -e 's/</fullcount.*/ /' | sed -e 's/.*fullcount>//'}
alias ipinfo="curl ifconfig.me && curl ifconfig.me/host"
getlocation() { lynx -dump http://www.ip-adress.com/ip_tracer/?QRY=$1|grep address|egrep 'city|state|country'|awk '{print $3,$4,$5,$6,$7,$8}'|sed 'sip address flag \'|sed 'sMy\';}
 
#Funny
kernelgraph() { lsmod | perl -e 'print "digraph "lsmod" {";<>;while(<>){@_=split/s+/; print ""$_[0]" -> "$_" " for split/,/,$_[3]}print "}"' | dot -Tpng | display -;}
alias busy="cat /dev/urandom | hexdump -C | grep "ca fe""

via: http://xmodulo.com/useful-bash-aliases-functions.html

作者:Adrien Brochard 译者:luoyutiantang 校对:wxy

 

===================== End

原文地址:https://www.cnblogs.com/lsgxeva/p/10797062.html