Bash——Shell编程总结


整体说明

  • Shell 脚本是一种用命令行解释器(Shell)来执行的程序
  • 常用语自动化重复性的任务、编写复杂的程序流程

Shell 脚本执行相关基础

  • 通常,Shell 脚本文件的扩展名是 .sh,但这不是强制的

  • 编写并执行一个 shell 脚本的步骤

  • 第一步,创建文件和编写代码:

    1
    2
    3
    4
    #!/bin/bash
    # 这是一个简单的脚本,用于打印 "Hello, Shell!"

    echo "Hello, Shell!"
    • #!/bin/bash: 这被称为 Shebang
      • 这行代码告诉系统使用 /bin/bash 这个解释器来执行这个脚本(建议指定)
    • #: 井号 # 后的内容是注释
    • echo: 用于在终端上打印文本的命令
  • 第二步,赋予执行权限: 默认情况下,新创建的文件没有执行权限,需要使用 chmod 命令来添加它

    1
    chmod +x hello.sh
    • +x 表示添加执行权限
  • 第三步,运行脚本: 现在,可以通过以下方式运行的脚本

    1
    ./hello.sh
    • ./ 表示在当前目录执行
    • 若没有执行权限的脚本可以使用 sh ./hello.sh 来执行(此时不再使用指定的解释器)
      • Shebang 只有在直接运行脚本时才生效

变量的使用

  • 变量用于存储数据,在 Shell 脚本中,定义变量不需要声明类型

  • 定义变量

    1
    2
    name="Gemini"
    age=25
    • 注意 : 赋值号 = 的两边 不能 有空格
  • 使用变量:要使用变量,需要在变量名前面加上美元符号 $

    1
    2
    3
    4
    5
    6
    7
    #!/bin/bash

    name="Alice"
    echo "我的名字是 $name。"

    # 也可以用大括号括起来,这在某些情况下很有用,比如变量名紧跟在其他文本后面
    echo "我的名字是 ${name},今年 ${age} 岁。"
  • 环境变量的读取:环境变量是系统预设的变量,可以在任何地方访问

    • 例如,$PATH 存储了系统命令的搜索路径,$HOME 存储了用户的主目录
      1
      echo "我的主目录是 $HOME"

特殊变量

  • Shell 提供了一些特殊的变量,用于获取脚本运行时的信息

    变量名 含义
    $0 脚本文件名
    $1, $2, … 命令行参数。$1 是第一个参数,$2 是第二个,以此类推。
    $# 传递给脚本的参数个数
    $* 所有的命令行参数,作为一个字符串
    $@ 所有的命令行参数,每个参数是独立的字符串
    $? 上一个命令的退出状态码。0 表示成功,非 0 表示失败。
    $$ 当前脚本的进程 ID
  • 特殊变量使用示例

    1
    2
    3
    4
    5
    6
    #!/bin/bash

    echo "脚本名称: $0"
    echo "参数个数: $#"
    echo "第一个参数: $1"
    echo "所有参数: $*"
    • 保存为 args.sh,然后运行 bash args.sh apple banana cherry,观察输出
      1
      2
      3
      4
      脚本名称: hello.sh
      参数个数: 3
      第一个参数: apple
      所有参数: apple banana cherry

条件判断(if 语句)

  • if 语句用于根据条件执行不同的代码块

  • if 基本语法

    1
    2
    3
    if [ 条件 ]; then
    # 如果条件为真,执行这里的代码
    fi
  • if 完整语法

    1
    2
    3
    4
    5
    6
    7
    if [ 条件 ]; then
    # 如果条件为真
    elif [ 其他条件 ]; then
    # 如果第一个条件为假,且第二个条件为真
    else
    # 所有条件都为假
    fi
    • 特别注意[] 之间必须有空格

if 语句相关的常用条件运算符号/表达式

  • 字符串比较:
    • ===: 字符串相等
    • !=: 字符串不相等
    • -z: 字符串为空
    • -n: 字符串不为空
  • 数字比较:
    • -eq: 相等(Equal)
    • -ne: 不相等(Not Equal)
    • -gt: 大于(Greater Than)
    • -lt: 小于(Less Than)
    • -ge: 大于等于(Greater than or Equal)
    • -le: 小于等于(Less than or Equal)
  • 文件测试:
    • -e: 文件或目录存在
    • -f: 是文件
    • -d: 是目录
    • -r: 可读
    • -w: 可写
    • -x: 可执行
  • 常用条件表达式示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #!/bin/bash

    if [ -f "test.txt" ]; then
    echo "文件 test.txt 存在。"
    else
    echo "文件 test.txt 不存在。"
    fi

    if [ 10 -gt 5 ]; then
    echo "10 大于 5。"
    fi

