CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Shell 与 Bash 入门/课时/变量与字符串
RawBlame
课时目录12 节

Shell 与 Bash 入门

  1. 01Shell 是什么
  2. 02目录与文件导航
  3. 03文件操作与权限
  4. 04管道与文本处理
  5. 05变量与字符串
  6. 06条件与循环
  7. 07函数与脚本结构
  8. 08正则表达式
  9. 09进程与定时任务
  10. 10网络命令
  11. 11系统管理与编辑器
  12. 12综合练习

变量与字符串

环境变量、数组、Here Doc。

30 分钟

变量与字符串

这一课你将能做到

  1. 正确赋值和引用变量,说清单引号、双引号、反引号三者的区别
  2. 用位置参数 $1 $2 $# $? 写出能接收参数的脚本,用 $? 判断上一条命令成败
  3. 掌握默认值 ${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 世界的做法。

讲清楚要点

  1. 赋值等号两边不能有空格:X=1 对,X = 1 错。因为 Shell 按「命令 参数」解析,X 被当成命令名。
  2. 引用变量永远加双引号:"$DIR" 保证含空格的路径不被拆成多个词。rm $DIR 当 DIR 含空格时会变成两条 rm 命令,删出事故。这是最值得写进肌肉记忆的一条。
  3. 单引号是「原文」,双引号是「解析后的原文」:正则、日期格式串用单引号防止 Shell 抢先解释 $ 和 *;拼接变量用双引号。
  4. $* 与 $@ 的区别(面试必背):不加引号时两者一样;加双引号后,"$*" 把所有参数合成一个字符串,"$@" 保持每个参数独立。遍历参数永远写 for x in "$@"。
  5. 命令替换用 $(),不用反引号:$(...) 可嵌套可读性好;反引号嵌套时要转义,脚本里属于「看到就该改成 $()」的历史遗留。
  6. 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 <参数>}

卡点急救

  1. command not found 但命令明明存在:多半是变量赋值带了空格或路径变量拼错。echo $PATH 检查;临时用绝对路径验证命令本身是否可用。
  2. 变量在子脚本里是空的:没 export,或者用了管道(管道右侧是新子进程)。检查是否 export,是否需要改为临时文件或命令替换传递数据。
  3. 字符串比较总说不相等:值末尾带了隐藏字符(如 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 文件 + 环境变量,密码不落代码库。

自测三问

  1. LOG=/opt/app logs 这种含空格的值,赋值和引用分别怎么写才安全?
  2. ${file##*.} 和 ${file%.*} 分别返回什么(file=app-1.0.tar.gz)?
  3. export 的变量能被子进程修改后传回父 Shell 吗?

小结一句话

变量让脚本从「写死」变「配置驱动」:等号无空格、引用加双引号、$() 做替换、${VAR:-默认} 做兜底,这四条规矩守住 90% 的坑。

延伸

  • Bash 官方手册:Shell Parameters
  • 下一课:条件与循环——让脚本学会「看情况办事」和「批量干活」。
加载本节练习…
← 上一课管道与文本处理下一课 →条件与循环