Shell入门指南
███████╗██╗ ██╗███████╗██╗ ██╗
██╔════╝██║ ██║██╔════╝██║ ██║
███████╗███████║█████╗ ██║ ██║
╚════██║██╔══██║██╔══╝ ██║ ██║
███████║██║ ██║███████╗███████╗███████╗
Shell简介
shell
Shell 本质上是一个用 C 语言编写的程序,它是连接用户与 Linux/Unix 操作系统内核的核心桥梁。对于开发者而言,Shell 具有不可替代的"双重人格":
- 作为
命令语言解释器:它为用户提供了访问操作系统内核服务的交互界面。当你输入命令(如 ls, grep)时,Shell 负责解释这些指令并传递给内核执行 - 作为
程序设计语言:Shell 拥有一套完整的编程语法(变量、循环、条件判断等)。我们将由 Shell 语法编写的程序称为Shell Script,这也是我们在开发和运维中常说的"写 Shell"
| Shell | Shell Script | |
|---|---|---|
| 本质 | 命令解释器 / 运行环境 | 文本文件 / 逻辑代码集合 |
| 核心职责 | 接收指令、调用内核、反馈结果 | 编排指令、处理逻辑、实现自动化 |
| 交互模式 | 实时交互(Read-Eval-Print Loop) | 批处理执行(编写 -> 运行) |
| 示例 | bash、zsh、sh |
install.sh、backup.sh |
| 依赖关系 | 脚本运行的宿主/解释器 | Shell 能力的扩展/载体 |
Shell环境
Shell 环境并非单纯的一个"黑色窗口",它是操作系统为用户分配的一个运行时上下文 (Runtime Context)。这个上下文决定了命令如何被解析、资源如何被分配以及进程如何被管理
一个完整的 Shell 运行环境通常由以下核心模块构成:
命令解释器:负责解析和执行用户输入的命令环境变量:定义系统运行时行为的全局键值对,决定了命令搜索路径、用户身份及语言环境等配置文件:控制 Shell 启动时的初始化行为,分为"登录/非登录"和"交互/非交互"加载模式用户自定义:包括别名alias、函数和快捷方式
编写 Shell 脚本并不需要特殊的 IDE,任何文本编辑器(Vim/VSCode)配合一个解释器即可。虽然 Linux 系统中存在多种 Shell,但主流阵营主要分为四大类:
| Shell | 全称 | 特点 |
|---|---|---|
| sh | Bourne Shell | Unix 标准默认 shell,遵循 POSIX 标准,兼容性好,基础广泛 |
bash |
Bourne Again Shell | Linux 标准默认 shell,功能丰富,兼容 sh,支持脚本编程 |
| fish | Friendly Interactive Shell | 用户友好,智能自动补全,语法高亮,开箱即用 |
| zsh | Z Shell | macOS 的默认 Shell。完全兼容 Bash,但在自动补全、主题插件(如 Oh My Zsh)方面拥有极致体验,是开发者桌面的首选 |
在大多数现代 Linux 系统(如CentOS/Ubuntu)中,
/bin/sh往往只是一个指向/bin/bash的符号链接当通过
sh script.sh运行脚本时,Bash 会以POSIX 兼容模式 启动,此时它会关闭部分 Bash 特有的扩展功能(如[[...]]条件判断),以模仿老式 Bourne Shell 的行为。这也是为什么有时候同一段脚本用 bash 跑没问题,用 sh 跑却报错的原因
Shell模式
Shell 的运作模式从根本上取决于其命令读取来源 。根据交互形式的不同,可以划分为交互模式和非交互模式
交互模式
用户登录终端后的默认模式。Shell 作为一个REPL(Read-Eval-Print Loop)环境运行
- Read:从标准输入(Stdin)等待用户键入命令
- Eval:解析并执行命令
- Print:将结果输出到终端(Stdout)
- Loop:再次显示提示符(Prompt),等待下一条指令
可以将Shell的交互模式简单理解为执行命令行。以下即为处于交互模式下
bash
# 典型的交互式提示符(PS1变量定义)
user@host:~$
# 此时可直接输入 ls, top, docker ps 等命令
非交互模式
Shell 脚本运行时的典型模式(批处理模式)。此模式下,Shell 不与用户进行实时交互,而是从文件或管道中读取一系列命令
生命周期:Shell 读取文件 -> 按顺序执行命令 -> 执行完毕后 Shell 进程自动终止 -> 控制权返回父进程
编写完脚本文件(如 script.sh)后,如何运行它是很多新手容易混淆的地方。执行方式不仅决定了权限需求,更决定了代码运行的进程作用域
方式一:解释器参数执行(Fork 子进程)
bash
# 直接调用 Shell 程序(如 bash 或 sh),将脚本路径作为参数传递
bash script.sh
sh script.sh
# 当前 Shell 启动一个新的 子 Shell 进程来解析脚本
# 脚本文件本身不需要执行权限(x),只需有读取权限(r)
方式二:直接路径执行(Fork 子进程)
bash
# 将脚本作为可执行程序直接运行
chmod +x script.sh # 必须先赋予执行权限
./script.sh
# Shell 依然会 Fork 一个子进程。解释器由脚本第一行的 Shebang 决定
# 必须拥有 r 和 x 权限
方式三:Sourcing 执行(当前进程)
bash
# 使用 source 或其别名 . 来加载脚本
source script.sh
# 点与文件名之间有空格
. script.sh
# 不启动子进程,直接在当前 Shell 环境中逐行执行脚本内容
# 脚本内定义的变量、函数会直接"污染"当前终端环境;脚本内的 exit 命令会导致当前终端窗口直接关闭
三种方式总结如下:
| 方式 | 典型指令 | 进程归属 | 变量作用域 | 权限依赖 | Shebang | 核心用途 |
|---|---|---|---|---|---|---|
| 解释器执行 | bash file.sh |
子进程 | 隔离(结束后释放) | 读取(Read) | 被忽略 | 临时运行、调试 |
| 直接执行 | ./file.sh |
子进程 | 隔离(结束后释放) | 读+执行 (R+X) | 依赖 | 标准化脚本 |
| 内建加载 | source file.sh |
当前进程 | 保留(污染环境) | 读取(Read) | 无视 | 环境配置、库加载 |
基本语法
解释器
Shell 脚本的第一行通常包含一个特殊的字符序列#!,技术术语称为Shebang (或 Hashbang)。它不仅是一行注释,更是写给操作系统内核看的"指令",告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 解释器
在 Shell 中,你会见到诸如以下的注释:
- 硬编码路径
bash
#!/bin/sh
#!/bin/bash
#!/usr/bin/python
直接指定解释器的绝对路径,但移植性差,一旦路径不匹配,脚本直接报错File not found
- 动态查找
bash
#!/usr/bin/env bash
#!/usr/bin/env python3
env会根据当前用户的环境变量PATH,自动查找第一个匹配的程序,此种方式最为推荐
注释
各种语言都有对应的注释语法,Shell语法中,注释是特殊的语句,会被Shell解释器忽略
- 单行注释:以
#开头,到行尾结束 - 多行注释:以
:<<EOF开头,到EOF结束
shell
# echo '这是单行注释'
:<<EOF
echo '这是多行注释'
echo '这是多行注释'
echo '这是多行注释'
EOF
引号
| 引号 | 含义 |
|---|---|
| 单引号 | 所见即所得,单引号里面内容原封不动输出 |
| 双引号 | 双引号内的字符串会进行变量替换 和转义处理 ,但不会执行命令替换,不解析{}和通配符 |
| 反引号 | 优先执行,先执行反引号里面的命令,会进行命令替换 |
在双引号中,变量引用或者命令置换是会被展开的。在单引号中不会
bash
echo "Your home: $HOME" # Your home: /Users/<username>
echo 'Your home: $HOME' # Your home: $HOME
当局部变量和环境变量包含空格时,它们在引号中的扩展要格外注意
bash
INPUT="A string with strange whitespace."
echo $INPUT # A string with strange whitespace.
echo "$INPUT" # A string with strange whitespace.
echo
基本使用
echo用于在shell中打印shell变量的值,或者直接输出指定的字符串,在shell编程中极为常用
bash
echo [Option] [String]
-e:启用转义字符
-E:不启用转义字符(默认)
-n:结尾不换行,默认情况下 echo 会在末尾追加一个 \n
- 不使用转义字符
bash
# 默认情况下,echo输出结果自带换行符\n
name=world
echo "hello, world" # hello, world
echo "hello, \"world\"" # hello, "world"
echo "hello, \"${name}\"" # hello, "world"
# 输出含换行符的字符串
echo "YES\nNO" # YES\nNO
# 输出重定向到文件
echo "test" > test.txt
# 输出执行结果
echo `pwd` # (当前目录路径)
- 使用转义字符
bash
echo -e "YES\nNO" # -e 开启转义
# Output:
# YES
# NO
echo -e "YES\c" # -e 开启转义 \c 不换行
echo "NO"
# Output:
# YESNO
进阶使用
控制echo输出文字的颜色
bash
echo -e "控制序列1文本内容控制序列2"
# 控制序列组成
\e[样式代码1;样式代码2;前景色代码;背景色代码m
以echo -e "\e[1;31mThis is red text\e[0m"为例
\e:等同于\033,表示 ANSI 转义字符的起始(Escape 字符)[:紧跟\e后,表示进入控制序列模式1;31m:1为样式代码,31为前景色代码,m表示控制序列结束\e[0m:0表示重置所有属性,m表示结束控制序列
以上命令,终端会输出加粗的红色文本 This is red text,随后颜色和样式恢复默认
| 类型 | 代码 | 描述 | 示例 |
|---|---|---|---|
| 样式代码 | 0 | 重置所有属性(颜色、加粗、下划线等) | \e[0m |
| 1 | 加粗/高亮文本 | \e[1;31m(红色加粗) | |
| 4 | 下划线文本 | \e[4mUnderline\e[0m | |
| 5 | 闪烁文本(闪烁频率由终端决定) | \e[5;31mFlashing Red\e[0m | |
| 7 | 反转颜色(交换前景色和背景色) | \e[7;30;47mInverted\e[0m | |
| 8 | 隐藏文本(部分终端支持) | \e[8mHidden Text\e[0m | |
| 22 | 取消加粗(恢复默认粗细) | \e[22mNormal Text\e[0m | |
| 24 | 取消下划线 | \e[24mNo Underline\e[0m | |
| 前景色代码 | 30 | 黑色 | \e[30mBlack Text\e[0m |
| 31 | 红色 | \e[31mRed Text\e[0m | |
| 32 | 绿色 | \e[32mGreen Text\e[0m | |
| 33 | 黄色 | \e[33mYellow Text\e[0m | |
| 34 | 蓝色 | \e[34mBlue Text\e[0m | |
| 35 | 紫色 | \e[35mMagenta Text\e[0m | |
| 36 | 青色 | \e[36mCyan Text\e[0m | |
| 37 | 白色 | \e[37mWhite Text\e[0m | |
| 背景色代码 | 40 | 黑色背景 | \e[40;37mBlack BG\e[0m |
| 41 | 红色背景 | \e[41;37mRed BG\e[0m | |
| 42 | 绿色背景 | \e[42;30mGreen BG\e[0m | |
| 43 | 黄色背景 | \e[43;30mYellow BG\e[0m | |
| 44 | 蓝色背景 | \e[44;37mBlue BG\e[0m | |
| 45 | 紫色背景 | \e[45;37mMagenta BG\e[0m | |
| 46 | 青色背景 | \e[46;30mCyan BG\e[0m | |
| 47 | 白色背景 | \e[47;30mWhite BG\e[0m | |
| 扩展功能 | 38;2;r;g;b | 自定义前景色(RGB,r/g/b为0-255) | \e[38;2;255;0;0mCustom Red\e[0m |
| 48;2;r;g;b | 自定义背景色(RGB,r/g/b为0-255) | \e[48;2;0;0;255;37mCustom Blue BG\e[0m | |
| 5m | 闪烁(与样式代码 5 相同) | \e[31;5mFlashing Red\e[0m |
bash
echo -e "\e[1;4;5;31;47m紧急状态\e[0m" # 输出闪烁的 带有下划线的 白色背景的 红色文字
printf
printf 是 Shell 中用于格式化输出的命令。与 echo 不同,printf 来源于 C 语言标准库,遵循 POSIX 标准 ,因此它具有更好的跨平台兼容性 (不会因为 OS 不同而导致行为诡异)和更强大的文本排版能力
其特点主要有:
- 不自动换行 :printf 不会在结尾默认添加
\n,你需要显式地在FORMAT字符串中控制换行 - 格式与参数分离:前面是"模版",后面是"数据"
bash
printf FORMAT [ARGUMENT]...
常见格式替换符
| 占位符 | 含义 | 备注 |
|---|---|---|
| %s | 字符串 (String) | 最常用 |
| %d | 十进制整数 (Decimal) | 如果参数是小数会被截断,是非数字字符则报错或显示为0 |
| %f | 浮点数 (Float) | 默认保留6位小数,可通过 %.2f 控制精度 |
| %b | 转义字符串 | 会解析参数中的转义序列(如 \n, \t) |
| %x | 十六进制数 | 常用于数值转换 |
常见转义字符
| 序列 | 含义 | 场景 |
|---|---|---|
| \n | 换行 | 几乎每条指令末尾都必须加 |
| \t | 水平制表符 (Tab) | 用于简易的列对齐 |
| \ | 反斜杠本身 | 输出路径时常用 |
| \r | 回车 (CR) | 光标回到行首(常用于覆盖当前行实现简易进度条) |
| \0ddd | 八进制字节 | 输出特殊 ASCII 字符 |
bash
# 单引号
printf '%d %s\n' 1 "abc"
# Output:1 abc
# 双引号
printf "%d %s\n" 1 "abc"
# Output:1 abc
# 无引号
printf %s abcdef
# Output: abcdef(并不会换行)
# 格式只指定了一个参数,但多出的参数仍然会按照该格式输出
printf "%s\n" abc def
# Output:
# abc
# def
printf "%s %s %s\n" a b c d e f g h i j
# Output:
# a b c
# d e f
# g h i
# j
# 如果没有参数,那么 %s 用 NULL 代替,%d 用 0 代替
printf "%s and %d \n"
# Output:
# and 0
# 格式化输出
printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg
printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234
printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543
printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876
# Output:
# 姓名 性别 体重kg
# 郭靖 男 66.12
# 杨过 男 48.65
# 郭芙 女 47.99
日常简单输出用
echo即可;但在需要严格控制对齐(如打印表格)或处理特殊格式时,printf是不可替代的神器
变量
Bash 中没有数据类型,变量可以保存一个数字、一个字符、一个字符串等。同时无需提前声明变量,给变量赋值会直接创建变量
- 只能使用英文字母,数字和下划线,且不能以数字开头
- 变量名和等号之间不能有空格 (
name = value是错的) - 不能使用 bash 里的关键字(help 命令查看保留关键字)
- 可以使用驼峰(personOfName)或者现代式(person_of_name)
访问变量的语法形式为:${var}和$var。花括号是为了帮助解释器识别变量的边界,推荐使用${var}
bash
word="hello"
echo ${word} # hello
变量赋值方法有很多,以下为常用的赋值方式:
| 赋值方法 | 格式 |
|---|---|
| 直接赋值 | name=zhangsan |
| 命令结果赋值 | path=`pwd` |
| 脚本传参 | user_name=$1 |
| read交互式赋值(不常用) | -p:交互时提示信息 -t:超过时间没操作则自动退出 -s:不显示用户的输入 |
变量类型
- 局部变量:在当前 Shell 进程或脚本中定义的变量,仅在当前进程有效。不会被子 Shell 或子脚本继承
- 环境变量: export 导出的变量,在当前进程及其产生的所有子进程中有效
- Shell特殊变量:由 Shell 程序(如 bash)启动时自动设置的只读或读写变量,用于维持 Shell 的运行
环境变量
常见的环境变量:
| 变量 | 描述 |
|---|---|
$HOME |
当前用户的用户目录 |
$LANG |
系统语言与字符集 |
$PATH |
记录命令位置的环境变量,运行命令的时候bash会在PATH的路径中查找 |
$PWD |
当前工作目录 |
$UID |
数值类型,当前用户的用户 ID |
$PS1 |
命令行的主提示符,用户在终端输入命令时看到的默认提示符 |
$PS2 |
辅助提示符,用于多行命令输入时的提示 |
| $RANDOM | 0~32767之间的整数 |
| $HISTSIZE | history 命令记录命令上限 |
| $HISTFILESIZE | history 历史记录文件的大小 |
| $HISTCONTROL | 控制历史命令记录或不记录哪些内容 |
| $HISTFILE | 指定历史命令的记录文件 |
这里 有一张更全面的 Bash 环境变量列表
在 Shell 中,与环境变量相关的常用关键字及命令如下
| 关键字 | 功能 | 示例 |
|---|---|---|
| export | 将变量导出为环境变量,使其对子进程可见 | export VAR=val |
| env | 查看当前所有的环境变量 | env |
| set | 显示当前Shell的所有变量(环境变量、局部变量、 Shell 函数) | set | grep VAR |
| unset | 删除变量(包括环境变量),但仅影响当前Shell及子进程 | unset VAR |
特殊变量
变量名只能包含数字、字母和下划线 ,因为某些包含其他字符的变量有特殊含义,这样的变量被称为特殊变量
位置变量
| 位置变量 | 含义 |
|---|---|
$0 |
当前脚本的文件名 |
$n |
传递给脚本或函数的参数。n 为数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2 当n>9时,需要加上{},避免产生歧义 |
$# |
传递给脚本或函数的参数个数 |
$* |
传递给脚本或函数的所有参数 |
$@ |
传递给脚本或函数的所有参数。被双引号包含时,与$*不同 |
shell
#!/bin/bash
echo "File Name: $0"
echo "First Parameter : $1"
echo "Second Parameter : $2"
echo "Quoted Values: $@"
echo "Quoted Values: $*"
echo "Total Number of Parameters : $#"
# output
# ./test.sh Hello World
# File Name : ./test.sh
# First Parameter : Hello
# Second Parameter : World
# Quoted Values: Hello World
# Quoted Values: Hello World
# Total Number of Parameters : 2
$*和$@的区别:1)不被双引号包含时,都以
"$1" "$2" ... "$n"的形式输出所有参数2)被双引号包含时
"$*":将所有参数压缩为一个字符串 ->"$1 $2 ... $n""$@":将每个参数保留为独立的字符串 ->"$1" "$2" ... "$n"在编写循环处理参数的脚本时(比如遍历输入的文件列表),永远使用 "$@",除非你非常确定你在做什么
状态变量
| 状态变量 | 含义 |
|---|---|
$? |
上个命令的退出状态,或函数的返回值,非0为异常 |
$$ |
当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID |
$! |
上一个脚本/命令(持续运行)的PID |
$_ |
上一个命令的最后一个参数 |
退出状态指上一个命令执行后的返回结果,为一个数字。大部分命令执行成功会返回 0,失败返回 1,也有一些命令返回其他值,表示不同类型的错误
bash
if [[ $? != 0 ]];then
echo "error"
exit 1;
fi
变量子串
变量子串 是指从一个变量的值中提取出一部分子字符串的操作,主要通过**参数扩展(Parameter Expansion)**实现
| 变量子串 | 含义 |
|---|---|
| ${#param} | 统计字符长度 |
| ${param#word} | 删除最短匹配前缀 |
| ${param##word} | 删除最长匹配前缀 |
| ${param%word} | 删除最短匹配后缀 |
| ${param%%word} | 删除最长匹配后缀 |
| ${param:offset} | 截取字符串(offset从0开始计算) |
| ${param:offset:length} | 截取字符串(offset从0开始计算) |
| ${param/pattern/replacement} | 替换第一个匹配项 |
| ${param//pattern/replacement} | 替换所有匹配项 |
bash
param1="example.txt"
param2="/usr/local/bin/zabbix_agentd"
param3="archive.tar.gz"
param4="archive.tar.gz"
echo ${param1#*.} # 输出 "txt"
echo ${param2##*/} # 输出 "zabbix_agentd"
echo ${param3%.*} # 输出 "archive.tar"
echo ${param4%%.*} # 输出 "archive"
str="apple banana apple"
echo ${str/apple/orange} # 输出 "orange banana apple"
echo ${str//apple/orange} # 输出 "orange banana orange"
basename等价于${dir##*/},dirname等价于${dir%/*}
变量扩展
可以通过参数扩展(Parameter Expansion)为变量设置默认值
| 变量扩展 | 含义 |
|---|---|
| ${variable:-default} | 如果变量未定义或为空,则使用默认值(不改变变量实际值) |
| ${variable:=default} | 如果变量未定义或为空,则将默认值赋值给变量 |
| ${variable:+alternate} | 如果变量已定义且非空,则使用替代值(不改变变量实际值) |
| ${variable:?message} | 如果变量未定义或为空,则显示错误消息并退出脚本 |
bash
#!/bin/bash
# 情况 1:变量未定义
unset username
echo "Username: ${username:-"guest"}" # 输出 "Username: guest"
# 情况 2:变量为空
username=""
echo "Username: ${username:="default_user"}" # 输出 "Username: default_user"
echo "After assignment: $username" # 输出 "After assignment: default_user"
# 情况 3:变量已定义
username="admin"
echo "Username: ${username:+"User exists"}" # 输出 "Username: User exists"
# 情况 4:强制检查变量是否已设置
unset password
echo ${password:?"Password is required"} # 输出错误并退出脚本
数组
Bash 仅支持一维数组,但其设计灵活:数组的大小没有限制,且下标是不连续的(稀疏数组)。数组下标从 0 开始
Bash 数组用括号 () 来表示,元素之间用空格(非逗号)分隔
bash
# 常规初始化(自动索引 0, 1, 2...)
colors=(red yellow "dark blue")
# 显式指定索引(稀疏数组,不连续)
# 索引为 0, 1, 9,中间空缺
nums=([0]=0 [1]=1 [9]=99)
访问所有元素时,可以使用${arr[@]}或${arr[*]}。在不加双引号 时,两者效果相同;但在双引号包裹下,差异巨大。这是 Shell 脚本中最容易产生 Bug 的地方
| 语法 | 双引号包裹时行为 | 逻辑模型 |
|---|---|---|
| "${arr[@]}" | 保持原状。将每个数组元素作为独立的参数传递 | List<String> |
| "${arr[*]}" | 合并字符串。将所有元素合并为一个长字符串,中间以第一个 IFS 字符连接(默认是空格) | String |
bash
colors=(red yellow "dark blue")
bash
# 测试:使用 printf 打印每个参数,验证参数个数
# ${arr[*]} 把所有东西粘在一起了
printf "+ %s\n" "${colors[*]}"
# Output:
# + red yellow dark blue
# ${arr[@]} 完美保留了数组边界和内部空格
printf "+ %s\n" "${colors[@]}"
# Output:
# + red
# + yellow
# + dark blue
数组操作速查表
| 操作类型 | 语法示例 | 说明 |
|---|---|---|
| 单元素读取 | ${colors[1]} | 读取索引为 1 的元素 |
| 获取长度 | ${#colors[@]} | 数组元素的个数(不是最大下标) |
| 元素长度 | ${#colors[0]} | 获取第一个元素字符串的长度 |
| 切片 | ${colors[@]:0:2} | 从索引 0 开始,取 2 个元素 |
| 追加/修改 | colors+=(green) | += 是最安全的追加方式;也可直接 colors[9]=black 赋值 |
| 删除 | unset colors[1] | 注意:只置空该位置,数组索引不会重排(变稀疏了) |
运算符
Shell 的运算符体系与其"容器"密不可分。学习具体操作符之前,必须先建立对计算容器的认知:
(( ... )):专用于整数运算。支持 C 语言风格的运算符(+=, ++),无需转义[[ ... ]]:专用于条件判断 。Bash 的关键字,支持正则和高级逻辑,比古老的[ ... ]更安全
| 语法 | bash (#!/bin/bash) | sh (#!/bin/sh) | 建议 |
|---|---|---|---|
[..] |
✅ 支持 | ✅ 支持 | 兼容性首选,但要注意变量引用陷阱 |
[[..]] |
✅ 支持 (更强大) | ❌ 不支持 | Bash 推荐。功能多(正则、逻辑符),不需担心变量为空的问题 |
((..)) |
✅ 支持 | ❌ 不支持 | Bash 推荐。C 语言风格数字运算 |
$((..)) |
✅ 支持 | ✅ 支持 | 通用推荐。所有 Shell 做数学运算取值的标准写法 |
算术运算符
Bash 原生仅支持整数运算。如需进行浮点数(小数)运算,必须借助 bc 或 awk。下表列出了常用的算术运算符
| 符号 | 描述 | 备注 |
|---|---|---|
| +、-、*、/、% | 加减乘除、取余 | 注意除法 10/3 结果为 3(取整) |
| ** | 幂运算 | 2**3 等于 8 |
| ++、-- | 自增/自减 | val++ |
| += | 累加 | count+=10 |
注意:
- 表达式和运算符之间要有空格 ,例如
2+2必须写成2 + 2 - 条件表达式要放在方括号之间,并且要有空格 ,例如:
[$x==$y]必须写成[ $x == $y ] - 乘号(
*)前边必须加反斜杠(\)才能实现乘法运算 - 完整的表达式要被 `` 包含(反引号)
bash
x=10
y=3
# 方式一:$(( )) (推荐 - 用于计算赋值)
result=$(( x * y + 5 ))
echo "Result: $result"
# 方式二:let (推荐 - 用于纯运算)
let "x += 5" # x 变为 15
# 方式三:(( )) (常用于循环或判断)
(( x > 20 )) && echo "Too large"
关系运算符
在 Shell 中,比较数字 和比较字符串使用的是两套完全不同的符号,切勿混用
| 操作符 (通用) | 含义 | C 风格替代 (仅限 (()) ) |
|---|---|---|
| -eq | 等于 (Equal) | == |
| -ne | 不等于 (Not Equal) | != |
| -gt | 大于 (Greater Than) | > |
| -lt | 小于 (Less Than) | < |
| -ge | 大于等于 | >= |
| -le | 小于等于 | <= |
bash
a=10
b=20
# 场景 1:if 条件判断 (使用中括号)
if [[ $a -lt $b ]]; then
echo "a < b"
fi
# 场景 2:C 语言风格 (使用双括号)
if (( a < b )); then
echo "a < b (C-style)"
fi
字符串运算符
在进行字符串比较时,变量最好总是包裹双引号 "$var",以防变量为空导致脚本报错
| 操作符 | 含义 | 示例 |
|---|---|---|
| = / == | 字符串相等 | [[ "s1" == "s2" ]](推荐使用 ==) |
| != | 不相等 | [[ "s1" != "s2" ]] |
| -z | Check if Zero (空) | [[ -z "$s" ]](长度为0则真) |
| -n | Check if Non-zero (非空) | [[ -n "$s" ]](长度不为0则真) |
| 无符号 | 默认判非空 | [[ "$s" ]] 等同于 -n |
bash
#!/bin/bash
app_env="dev"
target_env="prod"
api_key="" # 模拟一个空变量
user_name="admin"
# 字符串比对
if [[ "$app_env" == "$target_env" ]]; then
echo "环境匹配,可以部署。"
else
echo "环境不匹配: 当前为 [${app_env}],目标为 [${target_env}]"
fi
# 空值与非空检测,防御性编程,检查必要配置是否存在
if [[ -z "$api_key" ]]; then
echo "[Warning] API_KEY 为空,请检查配置文件!"
fi
# 确认用户身份存在
if [[ -n "$user_name" ]]; then
echo "当前操作用户: $user_name (Length: ${#user_name})"
fi
逻辑运算符
Shell 脚本中的逻辑控制通常通过[[...]]或命令短路实现
| 操作符 | 场景 | 说明 |
|---|---|---|
! |
非 | [[ ! -e file ]](文件不存在) |
&& |
逻辑与 | [[ a -gt 10 \&\& b -lt 20 ]](两边同时成立) |
| ` | ` |
在旧版 Old School[...]中,必须使用以下逻辑运算符,了解即可
| 运算符 | 说明 | 举例 |
|---|---|---|
-o |
或运算,有一个表达式为 true 则返回 true。 | [ $a -lt 20 -o $b -gt 100 ] 返回 true |
-a |
与运算,两个表达式都为 true 才返回 true。 | [ $a -lt 20 -a $b -gt 100 ] 返回 false |
bash
x=10
y=20
echo "x=${x}, y=${y}"
if [[ ${x} -lt 100 && ${y} -gt 100 ]]
then
echo "${x} -lt 100 && ${y} -gt 100 返回 true"
else
echo "${x} -lt 100 && ${y} -gt 100 返回 false"
fi
if [[ ${x} -lt 100 || ${y} -gt 100 ]]
then
echo "${x} -lt 100 || ${y} -gt 100 返回 true"
else
echo "${x} -lt 100 || ${y} -gt 100 返回 false"
fi
# Output:
# x=10, y=20
# 10 -lt 100 && 20 -gt 100 返回 false
# 10 -lt 100 || 20 -gt 100 返回 true
逻辑运算符,选新不选旧,不建议使用
-o和-a
文件测试运算符
这是 Shell 自动化运维中最强大的功能之一,用于检测文件属性
| 操作符 | 含义与检测对象 | 典型场景 |
|---|---|---|
| -e | Exist。检测对象是否存在(包括文件、目录、特殊文件等) | 最基础的判断,后续操作的大前提 |
| -f | F ile。检测是否为普通文件(排除目录、设备) | 确保操作对象不是目录 |
| -d | D irectory。检测是否为目录 | 创建日志目录前先判断是否存在 |
| -s | S ize。检测对象存在且大小不为 0 | 判断下载的文件是否损坏或为空 |
| -r | Read。检测当前用户是否可读 | 只有可读才能 grep 配置 |
| -w | Write。检测当前用户是否可写 | 判断日志文件是否被 chattr 锁定 |
| -x | Execute。检测当前用户是否可执行 | 判断脚本是否有 +x 权限 |
| -b | B lock。检测是否为块设备文件 | 挂载磁盘、检查 /dev/sda |
| -c | C har。检测是否为字符设备文件 | 检查串口设备、/dev/null |
| -p | P ipe。检测是否为命名管道 (FIFO) | 进程间通信检测 |
| -u | SUID。检测文件是否设置了 SUID 位 | 安全审计,如检查 /usr/bin/passwd |
| -g | SGID。检测文件是否设置了 SGID 位 | 协作组目录权限检查 |
| -k | Sticky。检测目录是否设置了粘着位 | 检查 /tmp 目录权限(防误删) |
bash
#!/bin/bash
# 待检测的配置文件
config_file="/etc/hosts"
echo "Checking config: $config_file ..."
# 第一层:先看存不存在 (-e)
if [[ ! -e "$config_file" ]]; then
echo "Error: File not found!"
exit 1
fi
# 第二层:存在的话,判断是否为普通文件
if [[ ! -f "$config_file" ]]; then
echo "Error: This is a directory or device, not a file!"
exit 1
fi
# 第三层:判断文件是否为空
if [[ ! -s "$config_file" ]]; then
echo "Warning: File is empty."
else
# 第四层:逻辑嵌套检测权限,既要存在,又要可读,且可写
if [[ -r "$config_file" && -w "$config_file" ]]; then
echo "OK: File is healthy (Readable & Writable)."
elif [[ -r "$config_file" ]]; then
echo "Note: Read-only file."
else
echo "Error: Permission denied."
fi
fi
流程控制
任何强大的脚本都离不开逻辑判断 与循环执行。Shell 提供了类 C 语言风格的流程控制,但其语法对空格和符号极其敏感
条件语句
Shell 的条件判断主要依赖 if 和 case。其中 if 的判断基元建议无脑使用现代 Bash 的[[ ]](而非旧式的[ ]),因为它能自动处理空变量且支持正则,表达式和方括号之间必须有空格
if-else
最经典的条件判断。注意 if 块结束后必须跟 fi
bash
#!/bin/bash
# 单行极简写法(适合简单防御逻辑)
[[ ! -d "build" ]] && mkdir "build"
# 标准多分支写法
score=85
if [[ $score -ge 90 ]]; then
echo "优秀"
elif [[ $score -ge 60 ]]; then
echo "及格"
else
echo "不及格"
fi
case
当你需要面对很多选项(尤其是用户交互 或参数解析 )时,case远比一堆if-elif优雅。它支持通配符匹配
|:分割多个模式(类似 OR);;:结束当前块(类似 break)
bash
#!/usr/bin/env bash
# 场景:根据后缀处理文件
file_name="data.tar.gz"
# "${file_name##*.}" 提取的是 "gz" (见变量操作章节)
case "$file_name" in
*.tar.gz|*.tgz)
echo "Decompressing with tar..."
# tar -zxvf "$file_name"
;;
*.zip)
echo "Decompressing with unzip..."
;;
*)
echo "Unknown format!"
;;
esac
每种情况都是匹配了某个模式的表达式。
|用来分割多个模式,)用来结束一个模式序列。第一个匹配上的模式对应的命令将会被执行。*代表任何不匹配以上给定模式的模式。命令块儿之间要用;;分隔
循环语句
Bash 提供了 for (遍历列表), while (条件循环), until (反向循环) 和 select (交互菜单) 四种机制
for循环
for与它在 C 语言中的姊妹非常像。看起来是这样:
shell
for arg in elem1 elem2 ... elemN
do
### 语句2
done
在每次循环的过程中,arg依次被赋值为从elem1到elemN。这些值还可以是通配符或者大括号扩展。
当然,我们还可以把for循环写在一行,但这要求do之前要有一个分号,就像下面这样:
shell
for i in {1..5}; do echo $i; done
还有,如果你觉得for..in..do对你来说有点奇怪,那么你也可以像 C 语言那样使用for,比如:
shell
for (( i = 0; i < 10; i++ )); do
echo $i
done
当我们想对一个目录下的所有文件做同样的操作时,for就很方便了。举个例子,如果我们想把所有的.bash文件移动到script文件夹中,并给它们可执行权限,我们的脚本可以这样写:
shell
DIR=/home/zp
for FILE in ${DIR}/*.sh; do
mv "$FILE" "${DIR}/scripts"
done
# 将 /home/zp 目录下所有 sh 文件拷贝到 /home/zp/scripts
while循环
while 是构建持续运行服务 或按行读取文件的神器
shell
while [[ condition ]]
do
### 语句
done
shell
# 基本条件循环
count=0
while [[ $count -lt 3 ]]; do
echo "Count: $count"
((count++)) # 现代 Bash 数学运算推荐写法,别用 expr
done
# 逐行读取文件
# 将 file.txt 的内容一行行读入 line 变量
while read -r line; do
echo "Processing: $line"
done < "config.txt"
until循环
逻辑与 while 刚好相反:直到条件成立才停止。实战中用得少,通常用于"等待某个服务启动"
shell
# 等待 Postgres 端口 (5432) 连通
until nc -z localhost 5432; do
echo "Waiting for DB..."
sleep 1
done
echo "DB started!"
select循环
select循环帮助我们组织一个用户菜单。它的语法几乎跟for循环一致:
shell
select answer in elem1 elem2 ... elemN
do
### 语句
done
select 会打印elem1..elemN以及它们的序列号到屏幕上,之后会提示用户输入。通常看到的是$?。用户的选择结果会被保存到answer中。如果answer是一个在1..N之间的数字,那么语句会被执行,紧接着会进行下一次迭代,跳出循环使用break语句
shell
PS3="请选择包管理器: " # 设置提示符
select item in bower npm gem pip; do
if [[ -n "$item" ]]; then
echo "你选择了: $item"
# 实际逻辑...
break # 选完后跳出菜单
else
echo "输入无效,请重试。"
fi
done
控制跳转
如果想提前结束一个循环或跳过某次循环执行,可以使用 shell 的break和continue语句来实现。它们可以在任何循环中使用
- break :直接跳出整个循环(结束遍历)
- continue :跳过本次循环剩余代码,直接进入下一次
bash
#!/bin/bash
# 寻找 10 到 20 之间的第一个能被 5 整除的数
for (( num=10; num<=20; num++ )); do
if (( num % 5 == 0 )); then
echo "Found divisible by 5: $num"
break # 找到一个就退出循环
fi
done
# 输出:Found divisible by 5: 10 (这里只是示例,实际上 10 就是)
函数
Shell 函数是脚本组织和复用代码的核心。定义函数有两种主流语法:
bash
# 推荐,POSIX 兼容性好
func_name() {
# 函数体
return 0
}
# Bash 特有,明确显示 function 关键字
function func_name {
# 函数体...
}
💡 说明:
- 函数定义时,
function关键字可有可无- 函数返回值 - return 返回函数返回值,返回值类型只能为整数(0-255)。如果不加 return 语句,shell 默认将以最后一条命令的运行结果,作为函数返回值
- 函数返回值在调用该函数后通过
$?来获得- Shell 是解释执行的,必须先定义、后调用 ,调用函数仅使用函数名即可
func_name arg1 arg2
作用域
在 Shell 函数中定义变量,默认是全局 (Global) 的,这会修改脚本其他地方的同名变量。在函数内部,始终使用local关键字声明变量
bash
# 错误示范 (污染全局)
bad_func() {
result="Hacked!" # 修改了外面的变量
}
# 正确示范 (局部隔离)
good_func() {
local result="Safe"
echo "Inside: $result"
}
函数传参
函数内部有自己的一套位置参数系统。当调用 my_func 10 20 时:
- 函数内的
$1是 10(而不是脚本本身的第一个参数) - 函数内的
$#是传递给该函数的参数个数 - 函数内的
$0仍然是脚本文件名,而非函数名(获取函数名需用 $FUNCNAME)
| 变量 | 描述 |
|---|---|
$0 |
脚本名称 |
$1 ... $9 |
函数接收的第 1 到 9 个参数 |
$* or $@ |
不包括$0,函数内接收的所有参数列表 |
$# |
不包括$0,函数内接收的参数总个数 |
$FUNCNAME |
函数名称(仅在函数内部有值) |
bash
#!/bin/bash
# 一个健壮的处理参数的函数模板
deploy_app() {
local app_name=$1
local version=${2:-"latest"} # 支持默认值
if [[ -z "$app_name" ]]; then
echo "Usage: $FUNCNAME <app_name> [version]"
return 1 # 返回错误状态码
fi
echo "Deploying [ $app_name ] with version [ $version ]..."
echo "Args count: $#"
echo "Args list: $@"
}
# 调用
deploy_app "nginx" "1.24"
# deploy_app "redis" <-- 使用默认 version
函数返回值
这是 Shell 函数与其他语言(Java/Python)最大的区别。函数有两个"输出通道":
:one:返回状态码(return)
用于控制逻辑流程,范围限制0~255,约定0为成功,非0为失败
bash
check_file() {
[[ -f "$1" ]] && return 0 || return 1
}
# 调用逻辑
if check_file "/etc/hosts"; then
echo "File exists!"
fi
:two:返回数据结果(return)
用于返回计算结果、字符串或处理后的数据。将结果打印到标准输出,调用者使用$()赋值给变量
bash
# 错误做法:尝试 return 字符串或大数字
# calc() { return 300; } -> $? 会变成 44 (因为 300%256 = 44)
# 正确做法:echo 输出结果
calc_sum() {
local a=$1
local b=$2
local result=$(( a + b ))
echo "$result" # 打印到 stdout
}
# 捕获数据
total=$(calc_sum 10 200)
echo "Total is: $total" # Output: 210
Shell进阶
在 Shell 接收到我们输入的一行命令后,并不会直接丢给内核执行。在此之前,它会进行一系列精密的解析和预处理
- 扩展(Expansions):将符号转换具体的值(如变量替换、算术计算)
- 重定向(Redirections):重新排布输入输出的数据流向
Shell扩展体系
扩展本质上是一种**"替换机制"**。Shell 会扫描命令行中的特殊标记(Tokens),将其替换为最终的字符串,然后才运行命令
大括号扩展
大括号扩展是最先被执行的扩展。它用于生成任意字符串序列,不仅限于已存在的文件
- 字符串枚举
shell
echo beg{i,a,u}n
# Output: begin began begun
- 序列生成
shell
echo {0..5} # 生成 0 1 2 3 4 5
echo {a..e} # 生成 a b c d e
echo {10..20..2} # [步长] 生成 10 12 14 16 18 20
⚡场景实战:
cp config.conf{,.bak}等价于cp config.conf config.conf.bak
命令置换
允许将一个命令的标准输出结果,作为另一个命令的参数。当一个命令被 `` 或 $() 包围时,命令置换将会执行
shell
# 获取当前日期并赋值
current_time=$(date +%T)
# 查找 docker 的绝对路径并显示详情
ls -l $(which docker)
算数扩展
Bash 处理整数运算的原生方式。算数表达式必须包在$(( ))中,双括号内部引用变量无需加$前缀
shell
x=10
y=5
# 基础运算
echo $(( x + y )) # Output: 15
# 复杂运算(先算乘法)
result=$(( (x + y) * 2 ))
# C 风格的自增(Side Effect)
echo $(( ++x )) # x 变为 11,输出 11
流与重定向
在 Linux 中,一切皆文件,Shell 程序之间的协同工作,依赖于**数据流(Streams)**的传送与重组
文件描述符
任何一个进程启动时,内核都会默认打开三个"文件"端口:
| 句柄 (FD) | 名称 | 缩写 | 默认指向 |
|---|---|---|---|
| 0 | 标准输入 | stdin |
键盘输入 |
| 1 | 标准输出 | stdout |
屏幕终端 |
| 2 | 标准错误 | stderr |
屏幕终端 |
💡为什么会有 2 个输出? 为了将"正常的业务结果"与"报错信息"分离。当我们在管道中
| grep时,默认只处理 stdout,确保报错信息不会污染下游的数据处理
重定向运算符
| 运算符 | 作用 | 记忆技巧 |
|---|---|---|
> |
覆盖输出 (stdout) | 箭头指向文件,原有内容被清空 |
>> |
追加输出 (stdout) | 双箭头代表追加,保留原内容 |
2> |
重定向错误 (stderr) | 显式指定 FD=2 (错误流) |
&> |
重定向所有 (1+2) | & 代表混合 |
< |
重定向输入 | 箭头反向,从文件读取内容 |
2>&1 |
将 stderr 指向 stdout | 让错误流并入正常流一起处理 |
- 错误日志分离
bash
# 正常的去 output.log,报错的去 error.log
./deploy.sh > output.log 2> error.log
- 全量日志合并
bash
# 将标准错误重定向到标准输出(&1),然后一起写到文件
# 2>&1 必须写在最后
./app_start.sh > app.log 2>&1
- 黑洞模式
bash
# 禁止一切输出(常用于静默运行定时任务)
./silent_task.sh > /dev/null 2>&1
/dev/null是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到
Here Document
Here Document(嵌入文档)是一种在 Shell 脚本中定义多行字符串输入的特殊语法,常用于自动生成配置文件或向命令投喂多行数据
bash
command << MARKER
...Content...
MARKER
# MARKER:定界符,常用 EOF (End Of File),但其实用什么词都可以
# 约束:结尾的定界符必须顶格写,且后面不能有空格
- 最常见的用法:动态生成一个配置文件
bash
# 将 EOF 之间的内容写入到 config.conf 文件中
cat > /etc/nginx/conf.d/default.conf << EOF
server {
listen 80;
server_name example.com;
}
EOF
- 通过
wc -l命令计算 document 的行数:
shell
wc -l << EOF
This is a simple lookup program
for good (and bad) restaurants
in Cape Town.
EOF
# output:
# 3
Debug
与其他高级语言拥有完善的 IDE 调试器不同,Shell 脚本的调试往往依赖日志和追踪模式。Shell 提供了一组强大的内建选项,帮助我们将脚本的执行过程可视化
三种调试方式
最常用的调试参数是-x (xtrace),开启后,Shell 会在执行每一行命令前,将变量替换后的完整命令打印到标准错误输出中
- 命令行临时开启(无需修改脚本内容,用于临时排查问题)
bash
bash -x script.sh
- 在头部Shebang永久开启(常用于开发阶段)
bash
#!/bin/bash -x
- 在脚本内动态开启(set命令)
bash
set -x # 开启追踪
# ... 代码 ...
调试输出
当我们启用-x模式运行如下脚本:
bash
#!/bin/bash
name="Shell"
echo "Hello, $name"
输出结果如下
bash
+ name=Shell <-- 赋值操作
+ echo 'Hello, Shell' <-- 变量已被展开,$name 变成了 Shell
Hello, Shell <-- 真实的脚本执行结果
+:这是调试信息的默认前缀(由环境变量 $PS4 控制),每一层调用(如函数嵌套)会增加一个+- 变量展开:最核心的价值!可清楚看到变量
$name在执行时变成了什么值,90% 的 Bug 都能通过这个发现
局部调试
如果脚本为几千行,全篇开启-x会导致屏幕被海量日志淹没。此时,我们只需要使用set命令包裹出可疑代码段即可
语法如下(有点反直觉)
set -x:开启调试(减号代表 Activate)set +x:关闭调试(加号代表 Deactivate)
示例:只调试循环内部
bash
#!/bin/bash
echo "Start process..."
# 开启局部调试:只关心循环里的变量变化
set -x
for (( i = 0; i < 3; i++ )); do
# 这里的信息会被打印出来,带有 '+' 前缀
echo "Processing index: $i"
done
set +x
echo "End process..." # 这句正常执行,不会被追踪
运行输出如下
bash
Start process...
+ (( i = 0 ))
+ (( i < 3 ))
+ echo 'Processing index: 0'
Processing index: 0
+ (( i++ ))
+ (( i < 3 ))
+ echo 'Processing index: 1'
Processing index: 1
...
+ set +x
End process...
调试选项
以下是开发中最常见的选项清单,按实用场景分类
🔧 核心调试与排错
| 参数 | 全称 | 作用 | 使用场景 |
|---|---|---|---|
| -x | xtrace | 打印每一行命令的执行轨迹 | 逻辑错误调试、变量值检查(最常用) |
| -v | verbose | 打印原始输入行 | 通常配合 -x 使用,用于对比"代码原样"和"执行实况" |
| -n | noexec | 只检查语法,不运行脚本 | 检查是否有 if 没写 fi、漏了引号等低级语法错误 |
| -u | nounset | 遇到未定义变量则报错 | 防御性编程。防止因为变量名打错(如 file_name 写成 filename)导致 rm -rf /$filename 变成 rm -rf / 的惨剧 |
⚙️ 行为控制与运行模式
| 参数 | 全称 | 作用 | 使用场景 |
|---|---|---|---|
| -f | noglob | 禁止通配符扩展。即 *, ? 不再自动扩展为文件名 | 当脚本需要处理大量包含 * 的特殊字符串(而非文件名)时开启 |
| -e | errexit | 遇到错误立即退出 | CI/CD 必备。构建脚本一旦某一步出错,不要继续执行后续操作 |
| -i | interactive | 强制脚本以交互 Shell 行为运行 | 调试.bashrc或需要模拟用户终端输入(TTY)的脚本 |
| -t | - | 读取并执行完一条命令后立刻退出 | 极少用的调试手段,用于精细控制脚本启动的第一步状态 |