编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

  • Shell-Bash

    • Shell & Bash 从0到1实战专栏
    • Shell 入门与变量
    • 流程控制与函数
    • 数据与 IO 处理
      • 3.1 数组操作
        • 3.1.1 索引数组——定义/访问/遍历
        • 3.1.2 关联数组——Shell 的"字典"
        • 3.1.3 数组长度、切片与 CRUD
      • 3.2 字符串处理
        • 3.2.1 长度/截取/替换/删除
        • 3.2.2 大小写转换与模式匹配
        • 3.2.3 Here Document 与 Here String
        • 3.2.4 printf 格式化输出
      • 3.3 重定向与管道
        • 3.3.1 stdin/stdout/stderr——三剑客
        • 3.3.2 > / >> / < / << 四大家族
        • 3.3.3 管道 | ——命令协作的艺术
        • 3.3.4 tee 分流与 /dev/null 黑洞
        • 3.3.5 文件描述符——深入理解 0/1/2
      • 3.4 综合案例:日志分析器
      • 3.5 新手陷阱 Top 5
      • 3.6 综合思考题
    • grep 搜索实战
    • sed 与 awk 编程
    • 文件查找与统计
    • 日志监控与告警
    • 备份进程与磁盘
    • 用户与服务管理
    • 网络调度与部署
    • 调试与脚本规范
    • 安全与兼容处理
    • 性能与打包分发
  • 工具脚本

  • 自动化脚本

  • ScriptHub
  • Shell-Bash
杨充
2025-11-11
目录

数据与 IO 处理

# 第 3 章 数据与 IO 处理

# 目录介绍

  • 3.1 数组操作
    • 3.1.1 索引数组——定义/访问/遍历
    • 3.1.2 关联数组——Shell 的"字典"
    • 3.1.3 数组长度、切片与 CRUD
  • 3.2 字符串处理
    • 3.2.1 长度/截取/替换/删除
    • 3.2.2 大小写转换与模式匹配
    • 3.2.3 Here Document 与 Here String
    • 3.2.4 printf 格式化输出
  • 3.3 重定向与管道
    • 3.3.1 stdin/stdout/stderr——三剑客
    • 3.3.2 > / >> / < / << 四大家族
    • 3.3.3 管道 | ——命令协作的艺术
    • 3.3.4 tee 分流与 /dev/null 黑洞
    • 3.3.5 文件描述符——深入理解 0/1/2
  • 3.4 综合案例:日志分析器
  • 3.5 新手陷阱 Top 5
  • 3.6 综合思考题

# 3.1 数组操作

# 3.1.1 索引数组——定义/访问/遍历

Bash 支持一维索引数组——索引从 0 开始,用 () 定义、空格分隔:

#!/bin/bash

# ===== 定义方式 =====
# 方式 1:直接赋值
fruits=("苹果" "香蕉" "橘子" "葡萄")
nums=(1 2 3 4 5)
mixed=("hello" 42 "world")              # 可以混类型(但不推荐)

# 方式 2:逐个赋值
fruits[4]="西瓜"                        # 追加到索引 4

# 方式 3:从命令输出构造
files=($(ls *.txt))                      # 把 ls 输出的每行作为数组元素

# ===== 访问元素 =====
echo "${fruits[0]}"                      # 苹果
echo "${fruits[1]}"                      # 香蕉
echo "${fruits[-1]}"                     # 西瓜(最后一个——bash 4.2+)

# ===== 遍历数组 =====
echo "--- 遍历全部元素 ---"
echo "${fruits[@]}"                      # 苹果 香蕉 橘子 葡萄 西瓜
echo "${fruits[*]}"                      # 苹果 香蕉 橘子 葡萄 西瓜

echo "--- for 循环遍历 ---"
for fruit in "${fruits[@]}"; do          # 安全方式——保留元素内空格
    echo "  水果:$fruit"
done

echo "--- 带索引遍历 ---"
for i in "${!fruits[@]}"; do             # ${!array[@]} 展开为所有索引
    echo "  [$i] = ${fruits[$i]}"
done

# ===== 添加到末尾 =====
fruits+=("芒果" "草莓")                  # 追加多个元素
echo "${fruits[@]}"                      # 苹果 香蕉 橘子 葡萄 西瓜 芒果 草莓

🔑 ${array[@]} vs ${array[*]}——引号下的关键差异:

#!/bin/bash
items=("a b" "c" "d e")

echo "=== @ ==="
for item in "${items[@]}"; do            # 每个元素独立——正确
    echo "  [$item]"
done
# 输出:
#   [a b]
#   [c]
#   [d e]

echo "=== * ==="
for item in "${items[*]}"; do            # 所有元素合并成一个字符串
    echo "  [$item]"
done
# 输出:
#   [a b c d e]           ← 全拼在一起了!

📌 铁律:遍历数组永远用 "${array[@]}"(双引号 + @)。

# 3.1.2 关联数组——Shell 的"字典"

Bash 4.0+ 支持关联数组——类似 Python 的 dict,需要 declare -A 声明:

#!/bin/bash

# ===== 声明与赋值 =====
declare -A student                     # 必须先 declare -A!
student["name"]="张三"
student["age"]="25"
student["city"]="深圳"

