Shell 脚本语言

  1. Shell简介

    1)Shell

    在计算机语言中,Shell是指一种命令行解释器,是为用户和操作系统之间通信提供的一种接口它接受来自用户输入的命令,并将其转换为一系列的系统调用送到内核执行,并将结果输出给用户。

    目前RedHat和CentOS发行版中默认安装了多种Shell,可以使用如下命令来确认系统中可用的Shell是什么版本:

    1
    2
    3
    4
    5
    6
    7
    [root@localhost ~] # cat /etc/Shells
    /bin/sh #Bourne Shell
    /bin/bash #Bourne again Shell
    /sbin/nologin # 非登录Shell
    /bin/tcsh #tC Shell
    /bin/csh #C Shell
    /bin/ksh #Korn Shell

    目前众多的Linux系统默认采用Bash Shell

    2)Bourne Shell

    是第一个重要的Shell,发布于1977年,作为UNIX 7的默认Shell,在系统中的位置是/bin/sh。目前大多数的UNIX和Linux系统还保留着这个/bin/sh,或者将其连接到其他Shell上,比如 RedHat和CentOS就是将这个文件作为一个连接文件连接到Bash Shell上:

    1
    2
    [root@localhost ~]# ls -l /bin/sh 
    lrwxrwxrwx 1 root root 4 Feb 26 21:14 /bin/sh -> bash
    3)Shell 功能

    当一台系统运行起来时,内核(kernel)会被调入内存中运行,由内核执行所有底层的工作,它会将所有应用程序及用户的操作翻译成CPU的基本指 令,并将其送至处理器。这些过程听起来非常复 杂,而且实际上也确实是非常底层和技术化的。为 了对用户屏蔽这些复杂的技术细节,同时也是为了保护内核不会因用户直接操作而受到损害,有必要 在内核之上创建一个层,该层就是一个“壳”,也就是Shell名称的由来。

    Bash Shell有两种工作模式,分别是互动模式和脚本模式。所谓互动模式就是由系统管理人员直接通过键盘输入命令,并等待其执行完毕后再执行下一条命令;而另一种模式是设计出一个脚本文件, 将所有需要执行的命令写在该文件中,由BashShell读取并执行。

  2. 第一个Shell脚本

    1
    2
    3
    4
    [root@localhost ~]# cat HelloWorld.sh 
    #!/bin/bash
    #This Line is a comment
    echo "Hello World"

    一个Shell脚本永远是以“#!”开头的,这是一个脚本开始的标记,它是在告诉系统执行这个文件需要使用某个解释器,后面的/bin/bash就是指明了解释器的具体位置。

    第二行同样是以“#”开头的,但是这里是一个注解。脚本中所有以“#”开头的都是注解(当然以“#!”开头的除外)。

    1)运行脚本

    第一种就是在该脚本所在的目录中直接bash这个脚本。实际上,如果使用这种方式来运行脚本,该脚本中的第一行“#!/bin/bash”就可以不需要了,因为直接bash一个文件就是指定了使用Bash Shell来解释脚本内容。

    1
    2
    [root@localhost ~]# bash HelloWorld.sh 
    Hello World

    第二种方式是给该脚本加上可执行权限,然后使用“./”来运行,它代表运行的是当前目录下的HelloWorld.sh脚本,如果采用这种方式而脚本没有可执行权限则会报错。

    1
    2
    3
    4
    5
    6
    [root@localhost ~]# chmod +x HelloWorld.sh 
    [root@localhost ~]# ./HelloWorld.sh
    Hello World
    #如果脚本没有可执行权限,则会报权限错误
    [root@localhost ~]# ./HelloWorld.sh
    -bash: ./HelloWorld.sh: Permission denied

    如果希望该脚本能成为默认的系统命令,简单地将该脚本复制到任一系统$PATH变量所包含的目录中,同时赋予可执行权限,下次运行的时候只需要直接输入该命令即可。也支持用Tab键补全命令。下例就是将其复制到了/bin目录,并执行该脚本的情况。

    1
    2
    3
    4
    [root@localhost ~]# chmod +x HelloWorld.sh 
    [root@localhost ~]# mv HelloWorld.sh /bin/
    [root@localhost ~]# HelloWorld.sh
    Hello World
    2)Shell 脚本的排错

    Shell脚本在执行时出错是很常见的,最简单的原因无外乎脚本在编写的过程中出现了语法错误或 者不小心输错了命令等。找出脚本中的错误是很重要的能力。假设在之前的脚本中,不小心把echo命令写成了ehco,那么执行的效果如下:

    1
    2
    [root@localhost ~]# bash HelloWorld.sh 
    HelloWorld.sh: line 2: ehco: command not found

    从报错信息很容易了解到,出错的原因是“命令不存在”。重新编辑这个文件修改成echo就可以了。如果只是语法上的错误或命令错误还是比较容易辨别的,但往往一些逻辑或算法错误不容易发 现,因为它的语法正确且本身并不会造成程序运行出错。比如说下面的脚本,本来是想连续10次做某些操作的,结果却迟迟没有任何输出。仔细观察一下就知道,其实是陷入了死循环。

    1
    2
    3
    4
    5
    [root@localhost ~]# cat BadLoop.sh 
    #!/bin/bash for ((i=10;i>0;i=i+1))
    do
    #do something here
    one

    如果在循环中间的代码块中加入了echo语句,那就容易发现问题了。也就是说,在排查错误时,==使用echo命令有助于观察代码执行的情况==。假设在 上面的脚本中使用echo,再次运行脚本时会注意 到脚本在不断地打印输出,这样就可以发现异常了。如果遇到确实需要执行很多次循环的代码段,由于每次循环的过程都很快,可能来不及观察就进入了下一次循环,那么可以加入sleep命令降低单次循环的速度,比如使用sleep 2这样的命令,单次循 环至少会耗时两秒,这就有足够的时间观察循环的过程了。

    1
    2
    3
    4
    5
    6
    7
    #!/bin/bash 
    for((i=10;i>0;i=i+1))
    do
    #do something here
    echo "i=$i";
    sleep 2
    done

    为了更清晰地看到脚本运行的过程,还可以借助-x参数来观察脚本的运行情况。比如在下面的代 码段中,从输出可以看到变量i的值一直在向上增 加,永远满足x>0的条件,所以这是一个死循环。只需要将原来代码中的i=i+1改成i=i-1就可以了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    [root@localhost ~]# bash -x BadLoop.sh 
    + (( i=10 ))
    + (( i>0 ))
    + echo i=10
    i=10
    + sleep 2
    + (( i=i+1 ))
    + (( i>0 ))
    + echo i=11
    i=11
    + sleep 2
    + (( i=i+1 ))
    + (( i>0 ))
    + echo i=12
    i=12
    + sleep 2
    [Ctrl+c]
    停止脚本
  3. Shell的内建命令

    所谓Shell内建命令,就是由Bash自身提供的命令,而不是文件系统中的某个可执行文件。例如,用于进入或者切换目录的cd命令,虽然我们一直在 使用它,但如果不加以注意很难意识到它与普通命 令的性质是不一样的:该命令并不是某个外部文件,只要在Shell中你就一定可以运行这个命令。打个比方,就像使用语言互相沟通是人类与生俱来的 能力,但是我们有时却需要使用移动电话来进行远 距离的沟通,那么人类本身具备的语言能力就 是“内建”的能力,而移动电话却是一个外部的工具。

    1
    2
    3
    4
    5
    6
    #cd 命令并不是一个可执行文件
    [root@localhost ~]# which cd
    /usr/bin/which: no cd in (/usr/kerberos/sbin:/usr/kerberos/bin:/usr/ local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin)
    #more 命令是一个可执行文件,文件位置为/bin/more
    [root@localhost ~]# which more
    /bin/more
    1)如何确定内建命令:type

    不要试图用脑子记住所有的命令,这不可能也不需要。判断一个命令是不是内建命令只需要借助于命令type即可。

    1
    2
    3
    4
    5
    6
    #cd 命令是个内建命令 
    [root@localhost ~]# type cd
    cd is a Shell builtin
    #ifconfig 命令不是内建命令,而是一个外部文件
    [root@localhost ~]# type ifconfig
    ifconfig is /sbin/ifconfig
    2)执行程序:“.”(点号)

    点号用于执行某个脚本,甚至脚本没有可执行权限也可以运行。有时候在测试运行某个脚本时可 能并不想为此修改脚本权限,这时候就可以使用“.”来运行脚本。

    1
    2
    3
    4
    5
    6
    #如果脚本没有可执行权限,则会报权限错误 
    [root@localhost ~]# ./HelloWorld.sh
    -bash: ./HelloWorld.sh: Permission denied
    #使用点号执行没有加执行权限的脚本可以正常运行
    [root@localhost ~]# . ./HelloWorld.sh
    Hello World

    与点号类似,source命令也可读取并在当前环境中执行脚本,同时还可返回脚本中最后一个命令的返回状态;如果没有返回值则返回0,代表执行成功;如果未找到指定的脚本则返回false。

    1
    2
    [root@localhost ~]# source HelloWorld.sh 
    Hello World
    3)别名:alias

    alias可用于创建命令的别名,若直接输入该命令且不带任何参数,则列出当前用户使用了别名的命令。应该就能理解类似ll这样的命令为什么与ls-l的效果是一样的。

    1
    2
    3
    4
    5
    6
    7
    8
    [root@localhost ~]# alias 
    alias cp='cp -i'
    alias l.='ls -d .* --color=tty'
    alias ll='ls -l --color=tty'
    alias ls='ls --color=tty'
    alias mv='mv -i'
    alias rm='rm -i'
    alias which='alias | /usr/bin/which --tty-only --read- alias --show-dot --show-tilde'

    使用alias可以自定义别名,比如说一般的关机命令是shutdown-h now,写起来比较长,这时可以重新定义一个关机命令,以后就方便多了。

    1
    [root@localhost ~]# alias myShutdown='shutdown -h now'

    注意,这样定义alias只能在当前Shell环境中有效,换句话说,重新登录后这个别名就消失了。为 了确保永远生效,可以将该条目写到用户家目录中的.bashrc文件中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [root@localhost ~]# cat .bashrc 
    # .bashrc
    # User specific aliases and functions
    alias rm='rm -i'
    alias cp='cp -i'
    alias mv='mv -i'
    #自定义关机命令的别名
    myShutdown='shutdown -h now'
    # Source global definitions
    if [ -f /etc/bashrc ]; then
    . /etc/bashrc
    fi
    4)删除别名:unalias

    该命令用于删除当前Shell环境中的别名。有两种使用方法,第一种用法是在命令后跟上某个命令的别名,用于删除指定的别名。第二种用法是在命令后接-a参数,删除当前Shell环境中所有的别名。同样,这两种方法都是在当前Shell环境中生效的。

    1
    2
    3
    4
    5
    #删除ll 别名
    [root@e-bai ~]# unalias ll
    #再运行该命令时,报找不到该命令的错误。说明该别名被删除了
    [root@e-bai ~]# ll
    -bash: ll: command not found
    5)任务前后台切换:bg、fg、jobs

    该命令用于将任务放置后台运行,一般会与Ctrl+z、fg、&符号联合使用。典型的使用场景是运行比较耗时的任务。比如打包某个占用较大空间的目录,若在前台执行,在任务完成前将会一直占用当前的终端,而导致无法执行其他任务,此时就应该将这类任务放置后台。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [root@localhost ~]# tar -zcf usr.tgz /usr
    tar: Removing leading `/' from member names
    # 开始打包
    # 占用前台导致无法运行其他任务,此处用Ctrl+z 组合键暂停前台任务 [1]+ Stopped tar -zcf usr.tgz /usr
    [root@localhost ~]# jobs
    # 查看暂停的任务,刚刚的tar 任务编号为1 [1]+ Stopped tar -zcf usr.tgz /usr
    [root@localhost ~]# bg 1
    # 把tar 任务放置后台继续运行 [1]+ tar -zcf usr.tgz /usr &
    #tar 任务继续运行了
    [root@localhost ~]# fg 1
    # 使用fg 把后台任务调至前台运行 tar -zcf usr.tgz /usr
    #如果预知某个任务耗时很久,可以一开始就将命令放入后台运行
    [root@localhost ~]# tar -zcf usr.tgz /usr &
    6)声明变量:declare、typeset

    这两个命令都是用来声明变量的,作用完全相同。很多语法严谨的语言(比如C语言)对变量的声明都是有严格要求的:变量的使用原则是必须在使用前声明、声明时必须说明变量类型,而Shell脚 本中对变量声明的要求并不高,因为Shell弱化了变 量的类概念,所以Shell又被称为弱类型编程语言,声明变量时并不需要指明类型。不过,若使用 declare命令,可以用-i参数声明整型变量。

    1
    2
    3
    4
    5
    6
    7
    8
    #声明变量i_num01 ,其值为1 
    [root@localhost ~]# i_num01=1
    #声明变量f_num01 ,其值为3.14
    [root@localhost ~]# f_num01=3.14
    #声明变量str01 ,其值为HelloWorld
    [root@localhost ~]# str01="HelloWorld"
    #使用declare 声明整型变量i_num02 ,其值为1
    [root@localhost ~]# declare -i i_num02=1

    使用-r声明变量为只读

    1
    2
    3
    4
    5
    [root@localhost ~]# declare -r readonly=100
    # 声明只读变量
    [root@localhost ~]# readonly=200
    # 试图改变变量值 -bash: readonly: readonly variable
    # 报错,提示尝试修改只读变量

    使用-a声明变量

    1
    2
    3
    4
    5
    6
    7
    [root@localhost ~]# declare - a arr='([0]="a" [1]="b" [2]="c")' 
    [root@localhost ~]# echo ${arr[0]}
    a
    [root@localhost ~]# echo ${arr[1]}
    b
    [root@localhost ~]# echo ${arr[2]}
    c

    使用-F、-f显示脚本中定义的函数和函数体

    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
    #创建脚本fun.sh ,内容如下
    [root@localhost ~]# cat fun.sh
    #!/bin/bash
    func_1()
    {
    echo "Funciotn 1"
    }
    func_2()
    {
    echo "Function 2"
    }
    echo "declare -F:"
    declare -F
    echo
    echo "declare -f:"
    declare -f
    #运行该脚本的输出效果如下
    [root@localhost ~]# bash fun.sh
    declare -F:
    declare -f func_1
    declare -f func_2
    declare -f:
    func_1 ()
    {
    echo "Funciotn 1"
    }
    func_2 ()
    {
    echo "Function 2"
    }
    7)打印字符:echo

    echo用于打印字符,典型用法是使用echo命令并跟上使用双引号括起的内容(即需要打印的内容),该命令会打印出引号中的内容,并在最后默认加上换行符。使用-n参数可以不打印换行符。

    1
    2
    3
    4
    5
    6
    7
    8
    [root@localhost ~]# echo "Hello World" 
    Hello World
    [root@localhost ~]#
    # 命令提示符出现在新的一行
    [root@localhost ~]# echo -n "Hello World"
    Hello World
    [root@localhost ~]#
    # 命令提示符在同一行

    默认情况下,echo命令会隐藏-e参数(禁止解释打印反斜杠转义的字符)。比如“\n”代表新的一行,如果尝试使用echo输出新的一行,在不加参数的情况下只会将“\n”当作普通的字符,若要打印转义字符,则需要通过使用-e参数来允许。

    1
    2
    3
    4
    5
    6
    7
    8
    #echo 默认禁止打印反斜杠转义的字符
    [root@localhost ~]# echo "\n"
    \n
    # 只是把“\n ”当做普通的字符
    #为了允许打印转义字符,需要使用-e 参数
    #下面的输出有两行,第一行是输出的新行,第二行是默认的换行符
    [root@localhost ~]# echo -e "\n"
    [root@localhost ~]#
    8)跳出循环:break

    从一个循环(for、while、until或者select)中退 出。break后可以跟一个数字n,代表跳出n层循环, n必须大于1,如果n比当前循环层数还要大,则跳出所有循环。下面的脚本演示了使用break和break 2的区别(运行前请将对应的注释符去掉)。

    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
    #创建演示脚本break_01.sh 
    [root@localhost ~]# cat break_01.sh
    #!/bin/bash
    for I in A B C D
    do
    echo -n "$I:"
    for J in `seq 10`
    do
    if [ $J -eq 5 ]; then
    break
    #break 2
    fi
    echo -n " $J"
    done
    echo
    done
    echo
    #判断当J 值为5 时,break 的输出结果(循环运行了4 次)
    [root@localhost ~]# bash break_01.sh
    A: 1 2 3 4
    B: 1 2 3 4
    C: 1 2 3 4
    D: 1 2 3 4
    #判断当J 值为5 时,break 2 的输出结果(仅运行了1 次循环便终止了)
    [root@localhost ~]# bash break_01.sh
    A: 1 2 3 4
    9)循环控制:continue

    停止当前循环,并执行外层循环(for、while、 until或者select)的下一次循环。continue后可以跟 上一个数字n,代表跳至外部第n层循环。n必须大于1,如果n比当前循环层数还要大,将跳至最外层的循环。下面的脚本演示了使用continue和continue

    2的区别(运行前请将对应的注释符去掉)。

    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
    #创建演示脚本continue_01.sh 
    [root@localhost ~]# cat continue_01.sh
    #!/bin/bash
    for I in A B C D
    do
    echo -n "$I:"
    for J in `seq 10`
    do
    if [ $J -eq 5 ]; then
    continue
    #continue 2
    fi
    echo -n " $J"
    done
    echo
    done
    echo
    #判断当J 值为5 时,continue 的输出结果
    [root@localhost ~]# bash continue_01.sh
    A: 1 2 3 4 6 7 8 9 10
    B: 1 2 3 4 6 7 8 9 10
    C: 1 2 3 4 6 7 8 9 10
    D: 1 2 3 4 6 7 8 9 10
    #判断当J 值为5 时,continue 2 的输出结果
    [root@localhost ~]# bash continue_01.sh
    A: 1 2 3 4B: 1 2 3 4C: 1 2 3 4D: 1 2 3 4
    10)将所跟的参数作为Shell的输入,并执行产生的命令:eval
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #eval 用法例一:将字符串解析成命令执行 
    #定义cmd 为一个字符串,该字符串为“ls -l /etc/passwd ”
    [root@localhost ~]# cmd="ls -l /etc/passwd"
    #如果使用eval ,则会将之前的字符串解析为命令并执行
    [root@localhost ~]# eval $cmd
    -rw-r--r-- 1 root root 1638 Mar 3 00:43 /etc/passwd
    #eval 用法例二:程序运行中根据某个变量确定实际的变量名 [
    root@localhost ~]# name1=john
    # 定义变量name1
    [root@localhost ~]# name2=wang
    # 定义变量name2
    [root@localhost ~]# num=1
    # 使用该变量确定真实的变量名name$num
    [root@localhost ~]# eval echo "$"name$num
    John
    #eval 用法例三:将某个变量的值当做另一个变量名并给其赋值
    [root@localhost ~]# name1=john
    [root@localhost ~]# name2=wang
    [root@localhost ~]# eval $name1="$name2"
    # 等价于john="wang"
    [root@localhost ~]# echo $john
    wang
    11)执行命令来取代当前的Shell:exec
    1
    2
    3
    4
    #列出系统中所有以.conf 结尾的文件
    [root@localhost ~]# find / -name "*.conf" -exec ls -l {} \;
    #删除系统中所有临时文件
    find / -name "*.tmp" -exec rm -f {} \;
    12)使变量能被子Shell识别:export
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #创建export.sh 脚本
    [root@localhost ~]# cat export.sh
    #!/bin/bash
    echo $var
    #直接执行这个脚本,由于变量var 在脚本中并没有定义,所以其值是空,脚本输出确实什么也没有
    [root@localhost ~]# bash export.sh
    [root@localhost ~]#
    #无任何输出
    #现在在当前Shell 中创建变量var ,并赋值为100 ,并尝试输出该值
    [root@localhost ~]# var=100
    [root@localhost ~]# echo $var
    100
    # 确实变量var被赋值了
    #由于这里的var和子Shell中的var都是局部变量,所以如果现在再运行子Shell ,依然会打印出空值
    [root@localhost ~]# bash export.sh
    [root@localhost ~]#
    # 无任何输出
    #但是如果在定义变量的时候使用了export 就不一样了,子Shell 可以读取该变量
    [root@localhost ~]# export var=100
    [root@localhost ~]# bash export.sh
    100
    # 这里读取到了父Shell的变量var值
    13)发送信号给指定PID或进程:kill

    Linux是一个多任务的操作系统,系统上经常同时运行着多个进程。我们需要知道如何控制这些进程。Linux操作系统包括3种不同类型的进程,第一种是交互进程,这是由一个Shell启动的进程,既可以在前台运行,也可以在后台运行;第二种是批处 理进程,与终端没有联系,是一个进程序列;第三 种是监控进程,也称系统守护进程,它们往往在系 统启动时启动,并保持在后台运行。

    kill命令用来终止进程,其工作的原理是向系统的内核发送一个系统操作信号和某个程序的进程标识号,然后系统内核就可以对进程标识号指定的进程进行操作。比如用ps命令可以看到许多进程,有时需要使用kill中止某些进程来提高系统资源。该命令可以向某个PID或进程发送信号。

    1
    2
    3
    4
    5
    6
    7
    [root@localhost ~]# kill [ -s signal | -p ] [ -a ] [ - - ] pid ... 
    [root@localhost ~]# kill -l [ signal ]
    #-s: 指定要发送的信号,信号可以是信号名或是信号数值
    #-p: 只打印出进程的PID ,并不真的发送信号
    #-l: 指定信号的名称列表
    #pid: 进程的ID号
    #signal: 信号
    14)整数运算:let

    let是Shell内建的整数运算命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #let 使用范例 
    let I=2+2 --->I=4
    let J=5-2 --->J=3
    let K=2*5 --->K=10
    let L=15/7 --->L=2 (整数计算,所以计算结果也是整数)
    let M=15%7 --->M=1 (求余)
    let N=2**3 --->N=8 (代表2 的3 次方)
    #let 也支持类C 的计算方式
    let i++ (i 自增1 )
    let i-- (i 自减1 )
    let i+=10 (i 值等于i 增加10 )
    let i-=10 (i 值等于i 减少10 )
    let i*=10 (i 值等于i 乘以10 )
    let i/=10 (i 值等于i 除以10 )
    let i%=10 (i 值等于i 模10 )
    15)显示当前工作目录:pwd

    pwd命令会打印当前工作目录的绝对路径名。如果使用-P选项,打印出的路径名中不会包含符号连接。如果使用了-L选项,打印出的路径中可以包含符号连接。

    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
    #运行pwd 可以显示当前所在目录的绝对路径 
    [root@localhost ~]# pwd
    /root
    #变量OLDPWD记录了上一次工作目录,如果你从登录系统之后一直没有改变工作目录,则 OLDPWD 为空
    [root@localhost ~]# echo $OLDPWD
    # 这里为空
    #变量PWD 记录了当前的工作目录,它与pwd 命令运行结果是一致的
    [root@localhost ~]# echo $PWD
    /root
    #进入/tmp 目录
    [root@localhost ~]# cd /tmp
    #这时OLDPWD 记录了上一次工作目录/root ,所以此处输出为/root
    [root@localhost ~]# echo $OLDPWD
    /root
    #下面将演示-P 和-L 参数
    #首先确定/var/mail 目录其实是一个软连接
    [root@localhost ~]# ls -l /var/mail
    lrwxrwxrwx 1 root root 10 Nov 27 17:54 /var/mail - > spool/mail
    #进入/var/mail 目录
    [root@localhost ~]# cd /var/mail
    #使用-L 参数和不加参数的pwd 命令输出结果是一样的
    [root@localhost mail]# pwd -L
    /var/mail
    #使用-P 参数则显示出真实的路径,而不是软链接
    [root@localhost mail]# pwd -P
    /var/spool/mail
    16)声明局部变量:local

    该命令用于在脚本中声明局部变量,典型的用法是用于函数体内,其作用域也在声明该变量的函 数体中。如果试图在函数外使用local声明变量,则会提示错误。

    17)从标准输入读取一行到变量:read

    有时候开发的脚本必须具有交互性,也就是在运行过程中依赖人工输入才能继续。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #根据输入的箱数计算一共有多少瓶啤酒 
    [root@localhost ~]# cat read.sh
    #!/bin/bash
    declare N
    echo "12 bottles of beer in a box"
    echo -n "How many box do you want:"
    read N
    echo "$((N*12)) bottle in total"
    #运行效果
    [root@localhost ~]# bash read.sh
    12 bottles of beer in a box
    How many box do you want:10
    # 这里输入数字
    120 bottle in total
    18)定义函数返回值:return

    典型的用于函数中,常见用法是return n,其中n是一个指定的数字,使函数以指定值退出。如果没有指定n值,则返回状态是函数体中执行的最后一个命令的退出状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [root@localhost ~]# cat return.sh
    #!/bin/bash
    #定义了一个函数fun_01 ,该函数简单地返回1
    function fun_01
    {
    return 1
    }
    #调用该函数 fun_01
    #查看之前函数的返回值
    echo $?
    #实际运行一下这个脚本的效果
    [root@localhost ~]# bash return.sh
    1
    19)向左移动位置参数:shift

    要想搞清楚shift的用法,首先需要了解脚本“位置参数”的概念。假设一个脚本在运行时可以接受参数,那么从左到右第一个参数被记作$1,第二个参数为$2,以此类推,第n个参数为$N。所有参数记作$@或$*,参数的总个数记作$#,而脚本本身 记作$0。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #通过该脚本了解$0 、$1 、$2 、$3 、$@、$# 等符号的含义 
    [root@localhost ~]# cat shift_01.sh
    #!/bin/bash
    echo "This script's name is:$0"
    echo "The First parameter is:$1"
    echo "The Second parameter is:$2"
    echo "The Third parameter is:$3"
    echo "All of the parameters are $@"
    echo "Count of parameter is:$#"
    #运行该脚本的效果
    [root@localhost ~]# bash shift_01.sh 1 2 3
    This script's name is:shift_01.sh
    The First parameter is:1
    The Second parameter is:2
    The Third parameter is:3
    All of the parameters are 1 2 3
    Count of parameter is:3

    shift命令可以对脚本的参数做“偏移”操作。假设脚本有A、B、C这3个参数,那么$1为A,$2为B, $3为C;shift一次后,$1为B,$2为C;再次shift后 $1为C。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #通过阅读该脚本了解shift 命令的作用 
    [root@localhost ~]# cat shift_02.sh
    #!/bin/bash
    until [ -z "$1" ]
    do
    echo "$@ "
    shift
    Done
    #运行该脚本,使用A B C 这3 个参数
    [root@localhost ~]# bash shift_02.sh A B C
    A B C
    B C
    C
    20)测试表达式:test

    该命令用于测试表达式EXPRESSION的值,根据测试结果返回0(测试失败)或1(测试成功)。

    1
    [root@localhost ~]# test EXPRESSION
  4. Shell版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #确认系统中使用的Shell 是bash
    [root@localhost ~]# echo $SHELL
    /bin/bash
    #查看系统中Bash Shell 的版本(方法一)
    [root@localhost ~]# bash --version
    GNU bash, version 3.2.25(1)-release (i686-redhat-linux-gnu) Copyright (C) 2005 Free Software Foundation, Inc.
    #查看系统中Bash Shell 的版本(方法二)
    [root@localhost ~]# echo $BASH_VERSION
    3.2.25(1)-release
  5. Shell编程基础

    1)变量

    ​ 从变量的实质来说,变量名是指向一片用于存储数据的 内存空间。变量有局部变量、环境变量之分。Shell 变量是一种弱类型的变量,也就是说在声明变量时并不需要指定其变量类型,而且也不需要遵循C语言中“先声明再使用”的规定,任何时候只要想用就 可以用。在脚本中,往往需要使用变量来存储有用信息,比如文件名、路径名、数值等,通过这些变量可以控制脚本的运行行为。

    ​ 局部变量就是指在某个Shell中生效的变量,对其他Shell来说无效,而且会随着当前Shell的消失而消失,局部变量的作用域被限定在声明它们的Shell中,可以使用local内建命令来“显式”的声明局部变量,但仅限于函数内使用。

    ​ 环境变量通常又称“全局变量”,以区别于局部变量。在Shell脚本中,变量默认就是全局的,无论 在脚本的任何位置声明,但是为了让子Shell继承当 前Shell的变量,则可以使用export内建命令将其导出为环境变量。

    1
    [root@localhost ~]# export VAR=value

    其中,VAR是变量的名字,value为值,使用等号相连,==注意等号两端没有空格==。

    变量:EUID

    记录当前用户的UID。当前的用户是root,所以该值应该为0。

    1
    2
    [root@localhost ~]# echo $EUID
    0

    变量:FUNCNAME

    在用户函数体内部,记录当前函数体的函数名。创建funcname.sh文件,内容如下(运行该脚本后注意查看脚本的输出):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [root@localhost ~]# cat funcname.sh 
    #!/bin/bash
    funcname()
    {
    echo $FUNCNAME
    }
    funcname
    [root@localhost ~]# bash funcname.sh
    funcname

    变量:HISTCMD

    记录下一条命令在history命令中的编号。如果运行history命令查看到history中一共记录了1016条已经执行过的命令,则下面一条命令的编号将是1018(因为本次history为第1017条命令)。

    1
    2
    3
    4
    5
    [root@localhost ~]# history 
    ......( 略去内容)......
    1016 history
    [root@localhost ~]# echo $HISTCMD
    1018

    变量:HISTFILE

    记录history命令记录文件的位置。运行history命令将打印出已经运行过的命令列表,即便重启机器之后还能保存以前的命令记录。但这是如何做到的呢?其实history只不过是找到$HISTFILE所指定的命令记录文件,并将其打印出来。一般默认每个用户的家目录下都有一个.bash_history文件,用于记录该用户运行过的命令历史记录。

    1
    2
    [root@localhost ~]# echo $HISTFILE 
    /root/.bash_history

    变量:OLDPWD

    记录之前目录,这个值是什么由之前所在的那个目录决定。

    1
    2
    3
    4
    5
    6
    7
    #进入/mnt 目录
    [root@localhost ~]# cd /mnt
    #进入/root 目录
    [root@localhost mnt]# cd
    #看看之前在哪个目录
    [root@localhost ~]# echo $OLDPWD
    /mnt
    2)变量赋值和取值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #定义变量:变量名=变量值
    #注意点一:变量名和变量值之间用等号紧紧相连,之间没有任何空格
    [root@localhost ~]# name=john
    # 定义变量
    #如果不注意,等号任何一边出现空格就会出错
    [root@localhost ~]# name = john
    -bash: name: command not found
    [root@localhost ~]# name="john"
    # 这样也是可以的
    #注意点二:当变量中有空格时必须用引号括起,否则会出现错误
    #其中的引号可以是双引号,也可以是单引号
    [root@localhost ~]# name="john wang"
    [root@localhost ~]# name=john wang
    #-bash: wang: command not found

    变量的取值也很简单,只需要在变量名前加上 $符号既可,严谨一点的写法是${}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    [root@localhost ~]# echo $name
    john wang
    [root@localhost ~]# echo ${name}
    john wang
    #使用${} 获取变量值是一种相对比较保险的方式
    [root@localhost ~]# name="sue "
    #这里本是想打印名字,后面跟Hello 的,但Shell 试图将nameHello 解释为一个变量。 从Shell 语法来说,也确实应该将nameHello 解释为变量
    [root@localhost ~]# echo $nameHello
    # 因为变量nameHello 未声明,所以值为空
    [root@localhost ~]# echo ${name}Hello
    sue Hello
    #注意点三:如果变量值引用的是其他变量,则必须使用双引号。因为单引号会阻止 Shell 解释特殊字符$
    [root@localhost ~]# name=john
    [root@localhost ~]# name1="$name"
    [root@localhost ~]# echo $name1
    john
    [root@localhost ~]# name1='$name'
    [root@localhost ~]# echo $name1
    $name

    由于Shell具有“弱变量”的特性,因此即便在没有预先声明变量的时候也是可以引用的,而且没有 任何报错或者提醒,这可能会造成脚本中引用不正 确的变量,从而导致脚本异常,但是却很难找出原因。在这种情况下,可以设置脚本运行时必须遵循“先声明再使用”的原则,这样一旦脚本中出现使用未声明的变量的情况则立刻报错。

    1
    2
    3
    4
    5
    [root@localhost ~]# echo $unDefinedVar
    # 因为该变量未声明,所以值为空,但没有任何报错 #设置变量必须先声明再使用
    [root@localhost ~]# shopt -s -o nounset
    [root@localhost ~]# echo $unDefinedVar
    -bash: unDefinedVar: unbound variable
    3)取消变量

    取消变量指的是将变量从内存中释放,使用的命令是unset,后面跟变量名。函数也是可以被取消的,所以unset后面还可以跟上函数名以取消函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #取消变量 
    [root@localhost ~]# name=john
    [root@localhost ~]# echo $name
    john
    # 此时变量有值
    [root@localhost ~]# unset name
    [root@localhost ~]# echo $name
    # 变量中已经没有内容了
    [root@localhost ~]#
    #取消函数
    unset_function()
    {
    echo "Hello World"
    }
    unset unset_function
    unset_function
    # 由于函数已被取消,这里调用会出错
    4)特殊变量

    Shell中还有一些预先定义的特殊只读变量,它们的值只有在脚本运行时才能确定。首先是“位置参数”,位置参数的命名简单直接,比如,脚本本身为$0,第一个参数为$1,第二个参数为$2,第三个为$3,以此类推。当位置参数的个数大于9时,需要用${}括起来标识,比如说第10个位置参数应该记为${10}。另外,$#表示脚本参数的个数总 和,$@或$*表示脚本的所有参数。下面的示例脚本使用了这些特殊的位置参数,请注意不同位置参数的输出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [root@localhost ~]# cat posion.sh 
    #!/bin/bash
    echo "This script's name is: $0"
    echo "$# parameters in total"
    echo "All parameters list as: $@"
    echo "The first parameter is $1"
    echo "The second parameter is $2"
    echo "The third parameter is $3"
    [root@localhost ~]# bash posion.sh para1 para2 para3 This script's name is: posion.sh 3 parameters in total All parameters list as: para1 para2 para3 The first parameter is para1 The second parameter is para2 The third parameter is para3

    脚本或命令返回值:$?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #输入错误的命令时的返回值
    [root@localhost ~]# ifconfi
    -bash: ifconfi: command not found
    [root@localhost ~]# echo $?
    127
    #尝试ping 主机ping 不通时的返回值
    [root@localhost ~]# ping 192.168.61.100 -c 1
    PING 192.168.61.100 (192.168.61.100) 56(84) bytes of data. From 192.168.61.131 icmp_seq=1 Destination Host Unreachable --- 192.168.61.100 ping statistics --- 1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms
    [root@localhost ~]# echo $?
    1
    5)数组
    1. 数组定义:

      数组的定义方法如下:用declare命令定义数组Array,并将第一个元素赋值为0,第二个元素赋值 为1,其下标(也就是索引)则分别是0和1(记住 数组的索引是从0开始计数的),然后打印出第一个元素的值。

      1
      2
      3
      4
      5
      #定义名为Array 的索引数组 
      [root@localhost ~]# declare -a Array
      #数组的下标从0 开始计数,定义了第一个元素值为0 ,第二个元素值为1
      [root@localhost ~]# Array[0]=0
      [root@localhost ~]# Array[1]=1

      如果说数组Array的前两个元素“类型相同”(严格意义上这么说是不对的),那么第三个元素就显得“另类”了:赋值为一个字符串。这又一次验证了 Shell变量是弱类型的,这在很多语言中是不可能的。

      1
      [root@localhost ~]# Array[2]="HelloWorld"

      和其他变量一样,Shell中对于数组变量的声明也非常宽松,而且随时都可以根据需要增加变量中的元素。相比其他语言,Shell的数据更为灵活。在 很多语言中,一旦对数组进行了初始化就不能再改变大小了。

      1
      2
      3
      4
      5
      6
      #数组还可以在创建的同时赋值
      [root@localhost ~]# declare -a Name=('john' 'sue')
      #增加元素
      [root@localhost ~]# Name[2]='wang'
      #更简单的创建数组的方式——不使用declare 关键字
      #[root@localhost ~]# Name=('john' 'sue')

      还可以给特定的元素赋值。下面的示例就是只对第四个、第六个、第八个元素进行赋值。

      1
      2
      #跳号赋值 
      [root@localhost ~]# Score=([3]=3 [5]=5 [7]=7)
    2. 数组操作 :

      数组取值:知道了如何定义数组和赋值元素后,下面就要了解数组的一些常见操作。最简单的操作就是数组取值,其格式为:${数组名[索引]}。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #取数组第一个元素的值 
      [root@localhost ~]# echo ${Array[0]}
      0
      [root@localhost ~]# echo ${Array[2]}
      HelloWorld
      #打印数组中的元素值
      [root@localhost ~]# echo ${Name[0]}
      john
      [root@localhost ~]# echo ${Name[1]}
      sue

      指定索引只能列举单个元素,要是想一次性取出所有元素的值,可以采取以下两种方式:

      1
      2
      3
      4
      [root@localhost ~]# echo ${Array[@]} 
      0 1 HelloWorld
      [root@localhost ~]# echo ${Array[*]}
      0 1 HelloWorld

      数组长度:即数组元素个数。利用“@”或“* ”字符,可以将数组扩展成列表,然后使 用“#”来获取数组元素的个数。

      1
      2
      3
      4
      [root@localhost ~]# echo ${#Array[@]}
      3
      [root@localhost ~]# echo ${#Array[*]}
      3

      如果某个元素是字符串,还可以通过指定索引的方式获得该元素的长度。

      1
      2
      [root@localhost ~]# echo ${#Array[2]}
      10

      数组截取:可以截取某个元素的一部分,对象可以是整个数组或某个元素。

      1
      2
      3
      4
      5
      6
      #取出数组的第一、第二个元素 
      [root@localhost ~]# echo ${Array[@]:1:2}
      1 HelloWorld
      #取出第二个元素从第0 个字符开始连续5 个字符
      [root@localhost ~]# echo ${Array[2]:0:5}
      Hello

      连接数组:将若干个数组进行拼接操作。

      1
      2
      3
      [root@localhost ~]# Conn=(${Array[@]} ${Name[@]}) 
      [root@localhost ~]# echo ${Conn[@]}
      0 1 HelloWorld john sue

      替换元素:将数组内某个元素的值替换成其他值。

      1
      2
      3
      [root@localhost ~]# Array=(${Array[@]/HelloWorld/HelloJohn}) 
      [root@localhost ~]# echo ${Array[@]}
      0 1 HelloJohn

      取消数组或元素:和取消一般变量一样,取消一个数组的方式也使用unset命令。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      #取消数组中的一个元素 
      [root@localhost ~]# unset Array[1]
      [root@localhost ~]# echo ${Array[@]}
      0 HelloJohn
      #取消整个数组
      [root@localhost ~]# unset Array
      [root@localhost ~]# echo ${Array[@]}
      # 本行打印为空,说明Array 已经被取消了
      [root@localhost ~]#
    6) 只读变量

    只读变量又称常量,是通过readonly内建命令创建的变量。这种变量在声明时就要求赋值,并且之 后无法修改,这和之前讲到的使用declare-r声明只读变量的效果是一致的。

    1
    2
    3
    4
    5
    #声明只读变量RO 并赋值为100 
    [root@localhost ~]# readonly RO=100
    #此处尝试修改RO 的值,Shell 会报错
    [root@localhost ~]# RO=200
    -bash: RO: readonly variable
    7) 转义和引用

    Shell中有两类字符,一类是普通字符,在Shell中除了本身的字面意思外没有其他特殊意义,即普 通纯文本(literal);另一类即元字符(meta),是 Shell的保留字符,在Shell中有着特殊意义。这在很多时候会造成麻烦:比如说想要在程序中用美元符 打印商品的价格,但是这个符号一般被解析成提取 变量的值。为了消除这些特殊字符的功能,就必须对其进行转义和引用。

    • 转义

    转义是指使用转义符引用单个字符,从而使其表达单纯的字符的字面含义。Shell中的转义符是反 斜线“\”,使用转义符的目的是使转义符后面的字符单纯地作为字符出现,而不解释其特殊的含义。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #试图打印“$Dollar ”字符串的错误演示
    [root@localhost ~]# echo $Dollar
    ---> 此处打印为空,因为Shell 尝试打印出变量Dollar 的值,但是这个变量并没有声明,所以打印空行
    #使用转义字符转义$ 字符
    [root@localhost ~]# echo \$Dollar
    $Dollar
    #更多的例子
    #打印乘号。如果不用转义符转义* 号,则* 号会作为一般的通配符使用,结果是将工作目录中的 所有目录和文件名替换它
    [root@localhost ~]# echo 8 \* 8 =64
    8 * 8 =64
    #句子中含有引号。如果不用转义符转义' 单引号,则Shell 会等待出现另一个单引号才能结束echo 进程
    [root@localhost ~]# echo john\'s cat
    john's cat
    • 引用

    引用是指将字符串用某种符号括起来,以防止特殊字符被解析为其他意思。比如说上一小节中的转义符就是一种引用。Shell中一共有4种引用符,分别是双引号、单引号、反引号(在键盘上和波浪 号位于同一个键)和转义符。其中双引号又叫“部分引用”或“弱引用”,可以引用除$符、反引号、转义符之外的所有字符;单引号又叫“全引用”或“强引用”,可以引用所有字符;反引号则会将反引号括起的内容解释为系统命令。

    1)部分引用

    部分引用是指用双引号括起来的引用。在这种 引用方式中,$符、反引号(`)、转义符(\)这3种特殊字符依然会被解析为特殊意义。比如,在定义一个变量后,使用echo打印该变量的时候,将它们用双引号括起来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #声明变量VAR03 ,并用echo 打印出来。第一次直接打印,第二次用引号括起来,从输出内容看好像没什么区别 
    [root@localhost ~]# VAR03=100
    [root@localhost ~]# echo $VAR03
    100
    [root@localhost ~]# echo "$VAR03"
    100
    #声明变量VAR03,内容为字符串,ABC 之间有多个空格
    [root@localhost ~]# VAR04="A B C"
    #直接打印变量时,输出内容只保留了每个字母间一个空格
    [root@localhost ~]# echo $VAR04
    A B C
    #使用引号括起的输出内容和变量定义时的内容是完全一致的
    [root@localhost ~]# echo "$VAR04"
    A B C

    2)全引用

    全引用是指用单引号括起来的引用。单引号中的任何字符都只当作是普通字符(除了单引号本身,也就是说单引号中间无法再包含单引号,即便用转义符转义单引号也不行)。所有在单引号中的字符都只能代表其作为字符的字面意义。如果用单引号引用之前声明的变量,输出的内容如下所示:

    1
    2
    3
    4
    [root@localhost ~]# echo '$VAR03' 
    $VAR03
    [root@localhost ~]# echo '$VAR04'
    $VAR04

    可以看到,输出内容就是单引号所括起来的所有内容,而不会将变量解析为其值。

    如果全引用括起的字符串中还含有单引号,则 会出现问题,因为Shell无法区分哪个单引号是引用的结束符,就像下面显示的一样:

    1
    2
    [root@localhost ~]# echo 'It's a dog' 
    >

    想要解决这个问题,可以采取如下两种方式:

    1
    2
    3
    4
    [root@localhost ~]# echo 'It'\''s a dog' 
    It's a dog
    [root@localhost ~]# echo "It's a dog"
    It's a dog
    8)其他算术运算
    • 使用$[]做运算

    $[]和$(())类似,可用于简单的算术运算,以下 给出使用方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [root@localhost ~]# echo $[1+1] 
    2
    [root@localhost ~]# echo $[2-1]
    1
    [root@localhost ~]# echo $[2*2]
    4
    #除法运算,由于是整数操作,舍去小数
    [root@localhost ~]# echo $[5/2]
    2
    #求余运算
    [root@localhost ~]# echo $[5%2]
    1
    #幂运算
    [root@localhost ~]# echo $[5**2]
    25
    • 使用expr做运算

    expr命令也可用于整数运算。和其他算术运算方式不同,expr要求操作数和操作符之间使用空格隔开(否则只会打印出字符串),所以特殊的操作符要使用转义符转义(比如*)。expr支持的算术运算符有加、减、乘、除、余等,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #操作符和操作数之间没有空格则只会打印出字符串 
    [root@localhost ~]# expr 1+1
    1+1
    #使用空格隔开后可以正常计算
    [root@localhost ~]# expr 1 + 1
    2
    #特殊符号(元字符)需要用转义符转义,否则出错
    [root@localhost ~]# expr 2 * 2
    expr: syntax error
    [root@localhost ~]# expr 2 \* 2
    4
    • 内建运算命令declare
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #例1 :不使用declare 定义变量
    [root@localhost ~]# I=1+1
    [root@localhost ~]# echo $I
    1+1
    #例2 :使用declare 定义变量
    [root@localhost ~]# declare -i J
    [root@localhost ~]# J=1+1
    [root@localhost ~]# echo $J
    2
    #注意,Shell 中的算术运算要求运算符和操作数之间不能有空格,而是紧密连接的;特殊符号在 这里也不需要用转义符转义(比如这里的+号);如果算术表达式中含有其他变量也不需要用$ 引用
    • 算术扩展

    算术扩展是Shell提供的整数变量的运算机制,是Shell的内建命令之一。其基本语法如下:

    1
    $(( 算术表达式))

    其中的算术表达式由变量和运算符组成,常见的用法是显示输出和变量赋值。若表达式中的变量没有定义,则在计算时,其值会被假设为0(但是并不会真的因此赋0值给该变量)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #显示输出: echo $(( 算术表达式))
    #例子:计算2*i+1 的值
    [root@localhost ~]# i=2
    [root@localhost ~]# echo $((2*i+1))
    # 注意这里变量i前并没有$符
    5
    [root@localhost ~]# echo $((2*(i+1)))
    # 用括号改变运算优先级 6#变量赋值
    var=$(( 算术表达式))
    #例子:将2*i+1 的值赋值给变量var
    [root@localhost ~]# var=$((2*i+1))
    [root@localhost ~]# echo $var
    5
    #未定义的变量参与算术表达式求值
    [root@localhost ~]# echo $((2*(j+1)))
    2