shell基础知识查缺补漏

最近在看《Linux程序设计(第4版)》,其中有一个章节主要讲了shell脚本方面的,内容不细,但是利用较短的篇幅讲的也不少了。对我们自己来说也是一个查缺补漏的过程,所以就写下这篇读书笔记,方便自己随时翻看。

本文的主要内容是来自《Linux程序设计(第4版)》,另外一小部分内容来自《Linux Shell脚本攻略(第3版)》。

本篇文章,内容是比较粗,很多内容也都没有写,比如重要的awk,sed之内的命令本篇文章压根就没有提及到。如果这篇文章能让大家对自己掌握的shell有一个查缺补漏,加深记忆的作用的话,我觉的这篇文章的目的也就达到了。

脚本注释以#符号开始,一直持续到该行的结束。

请注意第一行#! /bin/sh,它是一种特殊形式的注释,#!字符告诉系统同一行上紧跟在它后面的那个参数是用来执行本文件的程序。本文章中,/bin/sh是默认的shell程序。

1.变量

变量之前通常并不需要事先声明。

变量区分大小写。

在默认情况下,所有变量都被看作字符串并以字符串来存储,即使被赋值为数值时也是如此。

在shell中,可以通过在变量名前加一个$符号来访问它的内容。

当为变量赋值时,只需要使用变量名,该变量会根据需要被自动创建。

注意,如果字符串里包含空格,就必须用引号把它们括起来。此外,等号两边不能有空格。


read命令将用户的输入赋值给一个变量

1.1 使用引号

一般情况下,脚本文件中的参数以空白字符分隔(例如,一个空格、一个制表符或者一个换行符)。如果你想在一个参数中包含一个或多个空白字符,你就必须给参数加上引号

实例脚本:

#!/bin/sh

myvar="Hi there"

echo $myvar
echo "$myvar"
echo '$myvar' # ''内的内容会当做原样输出
echo \$myvar  # \会对$进行转移,也会原样输出

echo Enter some text
read myvar

echo '$myvar'  no equals $myvar

exit 0

输出结果

1.2 环境变量

当一个shell脚本程序开始执行时,一些变量会根据环境设置中的值进行初始化。这些变量通常用大写字母做名字。

3.参数变量

[email protected]和$*之间的区别:

双引号里面的[email protected]把各个参数扩展为彼此分开的域,而不受IFS值的影响。

访问脚本程序的参数,使用[email protected]是明智的选择。

1.3 declare、typeset

这两个命令也可以用来声明变量,作用完全相同。

它的主要参数有以下几个:

​ -i 声明整型变量

​ -r 声明变量为只读

​ -a 声明一个数组变量

​ -f 显示脚本中定义的函数体

​ -F 显示脚本中定义的函数

示例

#!/bin/sh

declare -i num=10
echo $num

declare -i n1="hello"
echo $n1

declare -ri readony=200
#readony=201   只读变量,不能修改 。这句执行会报错

declare -a arr='([0]="a" [1]="b" [2]="c")'
echo ${arr[0]}
echo ${arr[1]}
echo ${arr[2]}


func_1(){
  echo "Function 1"
}
func_2(){
  echo "Function 2"
}
echo "=========== declare -F ==========="
declare -F

echo "=========== declare -f ==========="
declare -f

exit 0

2.条件

