Shell编程基础

6139 字
31 分钟
Shell编程基础

Shell编程基础#

[TOC]


🧱 什么是Shell#

Shell 是==命令解释器==,负责翻译用户输入的命令给内核,内核处理完成后返回给 Shell

Terminal window
┌──────────────┐ ┌──────────────┐ ┌─────────────────┐
用户 │────▶│ Shell │────▶│ 内核
(输入命令) │ │ (翻译+调度) │ │ (真正干活) │
└──────────────┘ └──────┬───────┘ └────────┬────────┘
[调用系统调用] [访问硬件:磁盘/内存/CPU]
◀─────── 结果返回 ───────┘
'Shell 输出结果到终端 → 你看到!'
Tip

📌 一句话Shell 就是你跟内核之间的”翻译官”

  • Linux默认交互式 Shell(👤 人类用户)的解释器是 bash
    • 当你打开终端、SSH 登录服务器时,系统自动启动的那个 Shell(交互式shell
Terminal window
1)默认交互式shell
[root@Rocky ~]# bash
[root@Rocky ~]# exit
exit
[root@Rocky ~]# exit
logout
root@Ubuntu ~# bash
root@Ubuntu ~# exit
exit
root@Ubuntu ~# exit
logout

  • /bin/sh 是==🤖 脚本中==默认的解释器,但前提是脚本**没有写 Shebang(#!)**或者显式调用了 /bin/sh
    • 是一个标准化的系统接口用于执行非交互式的 Shell
Terminal window
2)Red Hat系
`/bin/sh` 指向的就是 `bash`
bash 好用、功能丰富
[root@Rocky ~]# ll /bin/sh
lrwxrwxrwx. 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/sh
lrwxrwxrwx 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*
Warning

正因为 Ubuntu 的 shdash,在编写 Shell 脚本时必须使用#!/bin/bash 作为 Shebang,而不能用 #!/bin/sh

  • 想用 bash 特性 → 必须写 #!/bin/bash
  • 永远不要假设 /bin/sh 就是 bash,尤其是在 Ubuntu/Debian 系统上!

🚢 Shell编程的作用#

面试题:给你一台服务器,对服务器操作流程是什么样的?

Terminal window
1)硬件准备
# 组RAID
2)安装操作系统
# 自动化安装 cobbler、kickstart 脚本
3)系统优化
# 优化SSH、加大文件描述符、时间同步、优化防火墙、安装常用软件、内核优化 ... 脚本
4)安装服务、优化服务
# yum、编译 脚本
5)监控服务
# 脚本
6)日志切割、定时任务
# 防止日志过大
7)日志统计处理
8)编写启停脚本
# python程序、jar包...
9)辅助程序正常运行
# 资源满导致负载过高 -> 自动杀死进程

📌 可以看到,Shell 编程贯穿了服务器运维的==整个生命周期==


⚙️ Shell脚本规范#

序号规范说明
脚本开头指定解释器#!/bin/bashRedhat系可写可不写(sh —> bash)
Debian系必须指定(sh —> dash)
脚本名称结尾.sh 结尾
见名知意脚本名称要能看出功能
避免中文符号脚本中不能出现中文符号 "" '' `` {} () [] , . <>
成对符号一次写完{} [] () 时一次写完,避免遗漏
统一存放位置脚本放在同一个位置,方便管理
写注释注明作用、功能、作者
Tip

💡 新服务器如何找到所有 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/bash
echo "Hello World!" # 将字符输出到屏幕,支持中文
# 执行脚本
[root@Rocky ~]# sh test.sh
Hello World!

⚙️ 执行Shell脚本的方式#

三种常用执行方式#

第一种:sh / bash#

