Shell 输入输出重定向详解

Shell输入输出重定向简介

输入输出重定向是Shell脚本中非常重要的功能,它允许我们控制命令的输入来源和输出目标。通过重定向,我们可以将命令的输出保存到文件、从文件读取输入、连接多个命令等。

输入重定向

将文件内容作为命令的输入,而不是从键盘读取。

输出重定向

将命令的输出保存到文件,而不是显示在终端。

错误重定向

将命令的错误信息重定向到文件或其他位置。

管道

将一个命令的输出作为另一个命令的输入。

Here文档

在脚本中嵌入多行文本作为命令的输入。

文件描述符

管理标准输入、输出和错误流的高级操作。

标准文件描述符

在Unix/Linux系统中,每个进程都有三个标准的文件描述符: 0 - stdin(标准输入)、 1 - stdout(标准输出)、 2 - stderr(标准错误)。 重定向操作就是对这些文件描述符的操作。

基本输入输出重定向

学习最基本的输入输出重定向操作。

重定向操作流程
0
标准输入
(stdin)
CMD
命令
1
标准输出
(stdout)
2
标准错误
(stderr)
操作符 说明 示例 效果
> 输出重定向(覆盖) command > file 将stdout输出到文件,覆盖原有内容
>> 输出重定向(追加) command >> file 将stdout输出到文件,追加到文件末尾
< 输入重定向 command < file 从文件读取stdin
2> 错误重定向(覆盖) command 2> file 将stderr输出到文件,覆盖原有内容
2>> 错误重定向(追加) command 2>> file 将stderr输出到文件,追加到文件末尾
&> 输出和错误重定向 command &> file 将stdout和stderr都输出到文件
&>> 输出和错误重定向(追加) command &>> file 将stdout和stderr都追加到文件

基本重定向使用示例

basic_redirection.sh
#!/bin/bash

# 基本重定向示例

echo "=== 输出重定向 ==="

# 将命令输出重定向到文件(覆盖)
echo "Hello, World!" > output.txt
echo "文件内容:"
cat output.txt

# 将命令输出重定向到文件(追加)
echo "第二行内容" >> output.txt
echo "追加后的文件内容:"
cat output.txt

# 将命令输出和错误都重定向到文件
ls /nonexistent /tmp &> all_output.txt
echo "所有输出(包括错误):"
cat all_output.txt

echo -e "\n=== 输入重定向 ==="

# 创建输入文件
cat > input.txt << EOF
苹果
香蕉
橙子
葡萄
EOF

# 从文件读取输入
echo "排序后的水果:"
sort < input.txt

# 统计文件行数、单词数、字符数
echo "文件统计信息:"
wc < input.txt

echo -e "\n=== 错误重定向 ==="

# 将错误信息重定向到文件
ls /nonexistent 2> error.log
echo "错误信息:"
cat error.log

# 将错误信息重定向到/dev/null(丢弃)
ls /nonexistent 2> /dev/null
echo "错误信息已被丢弃"

echo -e "\n=== 组合重定向 ==="

# 将stdout和stderr分别重定向到不同文件
ls /nonexistent /tmp > stdout.log 2> stderr.log
echo "标准输出:"
cat stdout.log
echo "标准错误:"
cat stderr.log

# 将stderr重定向到stdout
ls /nonexistent /tmp > combined.log 2>&1
echo "合并的输出:"
cat combined.log

# 现代写法(Bash 4+)
ls /nonexistent /tmp &> modern_combined.log
echo "现代写法的合并输出:"
cat modern_combined.log

echo -e "\n=== 重定向到标准输出 ==="

# 将文件内容显示到标准输出
echo "显示文件内容:"
cat < output.txt

# 清理临时文件
rm -f output.txt input.txt all_output.txt error.log stdout.log stderr.log combined.log modern_combined.log
重定向操作可视化:
$ echo "Hello" > file.txt
命令
echo
输出重定向
>
目标文件
file.txt

管道(Pipe)

管道用于将一个命令的输出作为另一个命令的输入。

管道操作流程
命令1
CMD1
管道
|
命令2
CMD2
最终输出
OUT

管道使用示例

pipe_examples.sh
#!/bin/bash

# 管道使用示例

echo "=== 基本管道操作 ==="

# 统计文件数量
echo "当前目录文件数量:"
ls | wc -l

# 查找特定文件并统计
echo "文本文件数量:"
ls *.txt 2>/dev/null | wc -l