2.1 test或[命令

这两个命令都是在shell的布尔判断。当使用[命令时,我们还使用符号]来结尾。

[符号和被检查的条件之间留出空格。

检查一个文件是否存在,命令是test -f

可以这样写

if test -f fred.c
then
...
fi

或者

if [ -f fred.c ]
then
...
fi

test命令的退出码(表明条件是否被满足)决定是否需要执行后面的条件代码。


如果把then和if放在同一行上,就必须要用一个分号把test语句和then分隔开。如下所示:

if [ -f fred.c ]; then
...
fi

test命令可以使用的条件类型可以归为3类:字符串比较、算术比较和与文件有关的条件测试。

  • 字符串比较

  • 算术比较

  • 文件有关的条件测试

3 控制结构

3.1 if语句

if语句非常简单:它对某个命令的执行结果进行测试,然后根据测试结果有条件地执行一组语句。

可以把then和if放在同一行上,就必须要用一个分号把test语句和then分隔开

模版:

if condition
then
  statements
elif condition; then    #then和if放在同一行上,用;分隔开
  statements
else
  statements
fi

示例:

#!/bin/sh

read name

if [ $name = "zhangsan" ]
then
  echo "zhang 1"
elif [ $name = "wangwu" ]; then
  echo "wang 2"
else
  echo "sorry name error"
  exit 1
fi
exit 0

执行结果:

如果在执行sh ifdemo.sh后,不输入任何内容,直接回车键,就会报错

这个问题主要是当我们直接输入回车键后,变量name就是一个空字符串,使得上面的条件变量变成了if [ = "zhangsan" ]elif [ $name = ];这就不是一个合法的条件。

这时,可以通过给变量加上引号,if [ "$name" = "zhangsan" ],如果是个空字符串,就会变成if [ "$name" = "zhangsan" ]

所以正确的示例脚本应该是这样:

#!/bin/sh

read name

if [ "$name" = "zhangsan" ]
then
  echo "zhang 1"
elif [ "$name" = "wangwu" ]; then
  echo "wang 2"
else
  echo "sorry name error"
  exit 1
fi
exit 0

3.2 for语句

for结构来循环处理一组值,这组值可以是任意字符串的集合。

模版:

for variable in value
do 
  statements
done

如果把do和for放在同一行上,就必须要用一个分号把value语句和do分隔开。如下所示:

for variable in value; do
  statements
done

示例代码

#!/bin/sh

for foo in bar fud 43 #如果给bar fud 43加上引号,变成"bar fud 43" 就会当做一个变量
do
  echo $foo
done
exit 0

示例代码:

列出当前目录下以.sh结尾的文件

#!/bin/sh

for file in $(ls *.sh);do
  echo $file
done
exit 0

3.3 while语句

模版:

while condition
do
  statements
done

如果把do和while放在同一行上,就必须要用一个分号把condition语句和do分隔开。如下所示:

while condition; do
  statements
done

示例代码:

#!/bin/sh
read name
while [ "$name" != "zhangsan" ]; do
  echo "name err"
  read name
done
echo "input correct"        

3.4 until语句

模版:

until condition
do
  statements
done

如果把do和until放在同一行上,就必须要用一个分号把condition语句和do分隔开。如下所

until condition; do
 statements
done

示例代码:

#!/bin/sh
#当指定用户登陆后,输出登陆 并退出
until who |grep "$1" >/dev/null
do
  sleep 1
done
echo  "$1" has just logged in

exit 0

3.5 case语句

模版:

case variable in
  pattern [ | pattern] ...) statements;;
  pattern [ | pattern] ...) statements;;
  ...
esac
~     

示例代码:

#!/bin/sh

read timeofday

case "$timeofday" in
  yes) echo "Good Morning";;
  no ) echo "Good Afternoon";;
  #这里用*来匹配前面条件都不匹配的场景,类似 default分支
  *  ) echo "Sorry, answer not recongnized";; 

esac
exit 0

case命令会对用来做比较的字符串进行正常的通配符扩展,因此你可以指定字符串的一部分并在其后加上一个*通配符。


示例代码:

  • 合并匹配模式
#!/bin/sh

read timeofday

case "$timeofday" in
  yes | y | Yes | YES ) echo "Good Morning";;
  n* | N* ) echo "Good Afternoon";;
  *  ) echo "Sorry, answer not recongnized";;
esac
exit 0

3.5 命令列表

