• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-09-08 22:35 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

概述

  1. Shell 本身不做"计算",它只做两件事
    1. 启动进程
    2. 把进程的输入输出接起来
  2. 管道 就是把前一个进程的 stdout 接到下一个进程的 stdin
  3. 重定向就是把 stdout/stderr 重新指向文件

命令行基本

文件系统导航

  1. pwd
  2. cd
    1. cd 并不是一个独立程序,而是 shell 的内建命令
    2. 如果它是外部程序,切换目录只会影响那个子进程,父 shell 的目录不会变
  3. ls

文件操作

  1. touch
  2. mkdir
  3. cp
  4. mv
    1. mv 之所以既能"移动"又能"重命名",是因为在 Linux 文件系统层面,重命名本质就是"在同一目录下移动
    2. 文件名只是目录里指向 inode(真正的数据结构)的一个标签而已
    3. 这也解释了为什么 mv 在同一分区内几乎是瞬间完成的(不管文件多大),而跨分区 mv 会变慢(因为要真的复制数据再删除源)
  5. rm

查看内容

  1. cat
    1. 一次性输出整个文件
  2. less
    1. 分页查看(可上下翻、搜索 /关键字、按 q 退出)
  3. head
    1. 看文件开头
    2. -n 20 看前20
  4. tail
    1. 看文件结尾
    2. -n 20 看后20
    3. -f 实时追踪(看日志必备)
  5. 本质
    1. 这几个命令都遵循同一个模式——读文件、把内容变成一串"字符流"往 stdout(标准输出)里写

重定向

  1. 先理解三个"文件描述符":每个进程默认打开三个通道:
    1. 0 = stdin(标准输入)
    2. 1 = stdout(标准输出)
    3. 2 = stderr(标准错误)
  2. 它们默认都连着你的终端
    1. 键盘是输入,屏幕是输出
  3. 重定向和管道做的事情,就是把这些通道从"终端"改接到别的地方
  4. 重定向符号
符号 作用
> stdout 写入文件(覆盖)
>> stdout 追加到文件末尾
< 把文件内容作为 stdin
2> stderr 重定向到文件
2>&1 stderr 重定向到"stdout 现在指向的地方"(顺序很关键)
&> stdoutstderr 一起重定向(bash 简写)

管道

  1. 把左边命令的 stdout,直接接到右边命令的 stdin,两个进程之间不落地到文件,靠内核里的一个缓冲区传数据

通配符

符号 含义
* 匹配任意长度(含0)的任意字符
? 匹配恰好一个任意字符
[abc] 匹配方括号中任意一个字符
[a-z] 匹配范围内任意一个字符
[!abc][^abc] 匹配不在集合内的字符
  1. 本质
    1. 通配符匹配是 shell 自己在命令执行前做的文本替换,跟被调用的程序毫无关系
    2. 也就是说,ls *.txt 这行命令,shell 会先把 *.txt 展开成当前目录下所有匹配的文件名
    3. 然后才把展开后的结果 ls file1.txt file2.txt 真正传给 ls 程序执行
    4. 这也是为什么在双引号里通配符不会展开(双引号只保留变量展开,关闭了通配符和空格分词),而单引号里通配符和变量都不展开

语法核心

变量与引用

  1. 定义与引用规则
    1. 变量在 shell 里没有类型,全部是字符串(数值运算是"临时"把字符串当数字解析)
    2. $NAME${NAME} 是同一件事,花括号只是为了帮 shell 明确"变量名到哪里结束",跟后面文字紧挨着时必须用花括号

  1. 单引号 vs 双引号 vs 反引号

  1. 命令替换(把命令的输出捕获成字符串)
    1. $(command) 会先开一个子 shell 跑这个命令,把它的标准输出(去掉末尾换行)当成字符串,替换到原来的位置

  1. 只读与取消定义