# 排序和去重
echo -e "香蕉\n苹果\n橙子\n香蕉\n葡萄" | sort | uniq

echo -e "\n=== 文本处理管道 ==="

# 创建测试文件
cat > fruits.txt << EOF
apple 5
banana 3
orange 8
grape 12
apple 2
EOF

echo "原始数据:"
cat fruits.txt

echo -e "\n按水果名排序:"
cat fruits.txt | sort

echo -e "\n按数量排序:"
cat fruits.txt | sort -k2,2n

echo -e "\n统计每种水果的总数:"
cat fruits.txt | awk '{sum[$1] += $2} END {for (fruit in sum) print fruit, sum[fruit]}' | sort

echo -e "\n=== 系统监控管道 ==="

# 查看进程信息
echo "内存使用最多的进程:"
ps aux --sort=-%mem | head -5

# 查看磁盘使用情况
echo "磁盘使用情况:"
df -h | grep -v tmpfs

# 查看网络连接
echo "ESTABLISHED连接数量:"
netstat -tun | grep ESTABLISHED | wc -l

echo -e "\n=== 复杂管道操作 ==="

# 分析日志文件(示例)
echo "创建示例日志文件..."
cat > app.log << EOF
[ERROR] 2023-01-01 10:00:00 Database connection failed
[INFO] 2023-01-01 10:00:01 Starting application
[WARN] 2023-01-01 10:00:05 High memory usage detected
[ERROR] 2023-01-01 10:00:10 File not found: config.json
[INFO] 2023-01-01 10:00:15 Application started successfully
EOF

echo "错误日志统计:"
grep "ERROR" app.log | cut -d' ' -f2 | sort | uniq -c

echo -e "\n=== 管道与重定向结合 ==="

# 将管道结果保存到文件
ls -la | grep "^-" | wc -l > regular_files_count.txt
echo "普通文件数量已保存到文件: $(cat regular_files_count.txt)"

# 将错误信息通过管道传递
ls /nonexistent *.txt 2>&1 | grep "ls:"
echo "错误信息已过滤"

echo -e "\n=== 命名管道(FIFO)==="

# 创建命名管道
mkfifo my_pipe

# 在后台向管道写入数据
echo "通过命名管道传递的数据" > my_pipe &

# 从管道读取数据
echo "从命名管道读取:"
cat my_pipe

# 清理
rm -f my_pipe fruits.txt app.log regular_files_count.txt
管道使用技巧:
  • 管道会创建子shell,变量修改不会影响父shell
  • 使用|&可以将stderr也通过管道传递
  • 复杂的管道操作可以使用临时文件提高可读性
  • 使用tee命令可以在管道中保存中间结果
  • 命名管道(FIFO)可以在不相关的进程间传递数据

Here文档和Here字符串

Here文档用于在脚本中嵌入多行文本,Here字符串用于传递单行文本。

Here文档语法
command << DELIMITER
  line 1
  line 2
  ...
DELIMITER
Here字符串语法
command <<< "string"

Here文档和Here字符串使用示例

here_documents.sh
#!/bin/bash

# Here文档和Here字符串示例

echo "=== 基本Here文档 ==="

# 基本Here文档
cat << EOF
这是一个Here文档示例
可以包含多行文本
不需要担心引号或特殊字符
EOF

echo -e "\n=== 带缩进的Here文档 ==="

# 使用<<-去除前导制表符(注意:必须是制表符,不是空格)
cat <<- EOF
	这是一个带缩进的Here文档
	第二行也有缩进
	这样可以使脚本更美观
EOF

echo -e "\n=== 带变量的Here文档 ==="

name="Alice"
age=25

cat << EOF
个人信息:
姓名: $name
年龄: $age
时间: $(date)
EOF

echo -e "\n=== 禁用变量扩展的Here文档 ==="

# 使用单引号或反斜杠禁用变量扩展
cat << 'EOF'
这里不会进行变量扩展
姓名: $name
年龄: $age
EOF

cat << EOF
这里会进行变量扩展
姓名: $name
年龄: $age
EOF

echo -e "\n=== Here文档与命令结合 ==="

# 使用Here文档作为命令输入
echo "排序后的数据:"
sort << END
orange
apple
banana
grape
END

# 使用Here文档创建配置文件
cat > config.txt << CONFIG
# 应用配置文件
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=myapp
DEBUG=true
CONFIG

echo "配置文件已创建:"
cat config.txt

echo -e "\n=== Here字符串 ==="

# 基本Here字符串
echo "小写转大写:"
tr 'a-z' 'A-Z' <<< "hello world"

# 使用变量
message="This is a test"
wc -w <<< "$message"

# 数值计算
echo "数学运算:"
bc <<< "2 + 3 * 5"

echo -e "\n=== 复杂Here文档示例 ==="

# 创建SQL脚本
cat > query.sql << SQL
SELECT
    users.name,
    orders.total
FROM
    users
JOIN
    orders ON users.id = orders.user_id
WHERE
    orders.created_at > '2023-01-01'
ORDER BY
    orders.total DESC
LIMIT 10;
SQL

echo "SQL脚本已创建:"
cat query.sql

echo -e "\n=== 交互式命令的Here文档 ==="

# 自动化交互式命令
ftp -n << EOF
open ftp.example.com
user myusername mypassword
binary
cd /pub
get file.txt
quit
EOF

echo "FTP操作完成"

echo -e "\n=== 多行字符串处理 ==="

# 处理多行字符串
multiline_text=$(cat << END
第一行
第二行
第三行
END
)

echo "多行文本:"
echo "$multiline_text"

echo "行数统计:"
wc -l <<< "$multiline_text"

# 清理临时文件
rm -f config.txt query.sql
Here文档使用技巧:
  • 使用<<-可以忽略前导制表符(必须是制表符)
  • 使用单引号分隔符(<<'EOF')可以禁用变量扩展
  • Here文档会进行命令替换和变量扩展,除非使用引号
  • Here字符串是Bash的特性,不是所有Shell都支持
  • 复杂的多行输入建议使用Here文档而不是多个echo

文件描述符操作

文件描述符是Unix/Linux系统中用于访问文件或I/O资源的抽象指示器。

文件描述符 名称 默认用途 示例
0 stdin 标准输入 command 0< file
1 stdout 标准输出 command 1> file
2 stderr 标准错误 command 2> file
3-9 自定义 用户定义 command 3> file

文件描述符使用示例

file_descriptors.sh
#!/bin/bash

# 文件描述符操作示例

echo "=== 自定义文件描述符 ==="

# 创建自定义文件描述符
exec 3> custom_output.txt
echo "这行文字会写入自定义文件描述符3" >&3
exec 3>&-  # 关闭文件描述符

echo "自定义文件描述符内容:"
cat custom_output.txt

echo -e "\n=== 同时重定向stdout和stderr ==="

# 方法1:传统方式
ls /nonexistent /tmp > output.log 2>&1
echo "传统方式 - 输出和错误都重定向到同一文件:"
cat output.log

# 方法2:现代方式
ls /nonexistent /tmp &> modern_output.log
echo "现代方式 - 输出和错误都重定向到同一文件:"
cat modern_output.log

echo -e "\n=== 文件描述符复制 ==="

# 将stderr重定向到stdout的副本
ls /nonexistent /tmp 2>&1 | tee combined.log
echo "通过tee命令同时输出到屏幕和文件:"
cat combined.log

echo -e "\n=== 读取自定义文件描述符 ==="

# 创建输入文件描述符
exec 4< custom_output.txt
read line <&4
echo "从文件描述符4读取: $line"
exec 4<&-

echo -e "\n=== 高级文件描述符操作 ==="

# 同时重定向到多个位置
echo "同时输出到屏幕和文件:" | tee screen.log > file.log
echo "屏幕日志:" ; cat screen.log
echo "文件日志:" ; cat file.log

# 使用进程替换
echo "比较两个命令的输出:"
diff <(ls /bin) <(ls /usr/bin) | head -5

echo -e "\n=== 错误处理与重定向 ==="

# 将错误信息重定向到不同位置
{
    echo "正常输出信息"
    ls /nonexistent
    echo "更多正常输出"
} > normal.log 2> error.log

echo "正常输出:"
cat normal.log
echo "错误输出:"
cat error.log

echo -e "\n=== 临时重定向 ==="

# 临时重定向stdout到stderr
echo "这是一条错误信息" 1>&2

# 临时重定向stderr到stdout
ls /nonexistent 2>&1 | grep "ls:"

echo -e "\n=== 文件描述符与函数 ==="

# 在函数中使用文件描述符
log_message() {
    local message="$1"
    echo "[$(date)] $message" >&3
}

# 设置日志文件描述符
exec 3>> function_log.txt

log_message "函数开始执行"
log_message "处理数据..."
log_message "函数执行完成"

echo "函数日志:"
cat function_log.txt

echo -e "\n=== 网络重定向 ==="

# 重定向到网络连接(示例)
echo "HTTP重定向示例:"
echo -e "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" | nc example.com 80 | head -10

# 清理临时文件
rm -f custom_output.txt output.log modern_output.log combined.log screen.log file.log normal.log error.log function_log.txt
文件描述符最佳实践:
  • 使用exec命令创建持久的文件描述符
  • 记得关闭自定义文件描述符,避免资源泄漏
  • 文件描述符3-9可供用户自定义使用
  • 使用&>简化stdout和stderr的重定向
  • 在脚本结束时清理所有打开的文件描述符

综合示例

通过实际脚本示例展示Shell输入输出重定向的综合应用。

日志处理系统

使用各种重定向技术实现完整的日志处理系统。

log_processor.sh
#!/bin/bash

# 日志处理系统 - 重定向综合示例

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 日志级别
LOG_ERROR=1
LOG_WARN=2
LOG_INFO=3
LOG_DEBUG=4

# 配置
LOG_LEVEL=$LOG_INFO
LOG_FILE="application.log"
MAX_LOG_SIZE=1024  # 1KB for demo

# 初始化日志系统
init_logging() {
    # 创建日志文件
    > "$LOG_FILE"

    # 设置文件描述符用于日志写入
    exec 3>> "$LOG_FILE"

    echo -e "${BLUE}日志系统初始化完成${NC}"
    echo "日志文件: $LOG_FILE"
    echo "日志级别: $LOG_LEVEL"
}

# 日志函数
log() {
    local level=$1
    local message=$2
    local level_name=""
    local color=$NC

    case $level in
        $LOG_ERROR)
            level_name="ERROR"
            color=$RED
            ;;
        $LOG_WARN)
            level_name="WARN"
            color=$YELLOW
            ;;
        $LOG_INFO)
            level_name="INFO"
            color=$BLUE
            ;;
        $LOG_DEBUG)
            level_name="DEBUG"
            color=$GREEN
            ;;
    esac

    # 如果请求的日志级别高于当前设置,则跳过
    if [ $level -gt $LOG_LEVEL ]; then
        return
    fi

    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    local log_entry="[$timestamp] [$level_name] $message"

    # 输出到屏幕(带颜色)
    echo -e "${color}${log_entry}${NC}"

    # 输出到日志文件(不带颜色)
    echo "$log_entry" >&3

    # 检查日志文件大小,如果过大则轮转
    check_log_size
}

# 检查日志文件大小
check_log_size() {
    local size=$(wc -c < "$LOG_FILE" 2>/dev/null || echo 0)

    if [ $size -gt $MAX_LOG_SIZE ]; then
        log $LOG_INFO "日志文件过大(${size}字节),执行轮转"
        rotate_logs
    fi
}

# 日志轮转
rotate_logs() {
    local timestamp=$(date '+%Y%m%d_%H%M%S')
    local backup_file="${LOG_FILE}.${timestamp}"

    # 关闭当前文件描述符
    exec 3>&-

    # 备份当前日志文件
    mv "$LOG_FILE" "$backup_file"

    # 重新打开日志文件
    exec 3>> "$LOG_FILE"

    log $LOG_INFO "日志已轮转: $backup_file"
    echo "备份文件: $backup_file" >&3
}

# 处理应用程序输出
process_application() {
    local app_name="$1"

    log $LOG_INFO "启动应用程序: $app_name"

    # 模拟应用程序输出
    {
        echo "Application $app_name starting..."
        echo "Config loaded successfully"
        echo "WARNING: Resource usage high"
        echo "Processing data..."
        echo "ERROR: Database connection failed"
        echo "Application completed with errors"
    } | while read line; do
        # 根据内容识别日志级别
        case $line in
            *"ERROR"*)
                log $LOG_ERROR "$app_name: $line"
                ;;
            *"WARNING"*)
                log $LOG_WARN "$app_name: $line"
                ;;
            *)
                log $LOG_INFO "$app_name: $line"
                ;;
        esac
    done

    log $LOG_INFO "应用程序 $app_name 处理完成"
}

# 分析日志文件
analyze_logs() {
    local log_file=${1:-$LOG_FILE}

    log $LOG_INFO "开始分析日志文件: $log_file"

    echo -e "\n${BLUE}=== 日志分析报告 ===${NC}"

    # 统计各级别日志数量
    echo -e "\n${GREEN}日志级别统计:${NC}"
    grep -o "\[[A-Z]*\]" "$log_file" | sort | uniq -c | sort -nr | while read count level; do
        echo "  $level: $count"
    done

    # 错误日志详情
    echo -e "\n${RED}错误日志详情:${NC}"
    grep "\[ERROR\]" "$log_file" | head -5 | while read line; do
        echo "  $(echo "$line" | cut -d']' -f3-)"
    done

    # 时间分布
    echo -e "\n${YELLOW}时间分布(最近10条):${NC}"
    grep -o "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}" "$log_file" | tail -10

    # 使用管道进行复杂分析
    echo -e "\n${BLUE}应用程序活动统计:${NC}"
    awk -F']' '/INFO.*应用程序/ {print $3}' "$log_file" | sort | uniq -c

    log $LOG_INFO "日志分析完成"
}

# 实时监控日志
monitor_logs() {
    local log_file=${1:-$LOG_FILE}

    echo -e "\n${GREEN}开始实时监控日志 (Ctrl+C 停止)${NC}"
    echo -e "${BLUE}监控文件: $log_file${NC}"

    # 使用tail -f实时监控
    tail -f "$log_file" | while read line; do
        # 根据日志级别着色显示
        case $line in
            *"[ERROR]"*)
                echo -e "${RED}$line${NC}"
                ;;
            *"[WARN]"*)
                echo -e "${YELLOW}$line${NC}"
                ;;
            *"[INFO]"*)
                echo -e "${BLUE}$line${NC}"
                ;;
            *"[DEBUG]"*)
                echo -e "${GREEN}$line${NC}"
                ;;
            *)
                echo "$line"
                ;;
        esac
    done
}

# 清理函数
cleanup() {
    log $LOG_INFO "正在清理资源..."

    # 关闭文件描述符
    exec 3>&-

    # 删除临时文件
    rm -f "$LOG_FILE"*

    echo -e "${GREEN}清理完成${NC}"
}

# 信号处理
trap cleanup EXIT INT TERM

# 主函数
main() {
    echo -e "${BLUE}=== Shell日志处理系统 ===${NC}"

    # 初始化日志系统
    init_logging

    # 处理多个应用程序
    process_application "WebServer" &
    process_application "Database" &
    process_application "CacheService" &

    # 等待所有后台进程完成
    wait

    # 分析日志
    analyze_logs

    # 显示日志文件内容
    echo -e "\n${BLUE}=== 日志文件内容 ===${NC}"
    cat "$LOG_FILE"

    # 询问是否实时监控
    read -p "是否启动实时监控?(y/n): " -n 1 -r
    echo
    if [[ $REPLY =~ ^[Yy]$ ]]; then
        monitor_logs
    fi
}

# 参数处理
case "${1:-}" in
    "monitor")
        init_logging
        monitor_logs
        ;;
    "analyze")
        analyze_logs "${2:-$LOG_FILE}"
        ;;
    "clean")
        cleanup
        ;;
    "help"|"-h"|"--help")
        echo "用法: $0 [command]"
        echo "命令:"
        echo "  monitor   实时监控日志"
        echo "  analyze   分析日志文件"
        echo "  clean     清理日志文件"
        echo "  help      显示帮助信息"
        ;;
    *)
        main
        ;;
esac
使用示例:
./log_processor.sh - 运行完整的日志处理演示
./log_processor.sh monitor - 实时监控日志
./log_processor.sh analyze - 分析日志文件
./log_processor.sh clean - 清理日志文件

重定向最佳实践

推荐做法
  • 使用&>简化stdout和stderr的重定向
  • 在脚本中明确关闭自定义文件描述符
  • 使用Here文档处理多行输入
  • 使用tee命令同时输出到屏幕和文件
  • 对临时文件使用mktemp命令
  • 使用trap命令确保资源清理
避免的做法
  • 不要忘记检查重定向是否成功
  • 避免在管道中修改父shell的变量
  • 不要过度使用复杂的重定向嵌套
  • 避免在重要脚本中使用/dev/null丢弃所有输出
  • 不要混用不同Shell的重定向语法
  • 避免文件描述符泄漏
良好实践示例:
不推荐:
command > file 2> file
可能导致输出交错
推荐:
command &> file
简洁且安全
调试技巧

使用 set -x 开启调试模式查看重定向操作。使用 ls -l /proc/$$/fd 查看当前进程的文件描述符。在关键重定向操作前后添加 echo 语句进行调试。