Linux下Shell脚本基础知识
主要参考视频:
这可能是B站讲的最好的Linux Shell脚本教程,3h打通Linux-shell全套教程,从入门到精通完整版_哔哩哔哩_bilibili
主要参考文档:
先用视频入门,然后参考手册学习,之后实践总结。
认识Shell和Bash
计算机里,我们基本都会使用操作系统,对于开发人员来说,我们肯定需要控制操作系统做一些事情,但是有个问题,那就是出于安全性考虑我们用户层没法直接访问操纵系统的内核,这种情况下,就有人提出解决方案,那就是在内核操作上套一层“壳”,也就是Shell,然后提供给用户一些指令,用户层再通过指令去完成要干的活,这样,就避免了直接操作内核,保证了内核的安全性以及系统的稳定性。
Shell命令就是由此而来,Shell命令按照一定格式组成的文本就是Shell脚本。
那Bash是怎么回事呢?
有Shell命令,那肯定要有命令的解释器,也就是将输入的命令转换成实际操作系统内核要干的活,其中,Bash就是Shell命令的解释器。
有了一种解释器,肯定就有它的优点和缺点,必然就会出现其他种类的解释器,各有各的特点,各有各的适用场景,于是,市场上就出现了多种Bash。
事实上,不同的Bash对应的Shell也有些不同。
Linux中默认使用Bash,我们操作Linux就需要学习各种Bash Shell。
Linux操作系统中有不同类型的Shell。其中一些如下:
- Bourne Shell
- C shell
- Korn Shell
- GNU Bourne Shell
要想知道操作系统支持哪种Shell类型,可在终端中输入以下命令:
cat /etc/shells要想知道bash在操作系统中的位置,可键入以下命令,将获得一个特定的位置:
which bash如下:
显示的都是绝对路径。
Bash是Linux附带的标准Shell。它是当今最流行的开源Shell,并且具有在下一主题中阅读的各种生产功能。它也可用于Linux发行版,MacOS,Solaris 11和Windows 10。它通过许多改进为用户提供最佳体验。
常见 Shell 汇总笔记(理清概念,区分【标准】和【具体程序】)
核心区分两件事:
- POSIX sh:是语法规范(一套规则),不是程序文件。
- bash / dash / zsh / ksh / csh:都是具体的可执行程序,是 shell 的不同实现。
一、Unix 家族主流 Shell(重点记前 3 个)
1. sh(Bourne Shell,原始始祖)
- 1977 年 Steve Bourne 写的最古老 shell,名字叫
sh(Bourne shell)- 现在几乎没人直接用原始 sh 程序,太老了。
- 现在大家说的
#!/bin/sh,指符合 POSIX 标准的 shell 解释器,是一个软链接,指向别的实现(dash 或者 bash)✅ POSIX 标准就是基于 Bourne shell 精简定义的最小语法集。
2. dash(Debian Almquist Shell)
- Almquist shell(ash)的 Debian 改版,轻量、小巧、启动极快、严格遵守 POSIX
- Ubuntu / Debian:
/bin/sh软链接 → dash- ❌ 不支持 bash 扩展:
[[ ]]、(( ))- 用途:系统开机脚本、系统自带小脚本(追求速度,功能极简)
3. bash(Bourne Again Shell,最常用)
- Linux交互式默认 shell(你打开终端敲命令,一般就是 bash)
- 完全兼容 POSIX (sh) 语法,在 sh 基础上加大量扩展
- ✅ 支持:
[[ ]]、(( ))、$(( ))、数组、=~正则、==等- 脚本写
#!/bin/bash,代表强制使用 bash 解释器,可以使用 bash 扩展语法。注意:如果 bash 被软链接成
sh启动,会自动进入 POSIX 兼容模式,关掉很多扩展。4. ksh(KornShell)
- 由 David Korn 开发,吸收了 sh 和 csh 优点
- 功能很强,很多语法和 bash 高度相似(bash 很多功能借鉴 ksh)
- 商用 Unix(AIX、HP-UX)默认 shell;Linux 一般不预装。
5. zsh(Z-shell)
- 兼容 sh,扩展比 bash 还丰富
- 交互式体验极强:自动补全、主题、高亮(oh-my-zsh)
- 很多人 macOS 默认 shell(新版 mac 默认就是 zsh)
⚠️ 写脚本不要默认依赖 zsh 扩展,服务器一般没有 zsh。
6. csh / tcsh(C Shell)
- 语法模仿 C 语言,和 sh 语法完全不兼容!
- 老 BSD 使用,写脚本不推荐,语法和 Bourne 家族差别巨大,坑很多。tcsh 是 csh 增强版。
二、两大阵营(非常重要!笔记必写)
- Bourne 家族(sh/dash/bash/ksh/zsh)✅ 一类语法,互相兼容基础语法
- 起源:原始 sh
- 变量、
if [ ]、for、$()、管道、重定向,通用- 我们学的 shell 脚本,全部属于这一派
- Csh 家族(csh/tcsh)❌ 语法完全不一样
if、变量、循环写法都不同,不要用来写 shell 脚本三、现代系统中,sh指的是一套标准
既是【历史上的具体程序】,又是【现在的 POSIX 标准名称】
一句话分清:原始 sh 是一个程序;现在说 sh,大多指 POSIX 标准,
/bin/sh是软链接1. 很久以前:sh = 一个实实在在的程序(Bourne Shell)
1977 年,Steve Bourne 写出第一个 shell 程序,程序名字就叫
sh,放在/bin/sh。这个就叫 Bourne Shell,是 Bourne 家族所有 shell 的老祖宗。
- 它是真实可执行文件,有自己的语法;
- 当年写脚本,就是调用这个
/bin/sh程序执行。这就是你说的:最原始的程序就叫 sh ✅,没错。
2. 后来:POSIX 把这套语法标准化,把标准命名为 sh
多年后,各个厂商都开发自己的 shell(ksh、ash 等),语法出现差异。于是 POSIX 组织,基于原始 Bourne Shell 的语法,提取出一套最小公共子集,把这个语法规范命名为 POSIX sh。
👉 此时:
- sh 变成一套语法标准,不再特指当年那个古老的 Bourne 程序;
- 任何 shell 程序,只要符合这套语法规范,就可以自称 “POSIX sh 兼容”。
3. 现代 Linux:
/bin/sh是软链接(不是固定程序)现代系统不再使用当年那个古老原始 Bourne sh 程序(太老旧,几乎绝迹)。
/bin/sh现在是一个符号链接(软链接),指向一个符合 POSIX sh 标准的 shell 实现:
- Ubuntu/Debian:
/bin/sh→dash(轻量 POSIX 实现)- 老 CentOS:
/bin/sh→bash(bash 会自动进入 POSIX 兼容模式)# 在Ubuntu执行这条命令看真相 ls -l /bin/sh # lrwxrwxrwx 1 root root 4 /bin/sh -> dash重点区分两个概念:
- 原始 Bourne sh:历史上的真实程序(古董,现在几乎没人用)
- POSIX sh:语法标准规范
- /bin/sh:系统上的软链接,指向某个兼容 POSIX 的 shell 程序(dash/bash)
Bash脚本
Bash = Bourne-Again SHell,是 GNU 项目的 Shell,名字是一个双关梗:Bourne Again(Born again,重生),致敬最早的 Bourne Shell(
sh)。一句话背景:当年 Unix 的 Bourne Shell 属于贝尔实验室私有版权,GNU 计划要做一套完全自由的操作系统,就需要一个自由、兼容 sh的 shell,于是诞生 Bash抖音百科。
前置铺垫:Bash 的前辈们
- 1971 Thompson shell:Ken Thompson 写的第一个 Unix shell,只有最基础命令执行、管道。
- 1979 Bourne Shell(sh):Stephen Bourne 开发,奠定 shell 脚本基础:if/for 循环、变量、函数、管道重定向,是现代 shell 的鼻祖,但闭源商业版权。
- csh(C shell):Bill Joy,C 语言风格语法,增加命令历史、别名。
- ksh(Korn shell):David Korn,融合 sh 脚本能力 + csh 交互特性,带行编辑、数组,但是早期也不是完全自由。
Bash 时间线
1988
Richard Stallman(RMS)雇佣 Brian Fox(布莱恩・福克斯),正式启动 Bash 开发,目标:完全兼容 Bourne sh,同时吸收 ksh、csh 好用的交互功能,开源自由抖音百科。
1989-06-08:Bash 0.99(首个公开 beta 版)
- 自带 readline 库(命令行编辑、上下翻历史)、作业控制(jobs/fg/bg)、命令别名、命令历史。
- 能直接跑传统 sh 脚本。
后面 Linux 内核诞生,Linux 发行版直接把 Bash 拿来做默认交互式 shell,一炮走红。
1993 左右:维护者交接
Brian Fox 离开 FSF,Chet Ramey(切特・雷米)接手,成为 Bash 至今三十多年的主维护者,直到现在依然是 Bash 的核心开发人抖音百科。
1996 Bash 2.0
重大版本:
- 索引数组
- 间接变量扩展
${!var}- 可编程命令补全(tab 补全的核心能力)
- 完善 POSIX 标准兼容,很多脚本扩展特性从这里开始增加
2004 Bash 3.0
- 引入
[[ =~ ]]正则匹配 +BASH_REMATCH捕获分组macOS 长期停留在 bash3.2,因为 GPLv3 协议问题,苹果不愿升级。这就是为什么 macOS 旧版 bash 缺少很多新特性。
2009 Bash 4.0(非常重要)
- 关联数组
declare -A(key-value 字典)globstar:**递归通配目录- coprocess 协进程很多企业老服务器还在用 bash4 系列。
2014:Shellshock(破壳漏洞)
著名高危漏洞,Bash 环境变量函数解析漏洞,当年影响全球大量 Linux 服务器。
2016 Bash 4.4
字符串转换参数扩展,unicode 处理优化。
2019 Bash 5.0
新增内置变量
EPOCHSECONDS/EPOCHREALTIME,改进关联数组、glob 匹配,修复大量历史坑。2022 Bash 5.2
目前主流稳定版本,优化单词分割、glob 展开、递归函数局部变量行为。
最新:5.3(2025)
创建和执行
在计算机编程中,脚本是用于适当的运行时环境的一组命令,这些命令用于自动执行任务。
Bash Shell脚本是一个纯文本文件,其中包含一组通常在命令行中键入的各种命令。它用于在Linux文件系统上自动执行重复性任务。它可能包含一组命令或一个命令,或者可能包含命令式编程的标志,例如循环,函数,条件构造等。实际上,Bash脚本是用Bash编程语言编写的计算机程序。
如何创建和运行Bash脚本?
- 首先,使用
cd命令进入保存脚本的目录;- (如果已有脚本则跳过,否则)使用
touch命令创建零字节大小的脚本,脚本以.sh作为扩展名(经验证,不以.sh结尾也可以,事实上,linux中没有扩展名的概念,之所以常常写扩展名,是为了方便人为查看);touch file_name- 打开文本,并输入bash脚本的shell命令;
- 使用chmod赋予该脚本以可执行的权限(这一步别忘了,要不文件没有可执行权限);
./file_name来执行bash脚本(不在当前目录下就需要输入绝对路径或者相对路径);比如,创建一个脚本,并输入如下内容:
#! /bin/bash
echo Hello World!然后执行。
就会在终端打印出Hello World!
注意,shell中的命令没有什么静默输出的概念,比如echo xxx,就只会输出xxx,不像makefile中,还能通过@或者-s来控制是否把指令以及指令的结果一起输出,别搞混了。总之,shell就是linux命令的集合,linux中执行是什么样,shell中执行的结果就是什么样。
Shell 脚本的 3 种常见执行方式(bash)
假设有脚本
test.sh:#!/bin/bash echo "脚本PID: $$" echo "位置参数 \$1=$1" VAR="我是脚本里的变量"核心区别一句话:会不会新建子 shell、会不会继承当前环境变量、能不能修改父 shell 变量
1.
bash test.sh(最常用,新开子 shell)bash test.sh hello
- 原理:fork 创建一个新的 bash 子进程,在子 shell 里面执行脚本
- 特点:
- 子 shell 复制父 shell 的环境变量;子 shell 里面定义 / 修改的变量,不会影响父 shell
- 脚本第一行
#!/bin/bash会被忽略,直接用你命令里指定的 bash 解释器运行- 位置参数
$1拿到hello- 退出:子 shell 执行完就销毁,变量全部消失
同理:
sh test.sh,用 sh 解释器跑(不一定是 bash,可能是 dash)2.
./test.sh(可执行文件方式,新开子 shell)先给执行权限:
chmod +x test.sh ./test.sh hello
- 原理:同样 fork子 shell,和上面
bash test.sh非常像- 区别:会读取脚本第一行
#!/bin/bash,由这行指定解释器
#!/bin/bash→ bash 执行#!/bin/sh→ sh 执行- 变量同样隔离,子 shell 的变量不会带回父 shell
⚠️ 忘记
chmod +x会报权限不足。
./test.sh需要可执行权限 (x);bash test.sh和source test.sh(. test.sh)不需要可执行权限,只需要读权限 (r)。原理区分:权限控制的是:能不能直接把这个文件当成程序来启动。
3.
source test.sh或者. test.sh(在当前 shell 直接执行,不创建子 shell)source test.sh hello # 等价简写(点命令,POSIX标准) . test.sh hello✅ 重点!没有子进程,直接在你当前终端 shell 里面跑脚本
- 脚本里定义 / 修改的变量,会直接保留在当前 shell!
$$打印的是你当前终端的 PID,不会变- 脚本执行完,里面的变量还在,
env/echo $VAR还能看到- 第一行
#!这一行会被忽略,用当前你正在用的 shell(bash)执行典型用途:加载环境配置脚本,比如
source ~/.bashrc,就是这个原理。
注释
1. 单行注释(最常用
#)
#之后,同一行所有内容都会被 shell 忽略,不会执行# 这是一整行注释 echo hello # #号后面是行尾注释注意:
#只有不在引号、不在替换语法里面才是注释。❌ 这种不是注释!
echo "#hello" # 双引号内,#只是普通字符,输出#hello2. 多行注释(两种写法)
每行前面加 #(✅推荐,可读性最好)
# 注释第1行 # 注释第2行 # 注释第3行 echo "脚本开始"没有 C 语言那种
/* */块注释!shell 原生不支持,不要写:/* 这不是注释!会当成命令执行,直接报错 */
开头
每个基于Bash的Linux脚本都以以下行开头:
#!/bin/bash这里
#!称为shebang,该行的其余部分是解释器的路径,用于指定bash shell在操作系统中的位置。shebang的格式很重要,格式不正确会导致命令工作不正常。因此,在创建脚本时,要始终记住SheBang格式的这两点:
- 它应该始终在脚本的第一行。
- 在
#!和解释器的路径之间,#之前不应有任何空格。经测试,貌似有空格也可以,不过,还是应当遵循标准规范。
注意,单独的#是注释。
注意,linux中路径是以撇/来分隔的,在windows下是以捺\作为分隔的。
echo
echo是bash中常用的linux命令
echo是Bash中的内置命令,用于通过传递参数来显示标准输出。它是用于将文本/字符串行打印到屏幕上的最广泛使用的命令。echo命令 – 输出字符串或提取后的变量值 – Linux命令大全(手册) (linuxcool.com)
单独的echo可以打印出空行。
比如:
显示结果定向至某个文件,也就是将显示结果写到某个文件里,其中>是重定向符号。
echo "It is a test" > myfileecho会自动回车换行。
关于引号
Bash 引号用法
核心目的:控制 Shell 的解析行为,控制变量替换、通配符、空格、特殊符号是否生效三种引号:无引号、单引号
' '、双引号" ",还有**反引号`**(老写法,不推荐),以及$' '。一句话总览
- 单引号
' ':原样保留,所有特殊符号全部关闭,不做任何替换。- 双引号
" ":大部分特殊符号失效;仅保留 $、`、\ 这三个的功能(变量替换、命令替换、转义)。- 无引号:会做单词分割 + 通配符扩展,最容易踩坑。
1. 单引号
'内容'(强引用,完全原样输出)规则:
- 引号内部所有字符原样保留
- 不解析变量
$var、不解析命令替换、不转义- 不能嵌套单引号;单引号里面写单引号无法直接转义
name=zhangsan echo '$name' # 输出:$name (不会替换变量,原样打印$name) echo 'hello $USER * > | ()' # 直接原样输出 hello $USER * > | ()✅适用场景:字符串原样输出,里面有大量特殊符号,不想 shell 做任何解析
2. 双引号
"内容"(弱引用,部分解析)在双引号内,只有 3 个符号还有特殊作用:
$:变量替换、算术替换$(( ))`:反引号命令替换(老旧写法)\:转义符,只能转义"、\、$、`,其他字符 \ 不生效重点:双引号会关闭单词分割、关闭通配符 * 扩展!
name=zhangsan echo "$name" # 输出 zhangsan,变量正常替换 echo "var=$name, date=`date`" # 变量、反引号命令替换生效 echo "hello *" # 输出 hello * # ⚠️ 这里*不会扩展成文件名!双引号抑制通配符✅适用场景:最常用!包含变量,但是字符串里面有空格,防止被切分成多个参数
经典坑:变量带空格不加双引号
file="my file.txt" ls $file # 无引号 → ls my file.txt 变成两个参数,报错 ls "$file" # 双引号 → ls "my file.txt",正确
3. 不加引号(裸字符串)
会做两件事:
- 单词分割(按 IFS 空白:空格、tab、换行切割)
- 通配符扩展
* ? [],匹配文件名echo hello * # *会展开当前目录所有文件⚠️风险极高:变量带空格、特殊字符直接炸。
原则:只要变量内容可能包含空格,一定要加双引号!
4. 反引号
`cmd`(旧命令替换,不推荐)作用:执行命令,把命令输出替换到字符串。
echo `date`缺点:嵌套很难写,转义麻烦。✅现代 bash 推荐改用:
$(cmd)echo $(date) # 双引号包裹,保留换行 echo "$(date)"
$( )不属于引号,是命令替换语法,经常和双引号搭配。5. $' 字符串 ' (ANSI C 转义,bash 特有扩展)
识别
\n \t \r这类转义字符echo $'hello\nworld' # 会换行输出⚠️高频坑(必记)
- 变量引用,优先双引号
"$var",防止空格拆分(shell 最经典坑)- 单引号里面,没有任何替换,
$就是普通字符- 双引号里面
*不会变成文件名;无引号*会匹配文件- 不要混用引号,引号必须成对;引号从第一个开始,遇到同类型引号结束
$(command)推荐包在双引号"$(command)",保留输出里的空格、换行,对比有无双引号的差别
# 假设文件有一行:hello world(中间多个空格) content=$(cat test.txt) echo $(cat test.txt) # 无引号:命令返回的多个空白,会被压缩成单个空格(单词分割) echo "$(cat test.txt)" # 包在双引号内:原样保留换行、多个空格,不会压缩空白如
在执行git commit时遇到一个问题
![]()
输入git commit -m "update aim`s name"后直接出现了箭头
问题就出在提交信息中的 **单引号'`** 干扰了命令解析。在 Bash 等终端中,双引号
"和单引号'都是字符串分隔符。你的提交信息里包含 aims`(中间有一个单引号),导致终端认为:
第一个双引号
"开始遇到
oh3rgs 中的'时,终端误以为是字符串结束或特殊符号因此终端会显示箭头(如 ),等待你补全语法
解决方法:
按
Ctrl + C终止当前卡住的提交(不会产生提交,安全操作)。修改提交信息,避开单引号冲突,有两种方式:
git commit -m "update aim\'s name"
git commit -m 'update oh3rg`s vid, pid and manufacturer'
用双引号包裹整个信息,并对内部的单引号转义(加
\):改用单引号包裹整个信息(此时内部的双引号无需转义):
这样终端就能正确解析提交信息,不会再卡住了。
或者,直接换一种写法,不要在中间使用单引号,比如:
git commit -m "update name for aim"这个就是典型的引号用错的问题,值得注意!!!
变量
变量部分内容可参考:
核心:shell 变量本质就是字符串!哪怕你存数字,底层也是文本。算术是单独用
$(( ))做的,变量本身没有类型。一、变量定义规则
1. 赋值语法
var=hello # ⚠️ 等号两边**不能有空格**! var = hello # ❌ 错误!shell会把var当成命令执行
- 变量名:只能字母、数字、下划线;不能数字开头;区分大小写
Var和var是两个变量。- 值包含空格、特殊字符:加双引号
name="zhang san"2. 取值:
$变量名或${变量名}a=10 echo $a echo ${a}
${}花括号:边界隔离,防止变量名和后面文字粘连。name=lee echo ${name}good # leegood,正确 echo $namegood # shell去找变量namegood,为空,错误二、三种引用变量写法(和前面引号联动!重点)
msg="hello world" echo $msg # 无引号:变量替换,之后【单词分割、通配扩展】,空格会压缩 echo '$msg' # 单引号:不替换,原样输出字符串 $msg echo "$msg" # 双引号:变量替换,**关闭单词分割、通配扩展,保留空格** ✅推荐✅最佳实践:引用变量,习惯写 "$var",防止空格、特殊字符踩坑。
三、环境变量 vs 普通变量
- 普通变量(本地变量):只在当前 shell 进程生效,子进程 / 脚本里面看不到
name=tom
- 环境变量:用
export导出,子 shell、子进程可以继承export name=tom # 或者先定义,再导出 name=tom export name子 shell 修改 export 变量不会影响父 shell,子进程拥有副本。
查看环境变量:
env、printenv;查看所有变量(本地 + 环境):set四、位置参数(脚本传入的参数,非常常用)
执行
./script.sh apple banana
变量 含义 $0脚本本身名字 $1 $2 $3...第 1、2、3 个参数 $#参数总个数 $@所有参数,单独独立参数列表(推荐 "$@")$*所有参数,合并成一个字符串,用 IFS 分隔 ⚠️ 重点坑:
"$@"vs"$*"
"$@":保留每个参数原始空格,推荐用来遍历参数"$*":全部参数合并成一个字符串示例脚本:
#!/bin/bash echo "脚本名:$0" echo "第一个参数:$1" echo "参数总数:$#" for arg in "$@"; do echo "参数: $arg" done五、特殊内置变量
$$当前 shell 的 PID(进程号)$?上一条命令的退出码,0 = 成功,非 0 失败(高频!)$!后台运行的最后一个进程 PID$_上一条命令最后一个参数六、变量默认值替换(参数扩展,POSIX 基础)
语法(笔记精简)
${var:-word}:变量为空 / 未定义,返回 word;不改变变量本身echo ${name:-"default"}${var:=word}:变量为空 / 未定义,返回 word,同时把 var 赋值为 word${var:+word}:变量有值才返回 word;空则返回空${var:?msg}:变量为空 / 未定义,打印 msg 并退出脚本,用于校验# 例子,如果var不存在,给默认值hello unset var echo ${var:-hello}七、字符串操作(bash 扩展,dash 不支持)
⚠️ 属于 bash 扩展,
#!/bin/sh(dash)不能用!str=abc123 ${#str} # 获取字符串长度 ${str#abc} # 从开头删除最短匹配abc ${str%123} # 从末尾删除最短匹配123八、数组(bash 独有扩展,dash 不支持)
arr=("apple" "banana" "orange") echo ${arr[0]} #取第一个元素 echo ${arr[@]} #全部元素,"${arr[@]}"推荐写法POSIX sh(dash)原生不支持数组。
九、删除变量 unset
a=10 unset a # 删除变量a📌 高频坑(必写笔记)
- 赋值
var=xxx,等号不能加空格- shell 变量没有类型,全部是字符串,数字只是文本;算术运算要
$(( ))- 引用变量优先
"$var",防止空格拆分 bug$@一定要双引号"$@",是处理脚本参数标准写法- export 导出的变量,子 shell 是副本;子 shell 修改变量无法传回父 shell
${}花括号用来界定变量边界,避免粘连错误- bash 的数组、字符串截取属于扩展;纯 POSIX/dash 脚本不能使用
注意,Makefile中引用变量也是使用美元符号$,不过Makefile中引用变量时可以不加括号,可以加花括号${},也可以加圆括号$(),但是shell里,也就是linux命令中,不支持圆括号来引用变量,比如:
关于shell中的变量都是字符串这个的理解,那数字怎么处理?
核心一句话
shell 变量存储的永远是文本(字符串),本身没有 int/float 类型。所谓 “数字运算”,不是变量变成数字,而是把字符串拿到 ** 专门的算术语法块
$(( )) / (( ))** 里面,临时解析成整数来计算。离开这个语法块,它依旧是字符串。1. 例子直观感受
a=10 # 变量a里面存的是字符串 "10" b=20 # 变量b里面存的是字符串 "20" # 在 $(( )) 内部,把字符串拿出来,转成整数计算 c=$(( a + b )) echo $c # c里面又变成字符串 "30"整个流程:字符串 "10" → 进入
$(( ))→ 临时转为整数运算 → 结果再转回字符串存入 c。如果写普通直接拼接,不会自动做加法!
a=10 b=20 c=$a+$b echo $c # 输出 10+20,只是字符串拼接!不会自动求和👉 不加
$(( )),+只是普通字符,不会当成加号。2. 比较的时候,也要区分【字符串比较】和【数字比较】
因为变量本质是字符串,所以比较规则要看你用哪套语法:
① POSIX
[ ](test 命令)
- 数字比较:
-gt -lt -eq -ne,会把字符串转成数字对比a=10 [ $a -gt 5 ] # 把"10"和"5"当成数字比较
- 字符串比较:
=/!=,直接按文本字典顺序比较,不转数字[ "$a" = "10" ] # 纯文本对比坑:
[ 10 > 2 ]这里>不是大于,是重定向!不是数字比较!② bash
[[ ]]
[[ $a > $b ]]:字符串字典比较(不是数字大小!)比如[[ 10 > 2 ]],字符串比较:字符1的 ASCII 码小于2,结果是 false!很多人踩这个大坑!10 作为字符串,居然比 2 小。
③ bash
(( ))
(( a > 5 )):算术比较,把字符串转为整数做数字大小判断a=10 if (( a > 5 ));then echo "数字10大于5" fi✅ 这里
>是数学大于,因为处在算术语法块(( ))内部。重点总结:符号含义由所在语法环境决定在
(( ))里面>是数字大于;在[[ ]]里面>是字符串字典比较;在普通[ ]里>直接被识别为重定向。3. 那如果字符串不是合法数字会怎么样?
a=abc echo $(( a + 1 )) # 报错:value too great for base (error token is "abc")
$(( ))尝试把字符串 abc 转整数,失败。4. 浮点数怎么办?
内置算术
$(( ))、(( ))只能转整数,不能解析带小数点的字符串。a=3.5 echo $(( a +1 )) # 报错!无法把"3.5"转为整数小数必须交给外部工具
bc,bc 会把字符串当成小数计算。a=3.5 res=$(echo "$a + 1.2" | bc) echo $res更多待补充……
关于位置参数补充
"$@"vs"$*"核心一句话:
$@:保留原本多个参数,每个参数独立$*:把所有参数拼成一整个字符串最关键:是否包双引号",差别巨大!不加引号时二者行为很像,但是实际开发永远用"$@"假设脚本执行:
./test.sh "hello world" foo一共 2 个参数:参数 1:
hello world(内部带空格)参数 2:foo1.
"$@"(✅推荐,日常写脚本首选)加双引号:还原原始的参数列表
- 每一个参数保持原样,参数里面的空格保留;
- 2 个参数仍然是 2 个独立参数。
for p in "$@"; do echo "---$p---" done输出:
---hello world--- ---foo---2.
"$*"(尽量少用)加双引号:把全部参数合并成 1 个字符串,用
IFS的第一个字符(默认空格)隔开。上面例子,合并成单个字符串:hello world foofor p in "$*"; do echo "---$p---" done循环只跑一次,输出:
---hello world foo---3. 不加引号:
$@和$*
for p in $@/for p in $*不写双引号:不管是
$@还是$*,都会触发单词分割 + 通配符扩展。上面例子,会拆成 hello、world、foo,变成 3 项。👉 无引号版本都有缺陷,不推荐!工程最佳实践:遍历脚本输入参数,永远写
for arg in "$@"
在Shell脚本中,
${@:n}是一种参数扩展语法,用于访问传递给脚本或函数的所有位置参数(即命令行参数),但是选取时从第n个参数开始。我们以
${@:2}为例讲解具体来说:
${@}表示所有的位置参数。${@:2}表示从第二个参数开始的所有参数。举个例子,假设你有一个名为
example.sh的脚本,内容如下:#!/bin/bash echo "All parameters: ${@}" echo "Parameters from the second one onwards: ${@:2}"如果你运行这个脚本并传递一些参数:
./example.sh arg1 arg2 arg3 arg4输出将会是:
All parameters: arg1 arg2 arg3 arg4 Parameters from the second one onwards: arg2 arg3 arg4详细解释
${@}会将所有传递给脚本的参数展开为一个字符串,每个参数之间用空格分隔。在这个例子中,${@}的值是arg1 arg2 arg3 arg4。
${@:2}则从第二个参数开始,将剩余的参数展开为一个字符串。在这个例子中,${@:2}的值是arg2 arg3 arg4。这种用法在处理命令行参数时非常有用,尤其是在需要跳过第一个参数而只处理后续参数的情况下。
数组
参考:Shell 变量 | 菜鸟教程 (runoob.com)
bash支持一维数组(不支持多维数组),并且没有限定数组的大小。
类似于 C 语言,数组元素的下标由 0 开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于 0。
⚠️ 重要前提:数组是 Bash 扩展,POSIX 标准 sh 没有数组。脚本开头必须
#!/bin/bash,#!/bin/sh不支持。两类数组:
- 索引数组(Indexed Array):数字下标,bash2.0+ 就支持
- 关联数组(Associative Array / 字典 key-value):bash4.0 + 才支持,必须显式
declare -A一、索引数组(最常用)
1. 数组定义
方式 1:直接初始化
# 整体赋值,空格分隔,带空格的元素用引号 arr=("apple" "banana" "orange" "hello world")语法:
arr=(元素1 元素2 元素3),括号内用空格分隔元素。方式 2:空数组
arr=()方式 3:按下标逐个赋值
arr[0]=100 arr[1]=200 arr[5]=600 # 可以跳过下标,下标2,3,4不存在,不会自动填充空元素下标默认从 0 开始。下标可以是数字表达式。
2. 读取数组元素
arr=("a" "b" "c") # 读取单个元素 echo ${arr[0]} # a echo ${arr[1]} # b # 读取全部元素(两种写法) echo ${arr[@]} # 展开成【独立的多个参数】(推荐!遍历优先用@) echo ${arr[*]} # 全部元素合并成**单个字符串**,以IFS分隔(默认空格)✅ 核心区别
${arr[@]}vs${arr[*]}arr=("hello world" "test") for i in "${arr[@]}"; do echo "[$i]"; done # 输出两行:[hello world] [test] 元素保留内部空格 for i in "${arr[*]}"; do echo "[$i]"; done # 只输出一行:[hello world test],合并成一个字符串写脚本遍历数组永远用
"${arr[@]}"3. 获取数组长度
arr=("a" "b" "c") echo ${#arr[@]} # 数组元素总个数:3 echo ${#arr[0]} # 第0个元素字符串长度:14. 动态追加元素(你刚才问的)
arr=() arr+=("one") # 追加单个 arr+=("two" "three") # 一次性追加多个
+=是追加,不要写成 arr = arr + ... 这种其他语言写法。带空格元素必须加引号arr+=("my item")5. 数组切片(截取一部分)
语法:
${arr[@]:起始下标:取几个元素}arr=(0 1 2 3 4 5) echo "${arr[@]:1:3}" # 从下标1开始,取3个元素 → 1 2 3 echo "${arr[@]:2}" # 从下标2一直取到末尾 →2 3 4 56. 删除元素 / 清空数组
arr=(a b c d) unset arr[1] # 删除下标1的元素,数组不会自动缩放下标! echo "${arr[@]}" # a c d echo "${arr[1]}" # 空,下标1被删掉,后面元素下标保持不变 unset arr # 删除整个数组,数组直接消失 arr=() # 保留数组变量,清空里面所有元素(推荐)⚠️ unset arr [1] 只会删掉该下标,不会自动把后面元素前移。如果想要连续数组,需要手动重建。
7. 数组下标列表
arr=("aa" "bb" "cc") echo "${!arr[@]}" # 输出所有存在的下标:0 1 2跳过下标赋值时,
${!arr[@]}会列出所有实际存在的下标。二、关联数组(key-value,bash4.0+)
必须先用
declare -A声明!不声明直接用会当成索引数组。# 声明关联数组 declare -A dict # 添加键值对 dict["name"]="zhangsan" dict["age"]=20 dict["city"]="nanjing" # 读取值 echo ${dict["name"]} # 获取所有key echo "${!dict[@]}" # 获取所有value echo "${dict[@]}" # 遍历 for k in "${!dict[@]}"; do echo "key=$k, value=${dict[$k]}" done❗关联数组不能用
+=追加键值对,+=只能追加 value 列表。删除单个 key:unset dict["age"]删除整个字典:unset dict三、遍历数组(高频写法)
索引数组遍历
arr=("apple" "banana" "hello world") for item in "${arr[@]}"; do echo "元素:$item" done按下标遍历
arr=(10 20 30) for idx in "${!arr[@]}"; do echo "下标 $idx : ${arr[$idx]}" done四、数组复制、拼接
# 复制数组 arr1=(1 2 3) arr2=("${arr1[@]}") # 数组合并拼接 a=(1 2) b=(3 4) c=("${a[@]}" "${b[@]}") echo "${c[@]}" # 1 2 3 4五、常见坑(重点,写脚本必看)
坑 1:不加引号
${arr[@]}arr=("hello world") for i in ${arr[@]}; do echo $i; done # 不加双引号,会被单词分割,变成 hello 和 world 两个元素!✅ 永远:
"${arr[@]}"坑 2:子 shell 修改数组,父 shell 无效
arr=() ( arr+=("test") ) # () 子shell echo "${arr[@]}" # 空!子shell副本,不影响外层管道也会创建子 shell,经典踩坑:
arr=() echo "a b c" | while read x; do arr+=("$x"); done echo ${arr[@]} # 数组为空!while在管道子shell里面解决办法:用进程替换
while read x; do ... done < <(echo "a b c")坑 3:混淆
arr[*]和arr[@]
*会用 IFS 合并成单行,处理带空格的文件名字符串直接崩。坑 4:关联数组忘记
declare -A低版本 bash 或者没写 declare -A,key 会被当成数字下标,直接出错。
坑 5:数组下标不会自动移位
unset arr [1],只是删掉该元素,后面元素下标不会自动往前填充。
六、快速查询速查表
语法 作用 arr=()定义空索引数组 arr+=("val")追加元素 ${arr[@]}所有元素(分开参数,推荐) ${arr[*]}所有元素合并为单个字符串 ${#arr[@]}数组元素个数 ${!arr[@]}列出所有下标 /key ${arr[@]:s:n}切片,从 s 开始取 n 个元素 unset arr[i]删除下标 i 元素 declare -A dict声明关联数组 七、小测试脚本,直接复制运行
#!/bin/bash # 索引数组演示 arr=("one" "two" "hello world") echo "全部元素:${arr[@]}" echo "长度:${#arr[@]}" arr+=("three") echo "追加后:${arr[@]}" echo "下标列表:${!arr[@]}" # 关联数组 declare -A d d["name"]="xiaoming" d["age"]=18 echo "name=${d[name]}" for k in "${!d[@]}";do echo "$k -> ${d[$k]}";done更多待补充。
命令替换
命令替换:执行一条命令,把命令的标准输出 (stdout) 捕获下来,替换成文本,嵌入到当前 shell 代码里。
两种写法:现代推荐
$(command);老式写法`command`(反引号,尽量不用)本质:先运行括号内的命令,拿到输出,把
$(...)整体替换成命令输出的字符串,再继续执行外层代码。1.
$(command)【推荐写法】now=$(date) echo 当前时间:$now执行步骤拆解:
- shell 识别
$(date),新开子 shell 执行date- 捕获
date打印出来的文本- 将
$(date)替换为该文本- 再执行外层 echo
✅ 可以嵌套(反引号很难嵌套)
# 先获取当前目录下文件数量 count=$(ls $(pwd) | wc -l)2.
`command`反引号(老旧写法,不推荐)now=`date` echo $now缺点:
- 嵌套的时候,里面的反引号需要大量转义 `,可读性很差
- 可读性低,容易看错
新脚本一律放弃反引号,改用
$()3. 重点!命令替换后,空白处理(和引号强关联,必考)
替换完成之后,shell 会对捕获到的输出做处理,是否单词分割、通配扩展,取决于有没有双引号包裹
① 不加双引号
var=$(cmd)命令执行拿到输出字符串之后:
- 触发 单词分割(IFS 切割空格、tab、换行)
- 触发 通配符扩展 * ? []多个空白会压缩成单个空格。
# 文件内容:hello world(多个空格) res=$(cat test.txt) echo $res # 输出 hello world,多个空格被压缩② 加双引号
"$(cmd)"✅推荐res="$(cat test.txt)" echo "$res"
- 仍然执行命令替换
- 关闭单词分割、关闭通配扩展
- 原样保留换行、多个连续空格
原则:绝大多数场景,命令替换外面包双引号
"$(command)"4. 只捕获标准输出 stdout,不捕获 stderr 标准错误
# 错误信息不会被捕获,直接打印在终端 out=$(ls noexistfile) echo "$out"如果想要把错误一起捕获,需要重定向:
out=$(ls noexistfile 2>&1)5. 命令替换返回的退出码
$?获取的是被执行命令的退出码,不是赋值操作的。out=$(ls xxx) echo $? # 是ls命令的退出码6. 和算术替换
$(( ))的区分(非常容易混!)
$(command):命令替换,运行 shell 命令,拿到文本输出$((expression)):算术扩展,做整数数学计算,不运行外部命令a=$(echo 1+2) # 命令替换,输出字符串 "1+2" b=$((1+2)) # 算术扩展,计算得到3📋对比
写法 名称 作用 嵌套 $(cmd)命令替换 执行命令,捕获输出文本 ✅支持嵌套 `cmd`反引号命令替换 执行命令,捕获输出文本 ❌嵌套需要转义,很难写 $(( ))算术扩展 整数数学运算,不调用外部命令 可以嵌套 ⚠️高频坑
- 命令替换会创建子 shell,子 shell 里面修改变量,不会影响外面父 shell 变量。
- 不加双引号:输出里面的空格、换行丢失,
*会被展开成文件名。- 只会捕获 stdout;stderr 默认直接输出,不会存入变量。
- 不要把
$()和(( ))搞混,完全两套语法。✍️一句话口诀
$():跑命令拿输出;
$(( )):做整数计算;想要保留空格换行,一定要写
"$(cmd)"示例:
从这里也能看出来为什么引用变量时不能使用圆括号$(),因为圆括号有别的用处,那就是用来执行linux命令,这就是为什么这个图里的报错是“命令未找到”
读取用户输入
读取Bash用户输入,需要使用内置的Bash命令
read。它用于从用户处获取输入并分配给变量。它仅从Bash shell中读取一行。以下是read命令的语法。read <variable_name>以下是从Bash脚本读取用户输入的示例:
更多继续
接下一篇:
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐









所有评论(0)