Skip to content

Shell基础语法

Shell 脚本在运维中主要用于串联系统命令和编写简单的自动化逻辑。基础语法集中在解释器声明、变量、引号、命令替换、退出码和位置参数这几块。

一、什么是 Shell 和 shebang

Shell 是命令解释器——用户输入命令,Shell 把它翻译给内核执行。服务器上最常见的是 Bash(Bourne Again SHell),很多运维脚本默认用 Bash 写。

查看当前使用的 Shell 和版本:

bash
echo "$SHELL"        # 当前登录用户的默认 Shell
bash --version       # Bash 版本,关联数组等语法和版本有关

脚本第一行通常写 shebang(#!),告诉系统用哪个解释器执行这个脚本:

bash
#!/usr/bin/env bash

echo "hello shell"

#!/usr/bin/env bash 从环境变量 PATH 里查找 bash,避免把解释器路径写死。如果生产环境路径是固定的,直接写 /bin/bash 也可以。

执行脚本的两种方式:

bash
chmod +x hello.sh    # 先赋予执行权限
./hello.sh           # 用 shebang 指定的解释器执行

或者直接交给 bash 执行:

bash
bash hello.sh

需要注意:sh script.sh 不一定等于 bash script.sh。在 Debian/Ubuntu 上,/bin/sh 通常指向 dash(Debian Almquist Shell),数组、[[ ]] 这类 Bash 扩展语法在 dash 下会报错。如果脚本用到了 Bash 特有的语法,就明确用 Bash 执行。

二、变量

Shell 变量在赋值时,等号两边不能有空格。这是一个和大多数编程语言不同的规则。

bash
name="nginx"
port=80

echo "$name"
echo "$port"

name = nginx 会被 Shell 解析为执行 name 命令,参数是 =nginx——一个基础但容易踩到的坑。

变量引用时默认加双引号,可以防止空格和通配符导致的意外参数拆分:

bash
app="api"
log_dir="/var/log/$app"

echo "$log_dir"
unset app            # 删除变量,之后引用会返回空或触发 set -u

默认情况下,变量只在当前 Shell 中可见。子进程要读取变量的值,需要用 export 导出:

bash
export APP_ENV="prod"
bash -c 'echo "$APP_ENV"'    # 子 Shell 能读到已导出的变量

变量命名按作用域区分,有助于阅读:

作用域写法示例
脚本内部小写 + 下划线app_namelog_dir
环境变量全大写APP_ENVDATABASE_URL
只读常量readonly 声明readonly config_file=/etc/app.conf

三、引号

Shell 里引号不是装饰——它直接决定变量和通配符是否被展开。

写法变量展开通配符展开使用场景
不加引号明确需要拆词或通配符展开时
单引号 '...'不会不会纯字面字符串
双引号 "..."不会变量拼接,最常用

空格导致的拆分问题:

bash
file="app log.txt"

ls $file      # 被拆成 app 和 log.txt 两个参数
ls "$file"    # 当作一个完整参数

空变量的处理:

bash
target=""

if [ -n "$target" ]; then
    # -n 判断字符串非空,这里不会进入
    echo "target is $target"
fi

路径、文件名、用户输入、命令输出,这些内容默认加双引号。只有确实需要通配符展开的少数场景,才故意不加。

四、命令替换

命令替换把一个命令的标准输出捕获到变量中。

bash
today="$(date +%F)"      # 生成 YYYY-MM-DD 格式日期
host="$(hostname)"       # 当前主机名

echo "backup-$host-$today.tar.gz"

老式写法用反引号:

bash
today=`date +%F`

反引号嵌套时很难读,$(...) 写法更清晰且支持嵌套。

命令替换会自动去掉输出末尾的换行符。如果需要保留多行输出,后续处理用 printf 而非 echo

bash
users="$(cut -d: -f1 /etc/passwd)"
printf '%s\n' "$users"

五、退出码

Shell 中约定退出码 0 表示成功,非 0 表示失败。每个命令执行完后都有一个退出码。

查看上一条命令的退出码:

bash
systemctl is-active nginx
echo "$?"        # $? 是上一条命令的退出码

在条件判断中直接使用退出码:

bash
if systemctl is-active --quiet nginx; then
    echo "nginx is running"
else
    echo "nginx is not running"
fi

&&(前一条成功才执行后一条)和 ||(前一条失败才执行后一条)也是基于退出码:

bash
mkdir -p /backup && echo "backup dir ready"
systemctl reload nginx || echo "reload failed"

脚本中主动退出并报告错误:

bash
if [ ! -f "/etc/nginx/nginx.conf" ]; then
    echo "missing nginx config" >&2    # 错误信息写到 stderr
    exit 1
fi

错误信息用 >&2 输出到 stderr,这样脚本被 cron、systemd 或 CI 调用时,stdout 和 stderr 可以分开收集和告警。

六、位置参数

脚本从命令行接收参数时使用的特殊变量:

变量含义
$0脚本本身的名称
$1$2 ...第 1 个、第 2 个参数
$#参数总个数
$@所有参数,每个参数保持独立
$*所有参数,合并为一个字符串

一个参数校验的示例:

bash
#!/usr/bin/env bash
set -euo pipefail

service_name="${1:-}"    # 没传参数时给空字符串,配合 set -u 不报错

if [ -z "$service_name" ]; then
    echo "usage: $0 <service-name>" >&2
    exit 1
fi

systemctl status "$service_name" --no-pager

${1:-} 的含义:如果 $1 未定义或为空,使用空字符串作为默认值。配合 set -u 时这个写法能防止因未定义变量导致脚本直接退出。

遍历所有参数时用 "$@"

bash
for item in "$@"; do
    echo "arg: $item"
done

"$@" 会保留每个参数的原样,包括其中包含的空格——它与 $* 不同,后者会把所有参数揉成一个字符串。

七、常用内置变量

Shell 提供了一些自动维护的内置变量,在脚本中很实用:

变量含义
$?上一条命令的退出码
$$当前 Shell 进程的 PID
$!最近一个后台进程的 PID
$PWD当前工作目录
$OLDPWD上一次所在目录
$UID当前用户的 UID
$RANDOM生成一个随机整数
$SECONDS脚本已运行的秒数

后台任务配合 $!wait

bash
sleep 60 &
pid="$!"

echo "background pid: $pid"
wait "$pid"        # 等待后台进程结束,并获取其退出码

wait 等待指定后台进程结束,然后返回该进程的退出码。脚本里并发启动多个任务后,用 wait 统一回收比丢在后台不管更可靠。

八、脚本开头的安全设置

运维脚本开头常见的三个设置:

bash
#!/usr/bin/env bash
set -euo pipefail
设置作用
set -e任何命令失败(返回非 0)时立即退出脚本
set -u使用未定义变量时视为错误并退出
set -o pipefail管道中任何命令失败都会导致整条管道失败

set -e 不是万能的——某些命令正常执行也会返回非 0。比如 grep 没匹配到内容时返回 1。对于这种情况,需要用条件判断来包裹:

bash
if grep -q "ERROR" app.log; then
    echo "found error"
else
    echo "no error found"
fi

脚本越是会被 cron、systemd、CI 这类自动化环境调用,就越要把退出码、日志、参数校验写清楚。手动敲命令可以靠眼睛盯着,自动化之后没有人在旁边看。