December 30, 2024
By: Kevin

及格线上的shell

  1. shell是古老的
  2. shell是可爱的
  3. shell是标准的
  4. 变量(Variable)
  5. 环境变量(evn)
  6. 参数(parameters)
  7. 内置命令(buildin)
  8. 引用(quote)
  9. 通配符(globs)
  10. 重定向(redirect)
  11. 各类括号
  12. 非标准特性
  13. 条件(if)
  14. 循环(for/while)
  15. 读取输入(read)
  16. 函数(Function)
  17. 管道(pipe)
  18. 参数扩展(${var})
  19. 后台进程
  20. 子shell(subshell)
  21. 信号捕获(trap)
  22. 错误处理
  23. 调试 (Debug)

了解以下内容, 在shell的世界应该能做到生活自理...

shell是古老的

有些老毛病...

  • x = 2 赋值不好使!
  • 光引号就有四种, 反引号 \ , 半引号 ` , 双引号", 单引号'!!
  • if 居然需要启动一个进程来执行判断!!!

shell是可爱的

  • 很容易上手

    ls /tmp
    
    |                                                                        |
    |------------------------------------------------------------------------|
    | 2f093fe9-a235-5d1c-9a62-3fae2cdf7eb1                                   |
    | 7c3338c5-8eec-50ae-bc4a-1f6969419936                                   |
    | 9e923688bfa34288aa93e2070da546bf                                       |
    | A262AD1E-C7E0-42C3-8A20-E532F767EEEE~16308~                            |
    | KoDF0sohej4R~bzJiALaHLuEGahtZOBlPKVzyXue1pw~                           |
    | com.apple.launchd.FMcex23J1V                                           |
    | com.logi.optionsplus.agent.logs                                        |
    | com.logi.optionsplus.logivoice.logs                                    |
    | com.logi.optionsplus.updater.logs                                      |
    | devio~semaphorelogihppOptionsPlusA7D4B139~-F9F9-44A6-9F5D-7BC0A9B8B80F |
    | homebrew.mxcl.emacs-plus.stderr.log                                    |
    | image                                                                  |
    | logitech~kirosagent~-80c9ef0fb86369cd25f90af27ef53a9e                  |
    | logitech~kiroslogivoice~-80c9ef0fb86369cd25f90af27ef53a9e              |
    | logitech~kirosupdater~                                                 |
    | perfcount                                                              |
    | powerlog                                                               |
    
  • 管道和重定向非常简单易用

    ls /tmp | wc -l
    
    17
    
  • 很适合批处理文件操作

    for f in *.png; do
        echo "$f" '->' "${f%.png}.jpg"
    done
    
    loop.png -> loop.jpg
    
  • 可以多进程执行

    for i in {1..12}; do
      sh -c 'echo 来自进程 $$ 的❤️' &
    done
    wait
    
    来自进程 90138 的❤️
    来自进程 90139 的❤️
    来自进程 90140 的❤️
    来自进程 90142 的❤️
    来自进程 90141 的❤️
    来自进程 90143 的❤️
    来自进程 90144 的❤️
    来自进程 90145 的❤️
    来自进程 90146 的❤️
    来自进程 90147 的❤️
    来自进程 90149 的❤️
    来自进程 90148 的❤️
    
  • 功能非常稳定, 30年前如此, 10年后依然如此.

  • 非常适合一些任务, 但也有一些局限

    • 不能显示图
    • 没有 数字 这个数据类型

shell是标准的

  • 多种 Unix shell, 包括 bash, zsh, fish, tcsh 等等, 查看用户的默认 shell.

    echo $SHELL
    
    /bin/zsh
    
  • POSIX标准定义了Unix shell的工作方式. 如果脚本符合POSIX标准, 可以在不同的shell上正常运行.

  • 不同的shell具有额外的功能, 这些功能不在POSIX标准中,

  • 本文介绍的都是 Bash/zsh 脚本, 大部分内容适用于其他shell.

变量(Variable)

  • 定义变量, 不能有空格, 以下为错误示例

    a = 100;
    echo $a
    
    a=100;
    echo $a
    
    100
    

    a = 100 相当与调用函数a, 并且传给它两个参数 =100.

  • 何使用变量

    echo $USER " " $HOME " " $PATH
    
    a123   /Users/a123   /usr/local/opt/sqlite/bin:/Users/a123/.jenv/shims:/usr/local/Cellar/apache-flink/1.17.1/libexec/bin:/usr/local/opt/dotnet@6/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin:/Library/TeX/texbin:/Applications/Wireshark.app/Contents/MacOS:/Applications/VMware Fusion.app/Contents/Public:/usr/local/share/dotnet:~/.dotnet/tools:/Users/a123/.cargo/bin:/Users/a123/.dotnet/tools:/usr/local/opt/fzf/bin
    
  • Bash 中只有字符串, 没有数字这个 类型.

    a=2
    b="2"
    
  • 用变量的时候, 双引号可以避免错误.

    fileName="abc       edf.txt"
    echo "$fileName"
    echo $fileName
    
    abc       edf.txt
    abc edf.txt
    
  • 防不小心加上的 后缀, 使用 ${}.

    a=大熊猫
    echo '$a2:' "$a2"
    echo '${a}2:' ${a}2
    
    $a2:
    ${a}2: 大熊猫2
    

环境变量(evn)

  • 每个进程都有环境变量, 查看当前 shell 的环境变量.

    env
    
    DISPLAY=123deiMac.local
    TERM=dumb
    MANPATH=
    LANG=en_US.UTF-8
    __CF_USER_TEXT_ENCODING=0x1F5:0x19:0x34
    LOGNAME=a123
    HOME=/Users/a123
    SHLVL=1
    XPC_SERVICE_NAME=0
    XPC_FLAGS=0x0
    PATH=/usr/local/opt/sqlite/bin:/Users/a123/.jenv/shims:/usr/local/Cellar/apache-flink/1.17.1/libexec/bin:/usr/local/opt/dotnet@6/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin:/Library/TeX/texbin:/Applications/Wireshark.app/Contents/MacOS:/Applications/VMware Fusion.app/Contents/Public:/usr/local/share/dotnet:~/.dotnet/tools:/Users/a123/.cargo/bin:/Users/a123/.dotnet/tools:/usr/local/opt/fzf/bin
    SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.FMcex23J1V/Listeners
    USER=a123
    TMPDIR=/var/folders/gl/10x44fsn6bn1799lkf_g096r0000gn/T/
    SHELL=/bin/zsh
    PWD=/Users/a123/sandbox/rc/learn-clojure/essays
    OLDPWD=/Users/a123/sandbox/rc/learn-clojure/essays
    _=/usr/bin/env
    
  • Bash 脚本中两种类型的变量: 环境变量和 shell 变量, 这两种变量的访问方式相同.

    echo 系统环境变量: '$HOME' "$HOME"
    shellVar=shell变量
    echo shell变浪 $shellVar
    
    系统环境变量: $HOME /Users/a123
    shell变浪 shell变量
    
  • 设置环境变量, 可以使用 export 命令, 也可以将 shell 变量转换为环境变量.

    export abc="10000"
    env | grep abc
    
    abc=10000
    
  • 子进程会继承父进程的环境变量, 在shell启动配置 .bashrc/.zshrc 文件中设置的 变量会影响所有从终端启动的程序.

  • shell 变量不会被继承, 也就是说, 在当前进程中设置的 shell 变量不会被子进程继承.

    ## 定义一个在当前Shell中的变量, 但不导出
    PARENT_VAR="shell变量, 不导出."
    
    ## 启动一个子Shell, 检查该变量是否可见
    bash -c 'echo "在子shell: PARENT_VAR = $PARENT_VAR"'
    
    ## 导出变量后再次检查
    export PARENT_VAR="I am exported."
    echo "当前 shell: PARENT_VAR = $PARENT_VAR"
    
    bash -c 'echo "在子shell: PARENT_VAR = $PARENT_VAR"'
    
    在子shell: PARENT_VAR =
    当前 shell: PARENT_VAR = I am exported.
    在子shell: PARENT_VAR = I am exported.
    
  • 如何在启动程序时设置环境变量

    MY_VAR="some_value" ./my_program
    

    或者

    export MY_VAR="some_value"
    ./my_program
    

参数(parameters)

  • 如何获取脚本的参数, 使用 $0, $1, $2 等变量来访问.

    svg2png a.svg a.png
      |       |      |
      v       v      v
      $0      $1     $2
    
  • 参数在编写简单脚本时的作用

    #!/bin/bash
    inkscape "$1" -b white --export-png="$2"
    

    以上脚本保存为 svg2png.sh

    执行的时候 svg2png a.svg a.png

  • $@ 变量可以获取除了 $0 之外的所有参数

    #!/bin/bash
    bash -c 'echo "$@"' arg1 arg2 arg3
    
    arg2 arg3
    
  • loop循环

    #!/bin/bash
    bash -c 'for i in "$@"
             do
                 echo $i
             done' arg1 arg2 arg3
    
    arg2
    arg3
    
  • shift 命令可以移除第一个参数

    #!/usr/bin/env bash
    bash -c 'echo "$@"
             shift
             echo "$@"' arg1 arg2 arg3
    
    arg2 arg3
    arg3
    

内置命令(buildin)

  • 大多数 Bash 命令都是程序

    which ls
    
    /bin/ls
    
  • Bash 中也有一些命令 比如 type, source alias read declare printf echo cd

    type sh
    type echo
    
    sh is /bin/sh
    echo is a shell builtin
    
  • alias的用处

    alias gc="git commit"
    

    放到 .bashrc, .zshrc 都很有用

引用(quote)

  • 双引号会展开变量, 而单引号不会展开变量.

    echo '$HOME'
    echo "$HOME"
    
    $HOME
    /Users/a123
    
  • 字符串可以是多行

    echo "这是一个
    多行的
    字符串"
    
    这是一个
    多行的
    字符串
    
  • 字符串靠在一起就会拼在一起, + 符号不能用于字符串拼接.

    echo "你好""青岛"
    
    你好青岛
    
    echo "你好"+"青岛"
    
    你好+青岛
    
  • 如果要输出 "', 需要用反引号 \

    echo ""''双引号单引号不"引起来"是看不到的
    echo \' 前面有单引号
    echo \" 前面有双引号
    
    双引号单引号不引起来是看不到的
    ' 前面有单引号
    " 前面有双引号
    

通配符(globs)

  • 通配符的作用, 它可以用来匹配字符串, 通配符和正则表达式不太一样

    symbolglobsregex
    ?1 charoptional
    *0+ charmultiple
    []any charclass
  • Bash 如何使用通配符来匹配文件名, 并用一个例子说明了如何使用通配符来查找所有以 .txt 结尾的文件.

    echo cat *.txt
    
    cat a.txt abc.txt b.txt 谭总问题.txt
    
    • cat 是完全不知道通配这个事情的
    • shell展开为 exec(cat, a.txt, abc.txt, b.txt)
  • 通配符中常用的三个特殊字符: *, ?[], 并说明了它们的含义.

  • 如何在命令中传递一个字面意义的星号, 并说明了需要使用引号来避免 Bash 将星号解释为通配符.

重定向(redirect)

  • Unix 程序通常使用标准输入(stdin), 标准输出(stdout)和标准错误输出(stderr)与终端交互

    wc shell.org
    
         330     912   10530 shell.org
    

    把文件重定向到wc程序的stdin

    wc < shell.org
    
         330     912   10530
    

    cat的stdout作为wc的stdin

    cat shell.org | wc
    
    347     945   10801
    
  • 2>&1 符号用于将标准错误输出重定向到标准输出 重定向本质上是 x>y 重定义 xy

    xoperatoryusage
    [1,2,file, default to 1][<>][&1,&2,file]redirect x to y

    举例

    operatorusage
    >xstdout to x
    1>xstdout to x
    2>xstderr to x
    1>&2stdout to stderr

    合并stderr到stdout

    ls 2>&1
    
    10things-learned.org
    2022-books.org
    数学雨伞下.org
    文档的重要性.org
    加缪写给老师的信.org
    
  • /dev/null 文件的作用, 它可以用来忽略程序的输出

    ls > /dev/null
    
  • 前面的 sudo 命令不会影响重定向操作

    sudo echo x > abc
    
    echo x | sudo tee abc
    

各类括号

  • (...) 用于声明一个数组

    a=(1 2 3)
    echo "$a"
    
    1 2 3
    
  • (...) 用于在子 shell 中执行命令 在新的进程中执行命令

    (cd .. ; pwd)
    pwd
    

    结果可以被赋值给变量

    a=$(cd .. ; pwd)
    echo $a
    
    /Users/a123/sandbox/rc/learn-clojure
    
  • {...} 用于将多个命令分组, 并在同一个进程中执行

    {cd .. ; pwd}
    pwd
    
    /Users/a123/sandbox/rc/learn-clojure
    /Users/a123/sandbox/rc/learn-clojure
    
  • $((...)) 用于进行算术运算

    echo $((1 + 1))
    
    2
    
  • [...] 用于判断条件 说来可能很难让人相信, 至少在linux上 /usr/bin/[ 是个程序, 对应 test, [_ _] _ 位置必须留空格.

    if [ "abc" = "abc" ]; then
        echo good
    fi
    
    good
    
  • [[...]] 用于判断条件 是首选, 它可以更便捷地处理模式匹配, 正则表达式以及更直观的逻辑条件

    if [[ "abc" = "abc" ]]; then
        echo good
    fi
    
    good
    

非标准特性

Bash/zsh 中一些功能不符合 POSIX 标准, 并且在 dash 和 sh 等 POSIX shell 中无法使用.

  • [[...]] 运算符, 扩展语法, 替代POSIX [...] 运算符.

  • a.{png,svg} 语法, 它可以用来生成多个文件

    echo *.{org,md}
    
  • diff <(./cmd1) <(./cmd2) 语法, 它使用进程替换来比较两个命令的输出, POSIX 只能用命名管道(named pipe).

  • local 关键字, 它可以用来声明局部变量, POSIX 只有全局变量.

  • for ((i=0; i < 3; i++)) 循环, 它类似于 C 语言中的循环, POSIX只能使用 for x in ... 循环.

条件(if)

  • shell 中每个命令都有一个退出状态, 0 表示成功, 其他数字表示失败. shell 将最后一个命令的退出状态存储在一个名为 $? 的特殊变量中.

  • 0 代表成功, 因为成功只有一种情况, 而失败有很多种情况. grep 命令为例, 说明了 grep 命令的退出状态可能为 1 或 2, 分别代表未找到匹配项和文件不存在.

    grep "shell 中每个" shell.org > /dev/null; echo $?
    
    0
    
    grep "!!!!!!!!" shell.org ; echo $?
    
    1
    
    grep "!!!!!!!!" no-such-file ; echo $?
    
    2
    
  • shell 中 if 语句的语法, 它会执行 COMMAND, 如果 COMMAND 返回 0(表示成功), 则执行 then 后面的语句.

    if  grep "shell 中每个" shell.org > /dev/null; then
      echo "成功"
    fi
    
    成功
    

循环(for/while)

  • for 循环的语法, 并用一个例子说明了如何使用 for 循环来遍历一个列表.

    fruits=("苹果" "香蕉" "橙子")
    
    for fruit in "${fruits[@]}"
    do
        echo "我喜欢吃 $fruit"
    done
    
    我喜欢吃 苹果
    我喜欢吃 香蕉
    我喜欢吃 橙子
    
  • for 循环中分号的使用, 分号需要放在 dodone 之前, 而不能放在 done 之后.

    for i in 1 2 3; do echo "数字: $i"; done
    
    数字: 1
    数字: 2
    数字: 3
    
  • for 循环的实际作用, 它会遍历文件中的每个单词, 而不是每行

    text="Hello World
    This is a test"
    
    for word in $text
    do
        echo "单词: $word"
    done
    
    单词: Hello
    单词: World
    单词: This
    单词: is
    单词: a
    单词: test
    

    zsh的行为是每行

    text="Hello World
    This is a test"
    
    for word in $text
    do
        echo "单词: $word"
    done
    
    单词: Hello World
    This is a test
    
  • while 循环的语法, 并说明了它的工作原理类似于 if 语句, 它会执行 COMMAND, 如果 COMMAND 返回 0(表示成功), 则执行 do 后面的语句.

    count=1
    
    while [ $count -le 5 ]
    do
        echo "计数: $count"
        count=$((count + 1))
    done
    
    计数: 1
    计数: 2
    计数: 3
    计数: 4
    计数: 5
    

读取输入(read)

  • read 命令用于读取标准输入, 并用一个例子说明了如何使用 read 命令读取用户输入并将其存储到一个变量中.

    echo "请输入你的名字: "
    read name
    echo "你好, $name!"
    
    请输入你的名字:
    
  • read 命令可以同时读取多个变量, 并用一个例子说明了如何使用 read 命令读取两个变量.

    echo "请输入你的名字和年龄, 用空格分隔: "
    read name age
    echo "你好, $name! 你 $age 岁了. "
    
    请输入你的名字和年龄, 用空格分隔:
    
  • read 命令默认会去除空白字符, 并用一个例子说明了如何使用 IFS 变量来控制去除空白字符的行为.

    echo "请输入一个带空格的字符串: "
    read input
    echo "你输入的字符串是: '$input'"
    
    请输入一个带空格的字符串:
    
    echo "请输入以逗号分隔的名字和年龄: "
    IFS=',' read name age
    echo "名字: $name"
    echo "年龄: $age"
    
    请输入以逗号分隔的名字和年龄:
    年龄:
    
  • IFS 变量在循环遍历文件时, 可以用来控制循环遍历的是每个单词还是每行.

    echo "按行遍历文件内容: "
    while IFS= read -r line
    do
        echo "行内容: $line"
    done < example.txt
    
    按行遍历文件内容:
    

函数(Function)

  • 定义函数: 使用 function_name() { ... } 语法定义.
  • 调用函数: 函数调用只需写函数名, 后面跟参数, 不需要括号.
  • 参数: 函数参数以 $1, $2, $3 等形式传递.
  • 退出码: 函数可以返回退出码(0 表示成功, 非零表示失败).
  • 局部变量: local 关键字声明局部变量, 这些变量只在函数内部有效, 不会影响全局范围.
  • 返回值: 函数只能返回退出码, 不能返回字符串.
  • 错误抑制: local x=VALUE 语法可以抑制访问可能不存在的变量时的错误.
#+BEGIN_SRC bash
#!/bin/bash

## 定义一个函数
function say_hello() {
  echo "Hello, $1!"
}

## 调用函数, 注意不需要有括号!
say_hello "World"

## 定义一个返回退出码的函数
function failing_function() {
  return 1
}

## 调用返回退出码的函数
failing_function
echo $?

## 定义一个使用局部变量的函数
function local_var() {
  local x=$(date)
  echo "Local variable x: $x"
}

## 调用使用局部变量的函数
local_var
Hello, World!
1
Local variable x: Tue Dec 31 09:40:07 CST 2024

管道(pipe)

  • 管道的作用, 它可以将一个进程的输出作为另一个进程的输入

    ls | wc -l
    
         108
    
  • 管道的工作原理, 它实际上是一对特殊的"文件描述符" ,一个用于写入, 另一个用于读取.

    ## 使用 exec 创建一个匿名管道, fd 3用于写入, fd 4用于读取
    exec 3> >(wc -l)
    echo -e "Line1\nLine2\nLine3" >&3
    exec 3>&-
    
           3
    
    • exec 3> >(wc -l): 创建一个匿名管道, 将文件描述符 3 指向 wc -l 的标准输入.
    • echo -e "Line1\nLine2{=latex}\nLine3{=latex}" >&3: 将三行文本写入文件描述符 3, 即传递给 wc -l.
    • exec 3>&-: 关闭文件描述符 3.
    • wc -l 接收到三行输入, 并输出 3, 表示有三行.
  • 管道是单向的, 只能从写入端写入数据, 不能从读取端写入数据.

    ## 创建一个管道
    ls | wc -l
    
    ## 尝试从管道的读取端写入数据(错误示例)
    echo "Test" >&0
    
         100
    Test
    
  • 命名管道, 它可以创建一个文件, 该文件可以像管道一样工作:

    ## 创建命名管道
    mkfifo my_pipe
    
    ## 在后台运行一个进程, 从命名管道中读取数据并进行处理
    cat my_pipe | wc -l &
    
    ## 将数据写入命名管道
    echo -e "Line1\nLine2\nLine3" > my_pipe
    
    ## 清理
    rm my_pipe
    
           3
    
  • 管道是进程通讯的基本方式之一, 所有的编程语言都可以利用管道, 不仅仅是shell

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h> // Include the header file for wait() function
    
    int main() {
      int pipefd[2];
      pid_t pid;
    
      pipe(pipefd); // 创建管道
    
      pid = fork(); // 创建子进程
    
      if (pid == 0) { // 子进程
        close(pipefd[1]); // 关闭写端
        char buffer[100];
        read(pipefd[0], buffer, sizeof(buffer)); // 读取数据
        printf("子进程读取到: %s\n", buffer);
        close(pipefd[0]); // 关闭读端
      } else { // 父进程
        close(pipefd[0]); // 关闭读端
        write(pipefd[1], "Hello from parent process!", 27); // 写入数据
        close(pipefd[1]); // 关闭写端
        wait(NULL); // 等待子进程结束
      }
    
      return 0;
    }
    
    子进程读取到: Hello from parent process!
    

参数扩展(${var})

有别于 "$var" ${} 非常强大, 除了 保护变量 之外可以进行很多操作:

  • ${var/bear/panda}${var//bear/panda} 用于替换字符串中的特定字符 text="Hello Bear eat Bear World"

    echo "${text/Bear/panda}"

    echo "${text//Bear/Panda}"

    #+end_src
    
    Hello panda eat Bear World
    Hello Panda eat Panda World
    
  • ${#var} 用于获取字符串或数组的长度.

    var="World"
    
    ## 需要在变量后直接拼接字符
    echo "Hello $var123"      ## 尝试访问变量 var123
    echo "Hello ${var}123"      ## 尝试访问变量 var123
    echo "Hello ${#var}123"    ## 正确拼接 "World123"
    
    Hello
    Hello World123
    Hello 5123
    
  • ${var:-$othervar} 用于设置默认值, 如果 var 未定义, 则使用 $othervar 作为默认值.

    ## var 未设置
    echo "变量 var 的值: ${var:-默认值}"
    
    ## 设置 var
    var="实际值"
    echo "变量 var 的值: ${var:-默认值}"
    
    变量 var 的值: 默认值
    变量 var 的值: 实际值
    
  • ${var#pattern}${var%pattern} 用于去除字符串的前缀或后缀

    filename="document.tar.gz"
    
    ## 删除最短匹配的后缀 ".gz"
    echo "${filename#document.}"
    
    ## 删除最短匹配的后缀 ".gz"
    echo "${filename%.gz}"
    
    ## 删除最长匹配的后缀 ".tar.gz"
    echo "${filename%%.tar.gz}"
    
    ## 使用表达式去除前缀
    echo "${filename#d*nt.}"
    
    ## 使用表达式去除后缀
    echo "${filename%.tar*}"
    
    ## 如果没有匹配到, 则返回原字符串
    echo "${filename#goodbye_}"
    
    tar.gz
    document.tar
    document
    tar.gz
    document
    document.tar.gz
    
  • ${var:offset:length} 用于获取字符串的子字符串.

    text="Hello World"
    
    ## 从位置6开始提取5个字符
    echo "${text:6:5}"
    
    ## 从位置0开始提取5个字符
    echo "${text:0:5}"
    
    ## 从位置6开始提取到末尾
    echo "${text:6}"
    
    World
    Hello
    World
    

后台进程

  • Bash 脚本可以同时运行多个进程

    #!/bin/bash
    
    ## 后台运行一个脚本
    cat "shell.org" > /dev/null &
    
    ## 后台运行一个 curl 命令
    curl http://example.com -o output.html &
    
    echo "两个进程已在后台运行"
    
  • wait 命令的作用, 它会等待所有后台进程完成

    ## 后台运行任务1
    sleep 3 &
    pid1=$!
    
    ## 后台运行任务2
    sleep 5 &
    pid2=$!
    
    echo "等待两个后台进程完成..."
    wait $pid1 $pid2
    echo "所有后台进程已完成. "
    
  • 后台进程有时会在关闭终端时退出, 并说明了如何使用 nohuptmux/screen 来保持后台进程运行.

    ## 使用 nohup 运行 python 脚本, 并将输出重定向到 nohup.out
    nohup python long_running_script.py > nohup.out 2>&1 &
    echo "脚本已在后台运行, 即使关闭终端也不会终止. "
    
  • jobs, fg, bgdisown 管理后台进程

    disownnohup 的区别在于 disown 支持在进程 执行之后 放到后台执行.

    #!/bin/bash
    
    ## 启动一个后台进程
    sleep 2 &
    
    ## 查看后台进程
    jobs
    
    ## 将后台进程带到前台
    fg %1
    
    ## 当进程在前台运行时, 按下 Ctrl+Z 暂停进程
    ## 使用 bg 将暂停的进程放入后台继续运行
    bg %1
    
    ## 使用 disown 将后台进程从当前Shell中分离
    disown %1
    

子shell(subshell)

子 shell 是一个子进程, 并用一个例子说明了如何使用子 shell 来执行一段代码.

  • 创建子 shell 的几种方法, 包括使用括号, 管道重定向, 以及进程替换.

    ## 括号会创建sub shell
    (cd /tmp; ls -l)
    ## 使用管道符 "|" 将命令连接起来, 会创建一个子 shell 来执行后面的命令.
    ls -l | grep "shell.org"
    ## 进程替换
    wc -l <(ls)
    
    total 16
    srwx------@  1 a123  wheel     0 Dec 31 09:12 2f093fe9-a235-5d1c-9a62-3fae2cdf7eb1
    srwx------@  1 a123  wheel     0 Dec 31 09:12 7c3338c5-8eec-50ae-bc4a-1f6969419936
    prw-rw-rw-   1 root  wheel     0 Dec 24 15:19 9e923688bfa34288aa93e2070da546bf
    prw-rw-rw-   1 root  wheel     0 Dec 24 15:18 A262AD1E-C7E0-42C3-8A20-E532F767EEEE_16308
    srwxr-xr-x@  1 a123  wheel     0 Dec 26 17:45 KoDF0sohej4R_bzJiALaHLuEGahtZOBlPKVzyXue1pw
    drwx------   3 a123  wheel    96 Dec 24 15:17 com.apple.launchd.FMcex23J1V
    drwxr-xr-x@  6 a123  wheel   192 Dec 31 09:12 com.logi.optionsplus.agent.logs
    drwxr-xr-x@  2 a123  wheel    64 Dec 29 00:00 com.logi.optionsplus.logivoice.logs
    drwxr-xr-x@  2 root  wheel    64 Dec 29 00:00 com.logi.optionsplus.updater.logs
    d----w--w-   2 root  wheel    64 Dec 29 21:37 devio_semaphore_logi_hpp_OptionsPlus_A7D4B139-F9F9-44A6-9F5D-7BC0A9B8B80F
    -rw-r--r--   1 a123  wheel  5863 Dec 28 13:39 homebrew.mxcl.emacs-plus.stderr.log
    drwxr-xr-x   3 a123  wheel    96 Dec 29 00:00 image
    srwxrwxrwx@  1 a123  wheel     0 Dec 24 15:18 logitech_kiros_agent-80c9ef0fb86369cd25f90af27ef53a9e
    srwxrwxrwx@  1 a123  wheel     0 Dec 24 15:19 logitech_kiros_logivoice-80c9ef0fb86369cd25f90af27ef53a9e
    srwxrwxrwx@  1 root  wheel     0 Dec 24 15:16 logitech_kiros_updater
    drwxrwxrwx@ 10 root  wheel   320 Dec 28 17:37 perfcount
    drwxr-xr-x   2 root  wheel    64 Dec 29 12:15 powerlog
    -rw-r--r--@  1 a123  staff   15458 Dec 30 10:42 emacs-as-shell.org
    -rw-r--r--@  1 a123  staff   33124 Dec 31 09:39 shell.org
         108 /dev/fd/10
    
    • 子shell和父shell完全独立

      (cd /tmp; pwd) ## 在 /tmp 目录下执行 ls -l 命令, 不会影响父 shell 的当前目录
      
      /tmp
      
  • 在子 shell 中设置的变量不会影响父 shell 中的变量值

    var1="你好"
    (var1="hello")
    echo "$var1"
    
    你好
    
  • 在函数中创建子 shell 可能会导致一些意想不到的行为, 因为函数中的子 shell 不会影响父 shell 的变量值.

信号捕获(trap)

  • trap 命令的作用, 它可以用来设置回调函数, 当发生某些事件时, 就会执行回调函数.
  • trap 命令的语法, 它需要两个参数, 第一个参数是回调函数, 第二个参数是事件类型.
  • trap 命令可以用来处理各种事件, 包括 Unix 信号, 脚本退出, 调试信息和函数返回值.
  • trap 命令可以用来处理脚本退出时的清理工作, 并用一个例子说明了如何使用 trap 命令来删除临时文件.
#!/bin/bash

## 创建临时文件
temp_file=$(mktemp)

## 设置 trap, 在脚本退出时删除临时文件
trap "rm -f $temp_file" EXIT

## 示例操作: 向临时文件写入内容
echo "This is a temporary file." > $temp_file

## 模拟脚本的其他操作
echo "Script is running..."
sleep 2

## 脚本正常退出时, trap 会自动删除临时文件

解释:

  1. mktemp 创建一个临时文件, 并将其路径存储在 temp_file 变量中.
  2. trap "rm -f $temp_file" EXIT 设置了一个回调函数, 当脚本退出(无论是正常退出还是因错误退出)时, 自动删除临时文件.
  3. 脚本执行完毕后, 临时文件会被自动清理, 无需手动删除.

错误处理

  • shell 默认情况下会忽略错误, 即使命令执行失败, 脚本也会继续执行.
  • shell 默认情况下不会检查未定义的变量, 即使变量不存在, 脚本也会继续执行.
  • shell 默认情况下不会检查管道中的错误, 即使管道中的某个命令执行失败, 脚本也会继续执行.

为了更严格地处理错误, 可以使用以下选项:

  1. set -e: 使脚本在命令失败时立即退出.
  2. set -u: 使脚本在遇到未定义的变量时立即退出.
  3. set -o pipefail: 使管道中的任何命令失败时, 整个管道失败.

以下是一个补充了错误处理的例子:

#!/bin/bash

## 启用严格错误处理
set -euo pipefail

## 示例命令
echo "Starting script..."

## 检查未定义的变量
echo "Undefined variable: $UNDEFINED_VAR"

## 执行可能失败的命令
ls /nonexistent_directory

## 管道中的错误处理
cat /nonexistent_file | grep "something"

echo "Script completed."
  • set -e: 如果 ls /nonexistent_directory 失败, 脚本会立即退出.
  • set -u: 如果 $UNDEFINED_VAR 未定义, 脚本会立即退出.
  • set -o pipefail: 如果 cat /nonexistent_file 失败, 整个管道会失败, 脚本会退出.

这样可以确保脚本在遇到错误时立即停止执行, 避免潜在的问题.

调试 (Debug)

set -x 命令

set -x 用于开启脚本的调试模式, 打印出脚本执行的每行代码以及所有变量的值.

实际执行的过程中, 会在stderr输出每一行代码本身.

#!/bin/bash
set -x  ## 开启调试模式
echo "Start"
var="Hello"
echo $var
set +x  ## 关闭调试模式

输出:

+ echo Start
Start
+ var=Hello
+ echo Hello
Hello
+ set +x

bash -x 命令

bash -x 与在脚本开头添加 set -x 的效果相同, 直接以调试模式运行脚本.

bash -x script.sh

trap read DEBUG 命令

trap read DEBUG 使脚本在执行每行代码之前暂停, 并等待用户输入.

#!/bin/bash
trap 'read -p "Press Enter to continue..."' DEBUG
echo "Line 1"
echo "Line 2"

执行时, 每行代码执行前都会暂停, 等待用户按 Enter 继续.

自定义 die 函数

die 函数可以在命令执行失败时打印错误信息并退出程序.

#!/bin/bash
die() {
    echo "Error: $1" >&2
    exit 1
}

## 示例用法
mkdir /tmp/test || die "Failed to create directory"

如果 mkdir 失败, die 函数会打印错误信息并退出脚本.

Tags: mac shell Linux