变量与字符串
这一课你将能做到
- 正确赋值和引用变量,说清单引号、双引号、反引号三者的区别
- 用位置参数
$1 $2 $# $? 写出能接收参数的脚本,用 $? 判断上一条命令成败
- 掌握默认值
${VAR:-默认}、子串截取、字符串替换等展开语法,写配置驱动的部署脚本
为什么要学
没有变量的脚本是「写死的」:日志路径变了要改代码、环境要区分测试/生产只能复制两份脚本。有了变量,一个脚本通过参数和环境变量适配所有场景——这正是 Spring Boot 里配置文件和 @Value 的思想在 Shell 里的对应物。面试中「$* 和 $@ 的区别」「单双引号区别」「$? 什么时候用」都是 shell 岗和后端岗的高频题。
人话:变量就是给值贴个标签。LOG_DIR=/opt/myapp/logs 之后,到处写 $LOG_DIR,路径改了只动一行。位置参数 $1 就像 Java 的 main(String[] args) 里的 args[0]。
先建立图像
Shell 变量全景图:
┌────────────── 变量从哪来 ──────────────┐
│ 1. 手工赋值 APP_ENV=prod │
│ 2. 位置参数 ./deploy.sh prod → $1 │
│ 3. 环境变量 $PATH $HOME $JAVA_HOME│
│ 4. 命令替换 $(date +%F) │
│ 5. 特殊变量 $? $# $@ $0 $$ │
└────────────────────────────────────────┘
特殊变量速查:
$0 = 脚本名 $1..$9 = 第 1..9 个参数
$# = 参数个数 $@ = 所有参数(各自独立)
$? = 上一命令退出码 $$ = 当前 Shell 的 PID
三种引号的行为对照(这是最容易懵的点,先看表再动手):
| 写法 | 变量展开 | 命令替换 | 典型用途 |
|---|
| '单引号' | ❌ 不展开 | ❌ 不展开 | 正则、固定文本 |
| "双引号" | ✅ 展开 | ✅ 展开 | 拼接变量、含空格的值 |
反引号 | — | ✅ | 旧写法,已被 $() 取代 |
跟着做
步骤 1:赋值与引用的基本功
name="myapp" # 等号两边绝不能有空格!
echo $name # myapp
echo "$name is ready" # myapp is ready
echo '$name is ready' # $name is ready(单引号原样输出)
port=8080
echo "http://localhost:$port/actuator/health"
name = "myapp"(带空格)会报 command not found,因为 Shell 把 name 当成了命令。这是初学者第一大坑。
步骤 2:命令替换:把命令的输出存进变量
today=$(date +%F) # 2026-10-06 这种格式
log=app-$today.log
echo "今天日志:$log" # app-2026-10-06.log
count=$(grep -c ERROR /opt/myapp/logs/app.log 2>/dev/null || echo 0)
echo "今日错误数:$count"
java_ver=$(java -version 2>&1 | head -1)
echo "Java 信息:$java_ver" # java -version 输出在 stderr,所以要 2>&1
$(...) 是推荐的命令替换写法,支持嵌套,比反引号 `date +%F` 可读性好。
步骤 3:位置参数:让脚本吃参数
cat > greet.sh << 'EOF'
#!/bin/bash
echo "脚本名:$0"
echo "第 1 个参数:$1"
echo "参数个数:$#"
echo "所有参数:$@"
if [ $# -lt 1 ]; then
echo "用法:$0 <环境名>"
exit 1
fi
echo "即将部署到:$1"
EOF
chmod +x greet.sh
./greet.sh prod # 试试不传参数 ./greet.sh,看 exit 1 的用法提示
exit 1 让脚本以失败退出,调用方(或 CI 流水线)能据此中断流程。
步骤 4:默认值与防御式展开
# 变量没值时用默认值,环境切换脚本必备
ENV=${1:-dev} # 参数没传就用 dev
echo "环境:$ENV"
LOG_DIR=${LOG_DIR:-/opt/myapp/logs}
echo "日志目录:$LOG_DIR"
# :坐下表——必须非空,否则报错退出
REQUIRED=${REQUIRED:?缺少必填变量 REQUIRED}
${VAR:-默认} 只在使用时兜底不改变变量;${VAR:?提示} 用于必填校验,部署脚本开头把数据库地址之类设成必填,能在启动时就拦住「连错库」的事故。
步骤 5:字符串三大操作(截取、长度、替换)
s="myapp-1.0.0.jar"
echo ${#s} # 长度 14
echo ${s:6} # 从下标 6 取到尾:1.0.0.jar
echo ${s:6:1} # 从 6 取 1 个字符:1
echo ${s#*-} # 从左删最短匹配 *-:1.0.0.jar
echo ${s##*-} # 从左删最长匹配:jar(贪心)
echo ${s%.jar} # 从右删最短 .jar:myapp-1.0.0
echo ${s/myapp/webapp} # 替换第一个:webapp-1.0.0.jar
echo ${s//0/O} # 替换全部:myapp-1.O.O.jar
实战:拿文件名去后缀做备份名 backup=$(tar -czf ${jar%.jar}.tar.gz ...);取扩展名 ext=${file##*.}。
步骤 6:环境变量与 export
# 脚本内定义的变量,子进程看不到
APP_NAME=demo bash -c 'echo $APP_NAME' # 输出空
# export 后变成环境变量,子进程能看到
export APP_NAME=demo
bash -c 'echo $APP_NAME' # 输出 demo
# 环境变量的典型消费场景:Java 进程读取配置
export SPRING_PROFILES_ACTIVE=prod
export DB_PASSWORD='S3cret!'
Spring Boot 的配置优先级里环境变量高于 application.yml,所以容器化部署时 SPRING_PROFILES_ACTIVE、DB_PASSWORD 全走环境变量——这个机制你已经在用了。
步骤 7:把配置收进一个文件
cat > app.env << 'EOF'
export APP_NAME=order-service
export APP_PORT=8080
export LOG_DIR=/opt/myapp/logs
EOF
# 在脚本里加载配置
cat > check.sh << 'EOF'
#!/bin/bash
source ./app.env
echo "检查 $APP_NAME (端口 $APP_PORT)..."
EOF
bash check.sh
source 让配置文件在当前 Shell 里执行,变量全部生效。这就是 Spring Boot「配置与代码分离」在 Shell 世界的做法。
讲清楚要点
- 赋值等号两边不能有空格:
X=1 对,X = 1 错。因为 Shell 按「命令 参数」解析,X 被当成命令名。
- 引用变量永远加双引号:
"$DIR" 保证含空格的路径不被拆成多个词。rm $DIR 当 DIR 含空格时会变成两条 rm 命令,删出事故。这是最值得写进肌肉记忆的一条。
- 单引号是「原文」,双引号是「解析后的原文」:正则、日期格式串用单引号防止 Shell 抢先解释
$ 和 *;拼接变量用双引号。
$* 与 $@ 的区别(面试必背):不加引号时两者一样;加双引号后,"$*" 把所有参数合成一个字符串,"$@" 保持每个参数独立。遍历参数永远写 for x in "$@"。
- 命令替换用
$(),不用反引号:$(...) 可嵌套可读性好;反引号嵌套时要转义,脚本里属于「看到就该改成 $()」的历史遗留。
- export 只向下传,不向上传:子进程(脚本、
bash -c)能看到 export 的变量,但子进程里的修改不会传回父 Shell。想让函数、alias 长驻当前终端,靠 source,第 7 课细讲。
你可能会懵的地方
-
环境变量和普通变量有什么本质区别?
普通变量只属于当前 Shell 进程;环境变量会随子进程继承(通过 execve 传入)。Java 里 System.getenv("PATH") 读的就是环境变量,而 PATH 之所以全局可用,就是因为登录 Shell 启动时它已被 export。
-
$(date +%F) 里的 %F 是什么?
date 的格式符:%F 等于 %Y-%m-%d。常用的还有 %H%M%S(时分秒)、%s(Unix 时间戳)。备份文件名带时间戳是标准做法:backup-$(date +%F-%H%M).tar.gz。
-
${VAR:-默认} 和 ${VAR:=默认} 差在哪?
:- 只是这次使用时给个替身,VAR 本身没变;:= 会顺手把默认值赋给 VAR,之后继续有效。想「本次兜底」用 :-,想「初始化一次处处生效」用 :=。
-
为什么 echo $s 有时会把 * 变成一堆文件名?
未加引号的展开会触发「文件名通配」(globbing)。echo "*" 原样输出,echo * 列出当前目录文件。写脚本时变量一律双引号,就能杜绝这类意外。
-
脚本里怎么读用户输入?
read -p "请输入: " answer,输入保存在 answer 变量。交互脚本用;无人值守的自动化脚本(crontab 场景)不要用,crontab 没有键盘可敲。
错法对照
| 错法 | 问题 | 正解 |
|---|
APP_NAME = demo | 空格使命令解析失败 | APP_NAME=demo |
rm $DIR(DIR 可能含空格) | 被拆成多个参数,误删 | rm "$DIR" |
if [$# -lt 1] | 语法错误,[ 后缺空格 | if [ $# -lt 1 ](空格必须有) |
log=`date +%F` 嵌套使用 | 反引号嵌套要转义,易错 | $(date +%F) |
脚本里用 $1 前不校验 | 空参导致诡异行为 | 开头 ${1:?用法:$0 <参数>} |
卡点急救
command not found 但命令明明存在:多半是变量赋值带了空格或路径变量拼错。echo $PATH 检查;临时用绝对路径验证命令本身是否可用。
- 变量在子脚本里是空的:没 export,或者用了管道(管道右侧是新子进程)。检查是否
export,是否需要改为临时文件或命令替换传递数据。
- 字符串比较总说不相等:值末尾带了隐藏字符(如 Windows 换行
\r)。echo "$VAR" | od -c | head 查看真实字节,用 dos2unix 或 ${VAR%$'\r'} 清理。
面试常问
-
单引号、双引号、反引号的区别?
单引号内完全字面化,变量和命令替换都不生效;双引号内变量展开、命令替换生效,通配符也被抑制;反引号与 $() 等价用于命令替换,但 $() 支持嵌套、可读性好,是现代推荐写法。
-
$* 和 $@ 的区别?
不加引号无区别;"$*" 把全部参数合并成一个以 IFS 首字符分隔的字符串,"$@" 保持参数边界。传参给函数或逐个遍历时必须用 "$@",否则含空格的参数会被拆坏。
-
Shell 脚本里怎么给 Java 应用传配置?
三种方式:export 环境变量(Spring Boot 环境变量优先级高于 yml)、启动参数 java -Dapp.port=8080 -jar app.jar、source 一个 env 配置文件统一管理。生产推荐 env 文件 + 环境变量,密码不落代码库。
自测三问
LOG=/opt/app logs 这种含空格的值,赋值和引用分别怎么写才安全?
${file##*.} 和 ${file%.*} 分别返回什么(file=app-1.0.tar.gz)?
- export 的变量能被子进程修改后传回父 Shell 吗?
小结一句话
变量让脚本从「写死」变「配置驱动」:等号无空格、引用加双引号、$() 做替换、${VAR:-默认} 做兜底,这四条规矩守住 90% 的坑。
延伸