Terminal window
[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.sh
Hello World!
[root@Rocky sh]# bash test.sh
Hello World!
Tip

✅ 推荐用 bash 执行,Debian系的 ==sh —> dash== ⚠️

  • dash 是一个严格遵循 POSIX 标准的轻量级 Shell,它不支持大量 Bash 扩展语法

第二种:路径方式(需要 +x 执行权限)#

Terminal window
# 绝对路径
'以根 / 开始'
[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.sh
Hello World!
# 相对路径
[root@Rocky sh]# ./test.sh # 使用 ./ 路径方式执行脚本
'点./ 路径'
Hello World!
Note

⚠️ 直接输入脚本名 test.sh 会报 command not found命令未找到),必须用 ./test.sh

❌ 上面 test.sh 会去找 —> PATH变量(压根没有这个命令

第三种:source / .(紧跟空格)#

Terminal window
[root@Rocky sh]# source test.sh
Hello World!
[root@Rocky sh]# . test.sh
Hello World!

其他执行方式#

1)管道传给 bash(最常用)
# 它把前一个命令原本要打印到'屏幕上的内容',转而塞进了后一个命令的标准输入通道里
[root@Rocky sh]# cat test.sh
#!/bin/bash
echo "Hello World!" # 将字符输出到屏幕,支持中文
[root@Rocky sh]# cat test.sh | bash
Hello World!
[root@Rocky sh]# echo touch haha.txt
touch 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/bash
echo "远程执行命令中ing..."
-S配合-s使用 # 静默模式下,如果发生错误,仍然显示错误信息
-L # 跟随重定向(如 301/302)
[root@Rocky sh]# curl -sSL https://kpyun.fun/test.sh |bash
远程执行命令中ing...
3)重定向方式
[root@Rocky sh]# bash < test.sh
Hello World!

执行方式的区别#

Important

📌 关键区别

  • bash / 路径方式 —> 在子进程中执行命令(执行完后,自动退出

  • source script.sh / . script.sh —> 当前 Shell(父进程)==逐行读取并解析==(不会创建新的进程

    • 无需 x 权限

⚠️ 路径方式(需要 +x 执行权限),其它都不需要 x 权限

  • 子进程:默认继承父进程中 export过的环境变量(全局变量),脚本中的变量不会影响当前终端
    • 不会继承父进程的普通变量
  • 父进程:脚本中的变量会保留在当前终端

📦 Shell变量#

变量分类#

分类作用域定义方式
==系统变量==(全局变量)全局生效(国法)export name=oldboy 写入 /etc/profile
==普通变量==(局部变量)局部生效(家规)直接定义 dir=/data/wordpress
Tip
  • env → 只看已 export 的环境变量,这些变量会传递给子进程
  • set → 看当前 Shell 的所有变量包括环境变量 + 未导出的局部/普通变量
Terminal window
[root@Rocky sh]# env
SHELL=/bin/bash
..............
HOSTNAME=Rocky
PWD=/server/sh
[root@Rocky sh]# set | wc -l
1698
1)子进程只继承全局变量
[root@Rocky ~]# name=jiuzhao # 普通变量
[root@Rocky ~]# echo ${name} # 当前shell生效
jiuzhao
[root@Rocky ~]# cat test.sh
echo ${name}
[root@Rocky ~]# bash test.sh
🈳 '子进程不会继承普通变量'
[root@Rocky ~]# export name=jiuzhao # 声明为全局变量
[root@Rocky ~]# bash test.sh
jiuzhao # 这样子进程就可以继承到了
2)source / . --> 当前shell
'无需 +x 执行权限' 无论是局部变量,还是全局变量都会生效
[root@Rocky ~]# class=kpyun
[root@Rocky ~]# echo $class
kpyun
[root@Rocky ~]# cat test.sh
echo ${class}
[root@Rocky ~]# ll test.sh
-rw-r--r-- 1 root root 14 Jun 22 14:46 test.sh
# 没有x执行权限
[root@Rocky ~]# source test.sh
kpyun
[root@Rocky ~]# . test.sh
kpyun
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.sh
kpyun
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 $name
jiuzhao
[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/0
root 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=oldboyname = oldboy
名字不能以数字开头1namename1
见名知意dir=/etc code=/data/wordpress
命名风格大驼峰 NameAge、小驼峰 nameAge、全大写 NAME、全小写 name
Terminal window
[root@Rocky ~]# name=oldboy # ✅
[root@Rocky ~]# name= oldboy # ❌
[root@Rocky ~]# name = oldboy # ❌
[root@Rocky ~]# 1name=oldboy # ❌
[root@Rocky ~]# _name=oldboy # ✅
[root@Rocky ~]# echo $_name
oldboy '这里是变量名称'

变量值的定义#

Note

值必须是连续的数字或字符串,⚠️ 如果不连续则使用单引号或双引号

数字定义#

Terminal window
[root@Rocky ~]# age=22
[root@Rocky ~]# echo $name
jiuzhao
[root@Rocky ~]# age=22 45
-bash: 45: command not found
不连续的必须用'单引号或双引号'
[root@Rocky ~]# age="23 423"
[root@Rocky ~]# echo $age
23 423
[root@Rocky ~]# age='23 423'
[root@Rocky ~]# echo $age
23 423

字符串定义#

Terminal window
[root@Rocky ~]# name=fjiewjfiw # ✅ 连续字符串
[root@Rocky ~]# name=fjie wjfiw # ❌
[root@Rocky ~]# name='fjie wjfiw' # ✅ 有空格(不连续)必须加引号
'单引号 or 双引号都是可以的'
[root@Rocky ~]# echo $name
fjie 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

命令定义#

Terminal window
使用反引号 `` $()
[root@Rocky ~]# IP=`hostname -I`
[root@Rocky ~]# echo $IP
10.0.0.71 172.16.1.71
[root@Rocky ~]# IP=`hostname -I | awk '{print $1}'`
[root@Rocky ~]# echo $IP
10.0.0.71
[root@Rocky ~]# HOST=$(hostname)
[root@Rocky ~]# echo $HOST
Rocky

变量拼接#

Terminal window
1)单个变量定义
[root@Rocky ~]# TIME=`date +"%F %T"`
[root@Rocky ~]# echo $TIME
2026-06-22 17:24:22
[root@Rocky ~]# echo $IP
10.0.0.71
[root@Rocky ~]# echo $HOST
Rocky
2)变量拼接
[root@Rocky ~]# DIR=$IP_$HOST_$TIME
[root@Rocky ~]# echo $DIR
2026-06-22 17:24:22
'${变量名} 用花括号明确变量边界'
[root@Rocky ~]# DIR=${IP}_${HOST}_$TIME
[root@Rocky ~]# echo $DIR
10.0.0.71_Rocky_2026-06-22 17:24:22