参数与输入

  1. 位置参数
    1. $1 $2 ...shell 在调用脚本时自动帮你把命令行后面的字符串按空格切好装进去的"内建数组"(严格说不是数组,是一组特殊变量)
    2. $*$@ 的区别只在加双引号遍历时才显现出来
    3. "$@" 是唯一能正确处理"参数里带空格"情况的写法,写脚本时几乎永远用 "$@" 而不是 $* 或不加引号的 $@

  1. read 读取用户输入
    1. read 从标准输入(stdin)逐行读取一行文本,按空格分割赋值给变量
    2. 如果只给一个变量名,整行(含空格)都会赋给它

  1. 环境变量 export

条件判断

  1. test / [ ] / [[ ]] 三种写法(本质相同,推荐用 [[ ]]

  1. 多分支 if / elif / else

  1. 逻辑组合 &&

  1. case 语句
    1. 多分支字符串匹配,比一长串 elif 更清晰

  1. 本质
    1. [ 条件 ] 实际上是在调用一个叫 test 的程序([ 本身就是这个程序的另一个名字)
    2. 它执行完会返回一个"退出状态码"给 shell0 表示条件为真(成功),非 0 表示假(失败)
    3. if 语句其实判断的是紧跟着它的命令的退出状态码,不是"布尔值"这种东西
    4. [[ ]]bash 的扩展语法,支持 && 直接写在里面、支持正则匹配 =~,比 [ ] 更安全好用,能用就优先用它

循环

  1. for 循环 —— 三种常见形态

  1. while 循环 —— 最常用于逐行读文件
    1. IFS=(清空字段分隔符)防止 read 吃掉行首行尾空格
    2. -r 防止反斜杠被转义误处理

  1. until
    1. while 相反,条件为假时才继续

  1. break / continue
    1. for...in 本质是对一个"词列表"逐个赋值执行
    2. 这个列表可以来自手写的字面量、通配符展开、命令替换
    3. while 本质是不断执行"条件命令",只要它的退出状态码是 0 就继续循环体

函数

  1. 定义与调用
    1. 函数内部的 $1 $2 $@ $# 指的是"调用这个函数时给的参数"
    2. 跟脚本级别的 $1 $2 是两套独立的东西

  1. 返回值:只能是 0-255 的整数状态码

  1. 想返回"数据"(不是状态码)—— 用 echo + 命令替换
    1. returnshell 里从设计上就是给"退出状态码"用的,不是给"数据"用的
    2. 这跟大多数编程语言的函数返回值语义不一样

  1. local 关键字(变量作用域)
    1. 如果不加 local,函数内部修改的就是外层的全局变量

  1. source 复用函数库
    1. source 会把 lib.sh 的内容"原地展开"到当前 shell 里执行,而不是开子进程
    2. 所以里面定义的函数和变量在 main.sh 里能直接用

数组

  1. 索引数组

  1. 遍历数组

  1. 关联数组(bash 4+,类似其他语言的字典/map
    1. 普通变量是"一个格子存一个字符串",数组是"一串编号连续(或不连续)的格子"
    2. 索引数组的下标是数字,关联数组的下标是字符串(本质是哈希表)
    3. "${arr[@]}" 加引号后,数组里每个元素会被当成独立的一个词处理(哪怕元素本身含空格),这跟 "$@" 的道理完全一致

grep

本质

  1. 逐行扫描输入,用正则表达式做匹配,匹配成功就把整行打印出来(默认粒度是"行",不是"字符")

基本用法

正则相关

单词边界(避免误匹配子串)

sed

本质

  1. 逐行读取输入,对每一行按你给的"指令"做变换(替换、删除、插入),然后把结果输出
  2. 它不是交互式编辑器,是"批处理式"改写工具,特别适合脚本里自动化修改配置文件

最常用:替换 s/查找/替换/标志

按行号/行范围操作

按正则匹配做替换

多条指令组合

其他

  1. sed& 代表"整个匹配到的内容"
  2. \1 \2 代表正则里用括号 () 捕获的分组,这在做"结构化替换"时非常有用:

awk

本质

  1. awk 把每一行自动按分隔符(默认是空格/制表符)切成若干"字段",可以直接用 $1 $2 $3... 引用第几列,$0 代表整行
  2. 本质上它是一个精简的"面向列"的编程语言,特别适合处理表格类、日志类数据

内置变量

变量 含义
$0 整行内容
$1, $2... 1、2...个字段
NF 当前行的字段数(Number of Fields
NR 当前是第几行(Number of Records
FS 输入字段分隔符(默认是空格)
OFS 输出字段分隔符

基础用法

指定分隔符(处理 CSV / 冒号分隔的 /etc/passwd 等)

条件过滤(awk 也能当 grep 用,但更强)

BEGINEND 块(初始化和收尾统计)

  1. BEGIN{} 在读任何一行之前只执行一次(常用来做初始化、打印表头),主体 {} 对每一行都执行一次
  2. END{} 在所有行处理完之后只执行一次(常用来做汇总输出)

进程、信号与脚本健壮性

退出状态码 $?

  1. Linux 里几乎所有程序执行完都会返回一个 0-255 的整数给操作系统,0 表示成功,非0表示各种类型的失败
  2. 这是整个 Shell 世界"判断对错"的唯一依据

  1. 自己的脚本里也可以主动设置退出码

set 家族——让脚本"该死的时候赶紧死"

  1. 默认行为的"危险性"
    1. Bash 脚本默认情况下,就算某一行命令失败了,也会硬着头皮继续往下执行,这在自动化场景里非常危险

  1. set -e 的例子

  1. set -u 的例子

  1. pipefail 的例子

  1. 一个真实场景

trap——捕获信号,保证清理工作一定执行

  1. trap 让你的脚本能"监听"某些信号或事件,一旦触发就执行你指定的清理函数
    1. 最常见用途是保证脚本不管是正常结束、出错退出、还是被用户 Ctrl+C 中断,都会执行清理临时文件等收尾工作
信号 触发场景
EXIT 脚本任何原因结束时(这不是真正的Unix信号,是bash特有的"伪信号",最常用)
INT 用户按 Ctrl+C
TERM kill 命令正常终止
ERR 命令返回非0状态码时(需配合 set -e
  1. 最常用模式:EXIT 陷阱做清理

  1. 捕获 Ctrl+C

  1. 同时处理多种情况

后台任务与 wait

  1. 在命令末尾加 &shell 会立刻返回,把这个命令扔到"后台"作为一个独立的子进程运行,当前 shell 不会被阻塞,可以继续往下执行其他命令

  1. 并发跑多个任务,最后统一等待(提速的常见手段)

  1. 获取后台进程的 PID 和退出状态

shell() vs 代码块 {}

  1. ()fork 出一个全新的子进程去执行里面的命令,这个子进程有自己独立的一份变量副本,执行完就消失,不会"泄漏"任何状态回父进程
  2. {} 只是把多条命令"打包"在当前进程里顺序执行,没有开新进程,所以变量修改是"真实"的、会保留下来

一个"生产级"的备份脚本

调试与工程规范

bash -x —— 单步跟踪执行过程

  1. -x 会在脚本每执行一行之前,先把"这行实际展开后的样子"打印到 stderr,前面带一个 + 号做标记

  1. 只在脚本内部某一段开启调试(不用整个脚本都跟踪,避免输出太多干扰)

  1. 给追踪输出加更多信息(默认只有一个 +,脚本嵌套多层函数时容易分不清层级)

shellcheck —— 静态代码检查

  1. shellcheck 是一个专门分析 shell 脚本的静态检查工具,它不运行你的脚本,而是通过分析代码文本,提前发现常见的语法陷阱、引用错误、可移植性问题

脚本工程规范

  1. 标准脚本头部

  1. 参数校验 + --help

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享