什么是 .sh 文件(超详解)

.sh 一般指 Shell 脚本文件(主要面向 POSIX shell,如 sh/bash/dash/zsh/ksh 等)。它以纯文本形式组织命令和控制流程,用来自动化运维、批处理任务、构建与部署、系统配置等。


1) .sh 与 “sh”/“bash”的关系

  • .sh 文件:约定俗成的扩展名,任何 Shell 都能读(关键在 shebang)。
  • /bin/sh:POSIX shell 的“通用入口”。在不同系统上可能链接到 bash(旧 macOS)、dash(Debian/Ubuntu)、ksh 等。
  • bash/zsh:功能更丰富(如数组、[[ ]]pipefail 等),但非纯 POSIX。若脚本写了 bash 专有语法,应使用 #!/usr/bin/env bash

可移植性:若追求跨发行版/精简系统(BusyBox/Alpine 等),优先写 POSIX sh 语法(避免数组、[[ ]]pipefail 等)。


2) shebang(第一行声明用哪个解释器)

常见写法:

#!/usr/bin/env sh       # 最通用、最可移植(POSIX)
#!/usr/bin/env bash     # 明确使用 bash 扩展
#!/bin/sh               # 依赖系统默认 /bin/sh(可能是 dash)
#!/bin/bash             # 直接指定 bash(某些系统可能不在 /bin)

建议:偏向 #!/usr/bin/env <shell>,让系统通过 PATH 定位解释器。


3) 执行方式与权限

  • 直接交给解释器:sh script.sh / bash script.sh(无需可执行位)

  • 作为程序运行:

    chmod +x script.sh
    ./script.sh
    
  • 当前 shell 环境内执行(不启动新进程):

    . script.sh      # POSIX(点命令)
    source script.sh # bash/zsh 同义(POSIX 不保证支持 'source')
    

4) 基本语法要点(POSIX 视角)

  • 注释# 从这里到行尾

  • 变量name=value(等号两边不能有空格);引用用 "$name"

  • 命令替换foo="$(date)"(推荐 $( ),避免反引号嵌套混乱)

  • 算术$(( a + b ))

  • 引号

    • "$var" 保留整体、禁止词分割与通配展开(默认应总是加引号
    • '...' 全字面量(不做变量/转义展开)
    • 无引号会触发词分割通配符,是安全隐患重灾区
  • 条件与测试

    # POSIX:使用 test 或 [ ]
    if [ "$x" = "hi" ] && [ -f "$file" ]; then
      ...
    fi
    
    # bash/zsh:[[ ... ]] 更稳健(避免很多转义问题,非 POSIX)
    
  • 分支与循环

    if ...; then ...; elif ...; else ...; fi
    
    case "$var" in
      start) ... ;;
      stop|restart) ... ;;
      *) echo "usage"; exit 1 ;;
    esac
    
    for f in *.txt; do ...; done
    while read -r line; do ...; done < file.txt
    
  • 函数

    my_func() {
      # POSIX 函数定义
    }
    
  • 参数$0(脚本名)、$1..$9$#(参数个数)、$@(逐个参数)、$*(合并为一个词)

    • 几乎总是用 "$@"(逐个保留边界),很少用 "$*"
  • 退出码0 成功,非 0 失败。获取上一条命令退出码用 $?;脚本返回 exit N


5) I/O、重定向与管道

  • 重定向> 覆盖、>> 追加、< 输入、2> 重定向 stderr、&>/2>&1 合并输出(&> 是 bash/zsh 扩展)

  • 管道cmd1 | cmd2cmd1 的 stdout 作为 cmd2 的 stdin)

  • Here-Document

    cat >config.ini <<'EOF'
    # 单引号界定符:内容完全字面量,不展开变量
    name=$USER
    EOF
    
  • Here-String(bash/zsh):cmd <<< "$var"


6) 子进程与分组

  • 子 shell(圆括号):( cd dir && do_something ) 退出后目录不变
  • 命令组(花括号):{ cmd1; cmd2; } 在当前 shell 执行(注意分号与空格)

7) 选项解析(入门到规范)

  • POSIX getopts(短选项,最通用):

    usage() { echo "Usage: $0 [-f FILE] [-v]"; }
    
    verbose=0; file=""
    while getopts ":f:v" opt; do
      case "$opt" in
        f) file=$OPTARG ;;
        v) verbose=1 ;;
        \?) usage; exit 2 ;;
        :)  echo "Option -$OPTARG requires an argument"; exit 2 ;;
      esac
    done
    shift $((OPTIND-1))
    
  • GNU getopt 提供长选项解析(但并非所有系统都有,可移植性差)。


8) 健壮性与错误处理

  • 常见“严格模式”(bash/zsh/ksh 可用;纯 POSIX 不含 pipefail):

    #!/usr/bin/env bash
    set -Eeuo pipefail
    IFS=$'\n\t'
    
    • -e 任一命令失败即退出(有例外,管道/条件判断等需小心)
    • -u 读取未定义变量时报错
    • -o pipefail 管道任一失败导致整个管道失败(非 POSIX)
  • 陷阱(trap):清理临时文件、捕捉 INT/TERM/EXIT/ERR

    tmp=$(mktemp) || exit 1
    cleanup() { rm -f "$tmp"; }
    trap cleanup EXIT INT TERM
    
  • 返回码检查cmd || { echo "failed"; exit 1; }


9) 安全最佳实践(很重要)

  • 始终给变量和扩展加引号"$var""$@"
  • 避免 eval(命令注入风险)
  • 不要解析 ls 输出(文件名可能含空格/换行);用 find … -print0 | xargs -0 …
  • 使用 read -r 防止反斜杠转义;不要用 for line in $(cat file)(会按空白切词)
  • **用 mktemp/受控目录
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