shell提供了一对特殊的结构,专门用于处理命令列表,它们是AND列表和OR列表

  • AND列表

    AND列表结构允许你按照这样的方式执行一系列命令:只有在前面所有的命令都执行成功的情况下才执行后一条命令。

    模版:

    statement1 && statement2 && statement3 && ...
    

    从左开始顺序执行每条命令,只有一条命令返回的是true,它右边的下一条命令才能够执行。&& 的作用是检查前一条命令的返回值。

    示例代码:

    #!/bin/sh
    
    touch file_one
    rm -f file_two
    
    if [ -f file_one ] && echo "hello" && [ -f file_two ] && echo " there"
    then
      echo "in if"
    else
      echo "in else"
    fi
    
    exit 0
    
  • OR列表

    OR列表结构允许我们持续执行一系列命令直到有一条命令成功为止,其后的命令将不再被执行。

    模版:

    statement1 || statement2 || statement3 || ...
    

    从左开始顺序执行每条命令,只有一条命令返回的是false,它右边的下一条命令才能够执行。||的作用是检查前一条命令的返回值。

当前,AND列表也可以和OR列表结合在一起。

3.6语句块

在某些只允许使用单个语句的地方(比如在AND或OR列表中)使用多条语句,你可以把它们括在花括号{}中来构造一个语句块。

4.函数

要定义一个shell函数,你只需写出它的名字,然后是一对空括号,再把函数中的语句放在一对花括号中。

模版:

function_name(){
}

示例代码:

#!/bin/sh

#定义函数
foo(){
    echo "Function foo is executing"
}

echo "script starting"
foo   #调用函数
echo "script ended"

exit 0

