Shell

Table of Contents

1 Shell

Shell俗称壳(用来区别于“核”),是用户和操作系统之间的接口。UNIX shell历史悠久,主要分支如图 1 所示。Bash是使用最为广泛的Shell,本文主要介绍Bash。

shell_history.gif

Figure 1: UNIX shell家族树

参考:
http://www.ibm.com/developerworks/aix/library/au-speakingunix_commandline/
http://en.wikipedia.org/wiki/Comparison_of_command_shells

2 Bash简介

Bash is the GNU Project's shell. Bash is the Bourne Again SHell. Bash is an sh-compatible shell that incorporates useful features from the Korn shell (ksh) and C shell (csh). It is intended to conform to the IEEE POSIX P1003.2/ISO 9945.2 Shell and Tools standard.

Advanced Bash-Scripting Guide: http://www.tldp.org/LDP/abs/html/index.html
Advanced Bash-Scripting Guide (Reference Cards): http://www.tldp.org/LDP/abs/html/refcards.html
Bash Reference Manual: http://www.gnu.org/software/bash/manual/bashref.html
Bash Reference Cards: http://www.tldp.org/LDP/abs/html/refcards.html
New Features in Bash 3: http://www.tldp.org/LDP/abs/html/bashver3.html
New Features in Bash 4: http://www.tldp.org/LDP/abs/html/bashver4.html

3 Parameter and Variable

A parameter is an entity that stores values. It can be a name, a number, or one of the special characters listed below. A variable is a parameter denoted by a name. A variable has a value and zero or more attributes. Attributes are assigned using the declare or typeset builtin command.

在bash中,术语Parameter是一个比术语Variable更通用的概念。$1, $!, $var1 等都是Parameter,其中 $1 是Positional Parameter, $! 是Special Parameter, $var1 才是Variable(变量)。

用下面形式可对变量赋值:

name=[value]

说明1:如果变量的值包含空格,应该把值放入引号中,如name="A B"或name='A B'。
说明2: 紧接等号前后的字符不能是空格! 如果是空格会怎么样呢?请看下面:

#  "VARIABLE =value"
#           ^
#% Script tries to run "VARIABLE" command with one argument, "=value".

#  "VARIABLE= value"
#            ^
#% Script tries to run "value" command with
#+ the environmental variable "VARIABLE" set to "".

要引用一个变量,在变量名前加个符号 $ 即可,如 $name ,变量名也可以放入大括号中,如 ${name} ,两者一样。

3.1 Bash采用动态作用域

Dynamic scoping means that variable lookups occur in the scope where a function is called, not where it is defined.

#!/bin/bash

#### bash动态作用域测试
value=1
function foo () {
    echo $value;
}
function bar () {
    local value=2;
    foo;
}

bar         # 由于bash采用动态作用域,这里会输出2,而不是1。

3.2 双引号和单引号

双引号中的变量引用会被替换为变量的值。单引号中的内容都是字面量。

不用引号时,变量中的连续多个空格仅会保留一个。

#!/usr/bin/env bash

var1='A B   C'
echo "$var1"     # output: A B   C
echo '$var1'     # output: $var1
echo $var1       # output: A B C

3.3 ANSI-C Quoting and Locale-Specific Translation

Bash中 $'string' (和转义相关)和 $"string" (和i18n相关)有特殊含义。

其中,$'string' 称为ANSI-C Quoting,实例:

~$ echo 'ab\ncd'
ab\ncd
~$ echo "ab\ncd"
ab\ncd
$ echo $'ab\ncd'
ab
cd

参考:
http://wiki.bash-hackers.org/syntax/quoting
https://www.gnu.org/software/bash/manual/bashref.html#ANSI_002dC-Quoting

3.4 用declare/typeset声明变量属性

declare和typeset作用相同,都用来声明变量的属性。typeset来自ksh,要兼容ksh应该使用typeset.

#!/usr/bin/env bash
var1=6/3
declare -i var2
var2=6/3

echo "var1 = $var1"       # output: var1 = 6/3
echo "var2 = $var2"       # output: var2 = 2

参考:
http://www.tldp.org/LDP/abs/html/declareref.html

3.5 Positional and Special Parameter

Table 1: Positional and Special Parameter in Bash
Parameter Meaning
$0 Filename of script
$1 - $9 Positional parameters #1 - #9
${10} Positional parameter #10
$# Number of positional parameters
"$*" All the positional parameters (as a single word)
"$@" All the positional parameters (as separate strings)
${#*} Number of positional parameters
${#@} Number of positional parameters
$? Return value
$$ Process ID (PID) of script
$- Flags passed to script (using set)
$_ Last argument of previous command
$! Process ID (PID) of last job run in background

3.5.1 $@和$*的区别

当执行下面命令时, $ ./my.sh p1 "p2 p3" p4
在脚本my.sh中,$@和$*是相同的!都会得到p1 p2 p3 p4

而如果放在双引号里,即使用soft quote,则不同,如上面例子:
"$@"会得到"p1" "p2 p3" "p4"这三个的词;
"$*"会得到"p1 p2 p3 p4"这一个词。

下面是测试例子:

#!/bin/bash

myecho()
{
    echo "$1,$2,$3,$4"
}

fun1()
{
    myecho $@          # output: p1,p2,p3,p4
    myecho $*          # output: p1,p2,p3,p4
    myecho "$@"        # output: p1,p2   p3,p4,
                       #           ^       ^  ^
    myecho "$*"        # output: p1 p2   p3 p4,,,
                       #                      ^^^
    echo $#
    echo ${#@}
    echo ${#*}
}

fun1 p1 "p2   p3" p4

注:$@和$*的行为会受到IFS的影响。

参考:
http://wiki.bash-hackers.org/scripting/posparams
http://www.tldp.org/LDP/abs/html/internalvariables.html

4 Loops and Branches

4.1 Loop (while)

while test-commands; do
  consequent-commands
done

只要test-commands返回零就执行consequent-commands。
其返回值是命令块中最后一个被执行的命令的返回值。如果命令块没有被执行则返回零。

4.2 Loop (until)

until test-commands; do
  consequent-commands
done

只要test-commands返回非零值就执行consequent-commands。
其返回值是命令块中最后一个被执行的命令的返回值。如果命令块没有被执行则返回零。

4.3 Loop (for)

# style 1
for arg [in list]; do
  commands
done

# style 2 (like C-style)
for (( expr1 ; expr2 ; expr3 )) ; do
  commands
done

第1种for循环形式,list中的每个元素都会赋值给arg并执行一次命令块。省略in list时相当于in "$@"。
其返回值是命令块中最后一个被执行的命令的返回值。如果对list的扩展没有得到任何元素,则不执行任何命令,并返回零。

第2种for循环形式,对算术表达式expr1进行求值,然后不断的对算术表达式expr2进行求值,直到其结果为零。每次求值时,如果expr2的值不是零,则执行一次命令块,并且计算算术表达式exp3的值。
其返回值是命令块中最后一个被执行的命令的返回值。如果表达式的值都是假的,则返回假。

#!/bin/bash

fun1()
{
#   for循环中省略了in list,相当于
#   for var in "$@"
    for var
    do
        echo -n "$var "
    done
}

fun1 a b c d   # output: "a b c d "

4.4 Branch (if)

if test-commands; then
  consequent-commands;
[elif more-test-commands; then
  more-consequents;]
[else alternate-consequents;]
fi

4.5 Branch (case)

case word in
  [ [(] pattern [ | pattern ] ... ) list ;; ] ...
esac

pattern左侧的小括号可省略,多个pattern可用|分隔。 如:

case word in
 ( pattern1 )
   commands
 ;;
 pattern2 | pattern3 )
   commands
 ;;
esac

4.5.1 Bash 4.0中case新功能

在Bash 4.0中增强了case语句,引入了两个新的分支结束符;;&和;&

;;&
当这个pattern满足后,执行对应的command-list后就不会退出case,接着测试下面的分支。
如:

case $a in
1) echo " 1 " ;;&
2) echo " 2 " ;;
3) echo " 3 " ;;
1 | 2 | 4) echo " 1 or 2 or 4 " ;;
5) echo " 5 " ;;
esac