# 一次性赋值
declare -A config=(
    ["host"]="localhost"
    ["port"]="8080"
    ["user"]="admin"
)

# ===== 访问 =====
echo "姓名:${student[name]}"          # 张三(key 可以加引号也可以不加)
echo "年龄:${student[age]}"           # 25

# ===== 遍历键值对 =====
for key in "${!student[@]}"; do        # ${!array[@]} → 所有键
    echo "  $key = ${student[$key]}"
done
# 输出:
#   name = 张三
#   age = 25
#   city = 深圳

# ===== 判断 key 是否存在 =====
if [[ -v "student[phone]" ]]; then     # -v 检查变量/键是否存在(bash 4.2+)
    echo "有手机号"
else
    echo "没有手机号"
fi

# ===== 删除某个键 =====
unset "student[age]"
echo "删除后:${!student[@]}"          # name city

🔑 关联数组实战:统计单词频率:

#!/bin/bash

text="the quick brown fox jumps over the lazy dog the fox"

declare -A freq
for word in $text; do
    ((freq[$word]++))                  # 自动计数——第一次访问时值=0,++后=1
done

for word in "${!freq[@]}"; do
    echo "  $word → ${freq[$word]}"
done
# 输出:
#   over → 1
#   quick → 1
#   the → 3
#   ...

# 3.1.3 数组长度、切片与 CRUD

#!/bin/bash

fruits=("苹果" "香蕉" "橘子" "葡萄" "西瓜" "芒果" "草莓")

# ===== 数组长度 =====
echo "元素个数:${#fruits[@]}"          # 7
echo "索引个数:${#fruits[*]}"          # 7
echo "第一个元素长度:${#fruits[0]}"    # 2("苹果"占 2 个字符)

# ===== 切片 =====
echo "${fruits[@]:1:3}"                # 香蕉 橘子 葡萄(索引1开始,取3个)
echo "${fruits[@]:3}"                  # 葡萄 西瓜 芒果 草莓(索引3到最后)
echo "${fruits[*]:2:2}"                # 橘子 葡萄

# ===== 替换元素 =====
fruits[1]="火龙果"                     # 替换索引 1
echo "${fruits[@]}"                    # 苹果 火龙果 橘子 葡萄 西瓜 芒果 草莓

# ===== 删除元素 =====
unset fruits[4]                        # 删除索引 4(西瓜)
echo "${fruits[@]}"                    # 苹果 火龙果 橘子 葡萄 芒果 草莓
echo "索引列表:${!fruits[@]}"         # 0 1 2 3 5 6 ← 索引不连续了

# ===== 清空数组 =====
fruits=()                              # 全部清空
echo "清空后:${#fruits[@]}"           # 0

# ===== 合并数组 =====
a=(1 2 3)
b=(4 5 6)
combined=("${a[@]}" "${b[@]}")
echo "${combined[@]}"                  # 1 2 3 4 5 6

🔑 数组 CRUD 速查表:

操作 语法 说明
定义 arr=(a b c) 小括号 + 空格分隔
访问 "${arr[i]}" 索引从 0 开始
遍历 for e in "${arr[@]}" 安全遍历
长度 "${#arr[@]}" 元素个数
追加 arr+=("x") 自动追加到末尾
切片 "${arr[@]:i:n}" 从索引 i 取 n 个
删除 unset arr[i] 删除后索引不重排
清空 arr=() 全部清空
拷贝 copy=("${arr[@]}") 复制数组

# 3.2 字符串处理

# 3.2.1 长度/截取/替换/删除

Shell 的 ${} 提供了丰富的字符串内建操作——不需要调用外部命令:

#!/bin/bash

str="Hello, World! Welcome to Shell."

# ===== 1. 字符串长度 =====
echo "长度:${#str}"                    # 30
echo "长度:$(echo -n "$str" | wc -c)"  # 30(外部命令方式——慢)

# ===== 2. 子串截取 =====
echo "${str:0:5}"                       # Hello(从 0 取 5 个字符)
echo "${str:7:5}"                       # World(从 7 取 5 个)
echo "${str:7}"                         # World! Welcome to Shell.(从 7 到结尾)
echo "${str:(-6)}"                      # Shell.(从末尾往前 6 个)
echo "${str:(-6):3}"                    # She(从末尾往前 6 个取 3 个)

# ===== 3. 字符串替换 =====
# 替换第一个匹配
echo "${str/World/宇宙}"                # Hello, 宇宙! Welcome to Shell.

# 替换所有匹配
text="foo foo foo bar"
echo "${text//foo/bar}"                 # bar bar bar bar

# 行首替换
echo "${str/#Hello/Hi}"                 # Hi, World! Welcome to Shell.
echo "${str/#Hi/Hi}"                    # 不匹配——原样输出

# 行尾替换
echo "${str/%Shell/Bash}"               # Hello, World! Welcome to Bash.
echo "${str/%Bash/Bash}"                # 不匹配——原样输出

# ===== 4. 字符串删除(模式匹配)=====
file="backup_2025-06-07.tar.gz"

