主要参考视频:

这可能是B站讲的最好的Linux Shell脚本教程,3h打通Linux-shell全套教程,从入门到精通完整版_哔哩哔哩_bilibili

主要参考文档:

Shell 教程 | 菜鸟教程 (runoob.com)

Bash Shell教程 (yiibai.com)

先用视频入门,然后参考手册学习,之后实践总结。 

认识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 汇总笔记(理清概念,区分【标准】和【具体程序】)

核心区分两件事:

  1. POSIX sh:是语法规范(一套规则),不是程序文件。
  2. 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

重点区分两个概念:

  1. 原始 Bourne sh:历史上的真实程序(古董,现在几乎没人用)
  2. POSIX sh:语法标准规范
  3. /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 的前辈们

  1. 1971 Thompson shell:Ken Thompson 写的第一个 Unix shell,只有最基础命令执行、管道。
  2. 1979 Bourne Shell(sh):Stephen Bourne 开发,奠定 shell 脚本基础:if/for 循环、变量、函数、管道重定向,是现代 shell 的鼻祖,但闭源商业版权。
  3. csh(C shell):Bill Joy,C 语言风格语法,增加命令历史、别名。
  4. 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脚本?

  1. 首先,使用cd命令进入保存脚本的目录;
  2. (如果已有脚本则跳过,否则)使用touch命令创建零字节大小的脚本,脚本以.sh作为扩展名(经验证,不以.sh结尾也可以,事实上,linux中没有扩展名的概念,之所以常常写扩展名,是为了方便人为查看);
    touch file_name
    
  3. 打开文本,并输入bash脚本的shell命令;
  4. 使用chmod赋予该脚本以可执行的权限(这一步别忘了,要不文件没有可执行权限);
  5. ./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"    # 双引号内,#只是普通字符,输出#hello

2. 多行注释(两种写法)

每行前面加 #(✅推荐,可读性最好)

# 注释第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" > myfile

echo会自动回车换行。

Linux Shell中的echo命令详解 - 知乎 (zhihu.com)

Linux echo命令(在终端输出文本)_echo输出命令结果-CSDN博客

关于引号

Bash 引号用法

核心目的:控制 Shell 的解析行为,控制变量替换、通配符、空格、特殊符号是否生效三种引号:无引号、单引号 ' '、双引号 " ",还有**反引号 `**(老写法,不推荐),以及 $' '。

一句话总览

  1. 单引号 ' ':原样保留,所有特殊符号全部关闭,不做任何替换。
  2. 双引号 " ":大部分特殊符号失效;仅保留 $、`、\ 这三个的功能(变量替换、命令替换、转义)。
  3. 无引号:会做单词分割 + 通配符扩展,最容易踩坑。

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'
# 会换行输出

⚠️高频坑(必记)

  1. 变量引用,优先双引号 "$var",防止空格拆分(shell 最经典坑)
  2. 单引号里面,没有任何替换,$就是普通字符
  3. 双引号里面*不会变成文件名;无引号*会匹配文件
  4. 不要混用引号,引号必须成对;引号从第一个开始,遇到同类型引号结束
  5. $(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 中的 ' 时,终端误以为是字符串结束或特殊符号

  • 因此终端会显示箭头(如 ),等待你补全语法

解决方法:

  1. 按 Ctrl + C 终止当前卡住的提交(不会产生提交,安全操作)。

  2. 修改提交信息,避开单引号冲突,有两种方式:

    git commit -m "update aim\'s name"

    git commit -m 'update oh3rg`s vid, pid and manufacturer'

    1. 用双引号包裹整个信息,并对内部的单引号转义(加 \):

    2. 改用单引号包裹整个信息(此时内部的双引号无需转义):

这样终端就能正确解析提交信息,不会再卡住了。

或者,直接换一种写法,不要在中间使用单引号,比如:

git commit -m "update name for aim"

这个就是典型的引号用错的问题,值得注意!!!

变量

变量部分内容可参考:

Shell 变量 | 菜鸟教程 (runoob.com)

核心: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 基础)

语法(笔记精简)

  1. ${var:-word}:变量为空 / 未定义,返回 word;不改变变量本身echo ${name:-"default"}
  2. ${var:=word}:变量为空 / 未定义,返回 word,同时把 var 赋值为 word
  3. ${var:+word}:变量有值才返回 word;空则返回空
  4. ${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

📌 高频坑(必写笔记)

  1. 赋值 var=xxx,等号不能加空格
  2. shell 变量没有类型,全部是字符串,数字只是文本;算术运算要$(( ))
  3. 引用变量优先 "$var",防止空格拆分 bug
  4. $@ 一定要双引号 "$@",是处理脚本参数标准写法
  5. export 导出的变量,子 shell 是副本;子 shell 修改变量无法传回父 shell
  6. ${}花括号用来界定变量边界,避免粘连错误
  7. 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:foo

1. "$@"(✅推荐,日常写脚本首选)

加双引号:还原原始的参数列表

  • 每一个参数保持原样,参数里面的空格保留;
  • 2 个参数仍然是 2 个独立参数。
for p in "$@"; do
  echo "---$p---"
done

输出:

---hello world---
---foo---

2. "$*"(尽量少用)

加双引号:把全部参数合并成 1 个字符串,用IFS的第一个字符(默认空格)隔开。上面例子,合并成单个字符串:hello world foo

for 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) 

Shell 数组 | 菜鸟教程 (runoob.com)

bash支持一维数组(不支持多维数组),并且没有限定数组的大小。

类似于 C 语言,数组元素的下标由 0 开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于 0。

⚠️ 重要前提:数组是 Bash 扩展,POSIX 标准 sh 没有数组。脚本开头必须 #!/bin/bash,#!/bin/sh 不支持。两类数组:

  1. 索引数组(Indexed Array):数字下标,bash2.0+ 就支持
  2. 关联数组(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个元素字符串长度:1

4. 动态追加元素(你刚才问的)

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 5

6. 删除元素 / 清空数组

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

执行步骤拆解:

  1. shell 识别$(date),新开子 shell 执行date
  2. 捕获date打印出来的文本
  3. 将 $(date) 替换为该文本
  4. 再执行外层 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`反引号命令替换执行命令,捕获输出文本❌嵌套需要转义,很难写
$(( ))算术扩展整数数学运算,不调用外部命令可以嵌套

⚠️高频坑

  1. 命令替换会创建子 shell,子 shell 里面修改变量,不会影响外面父 shell 变量。
  2. 不加双引号:输出里面的空格、换行丢失,*会被展开成文件名。
  3. 只会捕获 stdout;stderr 默认直接输出,不会存入变量。
  4. 不要把 $() 和 (( )) 搞混,完全两套语法。

✍️一句话口诀

$():跑命令拿输出;

$(( )):做整数计算;

想要保留空格换行,一定要写 "$(cmd)"

示例:

从这里也能看出来为什么引用变量时不能使用圆括号$(),因为圆括号有别的用处,那就是用来执行linux命令,这就是为什么这个图里的报错是“命令未找到”

读取用户输入

读取Bash用户输入,需要使用内置的Bash命令read。它用于从用户处获取输入并分配给变量。它仅从Bash shell中读取一行。以下是read命令的语法。

read <variable_name>

以下是从Bash脚本读取用户输入的示例:

更多继续

接下一篇:

Linux下Shell脚本基础知识(二)-CSDN博客

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