当a为1时,会输出第1行和第4行。

;&
当这个pattern满足后,执行对应的command-list后,接着执行(不进行测试)下一个分支的command-list。
如:

case $a in
1) echo " 1 " ;&
2) echo " 2 " ;;
3) echo " 3 " ;;
1 | 2 | 4) echo " 1 or 2 or 4 " ;;
5) echo " 5 " ;;
esac

当a为1时,会输出第1行和第2行。

相对来说;;&比较实用,而;&用处不大。

4.6 Branch (select)

select来自于ksh,用来生成选择菜单,提示用户选择指定的选项。 语法几乎和for一样。
select直到遇到break语句才结束。
select使用PS3作为提示符。

#!/bin/bash

PS3='Choose your favorite vegetable: ' # Sets the prompt string.
                                       # Otherwise it defaults to #? .

select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
do
  echo
  echo "Your favorite veggie is $vegetable."
  break  # What happens if there is no 'break' here?
done

执行上面脚本时,会把select中in后的选项自动编号,提示用户选择。如:

$ bash test.sh
1) beans
2) carrots
3) potatoes
4) onions
5) rutabagas
Choose your favorite vegetable: 2

Your favorite veggie is carrots.

当然不用select,用read和case也能实现类似的功能,如:

!#/bin/bash
echo "select the operation:"
echo "1) operation 1"
echo "2) operation 2"
echo "3) operation 3"

read n
case $n in
    1) commands for operation 1;;
    2) commands for operation 2;;
    3) commands for operation 3;;
    *) invalid option;;
esac

4.7 break和continue

break和continue可用在for, while, until和select语句中。
The break command terminates the loop (breaks out of it), while continue causes a jump to the next iteration of the loop, skipping all the remaining commands in that particular loop cycle.

break和continue后面都可以跟一个数字,表示要操作几层循环。
如,不跟数字时表示操作最里层循环:

#!/bin/bash
# break-levels.sh: Breaking out of loops.

# "break N" breaks out of N level loops.

for outerloop in 1 2 3 4 5
do
  echo -n "Group $outerloop:   "

  # --------------------------------------------------------
  for innerloop in 1 2 3 4 5
  do
    echo -n "$innerloop "

    if [ "$innerloop" -eq 3 ]
    then
      break  # Try   break 2   to see what happens.
             # ("Breaks" out of both inner and outer loops.)
    fi
  done
  # --------------------------------------------------------

  echo
done

上面例子会输出:

Group 1:   1 2 3
Group 2:   1 2 3
Group 3:   1 2 3
Group 4:   1 2 3
Group 5:   1 2 3

如果把前面例子中的break换成 break 2 ,则break会对2层循环(内层循环和外层循环)都有效,前面例子会输出:

Group 1:   1 2 3

参考:
http://tldp.org/LDP/abs/html/loopcontrol.html

5 Test

5.1 AND(&&) and OR(||)

&&|| 分别表示“和”和“或”。

