bash例子
- 命令行
- 文本操作
- BASH的保护性编程技巧
- 单元测试
- 简洁的bash编程技巧
- 1) 检查命令执行是否成功
- 2) 将标准输出与标准错误输出重定向到/dev/null
- 3) awk的使用
- 4) 将一个文本的所有行用逗号连接起来
- 6) grep查找单词
- 7) 临时设置环境变量
- 8) 参数的使用
- 9)退而求其次的写法
- 10)bash特殊参数–的用法
- 11)函数的返回值默认是最后一行语句的返回值
- 12) 将printf格式化的结果赋值给变量
- 13)打印文件行
- 14)善用let或者(())命令做算术运算
- 15)获取软连接指定的真实文件名
- 16)获取一个字符的ASCII码
- 17)清空一个文件
- 18) 不要忘记有here document
- 19)删除字符串中的第一个或者最后一个字符
- 20)使用逗号join数组元素
- 21) Shell中的多进程
- 22) bash中alias的使用
- 23)awk打印除第一列之外的其他列
- 附几则小技巧:
命令行
常用别名
# Alias definitions. # You may want to put all your additions in here. # See /usr/share/doc/bash-doc/examples in the bash-doc package. # Enable color support if [ -x /usr/bin/dircolors ]; then test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" alias ls='ls –color=auto' alias grep='grep –color=auto' alias fgrep='fgrep –color=auto' alias egrep='egrep –color=auto' fi # Extra ls aliases alias ll='ls -FAl' alias la='ls -FA' alias l='ls -FAoh' # Raspberry Pi Aliases alias raspi-audio="/usr/bin/alsamixer" alias raspi-cpu="/bin/cat /proc/cpuinfo" alias raspi-disk="/bin/df -h –total" alias raspi-firmware="/opt/vc/bin/vcgencmd version" alias raspi-mem="/usr/bin/free -th" alias raspi-ip="/bin/hostname -I" alias raspi-ping="/bin/ping -c 3 http://www.google.com" alias raspi-reboot="/sbin/reboot" alias raspi-shutdown="/sbin/shutdown -Ph 0" alias raspi-update="/usr/bin/apt-get update -y;/usr/bin/apt-get upgrade -y" alias raspi-version="/bin/cat /proc/version"
锁屏
- trap命令屏蔽信号(如中断信号)。
- stty-echo命令控制回显。
例:一个锁屏幕程序locktty
:
#!/bin/bash trap '' 1 2 3 18 # 屏蔽 CONTROL-Z等信号 stty -echo # 关闭回显 echo -n "pls input password : " read key_1 echo echo -n "pls input again : " read key_2 echo key_3= if [ "$key_1" = "$key_2" ]; then tput clear until [ "$key_3" = "$key_2" ]; do read key_3 done else echo "locktty: keys do not match" 1 >& 2 fi stty echo
文本操作
完美的方法,即第三种的while循环(while read line;do …. done < $FILE) 是最合适且 最简单的一行行地读文件的方法。请看以下例子。
要读取的文件:
Input: $ cat sample.txt This is sample file This is normal text file Source: $ cat readfile.sh
读取方式在:
#!/bin/bash i=1; FILE=sample.txt # Wrong way to read the file. # This may cause problem, check the value of 'i' at the end of the loop echo "###############################" cat $FILE | while read line; do echo "Line # $i: $line" ((i++)) done echo "Total number of lines in file: $i" # The worst way to read file. echo "###############################" for fileline in $(cat $FILE);do echo $fileline done # This is correct way to read file. echo "################################" k=1 while read line;do echo "Line # $k: $line" ((k++)) done < $FILE echo "Total number of lines in file: $k" Output: $ ./readfile.sh ############################### Line # 1: This is sample file Line # 2: This is normal text file Total number of lines in file: 1 ############################### This is sample file This is normal text file ################################ Line # 1: This is sample file Line # 2: This is normal text file Total number of lines in file: 3
BASH的保护性编程技巧
不可改变的全局变量
- 尽量少用全局变量
- 以大写命名
- 只读声明
-
用全局变量来代替隐晦的
$0
,$1
等
在我的程序中常使用的全局变量:
readonly PROGNAME=$(basename $0) readonly PROGDIR=$(readlink -m $(dirname $0)) readonly ARGS="$@"
一切皆是局部的
所有变量都应为局部的。
change_owner_of_file() { local filename=$1 local user=$2 local group=$3 chown $user:$group $filename }
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,把它声明为局部变量是很重要的。
- 局部变量不作用于全局域。
kfir@goofy ~ $ local a bash: local: can only be used in a function
main()
- 有助于保持所有变量的局部性
- 直观的函数式编程
代码中唯一的全局命令是:main
main() { local files="/tmp/a /tmp/b" local i for i in $files do change_owner_of_file kfir users $i done } main
一切皆是函数
-
唯一全局性运行的代码是:
- 不可变的全局变量声明
- main()函数
- 保持代码整洁
- 过程变得清晰
main() { local files=$(ls /tmp | grep pid | grep -v daemon) }
temporary_files() { local dir=$1 ls $dir \ | grep pid \ | grep -v daemon } main() { local files=$(temporary_files /tmp) }
第二个例子好得多。查找文件是temporary_files()
的问题而非main()
的。这段代码用
temporary_files()
的单元测试也是可测试的。
如果你一定要尝试第一个例子,你会得到查找临时文件以和main算法的大杂烩。
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
标志运行程序:
bash -x my_prog.sh
只调试一小段代码,使用set -x
和set +x
,会只对被set -x
和set +x
包含的
当前代码打印调试信息。
temporary_files() { local dir=$1 set -x ls $dir \ | grep pid \ | grep -v daemon set +x }
打印函数名和它的参数:
temporary_files() { echo $FUNCNAME $@ local dir=$1 ls $dir \ | grep pid \ | grep -v daemon }
调用函数:
temporary_files /tmp
会打印到标准输出:
temporary_files /tmp
代码的清晰度
这段代码做了什么?
main() { local dir=/tmp [[ -z $dir ]] \ && do_something... [[ -n $dir ]] \ && do_something... [[ -f $dir ]] \ && do_something... [[ -d $dir ]] \ && do_something... } main
让你的代码说话:
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
每一行只做一件事
用反斜杠\
来作分隔符。例如:
temporary_files() { local dir=$1 ls $dir | grep pid | grep -v daemon }
可以写得简洁得多:
temporary_files() { local dir=$1 ls $dir \ | grep pid \ | grep -v daemon }
符号在缩进行的开始
符号在行末的坏例子:(译注:原文在此例中用了temporary_files()
代码段,疑似是
贴错了。结合上下文,应为print_dir_if_not_empty()
)
print_dir_if_not_empty() { local dir=$1 is_empty $dir && \ echo "dir is empty" || \ echo "dir=$dir" }
好的例子:我们可以清晰看到行和连接符号之间的联系。
print_dir_if_not_empty() { local dir=$1 is_empty $dir \ && echo "dir is empty" \ || echo "dir=$dir" }
打印用法
不要这样做:
echo "this prog does:..." echo "flags:" echo "-h print help"
它应该是个函数:
usage() { echo "this prog does:..." echo "flags:" echo "-h print help" }
echo在每一行重复。因此我们得到了这个文档:
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个空格,你可以用这个替换命令:
:s/^ /\t/
命令行参数
这里是一个例子,完成了上面usage函数的用法。我从 Kirk’s blog post – bash shell script to use getopts with gnu style long positional parameters 得到这段代码
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变量:
main() { cmdline $ARGS } main
单元测试
- 在更高级的语言中很重要。
- 使用shunit2做单元测试
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命令的例子:
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不允许 改变全局域函数。
简洁的bash编程技巧
1) 检查命令执行是否成功
第一种写法,比较常见:
echo abcdee | grep -q abcd if [ $? -eq 0 ]; then echo "Found" else echo "Not found" fi
简洁的写法:
if echo abcdee | grep -q abc; then echo "Found" else echo "Not found" fi
当然你也可以不要if/else,不过这样可读性比较差:
[Sun Nov 04 05:58 AM] [kodango@devops] ~/workspace $ echo abcdee | grep -q abc && echo "Found" || echo "Not found" Found
2) 将标准输出与标准错误输出重定向到/dev/null
第一种写法,比较常见:
grep "abc" test.txt 1>/dev/null 2>&1
常见的错误写法:
grep "abc" test.txt 2>&1 1>/dev/null
简洁的写法:
grep "abc" test.txt &> /dev/null
3) awk的使用
举一个实际的例子,获取Xen DomU的id。
常见的写法:
sudo xm li | grep vm_name | awk '{print $2}'
简洁的写法:
sudo xm li | awk '/vm_name/{print $2}'
4) 将一个文本的所有行用逗号连接起来
假设文件内容如下所示:
[Sat Nov 03 10:04 PM] [kodango@devops] ~/workspace $ cat /tmp/test.txt 1 2 3
使用Sed命令:
[Sat Nov 03 10:14 PM] [kodango@devops] ~/workspace $ sed ':a;$!N;s/\n/,/;ta' /tmp/test.txt 1,2,3
简洁的写法:
[Sat Nov 03 10:04 PM] [kodango@devops] ~/workspace $ paste -sd, /tmp/test.txt 1,2,3
5) 过滤重复行
假设文件内容如下所示:
[Sat Nov 03 10:16 PM] [kodango@devops] ~/workspace $ sort /tmp/test.txt 1 1 2 3
常用的方法:
[Sat Nov 03 10:16 PM] [kodango@devops] ~/workspace $ sort /tmp/test.txt | uniq 1 2 3
简单的写法:
[Sat Nov 03 10:16 PM] [kodango@devops] ~/workspace $ sort /tmp/test.txt -u 1 2 3
6) grep查找单词
假设一个文本的每一行是一个ip地址,例如
[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地址。常见的写法:
[Sat Nov 03 10:22 PM] [kodango@devops] ~/workspace $ grep '10.0.0.1\>' /tmp/ip.list 10.0.0.1
简单的方法(其实这方法不见得简单,只是为了说明-w
这个参数还是很有用的)
[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) 临时设置环境变量
常见的写法:
[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
简洁的写法:
[六 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) 参数的使用
假设只想使用$2
,
$3
这几个参数,常见的做法是:
shift echo "$@"
为什么不这样写呢?
echo "${@:2}"
9)退而求其次的写法
相信大家会有这种需求,当一个参数值没有提供时,可以使用默认值。常见的写法是:
arg=$1 if [ -z "$arg" ]; then arg=0 fi
简洁的写法是这样的:
arg=${1:-0}
10)bash特殊参数–的用法
假设要用grep查找字符串中是否包含-i
,我们会这样尝试:
[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
简洁的方法是:
[Sat Nov 03 10:45 PM] [kodango@devops] ~/workspace $ echo 'abc-i' | grep -- -i abc-i
bash中–
后面的参数不会被当作选项解析。
11)函数的返回值默认是最后一行语句的返回值
# 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格式化的结果赋值给变量
例如将数字转换成其十六进制形式,常见的写法是:
[Sat Nov 03 10:55 PM] [kodango@devops] ~/workspace $ var=$(printf '%%%02x' 111)
简单的写法是:
[Sat Nov 03 10:54 PM] [kodango@devops] ~/workspace $ printf -v var '%%%02x' 111
看看printf的help
[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)打印文件行
打印文件的第一行:
head -1 test.txt
打印文件的第2行:
sed -n '2p' test.txt
打印文件的第2到5行:
sed -n '2,5p' test.txt
打印文件的第2行始(包括第2行在内)5行的内容:
sed -n '2,+4p' test.txt
打印倒数第二行:
$ tail -2 test.txt | head -1 $ tac test.txt | sed -n '2p'
14)善用let或者(())命令做算术运算
如何对一个数字做++
运算,可能你会这样用:
a=1 a=`expr a + 1`
为何不用你熟悉的:
a=1 let a++ let a+=2
15)获取软连接指定的真实文件名
如果你不知道,你可能会这样获取:
[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码
[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)清空一个文件
常见的用法:
echo "" > test.txt
简单的写法:
> test.txt
18) 不要忘记有here document
下面一段代码:
grep -v 1 /tmp/test.txt | while read line; do let a++ echo --$line-- done echo a:$a
执行后有什么问题吗?
[Sun Nov 04 05:35 AM] [kodango@devops] ~/workspace $ sh test.sh --2-- --3-- a:
发现a这个变量没有被赋值,为什么呢?因为管道后面的代码是在在一个子shell中执行的 ,所做的任何更改都不会对当前shell有影响,自然a这个变量就不会有赋值了。
换一种思路,可以这样做:
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:
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)删除字符串中的第一个或者最后一个字符
假设字符串为:
[Sun Nov 04 10:21 AM] [kodango@devops] ~/workspace $ str="aremoveb"
可能你第一个想法是通过sed或者其它命令来完成这个功能,但是其实有很简单的方法:
[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个……
有没有一次性删除第一个和最后一个字符的方法呢?答案当然是肯定的:
[Sun Nov 04 10:26 AM] [kodango@devops] ~/workspace $ echo "${str:1:-1}" remove
关于这些变量替换的内容在bash的man手册中都有说明。
20)使用逗号join数组元素
假设数组元素没有空格,可以用这种方法:
[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命令来达到:
[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的
多线程:
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其实是给常用的命令定一个别名,比如很多人会定义一下的一个别名:
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版本的问题,
RHEL 5U4)。翻阅了下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' ...
23)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
附几则小技巧:
-
sudo iptables -L -n | vim -
-
grep -v xxx | vim -
-
echo $’\”
-
set — 1 2 3; echo "$@"
- 搜索stackoverflow/superuser等站点