# 从开头删除最短匹配(#)
echo "${file#backup_}"                  # 2025-06-07.tar.gz
echo "${file#*/}"                       # 如果文件有路径...不匹配,原样

# 从开头删除最长匹配(##)
echo "${file##*_}"                      # 2025-06-07.tar.gz(和上一个一样——因为最短/最长一致)
url="https://example.com/path/file.txt"
echo "${url##*/}"                       # file.txt(删除最后一个 / 前的所有)
echo "${url#*/}"                        # /example.com/path/file.txt(删除第一个 / 前的所有)

# 从末尾删除最短匹配(%)
echo "${file%.tar.gz}"                  # backup_2025-06-07
echo "${file%.*}"                       # backup_2025-06-07.tar(最短匹配——删掉 .gz)

# 从末尾删除最长匹配(%%)
echo "${file%%.*}"                      # backup_2025-06-07(最长匹配——删掉 .tar.gz)

🔑 #/% 模式删除速查表:

语法 含义 示例:"abc.def.ghi"
${var#pattern} 行首最短删除 ${var#*.} → def.ghi
${var##pattern} 行首最长删除 ${var##*.} → ghi
${var%pattern} 行尾最短删除 ${var%.*} → abc.def
${var%%pattern} 行尾最长删除 ${var%%.*} → abc
# ===== 实战:批量修改文件扩展名 =====
for file in *.jpeg; do
    mv "$file" "${file%.jpeg}.jpg"       # .jpeg → .jpg
done

# 实战:提取文件名和路径
fullpath="/home/user/data/file.txt"
filename="${fullpath##*/}"               # file.txt
dir="${fullpath%/*}"                     # /home/user/data
basename="${filename%.*}"                # file
ext="${filename##*.}"                    # txt

echo "目录:$dir"
echo "文件名:$filename"
echo "不含扩展名:$basename"
echo "扩展名:$ext"

# 3.2.2 大小写转换与模式匹配

#!/bin/bash

# ===== 大小写转换(bash 4.0+)=====
msg="Hello World"

echo "${msg,,}"                          # hello world(全小写)
echo "${msg^^}"                          # HELLO WORLD(全大写)
echo "${msg,}"                           # hello World(首字母小写)
echo "${msg^}"                           # Hello World(首字母大写)

# ===== 模式匹配——[[ ]] 的 == 和 =~ =====
phone="13812345678"
email="user@example.com"
ip="192.168.1.1"

# 通配符匹配
if [[ "$phone" == 138* ]]; then
    echo "是联通号码"
fi

# 正则匹配
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "有效邮箱"
fi

if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
    echo "有效的 IP 地址"
fi

# ===== 检查包含关系 =====
if [[ "$msg" == *"World"* ]]; then
    echo "包含 World"
fi

# ===== 多条件模式匹配 =====
user_input="yes"
case "${user_input,,}" in                # 转小写后再匹配
    y | yes | yep | yeah)
        echo "确认"
        ;;
    n | no | nope)
        echo "取消"
        ;;
    *)
        echo "输入有误"
        ;;
esac

# 3.2.3 Here Document 与 Here String

Here Document(<<)——在脚本里嵌入多行文本:

#!/bin/bash

# ===== 基础用法 =====
cat <<EOF
这是第一行
这是第二行
当前用户是 $USER
EOF

# ===== 阻止变量展开——定界符加引号 =====
cat <<'EOF'                               # EOF 加引号——不展开变量!
变量不会被展开:$USER                    # 输出:$USER(原样)
$(date)                                   # 输出:$(date)(原样)
EOF

# ===== <<- 自动忽略行首 Tab =====
if true; then
    cat <<-EOF
		这一行的 Tab 缩进会被忽略
		包括这一行也是
	EOF
fi
# 输出(无缩进):
# 这一行的 Tab 缩进会被忽略
# 包括这一行也是

# ===== 实战:生成配置文件 =====
port=8080
db_name="myapp"
cat > config.yaml <<EOF
server:
  port: $port
database:
  name: $db_name
  host: localhost
EOF

# ===== 实战:生成 HTML 报告 =====
title="系统巡检报告"
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
cat > report.html <<HTML_END
<!DOCTYPE html>
<html>
<head><title>$title</title></head>
<body>
  <h1>$title</h1>
  <p>生成时间:$timestamp</p>
  <p>主机:$(hostname)</p>
</body>
</html>
HTML_END

# ===== 输出到变量(而不是文件)=====
html_content=$(cat <<EOF
<ul>
  <li>$USER</li>
  <li>$(hostname)</li>
</ul>
EOF
)
echo "$html_content"

Here String(<<<)——把字符串当作 stdin 输入:

#!/bin/bash

# ===== 基础 =====
# 不用 echo + 管道
echo "hello world" | wc -c               # 12(包括换行符)

# 用 Here String——更简洁
wc -c <<< "hello world"                  # 12(同样包括换行符)

# ===== 配合 read 使用 =====
read -r first rest <<< "apple banana orange"
echo "first=$first, rest=$rest"          # first=apple, rest=banana orange

# ===== 配合 grep/sed/awk =====
grep -o '[0-9]\+' <<< "abc123def456"     # 123\n456
sed 's/foo/bar/g' <<< "foo foo foo"      # bar bar bar
awk '{print $2, $1}' <<< "name=张三 age=25"