如果使用 [[ ,则 &&|| 可以在括号内或者括号外。

if [[ expression ]] && [[ expression ]] || [[ expression ]] ; then

They can also be used within a single [[ ]]:
if [[ expression && expression || expression ]] ; then

And, finally, you can group them to ensure order of evaluation:
if [[ expression && ( expression || expression ) ]] ; then

如果使用 [ ,则 &&|| 只能在括号外,如果放在括号内则报语法错误。

if [ -n "$var" ] && [ -e "$var" ]; then
   echo "\$var is not null and a file named $var exists!"
fi
$ if [ -n "/tmp" && -d "/tmp"]; then echo true; fi # Does not work
-bash: syntax error near unexpected token `then'

5.2 test和[和[[的区别

[和test是一样的,仅在用法的形式上有区别。它们都是POSIX要求的。

if test -z "$a"; then
    echo "test."
fi

## 相当于:
if [ -z "$a" ]; then
    echo "test."
fi

[[的功能更加强大,Bash,Korn shell中可用。
比如,[[可支持把逻辑操作符||, &&等放入在[[]]中间,而[不行。

参考:http://mywiki.wooledge.org/BashFAQ/031

5.3 [[应用

5.3.1 实例:测试字符串的开头或结束是否为某字符串

用[[可以方便测试字符串的开头或结束是否为某字符串。如下面例子可测试变量a是否以字符z开头:

[[ $a == z* ]]         # True if $a starts with an "z" (wildcard matching).

注:上面测试中,不要使用双引号包围z*,如果使用了双引号包围z*,则其含义是不同的:

[[ $a == "z*" ]]       # True if $a is equal to z* (literal matching).

5.3.2 实例:用正则表达式测试数字

正则匹配符 =~ 是在bash v3中引入的操作符。

注意:正则表达式不要用单引号包围,在bash v3中没有问题,但在bash v4中不会工作。
如测试数字时,不要这样使用(仅在bash v3中工作):

if [[ $line =~ '^[0-9]+$' ]]; ...

应该这样使用:

if [[ $line =~ ^[0-9]+$ ]]; ...

参考:http://stackoverflow.com/questions/218156/bash-regex-with-quotes

6 Shell Expansion

6.1 Brace Expansion

大括号里用逗号分隔的各个单词会被展开,如: cp file{,.bk} 相当于 cp file file.bk

6.2 Parameter Expansion

Table 2: Parameter Expansion
参数展开形式 str为unset时 str为null时 str为non-null时 备注
var=${str:-expr} var=expr var=expr var=$str expr可看作“默认值”
var=${str-expr} var=expr var= var=$str 同上
var=${str:=expr} str=expr; var=expr str=expr; var=expr str不变; var=$str expr可看作“默认值”
var=${str=expr} str=expr; var=expr str不变; var= str不变; var=$str 同上
var=${str:+expr} var= var= var=expr expr可看作“其他值”
var=${str+expr} var= var=expr var=expr 同上
var=${str:?expr} expr输出到stderr expr输出到stderr var=$str expr可看作"error message"
var=${str?expr} expr输出到stderr var= var=$str 同上

注1:一般情况下str的值不会被修改,仅当使用=时,才有可能修改str的值。
注2:一般情况下会使用对应的带冒号形式。 使用冒号形式时,当str为unset或null时,其行为是相同的。
注3:使用不带冒号的形式时,只检测str是不是设置过。str为null或non-null的行为一样。

6.3 Substring Extraction

${string:position}
Extracts substring from $string at $position.

If the $string parameter is "*" or "@", then this extracts the positional parameters, starting at $position.

${string:position:length}
Extracts $length characters of substring from $string at $position.

If the $string parameter is "*" or "@", then this extracts a maximum of $length positional parameters, starting at $position.

#!/bin/bash

stringZ=abcABC123ABCabc
#       0123456789.....
#       0-based indexing.

echo ${stringZ:0}                            # abcABC123ABCabc
echo ${stringZ:1}                            # bcABC123ABCabc
echo ${stringZ:7}                            # 23ABCabc

echo ${stringZ:7:3}                          # 23A

# Is it possible to index from the right end of the string?

echo ${stringZ:-4}                           # abcABC123ABCabc
# Defaults to full string, as in ${parameter:-default}.
# However . . .

echo ${stringZ:(-4)}                         # Cabc
echo ${stringZ: -4}                          # Cabc
# Now, it works.
# Parentheses or added space "escape" the position parameter.

# Thank you, Dan Jacobson, for pointing this out.

用GNU coreutils中的工具 expr 也能实现类似的功能。
注意:bash的Substring Expansion中第1个元素下标为0,而expr substr的第1个元素下标为1。

#!/bin/bash

stringZ=abcABC123ABCabc
#       123456789......
#       1-based indexing.

echo `expr substr $stringZ 1 2`              # ab
echo `expr substr $stringZ 4 3`              # ABC

参考:
http://www.tldp.org/LDP/abs/html/string-manipulation.html#AWKSTRINGMANIP
http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion

6.4 Substring Removal

Table 3: Substring Removal
参数形式 说明
${str#word} 若$str开头位置的数据匹配word,则删除最短的匹配数据
${str##word} 若$str开头位置的数据匹配word,则删除最长的匹配数据
${str%word} 若$str结尾位置的数据匹配word,则删除最短的匹配数据
${str%%word} 若$str结尾位置的数据匹配word,则删除最长的匹配数据

注1:word中支持'*' (Matches any string), '?' (Matches any single character), '[…]' (Matches any one of the enclosed characters)。
注2:#从前往后删;%从后往前删;两个叠加就是匹配“最长”。

#!/bin/bash
stringZ=abcABC123ABCabc
#       |----|          shortest
#       |----------|    longest

echo ${stringZ#a*C}      # 123ABCabc
# Strip out shortest match between 'a' and 'C'.

echo ${stringZ##a*C}     # abc
# Strip out longest match between 'a' and 'C'.

# You can parameterize the substrings.
X='a*C'

echo ${stringZ#$X}      # 123ABCabc
echo ${stringZ##$X}     # abc
                        # As above.

6.4.1 实例:检测字符串是否包含其个子字符串

下面函数可以检测字符串是否包含其个子字符串:

contains() {
    string="$1"
    substring="$2"
    if test "${string#*$substring}" != "$string"
    then
        return 0    # $substring is in $string
    else
        return 1    # $substring is not in $string
    fi
}

contains "abcd" "e" || echo "abcd does not contain e"
contains "abcd" "ab" && echo "abcd contains ab"
contains "abcd" "bc" && echo "abcd contains bc"
contains "abcd" "cd" && echo "abcd contains cd"
contains "abcd" "abcd" && echo "abcd contains abcd"
contains "" "" && echo "empty string contains empty string"
contains "a" "" && echo "a contains empty string"
contains "" "a" || echo "empty string does not contain a"
contains "abcd efgh" "cd ef" && echo "abcd efgh contains cd ef"
contains "abcd efgh" " " && echo "abcd efgh contains a space"

参考:http://stackoverflow.com/questions/2829613/how-do-you-tell-if-a-string-contains-another-string-in-unix-shell-scripting

6.4.2 实例:删除字符串第一个(或最后一个)字符

#!/bin/bash
string=abcABC

# 删除第一个字符
echo ${string#?}    # bcABC

# 删除最后一个字符
echo ${string%?}    # abcAB

6.5 Substring Replacement

Table 4: Substring Replacement
参数形式 说明
${string/substring/replacement} Replace first match of $substring with $replacement.
${string//substring/replacement} Replace all matches of $substring with $replacement.
${string/#substring/replacement} If $substring matches front end of $string, substitute $replacement for $substring.
${string/%substring/replacement} If $substring matches back end of $string, substitute $replacement for $substring.

6.5.1 实例:批量删除文件名指定后缀

假设当前文件夹很多文件有bak后缀,如1.c.bak,2.c.bak。如何批量去掉.bak后缀,把它们变为1.c,2.c呢?

用shell的变量替换容易实现这个目的。如:

$ for i in `ls *.bak`; do mv $i ${i%.*}; done;

说明:
如果当前文件夹下在的文件名含特殊字符(如*,-等),那么上面命令会失败!比较保险的方法是:

$ for i in *.bak; do [ -e "$i" ] && mv -- $i ${i%.*}; done;

为什么这样写,请参考:http://mywiki.wooledge.org/BashPitfalls

7 Function

Bash中函数有两种写法:

## Format 1
function func_name [()] {
command...
} [ redirections ]

## Format 2
func_name () {
command...
} [ redirections ]

推荐使用第2种形式,其兼容性更好。参见:http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_05

函数定义时不用声明参数,在函数体内可通过 $1, $2 等获取参数。
调用函数时,用 func_name arg1 arg2 即可。

7.1 return

函数体内可以使用 return 语句返回,如果省略return语句,那么函数中最后一个命令的退出码将作为函数的返回码。

return 语句后面只能是0到255的整数(不能是字符串等其他内容)。

fun1() {
    # something
    return 2
}

fun1
retval=$?

echo $retval    # 2

return不一定用在函数体内,可也以用在sourced script中。其帮助文档如下:

$ help return
return: return [n]
    Return from a shell function.

    Causes a function or sourced script to exit with the return value
    specified by N.  If N is omitted, the return status is that of the
    last command executed within the function or script.

    Exit Status:
    Returns N, or failure if the shell is not executing a function or script.

7.2 Recursion and local variables

man bash 的文档中,有这样的描述:
When local is used within a function, it causes the variable name to have a visible scope restricted to that function and its children.

#!/bin/bash

recurse1()
{
    local x=$1
    echo "rec1 hit1" $x
    if [ $1 -lt 3 ]; then
        recurse1 $(($1 + 1))
    fi
    echo "rec1 hit2" $x
    # 变量x是local的,递归调用时下层的recurse1不会修改它。
}

recurse2()
{
    x=$1
    echo "rec2 hit1" $x
    if [ $1 -lt 3 ]; then
        recurse2 $(($1 + 1))
    fi
    echo "rec2 hit2" $x
    # 变量x没有声明为local,递归调用时被下层的recurse2修改了。
    # 在这之后,引用x时应该注意它已经不是之前的值了。
}

recurse1 1
echo '---------------'

recurse2 1
echo '---------------'

#### Output:
## rec1 hit1 1
## rec1 hit1 2
## rec1 hit1 3
## rec1 hit2 3
## rec1 hit2 2
## rec1 hit2 1
## ---------------
## rec2 hit1 1
## rec2 hit1 2
## rec2 hit1 3
## rec2 hit2 3
## rec2 hit2 3
## rec2 hit2 3
## ---------------

说明:使用 typeset 也可以在函数体内定义局部变量,但这种写法已经是过时的。

参考:
http://www.tldp.org/LDP/abs/html/localvar.html

7.3 Nested function

bash中允许定义嵌套函数,它的用处不大。

#!/bin/bash

f1 ()
{

  f2 () # nested
  {
    echo "Function \"f2\", inside \"f1\"."
  }

}

f2  #  Gives an error message.
    #  Even a preceding "declare -f f2" wouldn't help.

echo

f1  #  Does nothing, since calling "f1" does not automatically call "f2".
f2  #  Now, it's all right to call "f2",
    #+ since its definition has been made visible by calling "f1".

    # Thanks, S.C.

参考:http://www.tldp.org/LDP/abs/html/functions.html

7.4 函数如何传递“指针”

在bash中定义了一个变量,如何通过调用一个函数来修改这个变量?如果在C语言中,则容易实现,把变量的地址传给函数能实现。

在bash中,可以通过eval实现。

#!/bin/bash
fun1() {
eval $1="bb"
}

var1="aa"
echo ${var1}
fun1 var1
echo ${var1}

执行上面脚本,会输出:

aa
bb

8 I/O重定向

默认地,stdin对应文件描述符为0,stdout对应文件描述符为1,stderr对应文件描述符为2。

M>N
    # Redirect file descriptor "M" to file "N".
    # "M" is a file descriptor, which defaults to 1, if not explicitly set.
    # "N" is a filename.

M>&N
    # Redirect file descriptor "M" to another file descriptor "N".
    # "M" is a file descriptor, which defaults to 1, if not set.
    # "N" is another file descriptor.

[j]<>filename
      #  Open file "filename" for reading and writing,
      #+ and assign file descriptor "j" to it.
      #  If "filename" does not exist, create it.
      #  If file descriptor "j" is not specified, default to fd 0, stdin.

n<&-
Close input file descriptor n. If n is 0 (stdin), 0 can be omitted.

n>&-
Close output file descriptor n. If n is 1 (stdout), 1 can be omitted.

8.1 文件描述符实例

#!/bin/bash

echo 1234567890 > File    # Write string to "File".
exec 3<> File             # Open "File" and assign fd 3 to it.
read -n 4 <&3             # Read only 4 characters.
echo -n . >&3             # Write a decimal point there.
exec 3>&-                 # Close fd 3.
cat File                  # ==> 1234.67890

参考:http://www.tldp.org/LDP/abs/html/io-redirection.html

8.2 stdout和stderr重定向到另一文件

方法1:

command >file1 2>&1

说明:不要写command 2>&1 >file1,它仅相当于command >file1,因为2>&1执行时stdout指向的是屏幕。

方法2:

command  &>file1

方法3:

command  >&file1

注:尽量不要用方法3的方式,它在形式上看起来很奇怪,因为只有用fd表示文件时才在文件前加符号&。

8.3 stdout和stderr以追加方式重定向到另一文件

要把stdout和stderr以追加方式重定向到另一文件,可以使用下面两种方式中的一种:

command &>> file2          # 方式一
command >>file2 2>&1       # 方法二

9 Here document

A here document is a special-purpose code block. It uses a form of I/O redirection to feed a command list to an interactive program or a command, such as ftp, cat, or the ex text editor.

COMMAND <<EOF
...
...
EOF

其中,EOF可以是其它字符。

9.1 忽略tabs

在EOF前加一个连字符,表示忽略here document中开头的tabs(空格会保留)

cat <<-EOF
        "leading tab(s) would be removed."
EOF

9.2 禁止变量解释

把EOF用单引号括起来,表示不解释here documents中的变量。

#!/bin/bash
a=test

cat <<EOF
echo ${a}
EOF

cat <<EOF
echo \${a}
EOF

cat <<'EOF'
echo ${a}
EOF

执行上面脚本会输出:

echo test
echo ${a}
echo ${a}

9.3 Here Strings (<<<)

A here string can be considered as a stripped-down form of a here document.
It consists of nothing more than COMMAND <<< $WORD, where $WORD is expanded and fed to the stdin of COMMAND.

如:

$ sed 's/a/b/g' <<< "aaa"
bbb

又如:

$ read first second <<< "hello world"
$ echo $second $first
world hello

参考:http://www.tldp.org/LDP/abs/html/x17837.html

10 Process Substitution (>(cmd) or <(cmd))

Process substitution feeds the output of a process (or processes) into the stdin of another process.

进程替换的功能是把一个进程的stdout回馈给另一个进程(即放入到其stdin中)。这和管道类似,但管道无法方便地同时处理多个命令的输出,而进程替换则可以。

进程替换语法:

>(command)
<(command)

进程替换实例1:

diff <(command1) <(command2)           # 查看两个命令输出的不同,(你不用把命令输出重定向到文件,然后比较文件了)

进程替换实例2:

$ sort -k 9 <(ls /bin) <(ls /usr/bin) <(ls /usr/X11R6/bin)   #列出系统中3个'bin'目录的所有文件,且按文件名排序
2to3
2to3-
2to3-2.7
2to32.6
BuildStrings
CpMac
DeRez
GetFileInfo
MergePef
MvMac
R
......
zless
zmore
znew
zprint
zsh

11 Bash内置命令

11.1 查看内置命令的帮助

man bash 中有所有内置命令的说明,但内容太多,不易快速找到想要的内容。
要精确找到内置命令的帮助文档,可用help命令,如:
help test
help ulimit

11.2 . (a period)

$ . filename [arguments]~
$ source filename [arguments]~        # same as above

Read and execute commands from the filename argument in the current shell context. This builtin is equivalent to source.

11.3 set

set有两个作用。一是用来打开或关闭bash的选项。二是用来设置位置参数。

用set打开或关闭bash的选项本文其它部分已经介绍,这里仅介绍它不常使用的设置位置参数的功能。

#!/bin/bash
fun1() {
set a b c d
echo $1 $2 $3 $4
}

fun1

上面例子中,不管给fun1传递什么参数,都会输出a b c d,因为set重置了它的位置参数。

11.4 exec

exec的语法格式为:
exec [-cl] [-a name] [file [redirection …]]
exec用file代替当前进程,并执行;如果file没有指定,则可以通过重定向来影响当前的shell环境。

即exec有两个功能。 一是在当前进程中执行命令。二是修改当前shell的文件描述符。

11.4.1 代替当前进程并执行

如:

#!/bin/bash
# self-exec.sh

# Note: Set permissions on this script to 555 or 755,
#       then call it with ./self-exec.sh or sh ./self-exec.sh.

echo

echo "This line appears ONCE in the script, yet it keeps echoing."
echo "The PID of this instance of the script is still $$."
#     Demonstrates that a subshell is not forked off.

echo "==================== Hit Ctl-C to exit ===================="

sleep 1

exec $0   #  Spawns another instance of this same script
          #+ that replaces the previous one.

echo "This line will never echo!"  # Why not?

注:上例中需要用Ctrl+C中止。上例中输出的进程号不会变,因为exec总是在当前进程中执行。

11.4.2 修改当前shell的文件描述符

如:

#!/bin/bash
# Redirecting stdin using 'exec'.


exec 6<&0          # Link file descriptor #6 with stdin.
                   # Saves stdin.

exec < data-file   # stdin replaced by file "data-file"

read a1            # Reads first line of file "data-file".
read a2            # Reads second line of file "data-file."

echo
echo "Following lines read from file."
echo "-------------------------------"
echo $a1
echo $a2

echo; echo; echo

exec 0<&6 6<&-
#  Now restore stdin from fd #6, where it had been saved,
#+ and close fd #6 ( 6<&- ) to free it for other processes to use.
#
# <&6 6<&-    also works.

echo -n "Enter data  "
read b1  # Now "read" functions as expected, reading from normal stdin.
echo "Input read from stdin."
echo "----------------------"
echo "b1 = $b1"

echo

exit 0

参考:http://www.tldp.org/LDP/abs/html/x17974.html

11.5 trap

trap语法:
trap [COMMANDS] [SIGNALS]
可以捕捉信号,并执行指定的动作。

被捕捉的SIGNALS,可以通过kill -l查看,即可直接写数字,也可以写名字。
如,下面两个语句的作用是一样的:

trap '' 1
trap '' SIGHUP

除了kill -l列出的信号外,还可以捕捉EXIT(0), DEBUG, ERR

$ cat 1.sh
trap "echo Goodbye" EXIT
echo "hello"

$ bash 1.sh
hello
Goodbye

http://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html

11.5.1 trap中使用单引号

实例:

trap "echo Error on line $LINENO" ERR ##trap 'echo Error on line $LINENO' ERR
echo hello |grep 'abc'

在上面例子中,grep找不到字符串abc,所以会返回非0。
我们期待脚本显示Error on line 2,但实际却显示的是Error on line 1,原因是在执行trap时,$LINENO已经被展开,要想得到期待的结果,可以把trap中的双引号改为单引号。

http://unix.stackexchange.com/questions/39623/trap-err-and-echoing-the-error-line

11.6 read

11.6.1 实例:一行一行地读入文本文本

如何一行一行地读入文本文本?

#!/bin/bash

## 下面是错误的用法!!!
while read line; do
    echo "Text read $line"
done < filename

注:上面的代码有下面问题:
1、行首的空格被删除了。
2、最后一行行尾如果没有换行符,则最后一行可能读不出。

正确的写法为:

#!/bin/bash
while IFS='' read -r line || [[ -n $line ]]; do
    echo "Text read $line"
done < "$1"

参考:http://stackoverflow.com/questions/10929453/bash-scripting-read-file-line-by-line

11.6.2 实例:打印每行指定列

假设有个文本如下:

I MM 933
A QQ 33
X EE 21

如何打印每行第3列?

while read first second last; do
    echo $last
done <file

11.7 处理命令行参数

11.7.1 shift

shift [N]
The positional parameters from $N+1 … are renamed to $1 … If N is not given, it is assumed to be 1.

#!/bin/bash

fun1() {
    echo $1
    echo $2
    echo $3
    echo $4
}

fun2() {
    echo $1
    echo $2

    shift 2     #把位置参数全部前移了两个!如之前的$3变成为$1。

    echo $1
    echo $2
}

fun1 1 2 3 4    #output 1 2 3 4
fun2 1 2 3 4    #output 1 2 3 4

如:假设某个脚本能接收-a, -b, -c为选项,且-b后能带自己的参数。
下面的例子能处理这个脚本的参数。

while [[ $1 == -* ]]; do
    case $1 in
        -a ) process option -a ;;
        -b ) process option -b
             $2 is the option’s argument
             shift ;;
        -c ) process option -c ;;
        * ) print 'usage: this-script [-a] [-b barg] [-c] args ...'
            exit 1 ;;
    esac
    shift   #这样,每次while中就可以仅测试$1了。
done
normal processing of arguments ...

11.7.2 getopts

getopts是bash(或ksh等)内置的非常强大的参数处理工具。用它可轻易地实现一些复杂的功能,如-ac和-a -c仅是两种不同的写法。
其语法格式为: getopts optstring name [arg]
optstring中如果某个字母后带冒号表示这个字母是带有参数的选项。
optstring中如果第一个字母是冒号表示采用“安静的错误报告”方式。

下面例子和前面例子实现相似的功能。

while getopts ":ab:c" opt; do
    case $opt in
        a ) process option -a ;;
        b ) process option -b
            $OPTARG is the option’s argument
        c ) process option -c ;;
        \? ) print 'usage: this-script [-a] [-b barg] [-c] args ...'
            exit 1 ;;
    esac
done
shift $(($OPTIND - 1))
normal processing of arguments ...

getopts stores in the variable OPTIND the number of the next argument to be processed. 所以 shift $(($OPTIND - 1)) 的作用相当于把剩下的位置参数设置为从$1开始。

说明:getopt和getopts的区别
getopt是独立的可执行程序;getopts是bash(或ksh等)内置的命令,用来代替getopt。

12 Bash的选项

12.1 如何打开一个选项

方法1:set -o option-name
例如:
#!/bin/bash
set -o verbose #设置这个选项,每个命令在执行前会先显示出来。

方法2:set -option-abbrev
直接用option的缩写,比较简单,例如:
#!/bin/bash
set -v #和前面的作用一样。

方法3:运行脚本时指定(注意:它又有好几种形式)
bash -v script-file
bash -o verbose script-file
bash –verbose script-file

方法4:在脚本第一行#!头中指定
#!/bin/bash -v

注意:要使方法4有效,必须给脚本赋予可执行权限后直接执行,如 $ ./1.sh ,而不能把脚本做为bash参数执行,如 $ bash 1.sh

http://www.tldp.org/LDP/abs/html/options.html

12.2 查看和关闭选项

查看选项,用 set -o 即可。

要关闭选项,把选项前面的-换成+即可,如关闭emacs风格的行内编辑器:

$ set +o emacs

12.3 shopt设置选项

bash中还有很多选项可通过命令shopt来控制。

查看所有可用shopt设置的选项的当前状态:

$ shopt

打开选项(以extglob为例):

$ shopt -s extglob

关闭选项(以extglob为例):

$ shopt -u extglob

详细列表参见:
http://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html

12.3.1 extglob (扩展的模式匹配操作符)

If the extglob shell option is enabled using the shopt builtin, several extended pattern matching operators are recognized. In the following description, a pattern-list is a list of one or more patterns separated by a ‘|’. Composite patterns may be formed using one or more of the following sub-patterns:

Table 5: Bash extglob
Extended Globbing Meaning
?(pattern-list) Matches zero or one occurrence of the given patterns
*(pattern-list) Matches zero or more occurrences of the given patterns
+(pattern-list) Matches one or more occurrences of the given patterns
@(pattern-list) Matches one of the given patterns
!(pattern-list) Matches anything except one of the given patterns

实例1:
列出以“ab”或者“xyz”开头的jpg或者png文件:

$ ls +(ab|xyz)*+(.jpg|.png)               # 使用 extglob
$ ls ab*.jpg ab*.png xyz*.jpg xyz*.png    # 和上相同,没有使用 extglob

实例2:

#!/bin/bash

shopt -s extglob
var1="abc";
var2="def|abc|123";

if [[ $var1 = @($var2) ]]; then
    echo "1";
else
    echo "2";
fi

上面脚本运行会输出2,因为变量var2中“包含了”变量var1。

12.3.1.1 实例:删除特定文件

假设当前文件夹中有下面文件:

$ ls
file1.bak file1.log file1.pdf file1.tmp file1.txt file2.log file2.txt

我们想删除“file1.*”,但要保留file1.txt和file1.pdf。用下面的extglob即可实现:

$ rm file1.!(txt|pdf)
$ ls
file1.pdf file1.txt file2.log file2.txt

13 Useful Examples

13.1 获取脚本所在目录的全路径

下面代码片断可获取脚本所在目录的全路径:

SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"

参考:https://stackoverflow.com/questions/4774054/reliable-way-for-a-bash-script-to-get-the-full-path-to-itself

13.2 从1到100,循环100次

方法1:用c语言风格的for循环

for ((i=1; i<=100; i ++)); do
    echo $i
done

方法2:用Extended Brace expansion,它是bash 3中引入的新功能

for i in {1..100}; do
    echo $i
done

方法3:用seq命令(推荐使用,可移植性好)

for i in `seq 1 100`; do
    echo $i
done

13.3 分析CSV格式文件

在while循环中用read命令可以分析CSV格式文件。
说明:通过IFS可以指定CSV文件的分隔符,如果指定IFS在read命令前(如下面例子所示),则IFS只会影响read命令,不会影响到后面的代码。

#! /bin/bash

while IFS=: read user pass uid gid full home shell
do
    echo -e "$full :\n\
 Pseudo : $user\n\
 UID :\t $uid\n\
 GID :\t $gid\n\
 Home :\t $home\n\
 Shell :\t $shell\n\n"
done < /etc/passwd

参考:
http://stackoverflow.com/questions/10929453/read-a-file-line-by-line-assigning-the-value-to-a-variable
http://ccm.net/faq/1757-how-to-read-a-linux-file-line-by-line
http://unix.stackexchange.com/questions/18922/in-while-ifs-read-why-does-ifs-have-no-effect

13.4 删除除某些文件外的所有文件

如何删除当前目录中除 *.iso 和 *.zip 外的所有文件?

方法1:

$ shopt -s extglob     # 确认开启 extglob 选项
$ rm !(*.iso|*.zip)

注意:如果没有开启(在shopt -s输出中找不到extglob),则可用shopt -s extglob开启extglob选项!

方法2:

$ export GLOBIGNORE=*.zip:*.iso
$ rm *
$ unset GLOBIGNORE

方法3 (这个方法比较通用,zsh等中也能使用):

$ find . -type f -not \( -name '*.zip' -or -name '*.iso' \) -delete

参考:http://www.linuxeden.com/html/softuse/20140606/152379.html

13.5 求字符串长度

在bash中求字符串长度有如下方法:
${#string}
`expr length $string`
`expr "$string" : '.*'`

#!/bin/bash
stringZ=abcABC123ABCabc

echo ${#stringZ}                 # 15
echo `expr length $stringZ`      # 15
echo `expr "$stringZ" : '.*'`    # 15

13.6 算术运算

有多种方式可以进行算术运算。

方法1:利用 expr ,如

$ expr 3 + 4         # 算术运算操作符(如加号)前后要有空格
7
$ expr 3 \* 4        # 乘法要用转义符
12
$ expr 4 / 2
2

方法2:利用 $(()) ,如:

$ echo $((3+4))
7
$ echo $((3*4))
12

方法3:利用 $[] (这种方法已经deprecated,不推荐使用),如:

$ echo $[3+4]
7
$ echo $[3*4]
12

方法4:利用 let ,如:

$ let a=3+4; echo $a
7
$ let a=3*4; echo $a
12

方法5:利用外部程序 bc ,如:

$ echo "scale=2; 15/4" | bc     # 要使bc进行浮点运算,用15/4.0的技巧是无效的,必须先设置scale!
3.75

注:前面4种方法不支持浮点数!

13.7 清空文件内容

下面是清空文件内容的几种方法。

方法1:删除它,再touch它。

$ rm file
$ touch file

方法2:

$ echo -n >file

方法3:

$ : >file

方法4:

$ >file

13.8 写入随机内容

如果你不关心文件的内容,只关心文件大小,则可以这样产生随机文件:

$ dd if=/dev/urandom of=file.txt bs=2048 count=10

如果你想得到的文件由有意义的单词组成,则可以这样产生随机文件:

$ ruby -e 'a=STDIN.readlines;X.times do;b=[];Y.times do; b << a[rand(a.size)].chomp end; puts b.join(" "); end' < /usr/share/dict/words > file.txt

运行上面命令时,请把X替换为总行数(如10),Y替换每行的单词数(如3)。

参考:http://www.skorks.com/2010/03/how-to-quickly-generate-a-large-file-on-the-command-line-with-linux/

13.9 实现进度条效果

下面脚本可实现进度条效果。其思想很简单:每次更新输出时不换行,而是利用 \r 更新当前行。

#!/bin/bash
b=''
i=0
while [ $i -le  100 ]
do
    printf "progress:[%-50s]%d%%\r" $b $i
    sleep 0.1
    i=`expr 2 + $i`
    b=#$b
done
echo

13.10 检测某程序是否在PATH中

用下面脚本可检测程序是否在PATH中。

if command -v foo >/dev/null 2>&1; then
    echo "foo exists."
fi

Command command is POSIX compliant, see here for its specification: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html

参考:http://stackoverflow.com/questions/592620/how-to-check-if-a-program-exists-from-a-bash-script

13.11 并行执行任务(推荐xargs -P num)

利用 xargs-P num 选项(指定最大并行数),可以并行地执行任务,具备简单的“线程池”功能。

第一步,把要执行的任务写入到一个文件(如commands.txt)中,每行一个任务。

$ cat commands.txt
sleep 2; echo Hello world
sleep 2; echo Goodbye world
sleep 2; echo Goodbye cruel world
sleep 2; echo Goodbye cruel world world

第二步,执行下面语句(下面例子中,同时有3个任务在执行)。

$ cat commands.txt | xargs -I CMD -P 3 bash -c CMD     # 当commands.txt比较复杂时可能出错

上面实例程序的一个可能输出(注:结果会乱序,且由于是并行执行的,多次运行的结果会不一样):

Goodbye world
Goodbye cruel world
Hello world
Goodbye cruel world world

特别说明:如果文件commands.txt中的命令包含单引号,且单引号意义重大(不可省略)。比如:

sleep 2; echo Hello world
sleep 2; echo Goodbye world
sleep 2; echo Goodbye cruel world
sleep 2; awk 'BEGIN {print "Goodbye cruel world world"}'

这时,需要用下面方式(重点是 -0 选项)并行执行任务:

$ cat commands.txt | tr '\n' '\0' | xargs -0 -I CMD -P 3 bash -c CMD     # 正确用法

参考:
Easy parallelization with Bash in Linux
Make xargs execute the command once for each line of input

13.12 获得CPU核数

有很多方法可以获得CPU核数,但没有一种方法是可移植的。

# 不同系统中,获得CPU核数的方法不同

Linux:
    1) getconf _NPROCESSORS_ONLN
    2) grep -c process /proc/cpuinfo

Solaris:
    1) psrinfo -p

OS X:
    1) getconf _NPROCESSORS_ONLN
    2) sysctl hw.ncpu

NetBSD:
    1) grep -c process /proc/cpuinfo
    2) sysctl hw.ncpu

OpenBSD:
    1) sysctl hw.ncpu

FreeBSD:
    1) sysctl hw.ncpu

13.13 Co-processes

co-processes are a ksh feature (already in ksh88). zsh has had the feature from the start (early 90s), while it has just only been added to bash in 4.0 (2009).

参考:http://unix.stackexchange.com/questions/86270/how-do-you-use-the-command-coproc-in-bash

13.13.1 ksh co-processes例子

先准备一个程序to_upper.ksh,内容如下:

$ cat to_upper.ksh
#!/bin/ksh

typeset -u arg

while [ 1 ]
do
  read arg
  print $arg
done
$ chmod u+x to_upper.ksh

下面是在ksh中使用co-processes的例子:

$ ./to_upper.ksh |&           # In ksh, |& start a coprocess with a 2-way pipe to it
[1]     16145
$ print -p abcd
$ read -p line
$ echo $line
ABCD
$ print -p xyz
$ read -p line
$ echo $line
XYZ

参考:http://www.livefirelabs.com/unix_tip_trick_shell_script/feb_2004/02092004.htm

不用co-processes,用“有名管道”也可以实现同样的功能,如下面例子在bash中也可以运行:

$ ./to_upper.ksh <in >out &
[2] 27462
$ exec 3> in 4< out
$ echo abcd >&3
$ read line <&4
$ echo $line
ABCD
$ echo xyz >&3
$ read line <&4
$ echo $line
XYZ

13.14 检测操作系统类型

如果在脚本中检测操作系统的类型呢?
在Bash中,可以通过测试变量 $OSTYPE 来得到操作系统的类型,但这种方法不通用,如在ksh中就不适应。

通用的可移植方法是测试命令 uname -s 的输出。
如:

if [ "$(uname -s)" = "Linux" ]; then
    echo "This is Linux"
fi

参考:
https://en.wikipedia.org/wiki/Uname
http://stackoverflow.com/questions/3466166/how-to-check-if-running-in-cygwin-mac-or-linux

14 Tips & Tricks

14.1 Bash Pitfalls

总结了40多条日常Bash编程中,老手和新手都容易忽略的错误编程习惯。

参考:
http://mywiki.wooledge.org/BashPitfalls
http://kodango.com/bash-pitfalls-part-1

14.2 自动补全

参考:
编写 Bash 补全脚本:http://kodango.com/bash-competion-programming
Linux中10个有用的命令行补齐命令,英文原版:http://www.thegeekstuff.com/2013/12/bash-completion-complete/
An introduction to bash completion: http://www.debian-administration.org/article/317/An_introduction_to_bash_completion_part_2

14.2.1 compgen和complete

Bash内置了两个补全命令: compgencomplete

compgen 根据不同的参数,生成匹配单词的候选补全列表( -W 指定完整的补全单词列表),例如:

$ compgen -W 'a1 a12 a123 b1 b12' -- a        # 输出单词列表中和前缀a匹配的单词
a1
a12
a123
$ compgen -W 'a1 a12 a123 b1 b12' -- a12      # 输出单词列表中和前缀a12匹配的单词
a12
a123
$ compgen -W 'a1 a12 a123 b1 b12' -- b        # 输出单词列表中和前缀b匹配的单词
b1
b12

complete 的作用是说明命令如何进行补全。它也可以使用 -W 参数指定候选的单词列表,如:

$ complete -W 'a1 a12 a123 b1 b12' tool1   # -W指定了使用<Tab>对tool1进行补全时的单词列表
$ tool1 a1<Tab>
a1    a12   a123
$ tool1 a12<Tab>
a12   a123

还可以通过 complete-F 参数指定一个补全函数:

$ complete -F _tool1 tool1

这样,在键入tool1命令后,会调用_tool1函数来生成补全的列表,完成补全的功能。

14.2.2 实例:补全命令行选项

下面将介绍如何实现“补全命令行选项”的功能。

其基本步骤是把想要补全的“命令行选项”放入内置变量 COMPREPLY 中:

function _foo() {
    local cur prev opts

    COMPREPLY=()                             # COMPREPLY是内置变量,表示候选的补全结果,这里把它清空

    cur="${COMP_WORDS[COMP_CWORD]}"          # 当前输入单词。COMP_WORDS和COMP_CWORD是内置变量
    prev="${COMP_WORDS[COMP_CWORD-1]}"       # 上一个输入单词,这里暂时没用

    opts="--log -h --help -f --file -o --output"   # 这是你想要补全的命令行选项

    if [[ ${cur} == -* ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
}
complete -F _foo foo                         # 通过 -F 参数指定foo的补全函数为_foo

下面是“补全命令行选项”的测试:

$ foo -<Tab>                 # 输入“foo -”后按Tab键会出现选项补全提示
--file    --help    --log     --output  -f        -h        -o

现在看一个更复杂的例子,补全 --log 选项的参数为“warn/info/debug”:

function _foo() {
    local cur prev opts

    COMPREPLY=()

    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"       # 上一个输入单词

    opts="--log -h --help -f --file -o --output"

    case "${prev}" in
        --log)
            COMPREPLY=( $(compgen -o filenames -W "warn info debug" -- ${cur}) )  # compgen的-W参数指定了候选项
            ;;
    esac
    
    if [[ ${cur} == -* ]] ; then
        COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
        return 0
    fi
}
complete -F _foo foo

下面是相关测试:

$ foo --log<Tab>
debug  info   warn

14.2.3 实例:补全目录或指定后缀的文件

假设你开发了一个工具,它能把txt文件转换为html文件,工具名为txt2html。你想定制txt2html的tab键自动补全功能,让其只补全目录或带有.txt后缀的文件。执行一次下面命令即可:

complete -f -o plusdirs -X '!*.txt' txt2html             # 为下次使用方便,可放入~/.bashrc中

如果你想补全目录或带有.txt或.text后缀的文件,可以执行下面命令:

complete -f -o plusdirs -X '!*.@(txt|text)' txt2html     # 为下次使用方便,可放入~/.bashrc中

14.3 $(())$() 还有 ${} 的区别

$(()) 用来作整数运算( $[] 也可以用作整数运行,但已经过时,不推荐使用),称为Arithmetic expansion。
$()`` 的作用一样(``已经过时,不推荐使用),用来作Command substitution(命令替换)。
${} 用来引用变量,用来作Parameter expansion(变量替换)。

14.4 使用$()而不是``进行“命令替换”

$()`` 的作用一样,用来作 Command substitution(命令替换,其功能是把命令的stdout放到当前位置)。但``是“过时的”用法,它有很多缺点(可参考:http://mywiki.wooledge.org/BashFAQ/082 ),所以请使用 $() 语法。

命令替换使用实例:

$ ls
file1.txt file2.txt file3.txt
$ files=$(ls *.txt)             # 也可以写为(过时的用法) files=`ls *.txt` ,变量files的内容为命令ls *.txt的输出
$ echo $files
file1.txt file2.txt file3.txt

14.5 ()和{}的区别

可以把一组命令放入()或{}中执行,含义不相同。

() 会启动一个子进程(subshell),不会对父进程的环境变量造成影响。
{} 在同一个进程中运行,它相当于“匿名函数”。

a=123
( a=321 )
echo "a = $a"   # output: a = 123

b=123
{ b=321; }
#      ^
echo "b = $b"    # output: b = 321

注意:{}中的最后一个命令后面必须有分号或换行符,否则会报语法错误。

参考:http://www.gnu.org/software/bash/manual/bashref.html#Command-Grouping

14.6 临时禁用alias

在需要执行的命令前面加个反斜杠字符即可,如禁用ls的alias: \ls

14.7 read -a和readarray的区别

read -a和readarray (mapfile)的区别

read -a
把输入内容按分隔符(空格或者跳格之类)分配给数组,连续的空格也算为1个分割。

readarray (mapfile)
把输入内容按行分配给数组。

14.8 更新当前行和之前行的内容(echo -e "\033[nA")

如果要更新当前行的内容,可以使用 echo -e "\r" 回到当前行的行首,再输出内容。

$ echo -n "Old line"; sleep 2; echo -e "\rThis new line"

执行上面命令时,会输出:

Old line

过2秒后,上面的输出会更新为:

This new line

如果要更新之前行的内容,可以使用 echo -e "\033[nA" (它表示向上移动光标n行)。如:

$ echo -e "line1 ...\nline2 ..."; sleep 2; echo -en "\033[2A"; echo "line1 ... done"; echo "line2 ... done"  # 输出两行后,再更新它。推荐用法
$ echo -e "line1 ...\nline2 ..."; sleep 2; echo -en "\e[2A"; echo "line1 ... done"; echo "line2 ... done"    # 和上相同。但有些版本的echo不支持\e,不推荐
$ echo -e "line1 ...\nline2 ..."; sleep 2; tput cuu 2; echo "line1 ... done"; echo "line2 ... done"          # 和上相同。

执行上面命令时,会输出:

line1 ...
line2 ...

过2秒后,上面的输出会更新为:

line1 ... done
line2 ... done

参考:
https://en.wikipedia.org/wiki/Escape_character#ASCII_escape_character
man 5 terminfo
man tput

14.9 测试交互式和非交互式shell

如何测试交互式和非交互式shell?

方法1:
测试 $PS1 变量。

方法2:
可以通过打印 $- 变量的值(代表着当前shell的选项标志),查看其中的“i”选项(表示interactive shell)来区分交互式与非交互式shell。

参考:
http://see.xidian.edu.cn/cpp/html/1519.html

14.10 输入上个命令的最后参数

bash中,如何输入上个命令的最后参数?

有3个方法:
!$
$_
Alt + .(或者<Esc> .)

注:
最后1个方法最好用,因为它可以在运行前直观地看到将会运行什么,有机会修改它再运行。
在tcsh中仅方法1可用。

14.11 bashdb (bash调试器)

bashdb是一个bash调试器。
在Debian系列的操作系统中,可以这样安装:

sudo apt-get install bashdb

Learning the bash Shell一书中介绍了一种实现bash debugger的方法,通过bash的trap接口实现的。

参考:
http://bashdb.sourceforge.net/
http://blog.csdn.net/yfkiss/article/details/8636758

14.12 只检查语法而不执行 (-n选项)

-n 选项可以只检查语法而不执行。bash和ksh都支持。

bash -n 1.sh
ksh -n 1.sh

为什么是 -n ,因为它对应的set -o为noexec,noexec的第一个字母为n

14.13 用shellcheck检测语法

用工具shellcheck可以检测Shell脚本中的一些不好用法。

在Debian系列系统中,可以用下面方法安装shellcheck:

$ apt-get install shellcheck

参考:http://www.shellcheck.net/

14.14 改变当前用户的登录shell (chsh)

直接执行 chsh 会提示输入密码,输入密码后再输入想要设置的shell的全路径即可。

所有可用的shell可通过命令 cat /etc/shells 得到。

注:由于更改shell需要更大的权限,所以文件 /usr/bin/chsh 的owner会为root,它的 setuid 或setgid位被置上。只有这样才能“使程序以owner的权限运行(即其effective UID为root)”。

14.15 编写移植性更好的脚本

推荐使用

#!/usr/bin/env bash

而不是

#!/bin/bash

说明:一般来说,/usr/bin/env在“所有”系统上都存在。

参考:
http://unix.stackexchange.com/questions/29608/why-is-it-better-to-use-usr-bin-env-name-instead-of-path-to-name-as-my

15 其它Shell

15.1 KornShell

ksh 88是商业软件,不开源。ksh 93于2000年已经开源,使用EPL(Eclipse Public License)协议!

15.1.1 ksh兼容性注意事项

15.1.1.1 数组定义方式

PD KSH中不支持下面方式定义数组:

array_name=( "XXX" "YYY" "ZZZ" )

下面的形式兼容性更好:

array_name[0]="XXX"
array_name[1]="YYY"
array_name[2]="ZZZ"
15.1.1.2 for循环形式

不要使用类似C语言形式的for循环

for (( [ expr1 ] ; [ expr2 ] ; [ expr3 ] )) ;do ... ;done

PD KSH不支持!
推荐使用下面的形式:

for vname in [ list ] ;do ... ;done

15.1.2 set -A和typeset -a

set -A和typeset -a用法有点不同。

#!/bin/ksh
set -A array1 a b c d e f g
typeset -a array2=(a b c d e f g)  #和上条语句作用一样。

# 不要写为下面形式
#set -A array1=(a b c d e f g)     #语法错误。
#typeset -a array2 a b c d e f g   #声明了8个数组,并非所要。

echo ${array1[0]};  #输出a
echo ${array1[1]};  #输出b
echo ${array2[0]};  #输出a
echo ${array2[1]};  #输出b

说明:bash中不支持set -A方式定义数组。

15.2 csh(tcsh)

Top Ten Reasons not to use the C shell
http://www.grymoire.com/Unix/CshTop10.txt

Some differences between BASH and TCSH
http://web.fe.up.pt/~jmcruz/etc/unix/sh-vs-csh.html

15.3 zsh

Zsh is an extended Bourne shell with a large number of improvements, including some features of bash, ksh, and tcsh.

参考:
使用 zsh 的九个理由:http://lostjs.com/2012/09/27/zsh/
24 Outstanding ZSH Gems:http://www.refining-linux.org/categories/13/Advent-calendar-2011/
zsh入门:http://wiki.gentoo.org/wiki/Zsh/HOWTO

15.3.1 array用法和bash中的区别

下面代码中bash中可运行,但zsh中不能运行。

typeset -a array=(a b c)

在zsh中应该写成两行。

typeset -a array
array=(a b c)

http://zshwiki.org/home/scripting/array

注意: zsh中数组的第一个元素下标为1。要使第一个元素下标从0开始可以通过setopt KSH_ARRAYS或者setopt KSH_ZERO_SUBSCRIPT

15.3.1.1 setopt SH_WORD_SPLIT

考虑下面例子:

str="one two three"
arr=(${str})
echo ${arr[0]}
echo ${arr[1]}
echo ${arr[2]}

在bash中,会输出:

one
two
three

但在zsh(设置了setopt KSH_ARRAYS),会输出:

one two three
[blank line]
[blank line]

想要和bash中输出一样,应该在zsh中设置下面选项:
setopt SH_WORD_SPLIT

15.3.2 Tips & Tricks

15.3.2.1 设置M-DEL停止在/处

把下面几行加入到.zshrc中

# M-DEL should stop at / in zsh
# Refer to http://chneukirchen.org/dotfiles/.zshrc
WORDCHARS="*?_-.[]~&;$%^+"
_backward_kill_default_word() {
  WORDCHARS='*?_-.[]~=/&;!#$%^(){}<>' zle backward-kill-word
}
zle -N backward-kill-default-word _backward_kill_default_word
bindkey '\e=' backward-kill-default-word   # = is next to backspace
15.3.2.2 设置M-m依次输入上个命令的各个参数

You probably know M-. to insert the last argument of the previous line. Sometimes, you want to insert a different argument. There are a few options: Use history expansion, e.g.!:-2 for the third word on the line before (use TAB to expand it if you are not sure), or use M-. with a prefix argument: M-2 M-.
Much nicer however is:

autoload -Uz copy-earlier-word
zle -N copy-earlier-word
bindkey "^[m" copy-earlier-word

Then, M-m will copy the last word of the current line, then the second last word, etc. But with M-. you can go back in lines too! Thus:

$ echo a b c
$ echo 1 2 3
$ echo <M-.><M-.><M-m>
$ echo b

Man, I wish I knew that earlier!

参考:http://chneukirchen.org/blog/archive/2013/03/10-fresh-zsh-tricks-you-may-not-know.html


Author: cig01

Created: <2011-07-03 Sun 00:00>

Last updated: <2018-05-11 Fri 14:03>

Creator: Emacs 25.3.1 (Org mode 9.1.4)