Appearance
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_name、log_dir |
| 环境变量 | 全大写 | APP_ENV、DATABASE_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 这类自动化环境调用,就越要把退出码、日志、参数校验写清楚。手动敲命令可以靠眼睛盯着,自动化之后没有人在旁边看。