# ===== 字符串分割到数组 =====
csv="apple,banana,orange"
IFS=',' read -ra items <<< "$csv"        # -r 保留反斜杠,-a 读入数组
echo "items[0]=${items[0]}"              # apple
echo "items[1]=${items[1]}"              # banana
echo "items[2]=${items[2]}"              # orange

# 3.2.4 printf 格式化输出

echo 够用但不够灵活——printf 是 Shell 的格式化神器(和 C 语言的 printf 语法一致):

#!/bin/bash

# ===== 基础——%s 字符串 / %d 整数 / %f 浮点 =====
printf "姓名:%s,年龄:%d\n" "张三" 25
# 姓名:张三,年龄:25

# ===== 宽度与对齐 =====
printf "|%-10s|%10s|\n" "左对齐" "右对齐"
# |左对齐      |     右对齐|

# ===== 浮点数精度 =====
printf "圆周率:%.2f\n" 3.14159          # 圆周率:3.14
printf "百分比:%.1f%%\n" 85.678         # 百分比:85.7%

# ===== 多个参数重复使用格式 =====
printf "%s\n" 苹果 香蕉 橘子              # 每个一行
# 苹果
# 香蕉
# 橘子

# ===== 实战:表格输出 =====
header="%-10s %-8s %-10s\n"
row="%-10s %-8s %-10s\n"
printf "$header" "姓名" "年龄" "城市"
printf "$header" "------" "----" "--------"
printf "$row" "张三" "25" "深圳"
printf "$row" "李四" "30" "北京"
printf "$row" "王五" "28" "上海"
# 姓名        年龄      城市
# ------      ----      --------
# 张三        25        深圳
# 李四        30        北京
# 王五        28        上海

# ===== 实战:对齐数字 =====
for i in 1 12 123 1234; do
    printf "序号:%4d\n" "$i"             # 右对齐,占 4 位
done
# 序号:   1
# 序号:  12
# 序号: 123
# 序号:1234

🔑 echo vs printf 对比:

特性 echo printf
换行 默认加(-n 取消) 需要 \n
格式化 ❌ 不支持 ✅ %s %d %f 等
对齐 ❌ ✅ 宽度 + 左右对齐
转义 -e 才能启用 原生支持 \n \t
跨平台 macOS/Linux 行为不同 ✅ 行为一致

📌 打印变量用 echo,格式化输出/表格用 printf。

# 3.3 重定向与管道

# 3.3.1 stdin/stdout/stderr——三剑客

每个 Linux 进程启动时都有三个标准文件描述符——它们是 Shell 重定向的基石:

                     ┌──────────────┐
    stdin (0) ←──────│              │
    (键盘输入)      │    进程      │──────→ stdout (1) ——正常输出(屏幕)
                     │              │──────→ stderr (2) ——错误输出(屏幕)
                     └──────────────┘
# 查看标准输入/输出/错误对应的设备
ls -l /dev/stdin    # lrwxrwxrwx  ...  /dev/stdin -> /proc/self/fd/0
ls -l /dev/stdout   # lrwxrwxrwx  ...  /dev/stdout -> /proc/self/fd/1
ls -l /dev/stderr   # lrwxrwxrwx  ...  /dev/stderr -> /proc/self/fd/2

🔑 三者用途:

描述符 编号 默认去向 用途
stdin 0 键盘 读取输入
stdout 1 屏幕 正常输出
stderr 2 屏幕 错误/诊断信息(与正常输出分离)

为什么 stderr 和 stdout 要分开?

#!/bin/bash

# 假设这个脚本正常输出和处理结果,错误输出诊断信息
echo "文件处理完成"                      # stdout → 结果报告
echo "处理了 100 条记录" >> /var/log/app.log  # 追加到日志
ls /nonexistent >/dev/null               # stderr → 错误信息(默认显示到屏幕)

# 分开的意义:
# ① 用户可以在屏幕上看到错误,同时把结果重定向到文件
# ② 管道只传递 stdout——stderr 不会被意外塞进下游
# ③ CI/CD 中可以根据 stderr 是否为空判断构建状态

# 3.3.2 > / >> / < / << 四大家族

#!/bin/bash

# ===== > —— 覆盖写入(截断后写入)=====
echo "第一行" > /tmp/test.txt            # 写入/覆盖
# 文件内容:第一行

# ===== >> —— 追加写入 =====
echo "第二行" >> /tmp/test.txt           # 追加
# 文件内容:
# 第一行
# 第二行

# ===== < —— 从文件读取 =====
cat < /tmp/test.txt                      # 把文件内容作为 cat 的输入

# ===== 2> —— 重定向 stderr =====
ls /nonexistent 2> /dev/null             # 错误信息进黑洞——屏幕干净了

# ===== 2>&1 —— 合并 stderr 到 stdout =====
cmd="ls /existing /nonexistent"
$cmd > /tmp/output.txt 2>&1              # stdout 和 stderr 都写入同一个文件

# ===== 现代写法(更清晰)=====
$cmd > /tmp/output.txt 2>&1              # 传统写法
$cmd &> /tmp/output.txt                  # bash 4.0+ 简写——等价

