BASH的保护性编程技巧
简单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/\n/,/;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
|
这样做会避免两种常见的问题:
- 引用未定义的变量(缺省值为“”)
- 执行失败的命令被忽略
需要注意的是,有些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里可以对变量进行有限的注解。最重要的两个注解是:
local
(函数内部变量)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脚本里的所有变量使用local
或readonly
进行注解。
用$()
代替反单引号(`)
反单引号很难看,在有些字体里跟正单引号很相似。$()
能够内嵌使用,而且避免了转义符的麻烦。
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 verbose
和set -o xtrace
来永久指定-v
和-o
。当在远程机器上执行脚本时,这样做非常有用,用它来输出远程信息。
什么时候不应该使用bash脚本
- 你的脚本太长,多达几百行
- 你需要比数组更复杂的数据结构
- 出现了复杂的转义问题
- 有太多的字符串操作
- 不太需要调用其它程序和跟其它程序管道交互
- 担心性能
这个时候,你应该考虑一种脚本语言,比如Python或Ruby。
这是我写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
}
|
调试函数
- 带-x标志运行程序:
1
|
bash
-x my_prog.sh
|
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
}
|
注意在每一行的行首应该有一个真正的制表符‘\t’。
在vim里,如果你的tab是4个空格,你可以用这个替换命令:
1
|
:s/^ /\t/
|
命令行参数
这里是一个例子,完成了上面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 &&
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 ]] \
&& 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
<<- 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
'\n'
' '
\
|
awk
"{print \$$column}"
}
disk_size() {
local
disk_device=$1
df_column $disk_device 2
}
|
这里我有个例外,为了测试,我在全局域中声明了DF为非只读。这是因为shunit2不允许改变全局域函数。
作为一个命令行探索者,你或许发现你自己一遍又一遍重复同样的命令。如果你总是用ssh进入到同一台电脑,如果你总是将一连串命令连接起来,如果你总是用同样的参数运行一个程序,你也许希望在这种不断的重复中为你的生命节约下几秒钟。
解决方案是使用一个别名(alias)。正如你可能知道的,别名用一种让你的shell记住一个特定的命令并且给它一个新的名字的方式。不管怎么样,别名有一些限制,它只是shell命令的快捷方式,不能传递或者控制其中的参数。所以作为补充,bash 也允许你创建你自己的函数,这可能更长一些和复杂一点,它允许任意数量的参数。
当然,当你有美食时,比如某种汤,你要分享它给大家。我这里有一个列表,列出了一些最有用bash别名和函数的。注意“最有用的”只是个说法,别名的是否有用要看你是否每天都需要在 shell 里面用它。
在你开始你的别名体验之旅前,这里有一个便于使用的小技巧:如果你的别名和原本的命令名字相同,你可以用如下技巧来访问原本的命令(LCTT 译注:你也可以直接原本命令的完整路径来访问它。)
例如,如果有一个替换了ls命令的别名 ls。如果你想使用原本的ls命令而不是别名,通过调用它:
提升生产力
这些别名真的很简单并且真的很短,但他们大多数是为了给你的生命节省几秒钟,最终也许为你这一辈子节省出来几年,也许呢。
简单但非常重要。使ls命令带着彩色输出。
以彩色的列表方式列出目录里面的全部文件。
类似,只是在grep里输出带上颜色。
我的最爱之一。创建一个目录并进入该目录里: mcd [目录名]。
类似上一个函数,进入一个目录并列出它的的内容:cls[目录名]。
简单的给文件创建一个备份: backup [文件] 将会在同一个目录下创建 [文件].bak。
因为我讨厌通过手工比较文件的md5校验值,这个函数会计算它并进行比较:md5check[文件][校验值]。
很容易用你上一个运行的命令创建一个脚本:makescript [脚本名字.sh]
只是瞬间产生一个强壮的密码。
清除你终端屏幕不能更简单了吧?
快速搜索你的命令输入历史:histg [关键字]
回到上层目录还需要输入 cd 吗?
自然,去到上两层目录。
很长,但是也是最有用的。解压任何的文档类型:extract: [压缩文件]
系统信息
想尽快地知道关于你的系统一切信息?
按列格式化输出mount信息。
以树形结构递归地显示目录结构。
安装文件在磁盘存储的大小排序,显示当前目录的文件列表。
接管某个进程的标准输出和标准错误。注意你需要安装了 strace。
查看你还有剩下多少内存。
可以很容易地找到某个进程的PID:ps? [名字]。
显示当前音量设置。
网络
对于所有用在互联网和本地网络的命令,也有一些神奇的别名给它们。
下载整个网站:websiteget [URL]。
显示出哪个应用程序连接到网络。
显示出活动的端口。
大概的显示你的谷歌邮件里未读邮件的数量:gmail [用户名]
获得你的公网IP地址和主机名。
返回你的当前IP地址的地理位置。
也许无用
所以呢,如果一些别名并不是全都具有使用价值?它们可能仍然有趣。
绘制内核模块依赖曲线图。需要可以查看图片。
在那些非技术人员的眼里你看起来是总是那么忙和神秘。
最后,这些别名和函数的很大一部分来自于我个人的.bashrc。而那些令人点赞的网站 alias.sh和commandlinefu.com我早已在我的帖子best online tools for Linux 里面介绍过。你可以去看看,如果你愿意,也可以分享下你的。也欢迎你在这里评论,分享一下你的智慧。
做为奖励,这里有我提到的全部别名和函数的纯文本版本,随时可以复制粘贴到你的.bashrc。(如果你已经一行一行的复制到这里了,哈哈,你发现你又浪费了生命的几秒钟~)
via: http://xmodulo.com/useful-bash-aliases-functions.html
作者:Adrien Brochard 译者:luoyutiantang 校对:wxy
===================== End