Shell入门指南

Shell入门指南

复制代码
███████╗██╗  ██╗███████╗██╗     ██╗
██╔════╝██║  ██║██╔════╝██║     ██║
███████╗███████║█████╗  ██║     ██║
╚════██║██╔══██║██╔══╝  ██║     ██║
███████║██║  ██║███████╗███████╗███████╗

Shell简介

shell

Shell 本质上是一个用 C 语言编写的程序,它是连接用户与 Linux/Unix 操作系统内核的核心桥梁。对于开发者而言,Shell 具有不可替代的"双重人格":

  • 作为命令语言解释器:它为用户提供了访问操作系统内核服务的交互界面。当你输入命令(如 ls, grep)时,Shell 负责解释这些指令并传递给内核执行
  • 作为程序设计语言:Shell 拥有一套完整的编程语法(变量、循环、条件判断等)。我们将由 Shell 语法编写的程序称为Shell Script,这也是我们在开发和运维中常说的"写 Shell"
Shell Shell Script
本质 命令解释器 / 运行环境 文本文件 / 逻辑代码集合
核心职责 接收指令、调用内核、反馈结果 编排指令、处理逻辑、实现自动化
交互模式 实时交互(Read-Eval-Print Loop) 批处理执行(编写 -> 运行)
示例 bashzshsh install.shbackup.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)环境运行

  1. Read:从标准输入(Stdin)等待用户键入命令
  2. Eval:解析并执行命令
  3. Print:将结果输出到终端(Stdout)
  4. 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,第二个参数是$2n>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依次被赋值为从elem1elemN。这些值还可以是通配符或者大括号扩展

当然,我们还可以把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 的breakcontinue语句来实现。它们可以在任何循环中使用

  • 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 {
    # 函数体...
}

💡 说明:

  1. 函数定义时,function关键字可有可无
  2. 函数返回值 - return 返回函数返回值,返回值类型只能为整数(0-255)。如果不加 return 语句,shell 默认将以最后一条命令的运行结果,作为函数返回值
  3. 函数返回值在调用该函数后通过$?来获得
  4. 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 - 读取并执行完一条命令后立刻退出 极少用的调试手段,用于精细控制脚本启动的第一步状态
相关推荐
心在飞扬1 天前
langchain学习总结-Runnable组件 bind 函数
全栈
liux35281 天前
Ansible集群批量管理与维护完全指南
自动化运维
飞雪飘摇1 天前
摆脱重复劳动的引力:深入解析 Elpis 框架的“配置驱动”哲学
全栈
陈佬昔没带相机2 天前
2025年终总结:Vibe Coding 之后,胆儿肥了
ai编程·全栈·next.js
mCell2 天前
2025:被 AI 推着往前走的一年
agent·年终总结·全栈
小星运维日记3 天前
2026年五大自动化运维系统测评:企业运维如何突破效率瓶颈?
自动化运维·自动化运维平台·自动化运维系统·自动化运维中心·自动化运维产品
ohyeah4 天前
打造 AI 驱动的 Git 提交规范助手:基于 React + Express + Ollama+langchain 的全栈实践
langchain·全栈·ollama
用户0591562441255 天前
# Go语言 Windows 桌面自动化实战:从零开始掌握 winput
自动化运维