不加引号 vs 单引号 vs 双引号#

方式行为示例
不加引号可以解析变量DIR=$IP
"双引号"可以解析变量DIR="$IP" → 解析
'单引号'==所见即所得==,不能解析变量DIR='$IP' → 原样输出
Terminal window
[root@Rocky ~]# DIR=${IP}_${HOST}_$TIME
[root@Rocky ~]# echo $DIR
10.0.0.71_Rocky_2026-01-19-11-29
[root@Rocky ~]# DIR="${IP}_${HOST}_$TIME"
[root@Rocky ~]# echo $DIR
10.0.0.71_Rocky_2026-01-19-11-29
[root@Rocky ~]# DIR='${IP}_${HOST}_$TIME'
[root@Rocky ~]# echo $DIR
${IP}_${HOST}_$TIME
# 单引号所见即所得!

命令定义方式的注意事项#

Note

命令可以定义为==命令==,也可以定义为==字符串==:

Terminal window
1)定义为命令(反引号)
[root@Rocky ~]# TIME=`date +%F-%H-%M`
⚠️ '时间已经被写死了'
[root@Rocky ~]# echo $TIME
2026-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 $TIME
date +%F-%H-%M
# '此时 $TIME 的值是一个字符串'
可以直接在终端执行
'字符串方式定义的 TIME,执行 $TIME 相当于直接执行 date +%F-%H-%M'
[root@Rocky ~]# $TIME
2026-01-19-11-42-53
[root@Rocky ~]# $TIME
2026-01-19-11-42-54
'每次执行都会获取最新时间!' 📌 会随时间变化

📦 变量相关文件#

🧱 两个维度:登录方式 × 交互方式#