循环语句

  • 循环用于重复执行一段代码,直到满足特定条件

for 循环

  • for 循环用于遍历列表中的项目
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #!/bin/bash

    # 遍历一个列表
    for fruit in apple banana cherry; do
    echo "水果: $fruit"
    done

    # 遍历数字范围
    for i in {1..5}; do
    echo "数字: $i"
    done

while 循环

  • 当条件为真时,持续执行循环

    1
    2
    3
    4
    5
    6
    7
    8
    #!/bin/bash

    count=1

    while [ $count -le 5 ]; do
    echo "计数: $count"
    count=$((count + 1)) # 算术运算
    done
    • $((...)) 是 Shell 中的算术扩展,用于执行数学运算

函数

  • 定义函数方式一:

    1
    2
    3
    4
    function my_func {
    # 函数体
    echo "这是一个函数。"
    }
  • 定义函数方式二:

    1
    2
    3
    4
    my_func() {
    # 函数体
    echo "这也是一个函数。"
    }
  • 调用函数

    1
    my_func
  • 函数参数:函数内部的参数使用 $1, $2 等来访问,与脚本的命令行参数类似(定义时不需要指定参数)

    1
    2
    3
    4
    5
    6
    7
    8
    #!/bin/bash

    greet() {
    echo "Hello, $1!"
    return 0
    }

    greet "Bob" # 调用函数并传递参数 "Bob"
    • return 命令可以返回一个退出状态码,通常 0 表示成功,非 0 表示失败

附录:最佳实践(持续更新)

常用实践

  • 为了避免因为文件名中含有空格而导致的错误,始终使用双引号来引用变量
    • "$name"
  • 使用 $((...)) 进行算术运算
    • 例如 $((a+b)),要特别注意是$加双括号
  • 使用 $(...) 来执行命令并获取其输出(注意是$加单括号)
    • 例如 current_date=$(date),这比老式的反引号 `` 更推荐使用

脚本开头的常用设置

  • 在 Shell 脚本的开头,通常会看到一些 set 命令,这些设置可以编写更健壮、更可靠的脚本,有效避免一些常见的错误

  • 通常,一个健壮的 Shell 脚本开头会包含以下几行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #!/bin/bash

    # 遇到错误立即退出
    set -e

    # 如果管道中的任何一个命令失败,整个管道命令就失败
    set -o pipefail

    # 打印所有执行的命令,方便调试
    # set -x

    # 未设置的变量会立即报错退出
    set -u
    • 这些命令可以单独使用,也可以组合在一起,比如 set -e -o pipefail 或更简洁的 set -euo pipefail
  • 对常见组合 set -xeo pipefail 的详解,包含了以下几个命令

    • set -eset -o errexit
      • 含义是如果脚本中的任何命令失败(返回非零退出状态),脚本会立即退出
      • 可以防止脚本在遇到错误后继续执行,从而导致意想不到的后果
        • 例如,正在删除一个目录,但 rm 命令失败了,如果没有 -e,脚本会继续执行下面的命令,可能导致数据不一致或更严重的错误
    • set -xset -o xtrace
      • 含义是在执行每个命令之前,先打印该命令及其所有参数
      • 对于 调试 非常有用,当运行脚本时,你会看到每个命令的完整展开形式,帮助你追踪脚本的执行流程和变量的值
        • 在调试完成后,通常会注释掉或删除 -x
    • set -o pipefail
      • 含义是如果管道(|)中的任何一个命令失败,整个管道命令的退出状态就是失败
        • 注:不论前面的管道命令是否成功,后面的管道都会执行,且整个管道的状态由最后一个状态决定
      • 默认情况下,管道命令的退出状态只取决于最后一个命令
        • 问题在于这意味着 command1 | command2,即使 command1 失败了,只要 command2 成功,整个命令依然被认为是成功的
        • pipefail 解决了这个问题,确保你不会忽略管道中间的错误
    • set -uset -o nounset
      • 含义是如果尝试使用一个未设置的变量,脚本会立即报错并退出(注意:如果不设置这个参数是不会报错,也不会退出的,shell 脚本会跳过这行命令)
      • 这行命令可以避免因拼写错误或变量未正确赋值而引发的问题
        • 例如,如果想使用 $user,但无意中写成了 $userrset -u 会立即提醒你,而不是让脚本使用一个空值继续执行

