Shell编程基础
Shell编程基础
[TOC]
🧱 什么是Shell
Shell 是==命令解释器==,负责翻译用户输入的命令给内核,内核处理完成后返回给 Shell
┌──────────────┐ ┌──────────────┐ ┌─────────────────┐│ 用户 │────▶│ Shell │────▶│ 内核 ││ (输入命令) │ │ (翻译+调度) │ │ (真正干活) │└──────────────┘ └──────┬───────┘ └────────┬────────┘ │ │ ▼ ▼ [调用系统调用] [访问硬件:磁盘/内存/CPU] │ │ ◀─────── 结果返回 ───────┘ │ ▼ 'Shell 输出结果到终端 → 你看到!'📌 一句话:Shell 就是你跟内核之间的”翻译官”
- Linux默认交互式 Shell(👤 人类用户)的解释器是
bash- 当你打开终端、SSH 登录服务器时,系统自动启动的那个 Shell(交互式shell)
1)默认交互式shell[root@Rocky ~]# bash[root@Rocky ~]# exitexit[root@Rocky ~]# exitlogout
root@Ubuntu ~# bashroot@Ubuntu ~# exitexitroot@Ubuntu ~# exitlogout/bin/sh是==🤖 脚本中==默认的解释器,但前提是脚本**没有写 Shebang(#!)**或者显式调用了/bin/sh- 是一个标准化的系统接口,用于执行非交互式的 Shell
2)Red Hat系`/bin/sh` 指向的就是 `bash`:✅ bash 好用、功能丰富[root@Rocky ~]# ll /bin/shlrwxrwxrwx. 1 root root 4 Oct 29 2024 /bin/sh -> bash[root@Rocky ~]# ll /bin/bash-rwxr-xr-x. 1 root root 1410688 Oct 29 2024 /bin/bash
3)Debian/Ubuntu 系`/bin/sh` 实际指向的就是 `dash`:⚠️ bash ≠ dash(不支持 `[[ ]]`、数组等 bash 特性,但快速、轻量)# 不可以随意使用 bash 扩展语法root@Ubuntu ~# ll /bin/shlrwxrwxrwx 1 root root 4 Mar 31 2024 /bin/sh -> dash*root@Ubuntu ~# ll /bin/dash-rwxr-xr-x 1 root root 129784 Mar 31 2024 /bin/dash*'他是有/bin/bash的'root@Ubuntu ~# ll /bin/bash-rwxr-xr-x 1 root root 1446024 Mar 31 2024 /bin/bash*正因为 Ubuntu 的 sh 是 dash,在编写 Shell 脚本时必须使用#!/bin/bash 作为 Shebang,而不能用 #!/bin/sh
- 想用 bash 特性 → 必须写
#!/bin/bash - 永远不要假设
/bin/sh就是 bash,尤其是在 Ubuntu/Debian 系统上!
🚢 Shell编程的作用
面试题:给你一台服务器,对服务器操作流程是什么样的?
1)硬件准备# 组RAID
2)安装操作系统# 自动化安装 cobbler、kickstart 脚本
3)系统优化# 优化SSH、加大文件描述符、时间同步、优化防火墙、安装常用软件、内核优化 ... 脚本
4)安装服务、优化服务# yum、编译 脚本
5)监控服务# 脚本
6)日志切割、定时任务# 防止日志过大
7)日志统计处理
8)编写启停脚本# python程序、jar包...
9)辅助程序正常运行# 资源满导致负载过高 -> 自动杀死进程📌 可以看到,Shell 编程贯穿了服务器运维的==整个生命周期==
⚙️ Shell脚本规范
| 序号 | 规范 | 说明 |
|---|---|---|
| ① | 脚本开头指定解释器 | #!/bin/bash,Redhat系可写可不写(sh —> bash)Debian系必须指定(sh —> dash) |
| ② | 脚本名称结尾 | 以 .sh 结尾 |
| ③ | 见名知意 | 脚本名称要能看出功能 |
| ④ | 避免中文符号 | 脚本中不能出现中文符号 "" '' `` {} () [] , . <> |
| ⑤ | 成对符号一次写完 | 写 {} [] () 时一次写完,避免遗漏 |
| ⑥ | 统一存放位置 | 脚本放在同一个位置,方便管理 |
| ⑦ | 写注释 | 注明作用、功能、作者 |
💡 新服务器如何找到所有 shell 脚本的位置?
- 查看定时任务
crontab -l✅ - 查看进程
ps aux | grep sh✅- 执行一个脚本文件时,操作系统会将该脚本的==完整路径==作为参数传递给==解释器进程==
- 而
ps aux恰好能捕获并显示这个完整的==命令行参数==
- 查看 history 记录 ✅
find可以查找,但业务运行中不要在/开始查找- 下下策:
grep -r bash /*
📌 学习 Shell 需要的基础:① Linux 系统命令 ② 三剑客(grep/sed/awk) ③ vim
⚙️ 第一个Shell脚本
# 脚本内容[root@Rocky ~]# cat test.sh#!/bin/bashecho "Hello World!" # 将字符输出到屏幕,支持中文
# 执行脚本[root@Rocky ~]# sh test.shHello World!⚙️ 执行Shell脚本的方式
三种常用执行方式
第一种:sh / bash
[root@Rocky ~]# ls /server/sh/bash_config.sh[root@Rocky ~]# mv test.sh /server/sh/[root@Rocky ~]# cd /server/sh/[root@Rocky sh]# sh test.shHello World![root@Rocky sh]# bash test.shHello World!✅ 推荐用 bash 执行,Debian系的 ==sh —> dash== ⚠️
dash是一个严格遵循 POSIX 标准的轻量级 Shell,它不支持大量 Bash 扩展语法
第二种:路径方式(需要 +x 执行权限)
# 绝对路径'以根 / 开始'[root@Rocky sh]# /server/sh/test.sh-bash: /server/sh/test.sh: Permission denied # 权限拒绝
[root@Rocky sh]# chmod +x test.sh[root@Rocky sh]# ll test.sh-rwxr-xr-x 1 root root 40 Jan 19 10:14 'test.sh'[root@Rocky sh]# /server/sh/test.shHello World!
# 相对路径[root@Rocky sh]# ./test.sh # 使用 ./ 路径方式执行脚本'点./ 路径'Hello World!⚠️ 直接输入脚本名 test.sh 会报 command not found(命令未找到),必须用 ./test.sh
❌ 上面 test.sh 会去找 —> PATH变量(压根没有这个命令)
第三种:source / .(紧跟空格)
[root@Rocky sh]# source test.shHello World![root@Rocky sh]# . test.shHello World!其他执行方式
1)管道传给 bash(最常用)# 它把前一个命令原本要打印到'屏幕上的内容',转而塞进了后一个命令的标准输入通道里[root@Rocky sh]# cat test.sh#!/bin/bashecho "Hello World!" # 将字符输出到屏幕,支持中文[root@Rocky sh]# cat test.sh | bashHello World!
[root@Rocky sh]# echo touch haha.txttouch haha.txt[root@Rocky sh]# echo touch haha.txt | bash[root@Rocky sh]# ll haha.txt-rw-r--r-- 1 root root 0 Jun 22 10:55 haha.txt
2)远程执行[root@Rocky sh]# curl https://kpyun.fun/test.sh#!/bin/bashecho "远程执行命令中ing..."-S配合-s使用 # 静默模式下,如果发生错误,仍然显示错误信息-L # 跟随重定向(如 301/302)[root@Rocky sh]# curl -sSL https://kpyun.fun/test.sh |bash远程执行命令中ing...
3)重定向方式[root@Rocky sh]# bash < test.shHello World!执行方式的区别
📌 关键区别:
-
bash/ 路径方式 —> 在子进程中执行命令(执行完后,自动退出) -
source script.sh/. script.sh—> 当前 Shell(父进程)==逐行读取并解析==(不会创建新的进程)- 无需
x权限
- 无需
⚠️ 路径方式(需要 +x 执行权限),其它都不需要 x 权限
- 子进程:默认继承父进程中 export过的环境变量(全局变量),脚本中的变量不会影响当前终端
- 它不会继承父进程的普通变量
- 父进程:脚本中的变量会保留在当前终端
📦 Shell变量
变量分类
| 分类 | 作用域 | 定义方式 |
|---|---|---|
| ==系统变量==(全局变量) | 全局生效(国法) | export name=oldboy 写入 /etc/profile |
| ==普通变量==(局部变量) | 局部生效(家规) | 直接定义 dir=/data/wordpress |
env→ 只看已 export 的环境变量,这些变量会传递给子进程set→ 看当前 Shell 的所有变量(包括环境变量 + 未导出的局部/普通变量)
[root@Rocky sh]# envSHELL=/bin/bash..............HOSTNAME=RockyPWD=/server/sh[root@Rocky sh]# set | wc -l1698
1)子进程只继承全局变量[root@Rocky ~]# name=jiuzhao # 普通变量[root@Rocky ~]# echo ${name} # 当前shell生效jiuzhao[root@Rocky ~]# cat test.shecho ${name}[root@Rocky ~]# bash test.sh🈳 '子进程不会继承普通变量'[root@Rocky ~]# export name=jiuzhao # 声明为全局变量[root@Rocky ~]# bash test.shjiuzhao # 这样子进程就可以继承到了
2)source / . --> 当前shell'无需 +x 执行权限' ✅ 无论是局部变量,还是全局变量都会生效[root@Rocky ~]# class=kpyun[root@Rocky ~]# echo $classkpyun[root@Rocky ~]# cat test.shecho ${class}[root@Rocky ~]# ll test.sh-rw-r--r-- 1 root root 14 Jun 22 14:46 test.sh# 没有x执行权限[root@Rocky ~]# source test.shkpyun[root@Rocky ~]# . test.shkpyun
3)路径(子进程)执行,必须有 +x 执行权限[root@Rocky ~]# ./test.sh-bash: ./test.sh: Permission denied[root@Rocky ~]# chmod +x test.sh[root@Rocky ~]# ./test.sh'🈳'[root@Rocky ~]# export class=kpyun # 依旧是必须得全局声明[root@Rocky ~]# ./test.shkpyun
4)⚠️ 取消变量[root@Rocky ~]# unset class[root@Rocky ~]# echo $class'🈳'
5)不加 export# 只对当前的 bash 生效[root@Rocky ~]# unset name[root@Rocky ~]# name =jiuzhao-bash: name: command not found '等于号两边没有空格'[root@Rocky ~]# name=jiuzhao[root@Rocky ~]# echo $namejiuzhao[root@Rocky ~]# bash # 进入到子进程[root@Rocky ~]# echo $name'🈳'[root@Rocky ~]# ps auxf | grep sh'f' --> 以树形结构显示root 1066 ? S 09:27 0:08 \_ sshd-session: root@pts/0root 1067 pts/0 Ss 09:27 0:00 \_ -bash ✅ 父进程root 1851 pts/0 S 15:09 0:00 \_ bash ✅ 子进程root 1873 pts/0 S+ 15:09 0:00 \_ grep --color=auto sh💡 只有全局变量(export) --> 对所有的 bash(子进程)生效变量名称定义规范
| 规则 | 示例 |
|---|---|
| 下划线、字符串、数字的组合 | name _name Name_Age |
| 等号两端==不允许有空格== | name=oldboy ✅ name = oldboy ❌ |
| 名字不能以数字开头 | 1name ❌ name1 ✅ |
| 见名知意 | dir=/etc code=/data/wordpress |
| 命名风格 | 大驼峰 NameAge、小驼峰 nameAge、全大写 NAME、全小写 name |
[root@Rocky ~]# name=oldboy # ✅[root@Rocky ~]# name= oldboy # ❌[root@Rocky ~]# name = oldboy # ❌[root@Rocky ~]# 1name=oldboy # ❌[root@Rocky ~]# _name=oldboy # ✅[root@Rocky ~]# echo $_nameoldboy '这里是变量名称'变量值的定义
值必须是连续的数字或字符串,⚠️ 如果不连续则使用单引号或双引号
数字定义:
[root@Rocky ~]# age=22[root@Rocky ~]# echo $namejiuzhao[root@Rocky ~]# age=22 45-bash: 45: command not found❌ 不连续的必须用'单引号或双引号'[root@Rocky ~]# age="23 423"[root@Rocky ~]# echo $age23 423[root@Rocky ~]# age='23 423'[root@Rocky ~]# echo $age23 423字符串定义:
[root@Rocky ~]# name=fjiewjfiw # ✅ 连续字符串[root@Rocky ~]# name=fjie wjfiw # ❌[root@Rocky ~]# name='fjie wjfiw' # ✅ 有空格(不连续)必须加引号'单引号 or 双引号都是可以的'[root@Rocky ~]# echo $namefjie wjfiw
# 变量在路径中的使用[root@Rocky ~]# dir=/etc/sysconfig/network-sh/[root@Rocky ~]# echo $dir/etc/sysconfig/network-sh/[root@Rocky ~]# cd $dir[root@Rocky network-sh]# pwd/etc/sysconfig/network-sh命令定义:
✅ 使用反引号 `` 或 $()[root@Rocky ~]# IP=`hostname -I`[root@Rocky ~]# echo $IP10.0.0.71 172.16.1.71
[root@Rocky ~]# IP=`hostname -I | awk '{print $1}'`[root@Rocky ~]# echo $IP10.0.0.71
[root@Rocky ~]# HOST=$(hostname)[root@Rocky ~]# echo $HOSTRocky变量拼接:
1)单个变量定义[root@Rocky ~]# TIME=`date +"%F %T"`[root@Rocky ~]# echo $TIME2026-06-22 17:24:22[root@Rocky ~]# echo $IP10.0.0.71[root@Rocky ~]# echo $HOSTRocky
2)变量拼接[root@Rocky ~]# DIR=$IP_$HOST_$TIME[root@Rocky ~]# echo $DIR2026-06-22 17:24:22 ❌'${变量名} 用花括号明确变量边界'[root@Rocky ~]# DIR=${IP}_${HOST}_$TIME[root@Rocky ~]# echo $DIR10.0.0.71_Rocky_2026-06-22 17:24:22不加引号 vs 单引号 vs 双引号
| 方式 | 行为 | 示例 |
|---|---|---|
| 不加引号 | 可以解析变量 | DIR=$IP |
"双引号" | 可以解析变量 | DIR="$IP" → 解析 |
'单引号' | ==所见即所得==,不能解析变量 | DIR='$IP' → 原样输出 |
[root@Rocky ~]# DIR=${IP}_${HOST}_$TIME[root@Rocky ~]# echo $DIR10.0.0.71_Rocky_2026-01-19-11-29
[root@Rocky ~]# DIR="${IP}_${HOST}_$TIME"[root@Rocky ~]# echo $DIR10.0.0.71_Rocky_2026-01-19-11-29
[root@Rocky ~]# DIR='${IP}_${HOST}_$TIME'[root@Rocky ~]# echo $DIR${IP}_${HOST}_$TIME# 单引号所见即所得!命令定义方式的注意事项
命令可以定义为==命令==,也可以定义为==字符串==:
1)定义为命令(反引号)[root@Rocky ~]# TIME=`date +%F-%H-%M`⚠️ '时间已经被写死了'[root@Rocky ~]# echo $TIME2026-01-19-11-37[root@Rocky ~]# $TIME-bash: 2026-01-19-11-37: command not found📌 固定死了的值 --> 无法作为命令
2)定义为字符串(单引号)[root@Rocky ~]# TIME='date +%F-%H-%M'[root@Rocky ~]# echo $TIMEdate +%F-%H-%M# '此时 $TIME 的值是一个字符串'✅ 可以直接在终端执行'字符串方式定义的 TIME,执行 $TIME 相当于直接执行 date +%F-%H-%M'[root@Rocky ~]# $TIME2026-01-19-11-42-53[root@Rocky ~]# $TIME2026-01-19-11-42-54'每次执行都会获取最新时间!' 📌 会随时间变化📦 变量相关文件
🧱 两个维度:登录方式 × 交互方式
判断一个 Shell 会加载哪些配置文件,需要同时看两个==正交的维度==:
| 维度 | 分类 | 判断标准 | 决定什么 |
|---|---|---|---|
| 登录方式 | Login / Non-login | 有没有走登录流程(输密码、su -) | 读不读 /etc/profile |
| 交互方式 | Interactive / Non-interactive | 有没有终端提示符(人能不能打字) | 读不读 ~/.bashrc |
这两个维度可以组合出 4 种场景:
| # | 场景 | 登录 | 交互 | 典型例子 |
|---|---|---|---|---|
| ① | 交互式登录 Shell | Login | Interactive | SSH 登录、tty 登录、su - |
| ② | 交互式非登录 Shell | Non-login | Interactive | 终端里敲 bash、su(无横杠) |
| ③ | 非交互式非登录 Shell | Non-login | Non-interactive | 执行脚本 bash script.sh、cron 定时任务 |
| ④ | 非交互式登录 Shell | Login | Non-interactive | bash -l script.sh、echo cmd | ssh user@host |
📌 运维中最常打交道的是 ①②③,④ 极少见
⚠️ ③ 是最大的坑:脚本执行时 既不读 /etc/profile,也不读 ~/.bashrc
🔗 四种场景的加载链条
① 交互式 Login Shell — SSH 登录 / tty / su -
🚪 进大门 → 刷门禁卡(加载 /etc/profile)→ 这是你一天中第一次也是唯一一次刷这张卡
/etc/profile(bash 自己读) ← ① 系统级”欢迎礼包”(所有用户通用) ↓~/.bash_profile(bash 自己读) ← ② 用户级”个人偏好” ↓ 通常它会主动 source ✅ '里面有判断' # 如果存在则拉进来~/.bashrc ← ③ 交互式配置(别名、颜色、补全) ↓ ✅ # 这里面也有判断/etc/bashrc ← ④ 系统级 bash 交互配置🔍 ==细节纠正==:其实 ==bash 自己只读了前 2 个==,后面的文件是被拉进来的
'Rocky'# /etc/profile ⭕ 👇 '子目录下的所有.sh结尾的文件'for i in /etc/profile.d/*.sh ; do if [ -r “$i” ]; then if [ “${-#*i}” != “$-” ]; then . “$i”========================================# ~/.bash_profileif [ -f ~/.bashrc ]; then . ~/.bashrc ✅ 包含 ~/.bashrcfi========================================# ~/.bashrcif [ -f /etc/bashrc ]; then . /etc/bashrc ✅ 包含 /etc/bashrcfi========================================# /etc/bashrc ⭕ 👇/etc/profile.d/下的所有.sh结尾的文件for i in /etc/profile.d/*.sh; do if [ -r "$i" ]; then if [ "$PS1" ]; then . "$i"② 交互式 Non-login Shell — 终端里敲 bash / su
🚶 进办公室 → 刷工位卡(加载 ~/.bashrc)→ 每进一间都要刷,但刷的是==另一张卡==
/etc/bashrc ← ① bash 自己先读这个(系统级) ↓~/.bashrc ← ② bash 再读这个(用户级,覆盖/追加系统设置)# 注:RedHat 系的 ~/.bashrc 里通常也会主动 source /etc/bashrc📌 所以实际上 /etc/bashrc 会被读两遍 —— bash 先读一次,~/.bashrc 再拉一次⚠️ 交互式 Non-login Shell 完全跳过 /etc/profile 和 ~/.bash_profile
所以你在 /etc/profile 里定义的 alias → 新开的 bash 里用不了
✅ 正确做法:alias 写进 /etc/bashrc 或 ~/.bashrc
③ 非交互式 Non-login Shell — 执行脚本 / cron
🤖 这是最容易被忽视的场景,也是坑最多的地方
'执行脚本时,bash 既不读 /etc/profile,也不读 ~/.bashrc'[root@Rocky ~]# cat test.shecho $NAMEecho $HOMEalias========================================'当前终端里 NAME 是有的(~/.bashrc 里定义的)'[root@Rocky ~]# unset NAME[root@Rocky ~]# tail -1 ~/.bashrcNAME=jiuzhao[root@Rocky ~]# echo $NAME🈳[root@Rocky ~]# source ~/.bashrc✅ source后才会生效[root@Rocky ~]# echo $NAMEjiuzhao========================================'但脚本里就是看不到!'[root@Rocky ~]# bash test.sh🈳 # NAME 为空!/root # HOME 是 bash 内置的,不受影响🈳 # alias 也没有!========================================[root@Rocky ~]# source test.shjiuzhao/rootalias cp='cp -i''直接 source 执行(在当前终端执行),可以读取到'为什么? 非交互式 Shell 的配置来源只有一个:
| 来源 | 说明 |
|---|---|
$BASH_ENV | 如果设置了这个环境变量,bash 会 source 它指向的文件 |
继承自父进程的 export 变量 | 跟配置无关,靠的是进程继承 |
| ==其他配置文件一概不读== | /etc/profile ~/.bashrc 统统跳过 |
⚠️ 这就是为什么:
- cron 任务里
alias全部失效 → cron 是非交互式 Non-login Shell - 脚本里用不了
ll→ll是 alias,脚本不走~/.bashrc - systemd 启动的服务读不到
JAVA_HOME→ 服务进程根本不走 Shell 初始化
✅ 解决方案:脚本里需要的变量要么 export,要么在脚本开头显式设置
'要让脚本读到 NAME,有三种办法'1)export 传递[root@Rocky ~]# export NAME=jiuzhao[root@Rocky ~]# bash test.shjiuzhao # ✅ 子进程继承了
2)脚本里显式 source[root@Rocky ~]# cat test.shsource ~/.bashrc # 手动加载配置echo $NAME[root@Rocky ~]# bash test.shjiuzhao # ✅ 手动拉进来了
3)通过 BASH_ENV 指定[root@Rocky ~]# export BASH_ENV=~/.bashrc[root@Rocky ~]# bash test.shjiuzhao # ✅ 非交互式 bash 也会读 BASH_ENV[root@Rocky ~]# unset BASH_ENV'千万别忘记取消定义'④ 非交互式 Login Shell — bash -l script.sh
极少见,但存在:用 -l(或 --login)强制以 Login 模式执行脚本
[root@Rocky ~]# bash -l script.sh# 此时会先加载 /etc/profile → ~/.bash_profile,再执行脚本'不读 ~/.bashrc'# 但如果脚本跑完就退出,环境变量也不会留在当前终端⚙️ 为什么分成两层?
把配置分成两层,是为了==效率 + 合理分工==:
第一层:/etc/profile → ~/.bash_profile(Login 专属) ↓ 放”一辈子只需要设置一次”的东西 - PATH 环境变量 - umask - 全局环境变量(JAVA_HOME 等) ✅ 你登录一次,设置一次就够了
第二层:~/.bashrc → /etc/bashrc ↓ 放”每次开新 bash 都需要”的东西 - alias 别名 - PS1 提示符颜色 - 命令补全脚本 ✅ 每个新的 bash 实例都来一遍📌 一句话总结:
/etc/profile 是 ”登录欢迎礼包”,不是 ”开机自检程序”
- 只有用户真正登入系统的那一刻,它才会被拆开
- 后续开新的 bash(Non-login Shell),都不会再重复拆这个礼包
- 服务器开机后如果一直没人登录,
/etc/profile永远等于一张废纸 🈚
🎯 一张表总结:哪种场景读哪个文件?
| 场景 | /etc/profile | ~/.bash_profile | ~/.bashrc | /etc/bashrc | $BASH_ENV |
|---|---|---|---|---|---|
① SSH 登录 / su - | ✅ | ✅ | ✅(被拉进来) | ✅(被拉进来) | ❌ |
② 终端敲 bash / su | ❌ | ❌ | ✅ | ✅ | ❌ |
③ 脚本 bash script.sh | ❌ | ❌ | ❌ | ❌ | ✅(如果设置了) |
④ bash -l script.sh | ✅ | ✅ | ✅(被拉进来) | ✅(被拉进来) | ❌ |
| cron 定时任务 | ❌ | ❌ | ❌ | ❌ | ❌(默认不设) |
| systemd 服务 | ❌ | ❌ | ❌ | ❌ | ❌ |
📦 /etc/profile.d/ 机制
想让变量对所有交互式 Shell 都可见?最稳妥的做法:
'不要直接改 /etc/profile,在 /etc/profile.d/ 下创建 .sh 文件'[root@Rocky ~]# vim /etc/profile.d/custom.shexport APP_HOME=/opt/myappexport APP_USER=admin💡 /etc/profile 源码里有一行 for i in /etc/profile.d/*.sh; do . $i; done
/etc/bashrc确实也会遍历/etc/profile.d/*.sh,这不是 bug,是刻意设计- 两段遍历是互补的:分别覆盖登录和非登录场景,确保所有交互式 Shell 都能获得一致的环境
- 放入
/etc/profile.d/的脚本必须是幂等的,因为它们可能被 source 多次-
更模块化、更好维护
-
⚠️ 但 /etc/profile.d/ 也只对交互式 Shell 生效,脚本(非交互式)仍然不会自动加载!
⚠️ 加载频率 vs 变量作用域
千万别把两件事混为一谈:
| 概念 | 意思 |
|---|---|
| 加载频率 | 这个文件什么时候被读取 |
| 变量作用域 | 这个变量谁能看到(子进程能不能继承) |
~/.bashrc 每次交互都加载 → 解决的是==加载频率==,跟==作用域==毫无关系
❓ 为什么会有”像全局变量”的错觉
'你开了 3 个终端'终端A → bash → ~/.bashrc 加载 → NAME=jiuzhao ✅终端B → bash → ~/.bashrc 加载 → NAME=jiuzhao ✅终端C → bash → ~/.bashrc 加载 → NAME=jiuzhao ✅
'看起来像是”全局”的,但其实是各自独立的三份副本!''一旦你从终端A 执行一个脚本'终端A → bash script.sh → 子进程 → 🈚 NAME 没传过来!# 脚本是非交互式 Non-login Shell,不读 ~/.bashrc# 而且 NAME 没有 export,所以子进程也继承不到📌 三句话总结:
~/.bashrc保证每个交互式终端都自动配好变量(靠的是==每次开 bash 都加载==)export保证所有子进程能继承(靠的是==作用域标记==)- 两者配合(
~/.bashrc里写export NAME=jiuzhao)→ 每个交互式终端自动有,且它启动的子进程也能用
📦 Shell位置变量
位置变量速查表
| 变量 | 含义 | 说明 |
|---|---|---|
$0 | 脚本名称 | 常用于启停脚本中的 usage 提示 $0 就是脚本自己的名字 |
$n | 第 n 个参数 | $1 第一个参数,$2 第二个… $10 后需用 ${10} |
$# | 传参的个数 | 常用于判断用户是否传入了正确数量的参数 |
$? | 上一条命令的返回结果 | 0 = 成功,非 0 = 失败 |
$$ | 脚本的 PID 号 | 用于写 PID 文件,防止重复启动 |
$! | 上一个后台运行脚本的 PID 号 | 调试脚本用 |
$* | 接收所有传参 | 在循环体中与 $@ 不同(见下方) |
$@ | 接收所有传参 | 在循环体中与 $* 不同(见下方) |
$_ | 脚本传参的最后一个参数 | 也表示上一条命令的最后一个参数 |
$n — 脚本传参
[root@Rocky sh]# cat test.sh#!/bin/bash#testecho $1[root@Rocky sh]# sh test.sh hehehehe[root@Rocky sh]# sh test.sh abcabc
# 多个参数[root@Rocky sh]# cat test.sh#!/bin/bash#testecho $1 $2[root@Rocky sh]# sh test.sh a ba b
# 传参支持序列[root@Rocky sh]# sh test.sh {a..z}a b[root@Rocky sh]# sh test.sh {1..10}1 2
# $10 需要用 ${10}✅ 作为一个整体'否则只识别前面的1,不会识别后面的0'[root@Rocky sh]# cat test.sh#!/bin/bash#testecho $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10}[root@Rocky sh]# sh test.sh {a..z}a b c d e f g h i j$# — 传参数量
[root@Rocky sh]# cat test.sh#!/bin/bash#testecho $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10}echo $#[root@Rocky sh]# sh test.sh {a..z}a b c d e f g h i j26案例:判断用户传参个数
[root@Rocky sh]# cat test.sh#!/bin/bash[ $# -ne 2 ] && echo "必须传入2个参数" && exit# 如果传入的参数 -ne 不等于2echo name=$1 age=$2
[root@Rocky sh]# sh test.sh必须传入2个参数[root@Rocky sh]# sh test.sh a c d必须传入2个参数[root@Rocky sh]# sh test.sh a cname=a age=c
# 使用 -x 查看脚本执行过程[root@Rocky sh]# sh -x test.sh a+ '[' 1 -ne 2 ']'+ echo 必须传入2个参数必须传入2个参数+ exit[root@Rocky sh]# sh -x test.sh a b+ '[' 2 -ne 2 ']'+ echo name=a age=bname=a age=b$? — 上一条命令返回结果
[root@Rocky ~]# ping -c1 -W1 www.baidu.com &>/dev/null[root@Rocky ~]# echo $?0# 0 = 成功[root@Rocky ~]# ping -c1 -W1 www.baiduaaaa.com &>/dev/null[root@Rocky ~]# echo $?2# 非0 = 失败
# 实用组合[root@Rocky ~]# [ $? -eq 0 ] && echo 域名通 || echo 域名不通域名不通[root@Rocky ~]# ping -c1 -W1 www.baidu.com &>/dev/null[root@Rocky ~]# [ $? -eq 0 ] && echo 域名通 || echo 域名不通域名通ping 检测脚本:
[root@Rocky sh]# cat ping.sh#!/bin/bashping -c1 -W1 $1 &>/dev/null[ $? -eq 0 ] && echo $1域名通 || echo $1域名不通
[root@Rocky sh]# sh ping.sh www.baidu.comwww.baidu.com域名通 ✅[root@Rocky sh]# sh ping.sh www.baiduaaaa.comwww.baiduaaaa.com域名不通 ❌⚠️ $? 的返回值==极易被覆盖==!它始终返回紧邻的上一条命令的结果
如果需要保留 $? 的值,务必==立即赋值给变量==:
ping -c2 -W2 $1 &>/dev/nullflag=$? # 将 $? 的值立即赋予给 flagecho hehe # 这条命令成功后 $? 变成 0!touch 1.txtecho $? # 现在 $? 是 touch 的返回值ech oldboyedu # 这条命令报错
[ $flag -eq 0 ] && echo $1域名通 || echo $1域名不通 # 用 flag 而非 $?$$ — 脚本的 PID 号
作用:将 PID 写入文件,启动时判断此文件,有则无需重复启动,没有则启动
[root@Rocky ~]# cat /var/run/nginx.pid1024[root@Rocky ~]# kill `cat /var/run/nginx.pid`$* vs $@ — 所有传参
[root@Rocky sh]# cat test.sh#!/bin/bashecho name=$1 age=$2
echo $*echo $@[root@Rocky sh]# sh test.sh {a..z}name=a age=ba b c d e f g h i j k l m n o p q r s t u v w x y za b c d e f g h i j k l m n o p q r s t u v w x y z# '不加引号时,$* 和 $@ 看起来一样'📌 加双引号后 $* 和 $@ 的区别:
[root@Rocky sh]# set -- "I am" oldboy
# "$*" → 把所有参数当成一个整体[root@Rocky sh]# for i in "$*"; do echo $i; doneI am oldboy
# "$@" → 每个参数独立处理(保留原始参数边界)[root@Rocky sh]# for i in "$@"; do echo $i; doneI amoldboy"$*":所有参数合并成一个字符串"$@":每个参数保持独立,==推荐在循环中使用"$@"== ✅
$_ — 最后一个参数
[root@Rocky sh]# sh test.sh a b c d ename=a age=ba b c d ea b c d e[root@Rocky sh]# echo $_e# '表示传参的最后一个参数'
[root@Rocky sh]# ll /etc/hosts /etc/passwd-rw-r--r-- 1 root root 158 Jun 23 2020 /etc/hosts-rw-r--r-- 1 root root 2046 Jan 19 10:58 /etc/passwd[root@Rocky sh]# echo $_/etc/passwd# '也表示上一条命令的最后一个参数'⚙️ 变量传参的三种方式
方法 1:直接传参
[root@Rocky sh]# cat test.sh#!/bin/bashecho name=$1 age=$2[root@Rocky sh]# sh test.sh old 123name=old age=123方法 2:赋值传参
案例:赋值传参 + 友好输出
[root@Rocky sh]# cat test.sh#!/bin/bashname=$1age=$2
echo 姓名: $nameecho 年龄: $age[root@Rocky sh]# sh test.sh oldgirl 123姓名: oldgirl年龄: 123案例:ping 检测(赋值传参)
[root@Rocky sh]# cat ping.sh#!/bin/bashurl=$1ping -c1 -W1 $url &>/dev/null[ $? -eq 0 ] && echo $url域名通 || echo $url域名不通
[root@Rocky sh]# sh ping.sh www.sina.comwww.sina.com域名通方法 3:read 读入
[root@Rocky sh]# cat test.sh#!/bin/bashread -p "请输入你的姓名: " nameread -p "请输入你的年龄: " age
echo "你的姓名是: " $nameecho "你的年龄是: " $age[root@Rocky sh]# sh test.sh请输入你的姓名: oldboy请输入你的年龄: 123你的姓名是: oldboy你的年龄是: 123案例:银行卡密码验证(-s 隐藏输入)
[root@Rocky sh]# cat a.sh#!/bin/bashread -p "请输入你的银行卡号: " numread -s -p "请输入你的银行密码: " pass # -s 隐藏密码echo -e "\n"[ $pass -eq 123456 ] && echo 银行卡$num 余额999999999RMB! || echo 密码错误
[root@Rocky sh]# sh a.sh请输入你的银行卡号: 666666请输入你的银行密码: # '输入时看不到密码'银行卡666666 余额999999999RMB!三种传参方式对比
| 方式 | 语法 | 适用场景 |
|---|---|---|
| 直接传参 | $1 $2 $3 | 脚本间调用、命令行快速执行 |
| 赋值传参 | name=$1 | 参数较多时提高可读性 |
read 读入 | read -p "提示: " var(变量名) | 交互式脚本、需要隐藏输入(-s) |
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!