# ===== 追加 stderr 到 stdout =====
$cmd >> /tmp/output.txt 2>&1             # 追加模式
$cmd &>> /tmp/output.txt                 # bash 4.0+ 简写

# ===== 丢弃所有输出 =====
$cmd > /dev/null 2>&1
$cmd &> /dev/null                         # 等价简写

# ===== 只保留 stderr,丢弃 stdout =====
$cmd > /dev/null                         # stdout → 黑洞,stderr → 屏幕

# ===== 管道只传 stdout,忽略 stderr =====
$cmd 2>&1 | grep "error"                 # 如果 stderr 里也有 error,先合并再 grep

🔑 重定向语法速查表:

语法 含义 示例
> stdout 覆盖写入 echo hi > file
>> stdout 追加写入 echo hi >> file
< 从文件读取 stdin cat < file
2> stderr 覆盖写入 cmd 2> err.log
2>> stderr 追加写入 cmd 2>> err.log
2>&1 stderr 合并到 stdout cmd > all.log 2>&1
&> stdout+stderr 覆盖 cmd &> all.log
&>> stdout+stderr 追加 cmd &>> all.log
# ===== 实战:正确分离正常输出和错误 =====
script_runner() {
    echo "开始处理..."                  # stdout
    some_command_that_might_fail
    if [[ $? -ne 0 ]]; then
        echo "处理失败!" >&2           # 显式写到 stderr
        return 1
    fi
    echo "处理完成"                     # stdout
}

# 调用——用户可以这样:
script_runner > result.log 2> error.log
# result.log:开始处理...\n处理完成
# error.log:处理失败!(如果有的话)

# 3.3.3 管道 | ——命令协作的艺术

管道(|)把前一个命令的 stdout 连接到后一个命令的 stdin——

  cmd1  │  cmd2  │  cmd3
stdin → │ stdin → │ stdin → 最终输出
#!/bin/bash

# ===== 入门三件套 =====
grep "ERROR" /var/log/app.log | wc -l    # 统计错误行数
cat /etc/passwd | cut -d: -f1 | sort     # 排序所有用户名
ps aux | grep nginx | awk '{print $2}'   # 获取 nginx 进程 PID

# ===== 长管道 =====
# 找出访问量 Top 10 的 IP
cat access.log \
    | awk '{print $1}' \
    | sort \
    | uniq -c \
    | sort -rn \
    | head -10

# ===== 管道 + xargs ——把前一个的输出作为后一个的参数 =====
# find + xargs(比 find -exec 更高效——批量传参)
find /var/log -name "*.log" -mtime +7 | xargs rm -f     # 删除7天前的日志

# xargs 的 -n 控制每次传几个参数
echo "a b c d e f" | xargs -n 3 echo     # 每次 echo 3 个
# a b c
# d e f

# xargs 的 -P 并行执行
find . -name "*.jpg" -print0 | xargs -0 -P 4 -I {} convert {} {}.png
# -print0 / -0:空字符分割(应对文件名含空格/换行)
# -P 4:4 个进程并行
# -I {}:用 {} 占位符

# ===== 管道 + tee:同时输出到屏幕和文件 =====
echo "启动服务..." | tee -a /var/log/setup.log

# ===== 管道 + 子 Shell:在子 Shell 中处理 =====
# 统计当前目录的文件大小分布
find . -type f -size +10M | while read -r f; do
    echo "大文件:$f"
done

⚠️ 管道陷阱——set -o pipefail:

set -e                                   # 遇到错误就退出

# ❌ 管道中只有最后的命令决定退出码
cat nonexistent.txt | wc -l              # cat 失败了,但 wc -l 成功(输出 0)
echo "还会继续执行——危险!"               # ← set -e 没拦截

# ✅ 加 pipefail——管道中任何命令失败都算失败
set -eo pipefail
cat nonexistent.txt | wc -l              # 立即退出
echo "不会执行到这里"

# 3.3.4 tee 分流与 /dev/null 黑洞

tee——T 型分流器,数据一份给屏幕、一份给文件:

#!/bin/bash

# ===== 基础:同时输出到屏幕和文件 =====
echo "Hello, World!" | tee output.txt
# 屏幕显示:Hello, World!
# 文件 output.txt:Hello, World!

# ===== -a 追加模式 =====
echo "第一行" | tee output.txt           # 覆盖
echo "第二行" | tee -a output.txt        # 追加

# ===== 写入多个文件 =====
echo "重要日志" | tee log1.txt log2.txt log3.txt

# ===== 结合 sudo——tee 解决权限问题 =====
# echo 后面跟着 > 重定向——但当前用户没有写权限时:
echo "nameserver 8.8.8.8" | sudo tee -a /etc/resolv.conf

# ===== 保留 stderr =====
cmd 2>&1 | tee output.txt               # 标准输出和错误都进文件和屏幕

/dev/null——Linux 的"黑洞":

#!/bin/bash

# ===== 丢弃不需要的输出 =====
find / -name "*.log" 2>/dev/null          # 只显示找得到的,忽略权限错误

# ===== 丢弃所有输出 =====
some_noisy_command > /dev/null 2>&1       # 静默执行

