• bash 教程4 shell 脚本 调试 环境 [MD]


    博文地址

    我的GitHub 我的博客 我的微信 我的邮箱
    baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

    目录

    Bash 教程

    本文改编自 网道的 Bash 教程,主要为了精简大量本人不感兴趣的内容。

    脚本基础

    脚本 script 就是包含一系列命令的一个文本文件,所有能够在命令行完成的任务,都能够用脚本完成。

    如何运行一个脚本

    运行 shell 脚本一般有两种方法:

    • 作为可执行程序运行:./test.sh
      • 此格式可让 shell 在当前目录寻找并执行 test.sh 文件
      • 注意一般不能写成 test.sh,否则 shell 会去 PATH 里找文件
      • 如果当前目录不在 PATH 里,写成 test.sh 会提示找不到文件
    • 作为解释器参数运行:bash test.sh
      • 直接运行解释器,其参数就是脚本文件路径
      • 以这种方式运行脚本时,脚本中指定的解释器将不起作用,而会以运行时指定的解释器为准

    Shebang 行:#! env

    脚本的第一行通常是指定解释器,这一行以 #! 字符开头,这个字符称为 Shebang,所以这一行就叫做 Shebang 行。注意:不能在 Shebang 行的行末添加注释。

    #!/bin/bash          # 【#!】后面是脚本解释器存放的位置,与脚本解释器之间可以有空格
    #!/bin/sh            # 必须确保 Bash 解释器存放在指定目录,否则脚本就无法执行
    
    ./script.sh          # 脚本中有 Shebang 行时,可以直接调用执行脚本
    /bin/sh ./script.sh  # 如果没有 Shebang 行,需要将脚本传给解释器才行执行
    

    一般情况下,我们并不需要区分 Bourne ShellBourne Again Shell,所以用 #!/bin/sh#!/bin/bash 都可以

    env 命令总是指向 /usr/bin/env 文件,如果不知道某个命令的具体路径,或者希望兼容其他用户的机器,可以使用 env 命令返回 Bash 可执行文件的位置。

    #!/usr/bin/env bash  # 查找【$PATH】环境变量里面第一个匹配的【bash】的路径
    #!/usr/bin/env node  # 其他脚本文件也可以使用,比如 Node.js 脚本的 Shebang 行
    

    权限和路径:chmod PATH

    脚本执行的前提条件是,脚本需要有执行权限。可以使用下面的命令,赋予脚本执行权限。

    chmod +x script.sh   # 给所有用户执行权限
    chmod +rx script.sh  # 给所有用户读权限和执行权限(755)
    chmod u+rx script.sh # 只给脚本拥有者读权限和执行权限(700)
    
    chmod 755 script.sh  # 另一种语法,给所有用户读权限和执行权限
    

    脚本调用时需要指定脚本的路径,如果脚本存放在环境变量 $PATH 中,就不需要指定路径。

    # 可在主目录新建一个【~/bin】子目录,专门存放可执行脚本,然后把【~/bin】加入【$PATH】
    export PATH=$PATH:~/bin  # 将目录【~/bin】添加到环境变量【$PATH】的末尾
    source ~/.bashrc         # 将命令加到【~/.bashrc】中,然后重新加载一次,配置即可生效
    

    脚本参数:$n $# $@

    调用脚本的时候,脚本文件名后面可以带有参数。脚本文件内部,可以使用特殊变量,引用这些参数。

    • $0:脚本文件名
    • $n:传递到脚本的第 n 个参数的值,建议使用 ${n} 来获取
    • $#:传递到脚本的参数数量
    • $@:全部的参数,参数之间使用空格分隔,可以利用 for 循环读取每一个参数
    • $*:全部的参数,参数之间使用 $IFS 的第一个字符(默认为空格)分隔,也可以自定义
    # 调用格式【./test.sh 29 男】
    chmod 777 test.sh
    echo $0   #【./test.sh】脚本文件名
    echo $1   #【29】注意,对于 command -o xxx yyy,$1 是 -o,$2 是 xxx
    echo $2   #【男】
    echo $#   #【2】传递到脚本的参数数量
    echo $*   #【29 男】以一个字符串显示所有向脚本或函数传递的参数
    echo $@   #【29 男】
    

    $*$@ 的区别:在双引号中,$* 的作用是将所有参数当做一个参数

    # 【./test.sh 29 男 白乾涛】
    for i in $@;   do echo -n $i-; done;  # 【29-男-白乾涛】
    for i in $*;   do echo -n $i-; done;  # 【29-男-白乾涛】
    
    for i in "$@"; do echo -n $i-; done;  # 【29-男-白乾涛】
    for i in "$*"; do echo -n $i-; done;  # 【29 男 白乾涛】注意,这里是将所有参数当做一个参数
    
    for i in '$@'; do echo -n $i-; done;  # 【$@】
    for i in '$*'; do echo -n $i-; done;  # 【$*】
    

    读取命令执行结果:$?

    环境变量 $? 可以读取前一个命令的返回值(即命令执行结果)。

    cd $some_directory
    if [ "$?" = "0" ]; then  # 可以直接写成【if cd $some_directory; then】
      rm *    # 如果执行成功,就删除该目录里面的文件
    else
      echo "无法切换目录!" 1>&2
      exit 1  # 否则退出脚本
    fi
    
    cd $some_directory && rm *   # 第一步执行成功,才会执行第二步
    cd $some_directory || exit 1 # 第一步执行失败,才会执行第二步
    

    配置项参数终止符:--

    --- 开头的参数,会被 Bash 当作配置项解释。如果它们不是配置项,而是实体参数的一部分,就需要使用 配置项参数终止符 -- 告诉 Bash,在它后面以 --- 开头的参数不是配置项,而是实体参数。

    cat -- -f       # 可以正确展示文件 -f 的内容
    cat -- --file   # 可以正确展示文件 --file 的内容
    
    ls -- $myPath   # 强制将变量 $myPath 当作实体参数(即路径名)解释
    grep -- "--xxx" test.txt # 在文件里面搜索字符串【--xxx】
    

    脚本基础命令

    移除脚本参数:shift

    使用 shift 命令可以移除脚本参数。

    shift    # 移除脚本当前的第一个参数,即原来的 $2 变成 $1,原来的 $3 变成 $2
    shift 3  # 移除脚本当前的前三个参数,即原来的 $4 变成 $1,原来的 $5 变成 $2
    

    解析脚本参数:getopts

    在脚本内部,可以使用 getopts 命令解析复杂的脚本参数,比如取出所有的带有前置连词线 - 的参数。

    它带有两个参数。

    • 第一个参数 optstring 是由脚本所有的连词线参数组成的字符串,顺序不重要
      • 带有参数值的配置项参数,后面必须带有一个冒号 :
      • 比如,某个脚本可以有三个配置项参数 -l-h-a,其中只有 -a 可以带有参数值,另外两个都是开关参数,那么第一个参数就可以写成 lha:
    • 第二个参数 name 是一个变量名,用来保存当前取到的配置项参数
    while getopts 'lha:' OPTION; do # 每次循环就会读取一个连词线参数以及对应的参数值
      count=$(($OPTIND - 1)) # 代表已经处理的参数个数,变量 OPTIND 在开始执行前是 1
      case "$OPTION" in      # 变量 OPTION 保存的是当前处理的那一个连词线参数
        l)
          echo "参数 l,count=$count"  # 不带参数值时,每次执行 OPTIND 就会加 1
          ;;
        h)
          echo "参数 h,count=$count"
          ;;
        a)  # 如果连词线参数带有参数值,处理参数的时候可以获取参数值
          echo "参数 a,count=$count"  # 带参数值时,每次执行 OPTIND 就会加 2
          echo "参数值是 $OPTARG"  # 环境变量 $OPTARG 会保存当前连词线参数的参数值
          ;;
        ?)  # 如果输入了 optstring 中未指定的参数,比如 -x,那么 OPTION 等于【?】
          echo "存在非法参数,脚本使用格式: $(basename $0) [-l] [-h] [-a xxx]" >&2
          exit 1  # 终止当前脚本的执行,并向 Shell 返回一个退出值,1 代表执行失败
          ;;
      esac
    done  # 正常退出 while 循环,就意味着连词线参数全部处理完毕
    
    echo "已处理的参数个数:$count"  # 不仅指连词线参数,还包括连词线参数携带的参数值
    shift $count   # 将这些连词线参数移除后,就可以使用 $1 $2 等处理命令的主参数
    echo "主参数:$@"
    
    • 只要遇到不带连词线的参数,getopts 就会执行失败,从而退出 while 循环
      • 可以解析 command -l -a xxx -h yyy 中的连词线参数 h
      • 不可以解析 command -l zzz -a xxx -h yyy 中的连词线参数 ah
    • getopts 也可以正确处理多个连词线参数写在一起的形式,比如command -lh
    • 变量 $OPTIND
      • getopts 开始执行前是 1,然后每次执行就会加 12(带参数值)
      • 连词线参数全部处理完毕后,$OPTIND - 1 就是已经处理的参数个数(包含参数值)
      • 使用 shift 命令将这些参数移除后,就可以使用 $1 $@ 等处理命令的主参数

    终止当前脚本:exit

    exit 命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。

    exit   # 终止当前脚本,并将最后一条命令的退出状态,作为整个脚本的退出状态
    exit 0 # 退出值 0 代表执行成功(正常结束),只要退出值非 0,就可认为执行失败
    exit 1 # 退出值 1 代表执行失败(发生错误),2 用法不对,126 不可执行,127 命令未发现
    

    exitreturn 命令的区别:

    • return 命令是函数的退出,并返回一个值给调用者,脚本依然执行
    • exit 是整个脚本的退出,如果在函数之中调用 exit,则退出函数,并终止脚本执行

    加载外部脚本:source

    • source 命令可以在当前 Shell 执行脚本,而不会新建一个子 Shell
    • source 命令执行脚本时,不需要 export 变量就可以在脚本中读取当前 Shell 的变量
    • source 命令通常用于重新加载一个配置文件,或在脚本内部加载外部库
    • source xxx 可简写为 . xxx
    #!/bin/bash
    source ./lib.sh      # 在脚本内部加载外部库
    function_from_lib    # 在脚本里面使用外部库定义的函数
    echo $var_from_shell # 在脚本里面读取当前 Shell 的变量
    

    指定命令别名:alias

    alias 命令用来为一个命令指定别名。一般来说,都会把常用的别名写在 ~/.bashrc 的末尾。

    alias NAME=DEFINITION # 等号两侧不能有空格
    alias search=grep     # 为 grep 命令起一个 search 的别名
    
    alias today='date +"%A, %B %-d, %Y"' # 为长命令指定一个更短的别名
    today    # 【星期六, 一月 22, 2022】
    
    alias rm='rm -i'     # 通过指定别名,可以修改已有命名的默认行为
    alias echo='echo →'  # 别名也可以接受参数,参数会直接传入原始命令
    
    alias       # 显示所有别名
    unalias lt  # 解除别名
    

    注意,只能为命令定义别名,为其他部分(比如很长的路径)定义别名是无效的。

    创建临时文件:mktemp

    mktemp 命令是为安全创建临时文件而设计的:

    • 生成的临时文件名是随机的,权限是只有用户本人可读写
    • 支持唯一文件名和清除机制
    • 可以使用 trap 命令指定退出时的清除操作
    • 注意:此命令创建临时文件前,不会 检查临时文件是否存在,需要自行判断是否创建成功
    • 注意:/tmp 目录是所有人可读写的,在此目录下创建的文件默认是所有人可读

    参数:

    • -d:使用默认的文件名模板创建一个临时目录,默认的文件名模板是 tmp. 后接十个随机字符
    • -p:指定临时文件所在的目录,默认是使用环境变量 $TMPDIR,未设置值时使用 /tmp 目录
    • -t:指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的 X 字符,表示随机字符
    mktemp                 # 【/tmp/tmp.4GcsWSG4vj】使用默认的文件名模板创建一个临时文件
    mktemp -d              # 【/tmp/tmp.Wcau5UjmN6】使用默认的文件名模板创建一个临时目录
    mktemp -p /home/bqt/   # 【/home/bqt/tmp.FOKEtvs2H3】指定临时文件所在的目录
    mktemp -t tmp.XXXXXXX  # 【/tmp/tmp.yZ1HgZV】指定临时文件的文件名模板
    
    echo "创建了临时文件 $(mktemp)" # 获取创建的临时文件
    TMPFILE=$(mktemp) || exit 1    # 创建失败时退出脚本
    trap 'rm -f "$TMPFILE"' EXIT   # 使用 trap 命令指定,脚本退出时清除临时文件
    

    响应系统信号:trap

    trap 命令用来在 Bash 脚本中响应系统信号,例如按 Ctrl + C 所产生的中断信号 SIGINT

    trap -l                       # 列出所有 64 个系统信号
    trap [动作] [信号1 信号2 ...]  # 命令格式,动作指的是一个 Bash 命令
    trap 'rm -f "$TMPFILE"' EXIT  # 遇到 EXIT 信号时(脚本退出时),执行指定的清理命令
    

    常用的信号有以下几个:

    • SIGHUP:编号 1,脚本与所在的终端脱离联系
    • SIGINT:编号 2,用户按下 Ctrl + C,意图让脚本终止运行
    • SIGQUIT:编号 3,用户按下 Ctrl + 斜杠,意图退出脚本
    • SIGKILL:编号 9,该信号用于杀死进程
    • SIGTERM:编号 15,这是 kill 命令发出的默认信号
    • EXIT:编号 0,这不是系统信号,而是 Bash 脚本特有的信号,只要退出脚本就一定会产生
    #!/bin/bash
    
    trap 'rm -f "$TMPFILE"' EXIT       # 脚本退出时清除临时文件
    TMPFILE=$(mktemp) || exit 1
    ls /etc > $TMPFILE
    if grep -qi "kernel" $TMPFILE; then echo 'find'; fi
    

    上面代码中,不管是脚本正常执行结束,还是用户按 Ctrl + C 终止,都会产生 EXIT 信号,从而触发删除临时文件。

    注意,trap 命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。

    脚本调试

    编写 Shell 脚本的时候,一定要考虑到命令失败的情况,否则很容易出错。

    #! /bin/bash
    dir=/path/not/exist
    cd $dir   # 如果目录不存在,cd 命令就会执行失败,就不会改变当前目录
    
    rm *             # 脚本会继续执行,从而删光当前目录的文件
    cd $dir && rm *  # 如果变量为空,cd 会进入用户主目录,从而删光用户主目录的文件
    [[-d $dir]] && cd $dir && rm *       # 比较安全的写法,先判断目录是否存在再执行
    [[-d $dir]] && cd $dir && echo rm *  # 不删除文件,而是先打印要删除的文件看一下
    

    命令错误处理

    如果脚本里面有运行失败的命令(返回值非 0),Bash 只是显示有错误,并且继续执行后面的命令,而不会终止执行。

    实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。

    command || exit 1       # 如果命令 command 执行失败了,脚本停止执行
    command1 && command2    # 如果 command1 执行成功了,才继续执行 command2
    
    command || {echo "失败"; exit 1;}             # 停止执行之前完成多个操作
    if ! command; then echo "失败"; exit 1; fi    # 写法二
    if ["$?" -ne 0]; then echo "失败"; exit 1; fi # 写法三
    

    几个调试参数

    为了方便 Debug,有时在启动 Bash 的时候,可以加上启动参数。

    bash -n scriptname  # 不运行脚本,只检查是否有语法错误
    bash -v scriptname  # 每一行语句输出运行结果前,先输出该行语句
    bash -x scriptname  # 每一个命令处理之前,先输出该命令,再执行该命令
    

    调试参数 -x

    • bash-x 参数可以在执行每一行命令之前,打印该命令
    • 输出的命令之前的 + 号,是由环境变量 PS4 决定的,可以修改这个变量的值
    • set 命令也可以设置 Shell 的行为参数,有利于脚本除错
    bash -x          # 加上 -x 参数启动一个 bash,后续执行每条命令前,都会先显示该命令
    bash -x test.sh  # 加上 -x 参数执行脚本,执行脚本中的每条命令前,都会先显示该命令
    #! /bin/bash -x  # 参数 -x 也可以写在脚本的 Shebang 行(演示代码,这一行不能有注释)
    export PS4='- '  # 可以修改环境变量 PS4 的值,默认为 + 号
    
    $ echo `pwd`     # 执行此命令前会先执行 pwd 命令,有 n 层调用就会有 n 个连续的 +
    ++ pwd           # 执行每条命令(pwd)前,都会先显示该命令,注意这里打印的是 ++
    + echo /home/bqt # 执行每条命令(echo)前,都会先显示该命令
    /home/bqt        # 这一行是 echo 命令自身的打印
    

    调试用的环境变量

    • LINENO:返回变量在脚本文件中的行号
    • FUNCNAME:返回当前的函数调用堆栈。数组的 0 号成员是当前调用的函数,1 号成员是调用当前函数的函数,记作 m(i)
    • BASH_SOURCE:返回当前的脚本调用堆栈。数组的 0 号成员是当前执行的脚本,1 号成员是调用当前脚本的脚本,记作 n(i)
    • BASH_LINENO:返回每一轮调用对应的行号。f(i)m(i) 是一一对应关系,表示 ${FUNCNAME[$i]} 在调用它的脚本文件 ${BASH_SOURCE[$i+1]} 里面的行号
    #!/bin/bash
    
    echo "当前行号 $LINENO"                        # 当前行号 3
    echo "${FUNCNAME[0]} - ${FUNCNAME[1]}"        # main -
    echo "${BASH_SOURCE[0]} - ${BASH_SOURCE[1]}"  # ./test.sh -
    echo "${BASH_LINENO[0]} - ${BASH_LINENO[1]}"  # 0 -
    

    脚本执行环境

    Bash 执行脚本时,会创建一个子 Shell。这个子 Shell 就是脚本的执行环境,Bash 默认给定了这个环境的各种参数。

    定制环境参数:set

    set 命令用来修改子 Shell 环境的运行参数,即定制环境。

    • set -u:遇到不存在的变量时,报错并停止执行,而不是继续向下执行
    • set -n:不运行命令,只检查语法是否正确
    • set -x:在运行结果之前,先输出执行的那一行命令。可使用 set +x 关闭
    • set -e:只要返回值非 0 就终止执行。可使用 set +e 关闭,等价于 command || true
    • set -f:不对通配符进行文件名扩展。可使用 set +f 关闭
    • set -v:打印 Shell 接收到的每一行输入。可使用 set +v 关闭
    • set -E函数内的命令在执行错误时,即使设置了 set -e,仍然可以被 trap 命令捕获
    • set -o pipefail:管道命令中只要一个子命令失败,整个管道命令就失败
    • set -o noclobber:防止使用重定向运算符 > 覆盖已经存在的文件
    set      # 显示所有的环境变量和 Shell 函数
    set -u   # 等同于【set -o nounset】遇到不存在的变量时,报错并停止执行
    set -n   # 等同于【set -o noexec】不运行命令,只检查语法是否正确
    
    set -x   # 等同于【set -o xtrace】在运行结果之前,先输出执行的那一行命令
    set -e   # 等同于【set -o errexit】只要返回值非 0 就终止执行
    set -f   # 等同于【set -o noglob】不对通配符进行文件名扩展
    set -v   # 等同于【set -o verbose】打印 Shell 接收到的每一行输入
    
    set -E            # 使函数内的命令在执行错误时,始终可以被 trap 命令捕获
    set -o pipefail   # 管道命令中只要一个子命令失败,整个管道命令就失败
    set -o noclobber  # 防止使用重定向运算符 > 覆盖已经存在的文件
    
    set -Eeuxo pipefail         # 参数可以采用混合写法,建议放在 Bash 脚本的头部
    bash -euxo pipefail test.sh # 也可以在执行 Bash 脚本的时候,从命令行传入参数
    

    关于 set -o pipefail 的解释:

    • 参数 set -e 不适用于管道命令(即多个子命令通过管道运算符 | 组合成的命令)
    • Bash 默认会把管道命令中,最后一个子命令的返回值,作为整个命令的返回值
    • 所以,只要最后一个子命令返回值非 0,管道命令就代表执行成功
    • 通过 set -eo pipefail 可实现,管道命令中只要一个子命令失败,整个管道命令就失败

    调整环境参数:shopt

    shopt 命令跟 set 命令的作用很类似,也可以用来调整 Shell 的参数。区别是:set 是 POSIX 规范的一部分,而 shopt 是 Bash 特有的命令。

    shopt           # 查看所有参数,以及它们的开关状态
    shopt xxx       # 查询某个参数的开关状态
    shopt -s xxx    # 打开某个参数
    shopt -u xxx    # 关闭某个参数
    shopt -q xxx    # 通过命令的执行结果 $? 查询某个参数的开关状态,0 表示打开,1 表示关闭
    

    对话 Session

    用户每次使用 Shell,都会开启一个与 Shell 的 Session。

    Session 有两种类型:登录 Session 和非登录 Session,也可以叫做 login shell 和 non-login shell。

    登录 Session

    登录 Session 是用户登录系统以后,系统为用户开启的原始 Session,通常需要用户输入用户名和密码进行登录。

    登录 Session 一般会进行 整个系统环境 的初始化,启动的初始化脚本依次如下:

    • 针对所有用户的初始化脚本
      • /etc/profile:全局配置脚本。Linux 更新时候会更新此文件,因此建议不要直接修改这个文件
      • /etc/profile.d/:此目录所有 .sh 文件。如果想修改所有用户的登陆环境,建议在此目录中新建 .sh 脚本
    • 针对当前用户的初始化脚本
      • ~/.bash_profile:个人配置脚本。该脚本若存在则不再往下执行。一般建议在此文件中修改个人的登录环境
      • ~/.bash_login:C shell 的初始化脚本。该脚本若存在则不再往下执行
      • ~/.profile:Bourne shell 和 Korn shell 的初始化脚本
    bash --login      # 强制执行登录 Session 时会执行的脚本
    bash --noprofile  # 跳过上面这些 profile 初始化脚本
    

    非登录 Session

    非登录 Session 是用户进入系统以后,手动创建的 Session,这时不会进行环境初始化。比如,在命令行执行 bash 命令,就会新建一个非登录 Session。

    非登录 Session 的初始化脚本依次如下"

    • /etc/bash.bashrc:针对所有用户的初始化脚本
    • ~/.bashrc:仅针对当前用户的初始化脚本

    对用户来说,~/.bashrc 通常是最重要的脚本。非登录 Session 默认会执行它,而登录 Session 一般也会通过调用执行它。每次新建一个 Bash 窗口,就相当于新建一个非登录 Session,所以 ~/.bashrc 每次都会执行。

    注意,执行脚本相当于新建一个非互动的 Bash 环境,这种情况不会调用 ~/.bashrc

    bash --norc           # 禁止在非登录 Session 执行 ~/.bashrc 脚本
    bash --rcfile testrc  # 指定用另一个脚本代替 ~/.bashrc 脚本
    

    退出时执行的脚本

    脚本在每次退出 Session 时都会执行脚本 ~/.bash_logout,通常用来做一些清理工作和记录工作,比如删除临时文件,记录用户在本次 Session 花费的时间。

    如果没有退出时要执行的命令,这个文件也可以不存在。

    2022-01-15

  • 相关阅读:
    我和计算机
    十四周学习记录
    十五周个人作业
    怎样成为一个高手
    C语言第0次作业
    C语言博客作业03函数
    C博客作业01分支、顺序结构
    C语言博客作业02循环结构
    Rails后台,Firefox Addons前端,JS的AJAX调用
    Ruby 三元一次线性方程组
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/15806604.html
Copyright © 2020-2023  润新知