脚本输出

  • 将脚本的输出重定向到文件,方便调试和后续查看,例如 my_script.sh > log.txt 2>&1
    • 这个命令将脚本的 标准输出标准错误 都重定向到一个文件中
  • > log.txt 2>&1 这个命令可以分解为以下几个部分来理解:
    • 标准输出 (Standard Output, stdout) :文件描述符为 1
    • 标准错误 (Standard Error, stderr) :文件描述符为 2
    • >标准输出(stdout)重定向
      • 注意这个操作符的定义是标准输出重定向 ,相当于是 1> 的简写
    • 2> 标准错误(stderr)重定向
    • log.txt:这是重定向的目标文件,标准输出的内容将被写入到这个文件中
    • &1:这是一种特殊的写法,表示将文件描述符 2(标准错误)重定向到与文件描述符 1(标准输出)相同的位置
    • 总体来看,这行命令将有两个效果:
      • 将文件描述符 2(标准错误)重定向到与文件描述符 1(标准输出)相同的位置
      • 1(标准输出)重定向到文件
  • TLDR:my_script.sh > log.txt 2>&1 表示 执行 my_script.sh,并将所有成功输出和错误输出都发送到 log.txt 文件中

输出脚本同时打印到屏幕

  • 如果想既将输出和错误记录到文件,又同时在屏幕上看到它们,可以使用 tee 命令
  • tee 命令的作用是“分流”输入,它将标准输入的内容复制一份到标准输出,同时写入一个或多个文件

将所有输出都分流

  • 使用 tee 命令,可以这样写:

    1
    my_script.sh 2>&1 | tee log.txt
    • my_script.sh 2>&1:首先,将脚本的标准错误(2)合并到标准输出(1
      • 这样,所有的成功信息和错误信息都变成了“标准输出”
    • |:这是一个管道符号,它将前一个命令的标准输出作为下一个命令的标准输入
    • tee log.txt:将管道接收到的所有内容(即脚本的所有输出)打印到屏幕(标准输出),同时将其 写入 log.txt 文件

只将标准输出分流,错误输出只打印到屏幕

  • 如果只希望将成功信息记录到文件,而错误信息只在屏幕上显示,可以这样:

    1
    my_script.sh | tee log.txt
    • 在这种情况下
      • 标准输出会通过管道传输给 tee,并同时显示在屏幕和写入文件;
      • 标准错误不会被管道捕获 ,它会直接打印到屏幕上

eval 命令的使用

  • shell 脚本中,eval 是一个用于执行字符串作为命令的内置命令
    • eval 会先对传入的参数进行二次解析 ,然后执行解析后的结果作为 shell 命令
  • 具体来说,eval 的功能包括:
    • 1)将所有参数拼接成一个字符串
    • 2)shell 会对这个字符串进行再次解析(包括变量替换、通配符扩展等)
    • 3)最后执行解析后的命令

eval 命令使用示例

  • 动态执行命令:当命令需要动态生成时(比如包含变量)

    1
    2
    cmd="ls -l"
    eval $cmd # 相当于直接执行 `ls -l`
  • 处理复杂变量引用:特别是多层变量嵌套时

    1
    2
    3
    var1="hello"
    var2="var1"
    eval echo \$$var2 # 输出 hello,相当于 `echo $var1`,并最终等价于 `echo hello`
  • 动态生成变量名:

    1
    2
    3
    4
    5
    for i in 1 2 3; do
    eval "num$i=$i" # 生成变量 num1=1, num2=2, num3=3
    done
    echo $num1 # 输出 1
    echo $num2 # 输出 2

eval 命令使用注意事项

  • eval 会执行任何解析后的命令,使用不当可能带来安全风险(特别是处理用户输入时)

    • 危险示例:下面的示例中,如果 user_input 包含恶意命令(如 ; rm -rf /),会被执行
      1
      2
      user_input="some input"
      eval "echo $user_input"
  • 由于会进行二次解析,可能导致意想不到的结果,建议谨慎使用

  • 复杂场景下,有时可以用函数或数组替代 eval 实现更安全的逻辑