# ===== 测试命令是否成功(只看退出码,不看输出)=====
if grep -q "pattern" /var/log/app.log 2>/dev/null; then
    echo "找到匹配"
fi

# ===== 创建空文件(清空文件内容)=====
> /tmp/empty_file.txt                     # 等价于 truncate -s 0

# ===== 验证 /dev/null 的特性 =====
cat /dev/null | wc -c                     # 0——永远是空的
dd if=/dev/zero of=/dev/null bs=1M count=1000  # 写 1GB 到黑洞——瞬间完成

# 3.3.5 文件描述符——深入理解 0/1/2

除了标准三剑客(0/1/2),Shell 还允许你操作额外的文件描述符:

#!/bin/bash

# ===== 在脚本内重定向自己的 stdin/stdout/stderr =====
exec 3> /tmp/debug.log                   # 打开文件描述符 3 用于写入
echo "调试信息" >&3                      # 写入 fd 3 → debug.log
exec 3>&-                                # 关闭 fd 3

# ===== exec 全局重定向 =====
# 脚本内所有 stdout 都写入文件
exec > /tmp/build.log 2>&1               # 自此以后所有输出都去文件
echo "这行不会出现在屏幕"                 # → /tmp/build.log
ls /nonexistent                          # 错误也去文件
echo "构建结束"

# ===== 交换文件描述符 =====
# 临时保存 stdout,重定向到 stderr
exec 3>&1                                # 把当前 stdout 存到 fd 3
exec 1>&2                                # stdout → stderr(都去错误输出)
echo "这行是错误输出"                     # 这行现在去 stderr
exec 1>&3                                # 恢复 stdout
echo "这行恢复正常"                       # 回到正常 stdout
exec 3>&-                                # 关闭临时 fd 3

# ===== 实战:在脚本内同时输出到屏幕和文件 =====
exec 3>&1                                # 保存 stdout 到 fd 3
exec > >(tee -a /var/log/setup.log)      # stdout → tee → 同时去屏幕+文件
echo "安装依赖..."
echo "启动服务..."
exec 1>&3                                # 恢复 stdout
exec 3>&-                                # 关闭 fd 3
echo "脚本结束"

🔑 文件描述符操作速查:

语法 含义
exec N> file 打开 fd N 写入文件
exec N< file 打开 fd N 读取文件
exec N>&M fd N 复制 fd M 的写入端
exec N<&M fd N 复制 fd M 的读取端
exec N>&- 关闭 fd N(写入)
exec N<&- 关闭 fd N(读取)
# ===== 高级实战:读取文件同时保留 stdin =====
# 场景:脚本想从一个文件读取配置,同时还要接受用户输入
exec 3< /etc/config.cfg                  # 用 fd 3 打开配置文件
read -r line <&3                         # 从 fd 3 读配置
exec 3<&-                                # 关闭

read -r -p "请输入操作:" user_input      # stdin(键盘)没受影响
echo "配置行:$line"
echo "用户输入:$user_input"

# 3.4 综合案例:日志分析器

把本章全部知识——数组 + 关联数组 + 字符串处理 + 重定向 + 管道——串联成一个生产级日志分析脚本:

#!/bin/bash
# log_analyzer.sh —— 日志分析器
# 用法:./log_analyzer.sh [日志文件] [选项]
# 示例:./log_analyzer.sh /var/log/nginx/access.log --top-ip --top-url

set -euo pipefail

# ===== 1. 配置 =====
LOG_FILE="${1:-/var/log/nginx/access.log}"
shift || true

# 选项标志
TOP_IP=false
TOP_URL=false
SHOW_ERRORS=false
OUTPUT_FILE=""

# ===== 2. 函数定义 =====

# 彩色输出
RED='\033[0;31m'; GREEN='\033[0;32m'
YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'

log_info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }

usage() {
    cat <<EOF
用法:$0 <日志文件> [选项]

选项:
  --top-ip      显示访问量 Top 10 的 IP
  --top-url     显示访问量 Top 10 的 URL
  --errors      显示 4xx/5xx 错误统计
  --output FILE 将报告输出到文件
  -h, --help    显示帮助
EOF
    exit 0
}

# ===== 3. 参数解析 =====
while [[ $# -gt 0 ]]; do
    case "$1" in
        --top-ip)    TOP_IP=true;  shift ;;
        --top-url)   TOP_URL=true; shift ;;
        --errors)    SHOW_ERRORS=true; shift ;;
        --output)    OUTPUT_FILE="$2"; shift 2 ;;
        -h|--help)   usage ;;
        *)           log_warn "忽略未知选项:$1"; shift ;;
    esac
done

# ===== 4. 检查文件 =====
if [[ ! -f "$LOG_FILE" ]]; then
    log_error "日志文件不存在:$LOG_FILE"
    exit 1
fi
if [[ ! -r "$LOG_FILE" ]]; then
    log_error "日志文件不可读:$LOG_FILE"
    exit 1
fi

# ===== 5. 主分析逻辑 =====

# 5.1 基本信息
total_lines=$(wc -l < "$LOG_FILE")
file_size=$(du -h "$LOG_FILE" | cut -f1)
log_info "分析文件:$LOG_FILE(${file_size},${total_lines} 行)"

# 生成报告的临时目录
REPORT_DIR=$(mktemp -d /tmp/log_analyzer.XXXXXX)
trap 'rm -rf "$REPORT_DIR"' EXIT          # 脚本退出时自动清理

# 5.2 Top IP 分析
if $TOP_IP; then
    log_info "正在统计 Top IP..."

    # 提取 IP -> 排序 -> 去重计数 -> 按数量排序 -> Top 10
    awk '{print $1}' "$LOG_FILE" \
        | sort \
        | uniq -c \
        | sort -rn \
        | head -10 \
        > "$REPORT_DIR/top_ip.txt"

    echo ""
    echo "━━━ Top 10 访问 IP ━━━━━━━━━━━━━━━━━━━"
    printf "%-6s %-18s %s\n" "排名" "IP 地址" "访问次数"
    printf "%-6s %-18s %s\n" "────" "────────────────" "──────"
    rank=1
    while read -r count ip; do
        printf "%-6d %-18s %s\n" "$rank" "$ip" "$count"
        ((rank++))
    done < "$REPORT_DIR/top_ip.txt"
fi

# 5.3 Top URL 分析
if $TOP_URL; then
    log_info "正在统计 Top URL..."

    # 假设 Nginx 日志格式:$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
    # $request 在第 7 列(用引号括着),我们用 awk 提取第 7 列作为 URL
    awk '{print $7}' "$LOG_FILE" \
        | sort \
        | uniq -c \
        | sort -rn \
        | head -10 \
        > "$REPORT_DIR/top_url.txt"

    echo ""
    echo "━━━ Top 10 访问 URL ━━━━━━━━━━━━━━━━━━━"
    printf "%-6s %-50s %s\n" "排名" "URL" "访问次数"
    printf "%-6s %-50s %s\n" "────" "─────────────────────────────────────" "──────"
    rank=1
    while read -r count url; do
        # URL 可能很长——截断显示
        display_url="${url:0:50}"
        printf "%-6d %-50s %s\n" "$rank" "$display_url" "$count"
        ((rank++))
    done < "$REPORT_DIR/top_url.txt"
fi

# 5.4 错误分析
if $SHOW_ERRORS; then
    log_info "正在统计 HTTP 错误..."

    # 统计 4xx 和 5xx 状态码
    declare -A error_codes
    while IFS= read -r line; do
        # 提取状态码(Nginx 第 9 列)
        status_code=$(echo "$line" | awk '{print $9}')
        if [[ "$status_code" =~ ^[45][0-9]{2}$ ]]; then
            ((error_codes["$status_code"]++))
        fi
    done < "$LOG_FILE"

    echo ""
    echo "━━━ HTTP 错误统计 ━━━━━━━━━━━━━━━━━━━━━"
    if [[ ${#error_codes[@]} -eq 0 ]]; then
        echo "  没有发现 4xx/5xx 错误 ✅"
    else
        printf "%-8s %s\n" "状态码" "次数"
        printf "%-8s %s\n" "──────" "────"
        # 按错误码排序输出
        for code in "${!error_codes[@]}"; do
            printf "%-8s %d\n" "$code" "${error_codes[$code]}"
        done | sort -k2 -rn

        # 总错误数
        total_errors=0
        for count in "${error_codes[@]}"; do
            ((total_errors += count))
        done
        echo ""
        echo "总错误数:$total_errors"
        error_rate=$(echo "scale=2; $total_errors * 100 / $total_lines" | bc)
        echo "错误率:${error_rate}%"
    fi
fi

# ===== 6. 生成报告文件 =====
if [[ -n "$OUTPUT_FILE" ]]; then
    {
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        echo "  日志分析报告"
        echo "  文件:$LOG_FILE"
        echo "  时间:$(date '+%Y-%m-%d %H:%M:%S')"
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        echo "总行数:$total_lines | 文件大小:$file_size"
        echo ""

        if [[ -f "$REPORT_DIR/top_ip.txt" ]]; then
            echo "━━ Top IP ━━━━━━━━━━━━━━━━━━━━━━━━━━"
            cat "$REPORT_DIR/top_ip.txt" | while read -r count ip; do
                printf "  %-18s → %s\n" "$ip" "$count"
            done
        fi

        if [[ -f "$REPORT_DIR/top_url.txt" ]]; then
            echo "━━ Top URL ━━━━━━━━━━━━━━━━━━━━━━━━━"
            cat "$REPORT_DIR/top_url.txt" | while read -r count url; do
                printf "  %-50s → %s\n" "$url" "$count"
            done
        fi
    } > "$OUTPUT_FILE"
    log_info "报告已保存至:$OUTPUT_FILE"
fi

echo ""
log_info "分析完成"

案例知识融合:这个脚本覆盖了本章全部核心技术——索引数组(error_codes 关联数组)、字符串处理(${url:0:50} 截断、${1:-default} 默认值、set -euo pipefail)、Here Document(usage 函数的帮助文本、trap 清理函数)、重定向与管道(> "$OUTPUT_FILE" 写入报告、>> 追加、>&2 错误输出、2>/dev/null 静默、trap 清理、最后生成报告的 { ... } > "$OUTPUT_FILE" 多行重定向、mktemp 创建临时目录);同时使用了 set -euo pipefail 安全执行、函数封装与 local 局部变量、数组操作(关联数组统计错误码)、管道链(awk | sort | uniq -c | sort -rn | head 数据分析流水线)。

# 3.5 新手陷阱 Top 5

# 陷阱 说明
1 if 里把 [ ] 和 [[ ]] 混用 [ "$a" > "$b" ] 的 > 被当作重定向!创建了一个叫 $b] 的文件
2 数组遍历忘加引号 for f in ${files[@]}——如果元素包含空格会被拆散。永远用 "${files[@]}"
3 管道让 while 变量丢失 ls \| while read f; do count=$((count+1)); done——count 在子 Shell 里
4 2>&1 的位置错误 cmd 2>&1 > file——先合并到当前 stdout(屏幕),然后重定向 stdout 到文件
5 $* 和 $@ 混淆 "$*" 把所有参数合成一个字符串——如果用 for arg in "$*" 只会循环一次

陷阱 1 详解——> 被当作重定向符:

# ❌ 灾难
if [ "$a" > "$b" ]; then                # > 被当作输出重定向!
    echo "大于"
fi
# 这行代码:
# 1. 创建了一个名为 "$b" 的空文件
# 2. 把空字符串写入该文件
# 3. [ 命令只收到一个参数 "$a"——永远为 true!

# ✅ 正确
if [[ "$a" > "$b" ]]; then               # [[ 内 > 是字符串比较
    echo "大于"
fi
if [[ "$a" -gt "$b" ]]; then             # 数值比较——更安全
    echo "大于"
fi
if (( a > b )); then                     # 或算术比较
    echo "大于"
fi

陷阱 3 详解——修复管道子 Shell 问题:

# ❌ 管道——子 Shell 变量丢失
count=0
cat /etc/hosts | while read -r line; do
    ((count++))
done
echo "$count"                            # 0——count 在子 Shell 里修改,外面不知道!

# ✅ 方案一:输入重定向
count=0
while read -r line; do
    ((count++))
done < /etc/hosts
echo "$count"                            # 正确

# ✅ 方案二:进程替换
count=0
while read -r line; do
    ((count++))
done < <(cat /etc/hosts)
echo "$count"                            # 正确

陷阱 4 详解——正确的重定向顺序:

# ❌ 错误顺序——看似合并了,实际没合并
cmd 2>&1 > file
# 1. 2>&1:stderr 指向当前的 stdout(屏幕)
# 2. > file:stdout 改为指向 file
# 结果:stdout → file,stderr → 屏幕(没有合并!)

# ✅ 正确顺序
cmd > file 2>&1
# 1. > file:stdout 指向 file
# 2. 2>&1:stderr 指向当前 stdout(即 file)
# 结果:stdout + stderr → file(正确合并)

# ✅ 现代简写
cmd &> file                              # 更清晰——推荐

# 3.6 综合思考题

  1. ${arr[@]} vs ${arr[*]} 的本质差异:两个写法在双引号中行为完全不同——"${arr[@]}" 保留元素边界,"${arr[*]}" 把 IFS 的第一个字符作为连字符合并。这个设计起源于 1970 年代 Unix 的命令行参数传递方式——为什么 @ 分开、* 合并?如果你是 Shell 设计者,会设计第三种写法吗?

  2. # % 模式删除的性能与可读性:Shell 内置的 ${var#pattern} 比 sed 's/^pattern//' 快约 100 倍(不需要 fork 子进程)。但代价是什么?对于超过 100MB 的大字符串,Shell 内置操作的性能如何?什么时候应该放弃 Shell 内置操作,转而用 awk/sed 甚至 Python?

  3. 文件描述符 >&2 的设计哲学:为什么 Shell 选择 >&2 而不是 2&> 或 2> 这样更直观的语法?2>&1 中间的 & 到底代表什么?(提示:>& 的语法源自 > file vs >& fd 的区分——& 表示"后面是文件描述符而不是文件名")。这个设计在哪种场景下会救命?

  4. Here Document 的 heredoc EOF vs file:cat <<EOF > file 和 cat > file <<EOF 在功能上等价——但它们背后的执行顺序完全不同(先重定向再输出 vs. 先构造 heredoc 再重定向)。在什么极端情况下这两种写法会产生不同的结果?(提示:考虑文件已存在的情况,以及 set -o noclobber 的影响)

  5. 管道 vs 临时文件的性能取舍:cmd1 | cmd2 是流式处理——cmd1 一产生输出,cmd2 就能开始消费。而 cmd1 > /tmp/tmp; cmd2 < /tmp/tmp 需要等 cmd1 完全结束后 cmd2 才开始。在什么情况下管道的流式特性反而会成为问题?(提示:考虑一个 cmd1 产生了 100GB 输出、cmd2 需要随机访问的场景——以及如何使用 sort 命令的 -T 选项找到出路)

#Shell#基础
上次更新: 2026/06/28, 17:55:19
流程控制与函数
grep 搜索实战

← 流程控制与函数 grep 搜索实战→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式