判断一个 Shell 会加载哪些配置文件,需要同时看两个==正交的维度==:

维度分类判断标准决定什么
登录方式Login / Non-login有没有走登录流程(输密码、su -读不读 /etc/profile
交互方式Interactive / Non-interactive有没有终端提示符(人能不能打字)读不读 ~/.bashrc

这两个维度可以组合出 4 种场景

#场景登录交互典型例子
交互式登录 ShellLoginInteractiveSSH 登录、tty 登录、su -
交互式非登录 ShellNon-loginInteractive终端里敲 bashsu(无横杠)
非交互式非登录 ShellNon-loginNon-interactive执行脚本 bash script.shcron 定时任务
非交互式登录 ShellLoginNon-interactivebash -l script.shecho cmd | ssh user@host
Important

📌 运维中最常打交道的是 ①②③,④ 极少见

⚠️ ③ 是最大的坑:脚本执行时 既不读 /etc/profile,也不读 ~/.bashrc


🔗 四种场景的加载链条#

① 交互式 Login Shell — SSH 登录 / tty / su -#

🚪 进大门 → 刷门禁卡(加载 /etc/profile)→ 这是你一天中第一次也是唯一一次刷这张卡

Terminal window
/etc/profile(bash 自己读) 系统级”欢迎礼包”(所有用户通用)
~/.bash_profile(bash 自己读) ← ② 用户级”个人偏好”
通常它会主动 source '里面有判断' # 如果存在则拉进来
~/.bashrc ← ③ 交互式配置(别名、颜色、补全)
# 这里面也有判断
/etc/bashrc 系统级 bash 交互配置

🔍 ==细节纠正==:其实 ==bash 自己只读了前 2 个==,后面的文件是被拉进来

Terminal window
'Rocky'
# /etc/profile ⭕ 👇 '子目录下的所有.sh结尾的文件'
for i in /etc/profile.d/*.sh ; do
if [ -r$i” ]; then
if [ “${-#*i}!=$-” ]; then
. $i
========================================
# ~/.bash_profile
if [ -f ~/.bashrc ]; then
. ~/.bashrc 包含 ~/.bashrc
fi
========================================
# ~/.bashrc
if [ -f /etc/bashrc ]; then
. /etc/bashrc 包含 /etc/bashrc
fi
========================================
# /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)→ 每进一间都要刷,但刷的是==另一张卡==

Terminal window
/etc/bashrc bash 自己先读这个(系统级)
~/.bashrc ← ② bash 再读这个(用户级,覆盖/追加系统设置)
# 注:RedHat 系的 ~/.bashrc 里通常也会主动 source /etc/bashrc
📌 所以实际上 /etc/bashrc 会被读两遍 —— bash 先读一次,~/.bashrc 再拉一次
Caution

⚠️ 交互式 Non-login Shell 完全跳过 /etc/profile~/.bash_profile

所以你在 /etc/profile 里定义的 alias → 新开的 bash 里用不了

✅ 正确做法:alias 写进 /etc/bashrc~/.bashrc

③ 非交互式 Non-login Shell — 执行脚本 / cron#

Warning

🤖 这是最容易被忽视的场景,也是坑最多的地方

Terminal window
'执行脚本时,bash 既不读 /etc/profile,也不读 ~/.bashrc'
[root@Rocky ~]# cat test.sh
echo $NAME
echo $HOME
alias
========================================
'当前终端里 NAME 是有的(~/.bashrc 里定义的)'
[root@Rocky ~]# unset NAME
[root@Rocky ~]# tail -1 ~/.bashrc
NAME=jiuzhao
[root@Rocky ~]# echo $NAME
🈳
[root@Rocky ~]# source ~/.bashrc
source后才会生效
[root@Rocky ~]# echo $NAME
jiuzhao
========================================
'但脚本里就是看不到!'
[root@Rocky ~]# bash test.sh
🈳 # NAME 为空!
/root # HOME 是 bash 内置的,不受影响
🈳 # alias 也没有!
========================================
[root@Rocky ~]# source test.sh
jiuzhao
/root
alias cp='cp -i'
'直接 source 执行(在当前终端执行),可以读取到'

为什么? 非交互式 Shell 的配置来源只有一个:

来源说明
$BASH_ENV如果设置了这个环境变量,bash 会 source 它指向的文件
继承自父进程的 export 变量跟配置无关,靠的是进程继承
==其他配置文件一概不读==/etc/profile ~/.bashrc 统统跳过
Warning

⚠️ 这就是为什么:

  • cron 任务里 alias 全部失效 → cron 是非交互式 Non-login Shell
  • 脚本里用不了 llll 是 alias,脚本不走 ~/.bashrc
  • systemd 启动的服务读不到 JAVA_HOME → 服务进程根本不走 Shell 初始化

✅ 解决方案:脚本里需要的变量要么 export,要么在脚本开头显式设置

Terminal window
'要让脚本读到 NAME,有三种办法'
1)export 传递
[root@Rocky ~]# export NAME=jiuzhao
[root@Rocky ~]# bash test.sh
jiuzhao # ✅ 子进程继承了
2)脚本里显式 source
[root@Rocky ~]# cat test.sh
source ~/.bashrc # 手动加载配置
echo $NAME
[root@Rocky ~]# bash test.sh
jiuzhao # ✅ 手动拉进来了
3)通过 BASH_ENV 指定
[root@Rocky ~]# export BASH_ENV=~/.bashrc
[root@Rocky ~]# bash test.sh
jiuzhao # ✅ 非交互式 bash 也会读 BASH_ENV
[root@Rocky ~]# unset BASH_ENV
'千万别忘记取消定义'

④ 非交互式 Login Shell — bash -l script.sh#

极少见,但存在:用 -l(或 --login)强制以 Login 模式执行脚本

Terminal window
[root@Rocky ~]# bash -l script.sh
# 此时会先加载 /etc/profile → ~/.bash_profile,再执行脚本
'不读 ~/.bashrc'
# 但如果脚本跑完就退出,环境变量也不会留在当前终端

⚙️ 为什么分成两层?#

把配置分成两层,是为了==效率 + 合理分工==:

Terminal window
第一层:/etc/profile ~/.bash_profile(Login 专属)
放”一辈子只需要设置一次”的东西
- PATH 环境变量
- umask
- 全局环境变量(JAVA_HOME 等)
你登录一次,设置一次就够了
第二层:~/.bashrc /etc/bashrc
放”每次开新 bash 都需要”的东西
- alias 别名
- PS1 提示符颜色
- 命令补全脚本
每个新的 bash 实例都来一遍
Note

📌 一句话总结

/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 都可见?最稳妥的做法:

Terminal window
'不要直接改 /etc/profile,在 /etc/profile.d/ 下创建 .sh 文件'
[root@Rocky ~]# vim /etc/profile.d/custom.sh
export APP_HOME=/opt/myapp
export APP_USER=admin
Tip

💡 /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 变量作用域#

Caution

千万别把两件事混为一谈:

概念意思
加载频率这个文件什么时候被读取
变量作用域这个变量谁能看到(子进程能不能继承)

~/.bashrc 每次交互都加载 → 解决的是==加载频率==,跟==作用域==毫无关系

❓ 为什么会有”像全局变量”的错觉

Terminal window
'你开了 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,所以子进程也继承不到
Important

📌 三句话总结

  • ~/.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
#test
echo $1
[root@Rocky sh]# sh test.sh hehe
hehe
[root@Rocky sh]# sh test.sh abc
abc
# 多个参数
[root@Rocky sh]# cat test.sh
#!/bin/bash
#test
echo $1 $2
[root@Rocky sh]# sh test.sh a b
a 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
#test
echo $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
#test
echo $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 j
26

案例:判断用户传参个数

[root@Rocky sh]# cat test.sh
#!/bin/bash
[ $# -ne 2 ] && echo "必须传入2个参数" && exit
# 如果传入的参数 -ne 不等于2
echo 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 c
name=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=b
name=a age=b

$? — 上一条命令返回结果#

Terminal window
[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/bash
ping -c1 -W1 $1 &>/dev/null
[ $? -eq 0 ] && echo $1域名通 || echo $1域名不通
[root@Rocky sh]# sh ping.sh www.baidu.com
www.baidu.com域名通
[root@Rocky sh]# sh ping.sh www.baiduaaaa.com
www.baiduaaaa.com域名不通
Warning

⚠️ $? 的返回值==极易被覆盖==!它始终返回紧邻的上一条命令的结果

如果需要保留 $? 的值,务必==立即赋值给变量==:

Terminal window
ping -c2 -W2 $1 &>/dev/null
flag=$? # 将 $? 的值立即赋予给 flag
echo hehe # 这条命令成功后 $? 变成 0!
touch 1.txt
echo $? # 现在 $? 是 touch 的返回值
ech oldboyedu # 这条命令报错
[ $flag -eq 0 ] && echo $1域名通 || echo $1域名不通 # 用 flag 而非 $?

$$ — 脚本的 PID 号#

作用:将 PID 写入文件,启动时判断此文件,有则无需重复启动,没有则启动

Terminal window
[root@Rocky ~]# cat /var/run/nginx.pid
1024
[root@Rocky ~]# kill `cat /var/run/nginx.pid`

$* vs $@ — 所有传参#

[root@Rocky sh]# cat test.sh
#!/bin/bash
echo name=$1 age=$2
echo $*
echo $@
[root@Rocky sh]# sh test.sh {a..z}
name=a age=b
a 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
a 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
# '不加引号时,$* 和 $@ 看起来一样'
Important

📌 加双引号后 $*$@ 的区别

Terminal window
[root@Rocky sh]# set -- "I am" oldboy
# "$*" → 把所有参数当成一个整体
[root@Rocky sh]# for i in "$*"; do echo $i; done
I am oldboy
# "$@" → 每个参数独立处理(保留原始参数边界)
[root@Rocky sh]# for i in "$@"; do echo $i; done
I am
oldboy
  • "$*":所有参数合并成一个字符串
  • "$@":每个参数保持独立,==推荐在循环中使用 "$@"== ✅

$_ — 最后一个参数#

Terminal window
[root@Rocky sh]# sh test.sh a b c d e
name=a age=b
a b c d e
a 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/bash
echo name=$1 age=$2
[root@Rocky sh]# sh test.sh old 123
name=old age=123

方法 2:赋值传参#

案例:赋值传参 + 友好输出

[root@Rocky sh]# cat test.sh
#!/bin/bash
name=$1
age=$2
echo 姓名: $name
echo 年龄: $age
[root@Rocky sh]# sh test.sh oldgirl 123
姓名: oldgirl
年龄: 123

案例:ping 检测(赋值传参)

[root@Rocky sh]# cat ping.sh
#!/bin/bash
url=$1
ping -c1 -W1 $url &>/dev/null
[ $? -eq 0 ] && echo $url域名通 || echo $url域名不通
[root@Rocky sh]# sh ping.sh www.sina.com
www.sina.com域名通

方法 3:read 读入#

[root@Rocky sh]# cat test.sh
#!/bin/bash
read -p "请输入你的姓名: " name
read -p "请输入你的年龄: " age
echo "你的姓名是: " $name
echo "你的年龄是: " $age
[root@Rocky sh]# sh test.sh
请输入你的姓名: oldboy
请输入你的年龄: 123
你的姓名是: oldboy
你的年龄是: 123

案例:银行卡密码验证(-s 隐藏输入)

[root@Rocky sh]# cat a.sh
#!/bin/bash
read -p "请输入你的银行卡号: " num
read -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

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Shell编程基础
https://www.kpyun.fun/posts/automation/shell/shell01/
作者
久棹
发布于
2026-06-20
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
久棹
只要胆子大,天天寒暑假!
公告
欢迎来到久棹的技术小站!本站专注 Linux 运维学习笔记分享,如有问题欢迎交流探讨 🎉
分类
标签
站点统计
文章
98
分类
11
标签
203
总字数
244,453
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.13.5
文章许可
CC BY-NC-SA 4.0

文章目录