当一个函数被调用时,脚本程序的位置参数($*、[email protected]、$#、$1、$2等)会被替换为函数的参数。


可以通过return命令让函数返回数字值。让函数返回字符串值的常用方法是让函数将字符串保存在一个变量中,该变量然后可以在函数结束之后被使用。此外,还可以echo一个字符串并捕获其结果。

#!/bin/sh

foo(){
    echo "Hello World"
}

result="$(foo)"

echo "result = "$result
fa(){
    return 123
}
result1="$(fa)"  #对于有返回值的函数,不能通过这种方式获取返回值
result2=fa
echo "result1 = "$result1
echo "result2 = "$result2
exit 0

可以使用local关键字在shell函数中声明局部变量,局部变量将仅在函数的作用范围内有效。

如果一个局部变量和一个全局变量的名字相同,前者就会覆盖后者,但仅限于函数的作用范围之内。

示例代码:

#!/bin/sh

sample_text="global variable"

foo(){
    local sample_text="local variable"
    echo "Function foo is executing"
    echo $sample_text
}

echo "script starting"
echo $sample_text

foo

echo "script ended"
echo $sample_text

exit 0

5.命令

可以在shell脚本程序内部执行两类命令。一类是可以在命令提示符中执行的“普通”命令,也称为外部命令(externalcommand),一类是我们前面提到的“内置”命令,也称为内部命令(internal command)。内置命令是在shell内部实现的,它们不能作为外部程序被调用。

通常情况下,命令是内部的还是外部的并不重要,只是内部命令的执行效率更高。

5.1 :命令

冒号(:)命令是一个空命令。它偶尔会被用于简化条件逻辑,相当于true的一个别名。

由于它是内置命令,所以它运行的比true快。

5.2 .命令

点(.)命令用于在当前shell中执行命令:

. ./shell_script

比较常用的地方就是我们修改环境变量时使用。比如. /etc/profile等等

通常,当一个脚本执行一条外部命令或脚本程序时,它会创建一个新的环境(一个子shell),命令将在这个新环境中执行,在命令执行完毕后,这个环境被丢弃,留下退出码返回给父shell。

但外部的source命令和点命令(这两个命令差不多是同义词)在执行脚本程序中列出的命令时,使用的是调用该脚本程序的同一个shell。

因为在默认情况下,shell脚本程序会在一个新创建的环境中执行,所以脚本程序对环境变量所作的任何修改都会丢失。而点命令允许执行的脚本程序改变当前环境。

5.3 eval命令

eval命令允许你对参数进行求值。它是shell的内置命令,通常不会以单独命令的形式存在。

示例代码:

#!/bin/sh

foo=10
x=foo
eval y='$'$x
echo $y

5.4 exec命令

exec命令有两种不同的用法。

  • 将当前shell替换为一个不同的程序。

    exec ls ifdemo.sh 
    

    脚本中的这个命令会用ls命令替换当前的shell。脚本程序中exec命令后面的代码都不会执行,因为执行这个脚本的shell已经不存在了。

  • 修改当前文件描述符

    exec 3< afile
    

    文件描述符3被打开以便从文件afile中读取数据。

5.5 exit n命令

exit命令使脚本程序以退出码n结束运行。

在shell脚本编程中,退出码0表示成功,退出码1~125是脚本程序可以使用的错误代码。其余数字具有保留含义。

5.6 export命令

export命令将作为它参数的变量导出到子shell中,并使之在子shell中有效。

在默认情况下,在一个shell中被创建的变量在这个shell调用的下级(子)shell中是不可用的。export命令把自己的参数创建为一个环境变量,而这个环境变量可以被当前程序调用的其他脚本和程序看见。

被导出的变量构成从该shell衍生的任何子进程的环境变量

set -a或set -o allexport命令将导出它之后声明的所有变量

5.7 expr命令

expr命令将它的参数当作一个表达式来求值。它的最常见用法就是进行如下形式的简单数学运算:

x=	`expr $x + 1`

反引号(“)字符使x取值为命令expr $x+1的执行结果,也可以用语法$( )替换反引号“

主要的一些求值计算

在较新的脚本程序中,expr命令通常被替换为更有效的$((…))语法

5.8 set命令

set命令的作用是为shell设置参数变量。许多命令的输出结果是以空格分隔的值,如果需要使用输出结果中的某个域,这个命令就非常有用。

例如,我们可以通过set命令从date命令的结果中得到当前月份

#!/bin/sh



#把date命令的输出设置为参数列表,然后通过位置参数$2获得月份。
echo the date is $(date)

set $(date)
echo The month is $2

exit 0

可以通过set命令和它的参数来控制shell的执行方式。其中最常用的命令格式是set -x,它让一个脚本程序跟踪显示它当前执行的命令。

5.9 shift命令

shift命令把所有参数变量左移一个位置,使$2变成$1, $3变成$2,以此类推。原来$1的值将被丢弃,而$0仍将保持不变。

$*、[email protected]和$#等其他变量也将根据参数变量的新安排做相应的变动。

在扫描处理脚本程序的参数时,经常要用到shift命令。如果你的脚本程序需要10个或10个以上的参数,你就需要用shift命令来访问第十个及其后面的参数。

不能通过$10来获取第10个元素(可以通过${10}这种方式来获取),这样获取到的相当于”$1″+”0″,所以需要通过shift前移后获取

示例代码:

#!/bin/sh

echo "$10"
echo "${10}"
while [ "$1" != "" ];do
  echo "$1"
  shift
done

exit 0

5.10 trap命令

trap命令用于指定在接收到信号后将要采取的行动。trap命令的一种常见用途是在脚本程序被中断时完成清理工作。

信号的名字,定义在头文件signal.h中,在使用信号名时需要省略SIG前缀。可以在命令提示符下输入命令trap -l来查看信号编号及其关联的名称。

rap命令有两个参数,第一个参数是接收到指定信号时将要采取的行动,第二个参数是要处理的信号名。

trap command signal

如果要重置某个信号的处理方式到其默认值,只需将command设置为-。如果要忽略某个信号,就把command设置为空字符串’’。一个不带参数的trap命令将列出当前设置的信号及其行动的清单。

5.11 unset命令

unset命令的作用是从环境中删除变量或函数。这个命令不能删除shell本身定义的只读变量(如IFS)。

5.12 find命令

find是个用于搜索文件的命令。

模版:

find [path] [options] [tests] [actions]
  • path部分可以使用绝对路径,如/bin;也可以使用相对路径,如.;也可以指定多个路径。如find /var /home

  • options有很多选项,一些主要的选项

  • tests是测试的部分。每种测试返回结果有两种可能:true或者false。find命令开始工作时,它按照顺序将定义的每种测试依次应用到它搜索到的每个文件上。如果一个测试返回false, find命令就停止处理它当前找到的这个文件,并继续搜索。如果一个测试返回true, find命令将继续下一个测试或对当前文件采取行动。

    常用的测试:


以用操作符来组合测试。大多数操作符有两种格式:短格式和长格式

可以通过使用圆括号来强制测试和操作符的优先级。由于圆括号对shell来说有其特殊的含义,所以还必须使用反斜线来引用圆括号。

如果你在文件名处使用的是匹配模式,你就必须在模式上使用引号以确保模式没有被shell扩展,而是直接传递给find命令。

#find . -name *    这个命令执行会报错
 find . -name "*"  #这个是正确命令
 
 #搜索比文件aaa新、且名字shift开头的文件
 #   -type f   搜索文件类型是文件的文件(不包括文件夹)
 find . -newer aaa  -name "shift*" -type f -print

-exec和-ok命令将命令行上后续的参数作为它们参数的一部分,直到被\;序列终止。实际上,-exec和-ok命令执行的是一个嵌入式命令,所以嵌入式命令必须以一个转义的分号结束,使得find命令可以决定什么时候它可以继续查找用于它自己的命令行选项。魔术字符串{}是-exec或-ok命令的一个特殊类型的参数,它将被当前文件的完整路径取代。

find . -name "a*"  -type f -exec ls -l {} \; 

5.13 grep命令

grep是通用正则表达式解析器(General RegularExpression Parser)的简写。

grep命令使用一个选项、一个要匹配的模式和要搜索的文件。

它的语法如下:

grep [options] PATTERN [FILES]

如果没有提供文件名,则grep命令将搜索标准输入

它的一些主要选项:

5.14 sort命令

sort命令能够对文本文件和stdin进行排序。

它的主要参数:

​ -n 采取数字排序

​ -t 指定分隔符

​ -k 指定第几列

​ -r 反向排序

​ -u 对输出的结果去重

-k后的整数指定了文本文件中的某一列。列与列之间由空格分隔。如果需要将特定范围内的一组字符(例如,第2列中的第4~5个字符)作为键,应该使用由点号分隔的两个整数来定义一个字符位置,然后将该范围内的第一个字符和最后一个字符用逗号连接起来

示例:

sort -nrk  1 data.txt     #对文件data.txt 依据第1列,以数字逆序形式排序
sort -k 2 data.txt        #对文件data.txt 依据第2列排序
sort -k 1.2,1.3 data.txt  #对文件data.txt 依据第1列的第2-3个字符排序
sort -k 1.3  data.txt     #对文件data.txt 依据第1列的第3个字符排序


uniq只能作用于排过序的数据,因此,uniq通常都与sort命令结合使用。

sort data.txt |uniq -u #显示去重后的结果
sort data.txt |uniq -c #统计各行在文件中出现的次数
sort data.txt |uniq -d #找出重复的航

# -s指定跳过前N个字符;
# -w指定用于比较的最大字符数

#使用-s 2跳过前两个字符,使用-w 2选项指定后续的两个字符作为判断重复的依据
sort data.txt |uniq -s 2 -w 2 

5.15 xargs命令

xargs命令应该紧跟在管道操作符之后。它使用标准输入作为主要的数据源,将从stdin中读取的数据作为指定命令的参数并执行该命令。

  • args命令重新格式化stdin接收到的数据,再将其作为参数提供给指定命令。xargs默认会执行echo命令

    示例:

    • 将多行输入转换成单行输出。

      xargs默认的echo命令可以用来将多行输入转换成单行输出:

    • 将单行输入转换成多行输出。

      xargs的-n选项可以限制每次调用命令时用到的参数个数。下面的命令将输入分割成多行,每行N个元素:

    xargs命令接受来自stdin的输入,将数据解析成单个元素,然后调用指定命令并将这些元素作为该命令的参数。xargs默认使用空白字符分割输入并执行/bin/echo。

    • 用-d选项将X定义为输入分隔符。

      echo "splitXsplit2Xsplit3Xsplit4Xslipt5" | xargs -d X
      #用X对字符串进行拆分。 注意,这个在MAC上提示没有-d这个参数
      

    xargs命令可以同find命令很好地结合在一起。find的输出可以通过管道传给xargs,由后者执行-exec选项所无法处理的复杂操作。如果文件系统的有些文件名中包含空格,find命令的-print0选项可以使用0(NULL)来分隔查找到的元素,然后再用xargs对应的-0选项进行解析

    #统计当前目录下所有文件的行数
    find . -type f  -name "*" -print0 | xargs -0  wc -l
    

    xargs有一个选项-I,可以用于指定替换字符串,这个字符串会在xargs解析输入时被参数替换掉。如果将-I与xargs结合使用,对于每一个参数,指定命令只会执行一次。

    #等价于上面的脚本,统计当前目录下所有文件的行数
    find . -type f  -name "*"  | xargs  -I {} wc -l {}
    
    • 结合stdin,巧妙运用while语句和子shell

      xargs会将参数放置在指定命令的尾部,因此无法为多组命令提供参数。我们可以通过创建子shell来处理这种复杂情况。子shell利用while循环读取参数并执行命令,就像这样

      cat data.txt | ( while read arg; do echo  $arg; done)
      #等价于 cat data.txt |xargs -I {} echo {}
      
      #查找当前目录下.c结尾的文件,
      find . -name '*.c' |xargs -I ^ sh -c "echo -ne '\n ^: '; grep main ^"
      

5.16 tr命令

tr可以对来自标准输入的内容进行字符替换、字符删除以及重复字符压缩。tr是translate(转换)的简写,因为它可以将一组字符转换成另一组字符。

tr只能通过stdin(标准输入)接收输入(无法通过命令行参数接收)

它的语法如下:

tr [options]  set1 set2

来自stdin的输入字符会按照位置从set1映射到set2(set1中的第一个字符映射到set2中的第一个字符,以此类推),然后将输出写入stdout(标准输出)。set1和set2是字符类或字符组。如果两个字符组的长度不相等,那么set2会不断复制其最后一个字符,直到长度与set1相同。如果set2的长度大于set1,那么在set2中超出set1长度的那部分字符则全部被忽略。

  • 字符替换

  • 字符删除

    tr有一个选项-d,可以通过指定需要被删除的字符集合,将出现在stdin中的特定字符清除掉:

  • 字符组补集

    可以利用选项-c来使用set1的补集,set2是可选的:

    如果只给出了set1,那么tr会删除所有不在set1中的字符。如果也给出了set2,tr会将不在set1中的字符转换成set2中的字符。如果使用了-c选项,set1和set2必须都给出。如果-c与-d选项同时出现,你只能使用set1,其他所有的字符都会被删除。

    • 从输入文本中删除不在补集中的所有字符:

    • 将不在set1中的字符替换成空格:

  • 压缩字符

    tr命令能够完成很多文本处理任务。例如,它可以删除字符串中重复出现的字符。基本实现形式如下:

    tr -s '[需要被压缩的一组字符]'
    
    • 去掉多余空格

    • 其他处理

    上面是我们的数据


    我们可以通过上面的方法对它的内容求和。

    下面分析下具体的逻辑,$[ ]也可以进行算术运算。 $(tr '\n' '+')是把sum.txt的内容中的回车换行替换成+,替换完应该就是1+2+3+4+5+。所以最后补个0,变成1+2+3+4+5+0。对它进行算术运算,结果就是15

  • 字符类

    tr可以将不同的字符类作为集合使用,所支持的字符类如下所示。

5.17 cut命令

cut命令可以按列,而不是按行来切分文件。该命令可用于处理使用固定宽度字段的文件、CSV文件或是由空格分隔的文件(例如标准日志文件)。

它的语法如下:

​ -f 指定定要提取的字段,默认的分隔符是制表符;可以使用-d参数指定分隔符

​ -s 对于没有使用分隔符的行,会将该行照原样打印出来。-s可以禁止打印出这种行

​ -b 表示字节提取

​ -c 表示字符提取

​ 示例:

cut -f1 -s -d" "  data.txt #用空格拆分,显示第一个字段。禁止显示没有空格拆分的行
cut -c2-5  data.txt  #打印每行的第2个到第5个字符
cut -c2-  data.txt  #打印每行的第2个到结束的字符
cut -c -2  data.txt  #打印每行的第0个到第5个的字符

6 命令的执行

要执行一条命令,并把该命令的输出放到一个变量中。$(command)语法来实现,也可以用一种比较老的语法形式`command`

这里是反引号(`),而不是单引号(’)(单引号的作用是防止变量扩展)

应该使用$(…)形式,引入这一形式的目的是为了避免在使用反引号执行命令时,处理其内部的$、`、\等字符所需要应用的相当复杂的规则。如果在反引号...结构中需要用到反引号,它就必须通过\字符进行转义。

6.1 算术扩展

expr命令,可以处理一些简单的算术命令,但这个命令执行起来相当慢,因为它需要调用一个新的shell来处理expr命令。

一种更新更好的办法是使用$((…))扩展。把你准备求值的表达式括在$((…))中能够更有效地完成简单的算术运算。

如下所示:

#!/bin/sh

x=0
while [ "$x" -ne 10 ]; do
  echo $x
  x=$(($x+1))   #   变量x +1 
  #let x=x+1     let 命令也可以进行算术运算 
done

exit 0

$[…]也可以进行算术运算

6.2 参数扩展

比如我们要访问两个文件,1_tmp,2_tmp。我们是不能通过下面的方式来实现的。

#!/bin/sh

#错误案例

for i in 1 2 ; do
  echo $i_tmp   #  不能通过。$i_tmp 的方式访问到 1_tmp,2_tmp
done

exit 0

正确的做法是我们需要将i放到花括号内,如下所示:

#!/bin/sh

for i in 1 2 ; do
  echo ${i}_tmp   #  通过${i}_tmp 的方式访问 1_tmp,2_tmp
done

exit 0

每次循环中,变量i的值替换了${i}


shell中有多种参数替换方法,一些常见的参数扩展方法如下图:

示例脚本:

#!/bin/sh

unset foo
echo ${foo:-bar}
echo "foo =  ${foo}" #这里可以看到 ${foo} 依旧是未设置状态

foo=fud
echo ${foo:-bar}

foo=/usr/bin/x11/startx
echo ${foo#*}
echo ${foo##*/}

bar=/usr/local/etc/local/networks
echo ${bar%local*}
echo ${bar%%local*}

exit 0

第一条语句${foo:-bar}给出的值是bar,这是因为在这条语句执行时foo没有值。这条语句执行后,变量foo未发生变化,它还停留在未设置状态。

如果这条语句是${foo:=bar},那么变量$foo就会被赋值。这个字符串操作符的作用是,检查变量foo是否存在且不为空。如果它不为空,就返回它的值,否则就把变量foo赋值为bar并返回这个值。

${foo:? bar}语句将在变量foo不存在或它设置为空的情况下,输出foo:bar并异常终止脚本程序。最后,${foo:+bar}语句将在变量foo存在且不为空的情况下返回bar。

{foo#*/}语句仅仅匹配并删除最左边的/(记住,*匹配零个或多个字符)。{foo##*/}语句匹配并删除尽可能多的字符,所以它删除最右边的/及其前面的所有字符。

{bar%local*}语句匹配从右边起直到第一次出现local(及跟在它后面的所有字符),而{bar%%local*}语句则从右边起尽可能多地匹配字符,直到遇到最靠左边的local。

7 调试脚本程序

要调试脚本,可以在执行shell时加上命令行选项,或是使用set命令。

下面列出来各种选项:

例如可以用-o选项启用set命令的选项标志,用+o选项取消设置,对简写版本也是一样的处理方法。

Tags: