文章目录
- 在 Unix/Linux 系统中,每个进程启动时都会自动打开三个标准文件描述符: 文件描述符 名称 默认设备 用途 0 stdin(标准输入) 键盘 程序读取输入 1 stdout(标准输出) 终端屏幕 程序正常输出 2 stderr(标准错误) 终端屏幕 程序错误信息 # 查看当前 shell 打开的文件描述符 ls -la /proc/$$/fd # 输出示例: # lrwx—— 1 user user 64 Jan 3 10:00 0 -> /dev/pts/0 # lrwx—— 1 user user 64 Jan 3 10:00 1 -> /dev/pts/0 # lrwx—— 1 user user 64 Jan 3 10:00 2 -> /dev/pts/0 # cxxu@CxxuDesk 17:14:09> <~> $ ls -la /proc/$$/fd total 0 dr-x—— 2 cxxu cxxu 6 Jan 3 16:57 . dr-xr-xr-x 9 cxxu cxxu 0 Jan 3 16:57 .. lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 0 -> /dev/pts/0 lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 1 -> /dev/pts/0 l-wx—— 1 cxxu cxxu 64 Jan 3 16:57 10 -> /home/cxxu/output.txt lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 2 -> /dev/pts/0 lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 255 -> /dev/pts/0 lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 5 -> /dev/ptmx 其中$$表示当前shell进程id
- 重定向操作符会在命令执行之前被 Shell 解释和处理。这意味着: # 即使命令不存在,文件也会被创建(因为重定向先于命令执行) nonexistent_command > output.txt ls -la output.txt # -rw-r–r– 1 user user 0 Jan 3 10:00 output.txt # 文件已创建,但为空 此外,shell是从左往右出来重定向的,这在包含多个重定向用法的命令行要注意.
- 重定向可以出现在命令的任何位置: # 以下三种写法完全等价 echo hello > file.txt > file.txt echo hello echo > file.txt hello shell优先识别(处理)命令行中的重定向部分>file.txt,剩下的是命令部分(非重定向部分),即echo hello 然后文件file.txt被创建(如果没有的话),然后echo命令的输出被重定向到file.txt中
- 重定向按照从左到右的顺序处理,这一点极其重要: # 示例 1:stdout 和 stderr 都重定向到 dirlist ls > dirlist 2>&1 # 执行顺序: # 1. > dirlist : fd 1 指向 dirlist 文件 # 2. 2>&1 : fd 2 复制 fd 1(此时 fd 1 已指向 dirlist) # 结果:fd 1 和 fd 2 都指向 dirlist # 示例 2:只有 stdout 重定向到 dirlist ls 2>&1 > dirlist # 执行顺序: # 1. 2>&1 : fd 2 复制 fd 1(此时 fd 1 还指向终端) # 2. > dirlist : fd 1 指向 dirlist 文件 # 结果:fd 2 指向终端,fd 1 指向 dirlist
- [n]<word 如果省略 n,默认为文件描述符 0(标准输入)。 并且n不一定是整数(虽然常见的情况是整数),还可以是单词{varname},这是高级用法.
- # 从文件读取输入(shell打开文件,cat接受输入) cat < input.txt # 等价于下面(但语义不同:一个是 shell 打开文件,一个是 cat 打开文件) # cat直接打开文件 cat input.txt # 区别演示,打开不存在的文件时,上述两种写法报错者不同(分别由shell程序和cat程序抛出) cat < nonexistent.txt # shell 报错:bash: nonexistent.txt: No such file or directory cat nonexistent.txt # cat 报错:cat: nonexistent.txt: No such file or directory
- # 使用 while 循环逐行读取文件 while read -r line; do echo “Line: $line” done < data.txt # 从文件读取数据进行排序(shell从左往右解释重定向,首先将sort命令的输入重定向为unsorted.txt,然后将sort处理结果重定向到sorted.txt) sort < unsorted.txt > sorted.txt # 用于交互式程序的自动化 mysql -u root -p < setup.sql # 多个输入重定向(后者覆盖前者) cat < file1.txt < file2.txt # 只会读取 file2.txt
- # 将文件描述符 3 关联到输入文件 exec 3< input.txt read line <&3 # 从 fd 3 读取一行(注意指针偏移,下一次读取自动读取原文中的第2行,依次类推) echo “$line” exec 3<&- # 关闭 fd 3 具体案例: # cxxu@CxxuDesk 17:46:08> <~> $ exec 3<config.txt # cxxu@CxxuDesk 17:46:25> <~> $ read l1 <&3 # cxxu@CxxuDesk 17:46:48> <~> $ echo “$l1” line1 # cxxu@CxxuDesk 17:46:57> <~> $ read l2 <&3 # cxxu@CxxuDesk 17:47:07> <~> $ echo “$l2” line2 检查指定文件描述符是否被关闭 # cxxu@CxxuDesk 17:48:14> <~> $ ls -la /proc/$$/fd total 0 dr-x—— 2 cxxu cxxu 8 Jan 3 16:57 . dr-xr-xr-x 9 cxxu cxxu 0 Jan 3 16:57 .. lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 0 -> /dev/pts/0 lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 1 -> /dev/pts/0 l-wx—— 1 cxxu cxxu 64 Jan 3 16:57 10 -> /home/cxxu/output.txt l-wx—— 1 cxxu cxxu 64 Jan 3 17:34 11 -> /home/cxxu/output.txt lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 2 -> /dev/pts/0 lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 255 -> /dev/pts/0 lr-x—— 1 cxxu cxxu 64 Jan 3 17:46 3 -> /home/cxxu/config.txt lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 5 -> /dev/ptmx # cxxu@CxxuDesk 17:48:30> <~> $ exec 3<&- # cxxu@CxxuDesk 17:48:43> <~> $ ls -la /proc/$$/fd total 0 dr-x—— 2 cxxu cxxu 7 Jan 3 16:57 . dr-xr-xr-x 9 cxxu cxxu 0 Jan 3 16:57 .. lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 0 -> /dev/pts/0 lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 1 -> /dev/pts/0 l-wx—— 1 cxxu cxxu 64 Jan 3 16:57 10 -> /home/cxxu/output.txt l-wx—— 1 cxxu cxxu 64 Jan 3 17:34 11 -> /home/cxxu/output.txt lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 2 -> /dev/pts/0 lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 255 -> /dev/pts/0 lrwx—— 1 cxxu cxxu 64 Jan 3 16:57 5 -> /dev/ptmx
- [n]>[|]word 如果省略 n,默认为文件描述符 1(标准输出)。 其中|启用的时候(变成>|)会强制重定向,即便shell选项设置为默认不覆盖(noclobber)
- # 创建新文件或覆盖现有文件 echo “Hello” > output.txt # 如果文件存在,内容被清空后写入 echo “World” > output.txt cat output.txt # World (注意:Hello 已被覆盖)
- noclobber 选项可以防止意外覆盖文件: # 启用 noclobber set -o noclobber # 或者 set -C # 尝试覆盖现有文件会失败 echo “test” > existing_file.txt # bash: existing_file.txt: cannot overwrite existing file # 使用 >| 强制覆盖 echo “test” >| existing_file.txt # 成功 # 禁用 noclobber set +o noclobber # 或者 set +C
- # 只重定向标准错误 ls nonexistent 2> error.log # 标准输出和标准错误分别重定向 command > stdout.log 2> stderr.log # 丢弃错误信息 command 2> /dev/null # 丢弃所有输出 command > /dev/null 2>&1
- [n]>>word
- # 追加内容到文件 echo “First line” > log.txt echo “Second line” >> log.txt echo “Third line” >> log.txt cat log.txt # First line # Second line # Third line # 追加错误输出 command 2>> error.log # 实际应用:日志记录(演示目录/var/log/可能会遇到权限问题) log() { echo “$(date ‘+%Y-%m-%d %H:%M:%S’) – $*” >> ~/myapp.log } log “Application started” log “Processing data…”
- # 形式 1(推荐) &>word # 形式 2 >&word 当使用第二种形式时,word 不可能扩展为一个数字或‘ – ’。 两者在语义上等同于: >word 2>&1
- # 所有输出都写入文件 command &> all_output.txt # 丢弃所有输出 command &> /dev/null # 等价写法对比 ls /exists /nonexistent &> output.txt ls /exists /nonexistent > output.txt 2>&1 # 等价
- # 追加所有输出 command &>> all_output.txt # 等价于 command >> all_output.txt 2>&1 相比于> word 2>&1只在开头更改为>>
- 当使用 >&word 这个形式时,word 不能是数字或 – 因为那会被解释为文件描述符操作 # 这是复制文件描述符,而不是将输出聚合到名为3的文件 command >&3 # fd 1 复制到 fd 3,相当于1>&3 # 安全起见,推荐使用 &> 形式 command &>output.txt # 清晰明确 # ls /usr/bin/env ‘abab’ &> 3 # cxxu@CxxuDesk 18:12:30> <~> $ ls /usr/bin/env ‘abab’ &> 3 # cxxu@CxxuDesk 18:15:07> <~> $ cat 3 ls: cannot access ‘abab’: No such file or directory /usr/bin/env
- [n]<<[-]word here-document delimiter
- # 基本的 here document cat << EOF This is line 1 This is line 2 This is line 3 EOF # 输出: # This is line 1 # This is line 2 # This is line 3
- name=”World” # 不带引号的分隔符:变量会被展开 cat << EOF Hello, $name! Current directory: $(pwd) Sum: $((1 + 2)) EOF # 输出: # Hello, World! # Current directory: /home/user # Sum: 3 # 带引号的分隔符:禁止所有展开 cat << ‘EOF’ Hello, $name! Current directory: $(pwd) Sum: $((1 + 2)) EOF # 输出: # Hello, $name! # Current directory: $(pwd) # Sum: $((1 + 2)) # 部分引用也会禁止展开 cat << “EOF” Hello, $name! EOF # 输出: # Hello, $name! cat << E”O”F Hello, $name! EOF # 输出: # Hello, $name!
- # 使用 <<- 可以在脚本中保持良好的缩进 if true; then cat <<- EOF This line has a tab prefix So does this one And this one too EOF fi # 输出(前导制表符被去除): # This line has a tab prefix # So does this one # And this one too # 注意:只去除制表符(Tab),不去除空格
- # 生成配置文件 cat << EOF > /etc/myapp.conf # Configuration file for MyApp # Generated on $(date) server_name = localhost port = 8080 debug = false EOF # SQL 脚本 mysql -u root -p << EOF CREATE DATABASE IF NOT EXISTS mydb; USE mydb; CREATE TABLE IF NOT EXISTS users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) ); EOF # 多行 SSH 命令 ssh user@remote << ‘EOF’ cd /var/log tail -n 100 syslog df -h EOF # 函数中使用 generate_html() { cat << EOF <!DOCTYPE html> <html> <head><title>$1</title></head> <body> <h1>$1</h1> <p>$2</p> </body> </html> EOF } generate_html “Welcome” “Hello, World!” > index.html
- [n]<<< word
- # 基本用法 cat <<< “Hello, World!” # Hello, World! # 与 here document 对比 # Here document(多行) cat << EOF Hello EOF # Here string(单行,更简洁) cat <<< “Hello” # 变量展开 name=”Alice” cat <<< “Hello, $name!” # Hello, Alice! # 命令替换 cat <<< “Today is $(date +%A)” # Today is Friday # 用于需要 stdin 输入的命令 read var <<< “input value” echo “$var” # input value # 实际应用:处理字符串 # 计算字符串中的单词数 wc -w <<< “one two three four” # 4 # 字符串分割 IFS=: read user pass uid gid gecos home shell <<< “root:x:0:0:root:/root:/bin/bash” echo “User: $user, Home: $home, Shell: $shell” # User: root, Home: /root, Shell: /bin/bash # bc 计算器 bc <<< “scale=2; 10/3” # 3.33
- # 使用 here string(更高效,不创建子进程) cat <<< “Hello” # 使用 echo + 管道(创建子进程) echo “Hello” | cat # 性能差异示例 time for i in {1..10000}; do cat <<< “test” > /dev/null; done time for i in {1..10000}; do echo “test” | cat > /dev/null; done # here string 通常更快
- [n]<&word # 将 fd 3 设为 fd 0 的副本 exec 3<&0 # 实际应用:保存和恢复 stdin exec 3<&0 # 保存原始 stdin 到 fd 3 exec 0< input.txt # 重定向 stdin 到文件 # … 一些操作 … exec 0<&3 # 从 fd 3 恢复 stdin exec 3<&- # 关闭 fd 3
- [n]>&word # 将 fd 3 设为 fd 1 的副本 exec 3>&1 # 经典模式:交换 stdout 和 stderr exec 3>&1 1>&2 2>&3 3>&- # 执行后:原来的 stdout 变成 stderr,原来的 stderr 变成 stdout
- # 关闭输入文件描述符 exec 3<&- # 关闭输出文件描述符 exec 3>&- # 关闭标准输入 exec 0<&- # 关闭标准输出 exec 1>&-
- # 示例:同时捕获 stdout 和 stderr 到不同变量 { output=$(command 2>&1 1>&3) exit_code=$? } 3>&1 error=$output # 此时 $output 包含 stderr,stdout 正常显示 # 更完整的版本 capture_output() { local stdout stderr exit_code exec 3>&1 4>&2 stdout=$( { stderr=$( “$@” 2>&1 1>&3 3>&- ); exit_code=$?; } 2>&1 ) exec 3>&- 4>&- echo “stdout: $stdout” echo “stderr: $stderr” echo “exit: $exit_code” }
- [n]<&digit- 移动 = 复制 + 关闭原描述符 # 将 fd 3 移动到 fd 0 exec 0<&3- # 等价于: # exec 0<&3 # exec 3<&-
- [n]>&digit- # 将 fd 3 移动到 fd 1 exec 1>&3- # 实际应用:日志重定向后恢复 exec 3>&1 # 保存 stdout exec 1> logfile.txt # stdout 重定向到文件 echo “This goes to log” exec 1>&3- # 恢复 stdout 并关闭 fd 3(一步完成) echo “This goes to terminal”
- # 复制:原文件描述符保持打开 exec 3>&1 # fd 3 是 fd 1 的副本,fd 1 仍然有效 # 移动:原文件描述符被关闭 exec 3>&1- # fd 3 是 fd 1 的副本,fd 1 被关闭
- [n]<>word 以读写模式打开文件,如果文件不存在则创建。
- # 以读写模式打开文件 exec 3<> data.txt # 读取内容 read line <&3 echo “Read: $line” # 写入内容(注意:会覆盖当前位置的内容) echo “New content” >&3 # 关闭 exec 3>&-
- # 简单的文件锁实现 lockfile=”/tmp/mylock” exec 200<>$lockfile flock -n 200 || { echo “Another instance running”; exit 1; } # … 执行需要锁保护的操作 … # 修改文件的特定部分(需要配合 seek,通常用其他工具更方便) # Bash 本身不支持 seek,这种用法有限
- Bash 4.1+ 支持使用 {varname} 让 shell 自动分配一个 ≥10 的文件描述符: # 传统方式:手动指定 fd 编号 exec 3> output.txt # 新方式:自动分配 exec {myfd}> output.txt echo “Allocated fd: $myfd” # 输出类似:Allocated fd: 10 # 使用分配的 fd echo “Hello” >&$myfd # 关闭 exec {myfd}>&-
- 这是 {varname} 最强大的特性: # 在普通命令中打开 fd,且 fd 会持续存在 echo “First line” {fd}> output.txt # fd 在命令结束后仍然有效 echo “Second line” >&$fd echo “Third line” >&$fd cat output.txt # First line # Second line # Third line # 手动关闭 exec {fd}>&-
- # 方式 1:exec + 固定编号 exec 3> file.txt echo “data” >&3 exec 3>&- # 缺点:可能与其他代码冲突 # 方式 2:exec + {varname} exec {fd}> file.txt echo “data” >&$fd exec {fd}>&- # 优点:自动分配,不冲突 # 缺点:仍需 exec # 方式 3:命令 + {varname}(最灵活) : {fd}> file.txt # : 是空命令 echo “data” >&$fd exec {fd}>&- # 优点:无需 exec,自动分配
- # 查看当前设置 shopt varredir_close # 启用:当变量离开作用域时自动关闭 fd shopt -s varredir_close # 禁用(默认) shopt -u varredir_close
- # 日志系统 init_logging() { : {LOG_FD}>> /var/log/myapp.log } log() { echo “$(date): $*” >&$LOG_FD } close_logging() { exec {LOG_FD}>&- } # 使用 init_logging log “Application started” log “Processing…” close_logging # 多文件处理 process_files() { : {input_fd}< input.txt : {output_fd}> output.txt : {error_fd}>> errors.log while read -u $input_fd line; do if process “$line”; then echo “$line” >&$output_fd else echo “Failed: $line” >&$error_fd fi done exec {input_fd}<&- {output_fd}>&- {error_fd}>&- }
- # 复制文件描述符 echo “Hello” > /dev/fd/1 # 等同于 echo “Hello”(写入 stdout) # 实际应用:让不支持 fd 的程序使用管道 diff <(sort file1) <(sort file2) # 内部使用类似 /dev/fd/63 的机制
- # 明确指定标准流 cat /dev/stdin # 从标准输入读取 echo “Hello” > /dev/stdout # 写入标准输出 echo “Error” > /dev/stderr # 写入标准错误 # 在脚本中恢复标准流 some_function() { # 即使 stdout 被重定向,仍可写入终端 echo “Debug info” > /dev/stderr }
- # TCP 连接 exec 3<>/dev/tcp/www.example.com/80 echo -e “GET / HTTP/1.1rnHost: www.example.comrnConnection: closernrn” >&3 cat <&3 exec 3>&- # 检查端口是否开放 timeout 1 bash -c ‘cat < /dev/tcp/localhost/22’ && echo “SSH port open” # 简单的 HTTP 请求函数 http_get() { local host=$1 local path=${2:-/} exec 3<>/dev/tcp/$host/80 echo -e “GET $path HTTP/1.1rnHost: $hostrnConnection: closernrn” >&3 cat <&3 exec 3>&- } http_get “example.com” “/index.html” # UDP 示例(发送数据) echo “test” > /dev/udp/localhost/514 # 发送到本地 syslog # 注意:这些是 Bash 特有功能,不是 POSIX 标准 # 某些系统可能需要编译时启用此功能
- # Shell 内部可能使用 fd 10 及以上 # 手动使用大编号 fd 时要小心 # 不推荐 exec 10> myfile.txt # 可能与 shell 内部冲突 # 推荐:使用 {varname} 语法 exec {fd}> myfile.txt # 让 shell 分配安全的 fd 编号
- # 错误:打开 fd 后忘记关闭 for i in {1..1000}; do exec {fd}> “/tmp/file$i.txt” echo “data” >&$fd # 忘记关闭 fd! done # 可能导致 “Too many open files” 错误 # 正确:总是关闭 fd for i in {1..1000}; do exec {fd}> “/tmp/file$i.txt” echo “data” >&$fd exec {fd}>&- # 关闭 done
- # 文件描述符默认被子进程继承 exec 3> shared.txt ( echo “From subshell” >&3 # 子 shell 可以使用 fd 3 ) # 阻止继承(使用 close-on-exec 标志) # Bash 本身不直接支持,需要其他手段
- #!/bin/bash # 初始化日志 LOG_DIR=”/var/log/myapp” mkdir -p “$LOG_DIR” # 打开日志文件描述符 exec {LOG_INFO}>> “$LOG_DIR/info.log” exec {LOG_ERROR}>> “$LOG_DIR/error.log” exec {LOG_DEBUG}>> “$LOG_DIR/debug.log” # 日志函数 log_info() { echo “$(date ‘+%F %T’) [INFO] $*” >&$LOG_INFO; } log_error() { echo “$(date ‘+%F %T’) [ERROR] $*” >&$LOG_ERROR; } log_debug() { echo “$(date ‘+%F %T’) [DEBUG] $*” >&$LOG_DEBUG; } # 清理函数 cleanup() { exec {LOG_INFO}>&- {LOG_ERROR}>&- {LOG_DEBUG}>&- } trap cleanup EXIT # 使用 log_info “Application started” log_debug “Initializing components…” if ! some_operation; then log_error “Operation failed” fi log_info “Application finished”
- #!/bin/bash # 保存原始 stdout 和 stderr exec {ORIG_STDOUT}>&1 exec {ORIG_STDERR}>&2 # 重定向所有输出到日志 exec 1>> process.log 2>&1 # 进度信息写入原始终端 progress() { echo “$*” >&$ORIG_STDOUT } # 正常输出写入日志 echo “Starting process…” for i in {1..10}; do echo “Processing step $i” # 写入日志 progress “Progress: ${i}0%” # 显示在终端 sleep 1 done echo “Process complete” progress “Done!” # 恢复 exec 1>&$ORIG_STDOUT 2>&$ORIG_STDERR exec {ORIG_STDOUT}>&- {ORIG_STDERR}>&-
- #!/bin/bash # 创建临时文件并打开 fd(文件可以立即删除,fd 仍然有效) tmpfile=$(mktemp) exec {tmp_fd}<>”$tmpfile” rm “$tmpfile” # 删除文件,但 fd 仍然可用 # 写入临时数据 echo “Temporary data line 1” >&$tmp_fd echo “Temporary data line 2” >&$tmp_fd # 回到文件开头读取 exec {tmp_fd}<&- exec {tmp_fd}< /dev/fd/$tmp_fd # 不能直接 seek,需要其他方式 # 更实用的方式:使用进程替换 data=$(cat << ‘EOF’ line 1 line 2 line 3 EOF ) while read line; do echo “Processing: $line” done <<< “$data”
- #!/bin/bash # 方法:使用临时文件和 fd capture_both() { local cmd=”$*” local stdout_file stderr_file stdout_file=$(mktemp) stderr_file=$(mktemp) eval “$cmd” > “$stdout_file” 2> “$stderr_file” local exit_code=$? CAPTURED_STDOUT=$(cat “$stdout_file”) CAPTURED_STDERR=$(cat “$stderr_file”) rm “$stdout_file” “$stderr_file” return $exit_code } # 使用 capture_both ls /exists /nonexistent echo “Exit code: $?” echo “Stdout: $CAPTURED_STDOUT” echo “Stderr: $CAPTURED_STDERR”
- # 问题:管道中的循环在子 shell 中运行 count=0 cat file.txt | while read line; do ((count++)) done echo “$count” # 输出 0!变量修改在子 shell 中丢失 # 解决方案 1:使用进程替换 count=0 while read line; do ((count++)) done < <(cat file.txt) echo “$count” # 正确的计数 # 解决方案 2:使用 here string count=0 while read line; do ((count++)) done <<< “$(cat file.txt)” echo “$count” # 正确的计数 # 解决方案 3:使用 lastpipe 选项(Bash 4.2+) shopt -s lastpipe count=0 cat file.txt | while read line; do ((count++)) done echo “$count” # 正确的计数
- # 重定向:直接连接文件和 fd command < input.txt > output.txt # 管道:连接两个进程的 stdout 和 stdin command1 | command2 # 区别: # – 重定向不创建额外进程 # – 管道两边各有一个进程 # – 重定向的文件需要存在(输入)或可创建(输出)
- # 丢弃 stdout command > /dev/null # 丢弃 stderr command 2> /dev/null # 丢弃所有输出 command > /dev/null 2>&1 command &> /dev/null # 简写 # 不要这样做(创建名为 /dev/null 的普通文件的风险) command > /dev/null 2> /dev/null # 两次打开,通常 OK,但不必要
目录
- 一、重定向概述
- 1.1 文件描述符基础
- 1.2 重定向的基本原理
- 1.3 重定向操作符的位置
- 1.4 重定向的处理顺序
- 二、输入重定向(Input Redirection)
- 2.1 基本语法
- 2.2 判断是shell还是外部程序打开文件
- 2.3 实际应用
- 2.4 指定文件描述符
- 三、输出重定向(Output Redirection)
- 3.1 基本语法
- 3.2 基本行为
- 3.3 noclobber 选项和>|
- 3.4 重定向错误输出
- 四、追加重定向(Appending Redirected Output)
- 4.1 基本语法
- 4.2 使用示例
- 五、同时重定向标准输出和标准错误(输出聚合)
- 5.1 两种语法形式(&>,>&)
- 5.2 使用示例
- 5.3 追加形式&>>
- 5.4 注意事项
- 六、Here Documents
- 6.1 基本语法
- 6.2 基本用法
- 6.3 变量展开行为
- 6.4 <<- 去除前导制表符
- 6.5 实际应用
- 七、Here Strings
- 7.1 基本语法
- 7.2 使用示例
- 7.3 Here String vs echo + 管道
- 八、复制文件描述符
- 8.1 复制输入文件描述符
- 8.2 复制输出文件描述符
- 8.3 关闭文件描述符
- 8.4 实际应用示例
- 九、移动文件描述符
- 9.1 移动输入文件描述符
- 9.2 移动输出文件描述符
- 9.3 复制 vs 移动 对比
- 十、读写文件描述符
- 10.1 基本语法
- 10.2 使用示例
- 10.3 实际应用
- 十一、{varname} 语法详解
- 11.1 自动分配文件描述符
- 11.2 无需 exec 的持久文件描述符
- 11.3 与 exec 方式的对比
- 11.4 varredir_close 选项
- 11.5 实际应用示例
- 十二、特殊文件名处理
- 12.1 /dev/fd/n
- 12.2 /dev/stdin, /dev/stdout, /dev/stderr
- 12.3 /dev/tcp 和 /dev/udp
- 十三、重定向中的展开
- 13.1 支持的展开类型
- 13.2 多单词错误
- 十四、文件描述符使用注意事项
- 14.1 避免与 shell 内部 fd 冲突
- 14.2 fd 泄漏
- 14.3 子进程继承
- 十五、综合实战示例
- 15.1 日志系统
- 15.2 进度和输出分离
- 15.3 安全的临时文件处理
- 15.4 同时捕获 stdout 和 stderr
- 十六、常见问题与陷阱
- 16.1 在管道中的变量作用域
- 16.2 重定向 vs 管道
- 16.3 /dev/null 的正确使用
- 总结
重定向是 Shell 编程中最核心的概念之一。它允许你改变命令的输入来源和输出目的地,而不是使用默认的键盘输入和屏幕输出。
在 Unix/Linux 系统中,每个进程启动时都会自动打开三个标准文件描述符:
| 文件描述符 | 名称 | 默认设备 | 用途 |
|---|---|---|---|
| 0 | stdin(标准输入) | 键盘 | 程序读取输入 |
| 1 | stdout(标准输出) | 终端屏幕 | 程序正常输出 |
| 2 | stderr(标准错误) | 终端屏幕 | 程序错误信息 |
# 查看当前 shell 打开的文件描述符 ls -la /proc/$$/fd # 输出示例: # lrwx------ 1 user user 64 Jan 3 10:00 0 -> /dev/pts/0 # lrwx------ 1 user user 64 Jan 3 10:00 1 -> /dev/pts/0 # lrwx------ 1 user user 64 Jan 3 10:00 2 -> /dev/pts/0 # cxxu@CxxuDesk 17:14:09> <~> $ ls -la /proc/$$/fd total 0 dr-x------ 2 cxxu cxxu 6 Jan 3 16:57 . dr-xr-xr-x 9 cxxu cxxu 0 Jan 3 16:57 .. lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 0 -> /dev/pts/0 lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 1 -> /dev/pts/0 l-wx------ 1 cxxu cxxu 64 Jan 3 16:57 10 -> /home/cxxu/output.txt lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 2 -> /dev/pts/0 lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 255 -> /dev/pts/0 lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 5 -> /dev/ptmx
其中
$$表示当前shell进程id
重定向操作符会在命令执行之前被 Shell 解释和处理。这意味着:
# 即使命令不存在,文件也会被创建(因为重定向先于命令执行) nonexistent_command > output.txt ls -la output.txt # -rw-r--r-- 1 user user 0 Jan 3 10:00 output.txt # 文件已创建,但为空
此外,shell是从左往右出来重定向的,这在包含多个重定向用法的命令行要注意.
重定向可以出现在命令的任何位置:
# 以下三种写法完全等价 echo hello > file.txt > file.txt echo hello echo > file.txt hello
shell优先识别(处理)命令行中的重定向部分
>file.txt,剩下的是命令部分(非重定向部分),即echo hello然后文件
file.txt被创建(如果没有的话),然后echo命令的输出被重定向到file.txt中
重定向按照从左到右的顺序处理,这一点极其重要:
# 示例 1:stdout 和 stderr 都重定向到 dirlist ls > dirlist 2>&1 # 执行顺序: # 1. > dirlist : fd 1 指向 dirlist 文件 # 2. 2>&1 : fd 2 复制 fd 1(此时 fd 1 已指向 dirlist) # 结果:fd 1 和 fd 2 都指向 dirlist # 示例 2:只有 stdout 重定向到 dirlist ls 2>&1 > dirlist # 执行顺序: # 1. 2>&1 : fd 2 复制 fd 1(此时 fd 1 还指向终端) # 2. > dirlist : fd 1 指向 dirlist 文件 # 结果:fd 2 指向终端,fd 1 指向 dirlist
[n]<word
- 如果省略
n,默认为文件描述符 0(标准输入)。
- 并且
n不一定是整数(虽然常见的情况是整数),还可以是单词{varname},这是高级用法.
n,默认为文件描述符 0(标准输入)。n不一定是整数(虽然常见的情况是整数),还可以是单词{varname},这是高级用法.
# 从文件读取输入(shell打开文件,cat接受输入)
cat < input.txt
# 等价于下面(但语义不同:一个是 shell 打开文件,一个是 cat 打开文件)
# cat直接打开文件
cat input.txt
# 区别演示,打开不存在的文件时,上述两种写法报错者不同(分别由shell程序和cat程序抛出)
cat < nonexistent.txt # shell 报错:bash: nonexistent.txt: No such file or directory
cat nonexistent.txt # cat 报错:cat: nonexistent.txt: No such file or directory
# 使用 while 循环逐行读取文件
while read -r line; do
echo "Line: $line"
done < data.txt
# 从文件读取数据进行排序(shell从左往右解释重定向,首先将sort命令的输入重定向为unsorted.txt,然后将sort处理结果重定向到sorted.txt)
sort < unsorted.txt > sorted.txt
# 用于交互式程序的自动化
mysql -u root -p < setup.sql
# 多个输入重定向(后者覆盖前者)
cat < file1.txt < file2.txt # 只会读取 file2.txt
# 将文件描述符 3 关联到输入文件
exec 3< input.txt
read line <&3 # 从 fd 3 读取一行(注意指针偏移,下一次读取自动读取原文中的第2行,依次类推)
echo "$line"
exec 3<&- # 关闭 fd 3
具体案例:
# cxxu@CxxuDesk 17:46:08> <~> $ exec 3<config.txt # cxxu@CxxuDesk 17:46:25> <~> $ read l1 <&3 # cxxu@CxxuDesk 17:46:48> <~> $ echo "$l1" line1 # cxxu@CxxuDesk 17:46:57> <~> $ read l2 <&3 # cxxu@CxxuDesk 17:47:07> <~> $ echo "$l2" line2
检查指定文件描述符是否被关闭
# cxxu@CxxuDesk 17:48:14> <~> $ ls -la /proc/$$/fd total 0 dr-x------ 2 cxxu cxxu 8 Jan 3 16:57 . dr-xr-xr-x 9 cxxu cxxu 0 Jan 3 16:57 .. lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 0 -> /dev/pts/0 lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 1 -> /dev/pts/0 l-wx------ 1 cxxu cxxu 64 Jan 3 16:57 10 -> /home/cxxu/output.txt l-wx------ 1 cxxu cxxu 64 Jan 3 17:34 11 -> /home/cxxu/output.txt lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 2 -> /dev/pts/0 lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 255 -> /dev/pts/0 lr-x------ 1 cxxu cxxu 64 Jan 3 17:46 3 -> /home/cxxu/config.txt lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 5 -> /dev/ptmx # cxxu@CxxuDesk 17:48:30> <~> $ exec 3<&- # cxxu@CxxuDesk 17:48:43> <~> $ ls -la /proc/$$/fd total 0 dr-x------ 2 cxxu cxxu 7 Jan 3 16:57 . dr-xr-xr-x 9 cxxu cxxu 0 Jan 3 16:57 .. lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 0 -> /dev/pts/0 lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 1 -> /dev/pts/0 l-wx------ 1 cxxu cxxu 64 Jan 3 16:57 10 -> /home/cxxu/output.txt l-wx------ 1 cxxu cxxu 64 Jan 3 17:34 11 -> /home/cxxu/output.txt lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 2 -> /dev/pts/0 lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 255 -> /dev/pts/0 lrwx------ 1 cxxu cxxu 64 Jan 3 16:57 5 -> /dev/ptmx
[n]>[|]word
如果省略 n,默认为文件描述符 1(标准输出)。
其中|启用的时候(变成>|)会强制重定向,即便shell选项设置为默认不覆盖(noclobber)
# 创建新文件或覆盖现有文件
echo "Hello" > output.txt
# 如果文件存在,内容被清空后写入
echo "World" > output.txt
cat output.txt
# World (注意:Hello 已被覆盖)
noclobber 选项可以防止意外覆盖文件:
# 启用 noclobber set -o noclobber # 或者 set -C # 尝试覆盖现有文件会失败 echo "test" > existing_file.txt # bash: existing_file.txt: cannot overwrite existing file # 使用 >| 强制覆盖 echo "test" >| existing_file.txt # 成功 # 禁用 noclobber set +o noclobber # 或者 set +C
# 只重定向标准错误
ls nonexistent 2> error.log
# 标准输出和标准错误分别重定向
command > stdout.log 2> stderr.log
# 丢弃错误信息
command 2> /dev/null
# 丢弃所有输出
command > /dev/null 2>&1
[n]>>word
# 追加内容到文件
echo "First line" > log.txt
echo "Second line" >> log.txt
echo "Third line" >> log.txt
cat log.txt
# First line
# Second line
# Third line
# 追加错误输出
command 2>> error.log
# 实际应用:日志记录(演示目录/var/log/可能会遇到权限问题)
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" >> ~/myapp.log
}
log "Application started"
log "Processing data..."
# 形式 1(推荐)
&>word
# 形式 2
>&word
当使用第二种形式时,word 不可能扩展为一个数字或‘ – ’。
两者在语义上等同于:
>word 2>&1
# 所有输出都写入文件
command &> all_output.txt
# 丢弃所有输出
command &> /dev/null
# 等价写法对比
ls /exists /nonexistent &> output.txt
ls /exists /nonexistent > output.txt 2>&1 # 等价
# 追加所有输出
command &>> all_output.txt
# 等价于
command >> all_output.txt 2>&1
相比于> word 2>&1只在开头更改为>>
当使用 >&word 这个形式时,word 不能是数字或 - 因为那会被解释为文件描述符操作
# 这是复制文件描述符,而不是将输出聚合到名为3的文件 command >&3 # fd 1 复制到 fd 3,相当于1>&3 # 安全起见,推荐使用 &> 形式 command &>output.txt # 清晰明确 # ls /usr/bin/env 'abab' &> 3
# cxxu@CxxuDesk 18:12:30> <~> $ ls /usr/bin/env 'abab' &> 3 # cxxu@CxxuDesk 18:15:07> <~> $ cat 3 ls: cannot access 'abab': No such file or directory /usr/bin/env
[n]<<[-]word
here-document
delimiter
# 基本的 here document
cat << EOF
This is line 1
This is line 2
This is line 3
EOF
# 输出:
# This is line 1
# This is line 2
# This is line 3
name="World"
# 不带引号的分隔符:变量会被展开
cat << EOF
Hello, $name!
Current directory: $(pwd)
Sum: $((1 + 2))
EOF
# 输出:
# Hello, World!
# Current directory: /home/user
# Sum: 3
# 带引号的分隔符:禁止所有展开
cat << 'EOF'
Hello, $name!
Current directory: $(pwd)
Sum: $((1 + 2))
EOF
# 输出:
# Hello, $name!
# Current directory: $(pwd)
# Sum: $((1 + 2))
# 部分引用也会禁止展开
cat << "EOF"
Hello, $name!
EOF
# 输出:
# Hello, $name!
cat << E"O"F
Hello, $name!
EOF
# 输出:
# Hello, $name!
# 使用 <<- 可以在脚本中保持良好的缩进
if true; then
cat <<- EOF
This line has a tab prefix
So does this one
And this one too
EOF
fi
# 输出(前导制表符被去除):
# This line has a tab prefix
# So does this one
# And this one too
# 注意:只去除制表符(Tab),不去除空格
# 生成配置文件
cat << EOF > /etc/myapp.conf
# Configuration file for MyApp
# Generated on $(date)
server_name = localhost
port = 8080
debug = false
EOF
# SQL 脚本
mysql -u root -p << EOF
CREATE DATABASE IF NOT EXISTS mydb;
USE mydb;
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100)
);
EOF
# 多行 SSH 命令
ssh user@remote << 'EOF'
cd /var/log
tail -n 100 syslog
df -h
EOF
# 函数中使用
generate_html() {
cat << EOF
<!DOCTYPE html>
<html>
<head><title>$1</title></head>
<body>
<h1>$1</h1>
<p>$2</p>
</body>
</html>
EOF
}
generate_html "Welcome" "Hello, World!" > index.html
[n]<<< word
# 基本用法
cat <<< "Hello, World!"
# Hello, World!
# 与 here document 对比
# Here document(多行)
cat << EOF
Hello
EOF
# Here string(单行,更简洁)
cat <<< "Hello"
# 变量展开
name="Alice"
cat <<< "Hello, $name!"
# Hello, Alice!
# 命令替换
cat <<< "Today is $(date +%A)"
# Today is Friday
# 用于需要 stdin 输入的命令
read var <<< "input value"
echo "$var"
# input value
# 实际应用:处理字符串
# 计算字符串中的单词数
wc -w <<< "one two three four"
# 4
# 字符串分割
IFS=: read user pass uid gid gecos home shell <<< "root:x:0:0:root:/root:/bin/bash"
echo "User: $user, Home: $home, Shell: $shell"
# User: root, Home: /root, Shell: /bin/bash
# bc 计算器
bc <<< "scale=2; 10/3"
# 3.33
# 使用 here string(更高效,不创建子进程)
cat <<< "Hello"
# 使用 echo + 管道(创建子进程)
echo "Hello" | cat
# 性能差异示例
time for i in {1..10000}; do cat <<< "test" > /dev/null; done
time for i in {1..10000}; do echo "test" | cat > /dev/null; done
# here string 通常更快
[n]<&word
# 将 fd 3 设为 fd 0 的副本
exec 3<&0
# 实际应用:保存和恢复 stdin
exec 3<&0 # 保存原始 stdin 到 fd 3
exec 0< input.txt # 重定向 stdin 到文件
# ... 一些操作 ...
exec 0<&3 # 从 fd 3 恢复 stdin
exec 3<&- # 关闭 fd 3
[n]>&word
# 将 fd 3 设为 fd 1 的副本
exec 3>&1
# 经典模式:交换 stdout 和 stderr
exec 3>&1 1>&2 2>&3 3>&-
# 执行后:原来的 stdout 变成 stderr,原来的 stderr 变成 stdout
# 关闭输入文件描述符
exec 3<&-
# 关闭输出文件描述符
exec 3>&-
# 关闭标准输入
exec 0<&-
# 关闭标准输出
exec 1>&-
# 示例:同时捕获 stdout 和 stderr 到不同变量
{
output=$(command 2>&1 1>&3)
exit_code=$?
} 3>&1
error=$output
# 此时 $output 包含 stderr,stdout 正常显示
# 更完整的版本
capture_output() {
local stdout stderr exit_code
exec 3>&1 4>&2
stdout=$( { stderr=$( "$@" 2>&1 1>&3 3>&- ); exit_code=$?; } 2>&1 )
exec 3>&- 4>&-
echo "stdout: $stdout"
echo "stderr: $stderr"
echo "exit: $exit_code"
}
[n]<&digit-
移动 = 复制 + 关闭原描述符
# 将 fd 3 移动到 fd 0 exec 0<&3- # 等价于: # exec 0<&3 # exec 3<&-
[n]>&digit-
# 将 fd 3 移动到 fd 1
exec 1>&3-
# 实际应用:日志重定向后恢复
exec 3>&1 # 保存 stdout
exec 1> logfile.txt # stdout 重定向到文件
echo "This goes to log"
exec 1>&3- # 恢复 stdout 并关闭 fd 3(一步完成)
echo "This goes to terminal"
# 复制:原文件描述符保持打开
exec 3>&1 # fd 3 是 fd 1 的副本,fd 1 仍然有效
# 移动:原文件描述符被关闭
exec 3>&1- # fd 3 是 fd 1 的副本,fd 1 被关闭
[n]<>word
以读写模式打开文件,如果文件不存在则创建。
# 以读写模式打开文件
exec 3<> data.txt
# 读取内容
read line <&3
echo "Read: $line"
# 写入内容(注意:会覆盖当前位置的内容)
echo "New content" >&3
# 关闭
exec 3>&-
# 简单的文件锁实现
lockfile="/tmp/mylock"
exec 200<>$lockfile
flock -n 200 || { echo "Another instance running"; exit 1; }
# ... 执行需要锁保护的操作 ...
# 修改文件的特定部分(需要配合 seek,通常用其他工具更方便)
# Bash 本身不支持 seek,这种用法有限
Bash 4.1+ 支持使用 {varname} 让 shell 自动分配一个 ≥10 的文件描述符:
# 传统方式:手动指定 fd 编号
exec 3> output.txt
# 新方式:自动分配
exec {myfd}> output.txt
echo "Allocated fd: $myfd" # 输出类似:Allocated fd: 10
# 使用分配的 fd
echo "Hello" >&$myfd
# 关闭
exec {myfd}>&-
这是 {varname} 最强大的特性:
# 在普通命令中打开 fd,且 fd 会持续存在
echo "First line" {fd}> output.txt
# fd 在命令结束后仍然有效
echo "Second line" >&$fd
echo "Third line" >&$fd
cat output.txt
# First line
# Second line
# Third line
# 手动关闭
exec {fd}>&-
# 方式 1:exec + 固定编号
exec 3> file.txt
echo "data" >&3
exec 3>&-
# 缺点:可能与其他代码冲突
# 方式 2:exec + {varname}
exec {fd}> file.txt
echo "data" >&$fd
exec {fd}>&-
# 优点:自动分配,不冲突
# 缺点:仍需 exec
# 方式 3:命令 + {varname}(最灵活)
: {fd}> file.txt # : 是空命令
echo "data" >&$fd
exec {fd}>&-
# 优点:无需 exec,自动分配
# 查看当前设置
shopt varredir_close
# 启用:当变量离开作用域时自动关闭 fd
shopt -s varredir_close
# 禁用(默认)
shopt -u varredir_close
# 日志系统
init_logging() {
: {LOG_FD}>> /var/log/myapp.log
}
log() {
echo "$(date): $*" >&$LOG_FD
}
close_logging() {
exec {LOG_FD}>&-
}
# 使用
init_logging
log "Application started"
log "Processing..."
close_logging
# 多文件处理
process_files() {
: {input_fd}< input.txt
: {output_fd}> output.txt
: {error_fd}>> errors.log
while read -u $input_fd line; do
if process "$line"; then
echo "$line" >&$output_fd
else
echo "Failed: $line" >&$error_fd
fi
done
exec {input_fd}<&- {output_fd}>&- {error_fd}>&-
}
# 复制文件描述符
echo "Hello" > /dev/fd/1 # 等同于 echo "Hello"(写入 stdout)
# 实际应用:让不支持 fd 的程序使用管道
diff <(sort file1) <(sort file2)
# 内部使用类似 /dev/fd/63 的机制
# 明确指定标准流
cat /dev/stdin # 从标准输入读取
echo "Hello" > /dev/stdout # 写入标准输出
echo "Error" > /dev/stderr # 写入标准错误
# 在脚本中恢复标准流
some_function() {
# 即使 stdout 被重定向,仍可写入终端
echo "Debug info" > /dev/stderr
}
# TCP 连接
exec 3<>/dev/tcp/www.example.com/80
echo -e "GET / HTTP/1.1rnHost: www.example.comrnConnection: closernrn" >&3
cat <&3
exec 3>&-
# 检查端口是否开放
timeout 1 bash -c 'cat < /dev/tcp/localhost/22' && echo "SSH port open"
# 简单的 HTTP 请求函数
http_get() {
local host=$1
local path=${2:-/}
exec 3<>/dev/tcp/$host/80
echo -e "GET $path HTTP/1.1rnHost: $hostrnConnection: closernrn" >&3
cat <&3
exec 3>&-
}
http_get "example.com" "/index.html"
# UDP 示例(发送数据)
echo "test" > /dev/udp/localhost/514 # 发送到本地 syslog
# 注意:这些是 Bash 特有功能,不是 POSIX 标准
# 某些系统可能需要编译时启用此功能
重定向操作符后面的 word 会经历以下展开:
# 1. 花括号展开(注意:通常不用于重定向)
# echo test > {a,b}.txt # 这会导致错误,因为展开后有多个单词
# 2. 波浪号展开
echo "test" > ~/output.txt # 展开为 /home/user/output.txt
echo "test" > ~other_user/file.txt # 展开为 /home/other_user/file.txt
# 3. 参数和变量展开
logfile="/var/log/app.log"
echo "test" > "$logfile"
# 4. 命令替换
echo "test" > "$(date +%Y%m%d).log" # 例如:20260103.log
# 5. 算术展开
n=1
echo "test" > "file$((n+1)).txt" # file2.txt
# 6. 引号去除
echo "test" > "output.txt" # 引号被去除
# 7. 文件名展开(通配符)
# 注意:如果展开后得到多个文件,会报错
echo "test" > *.txt # 如果匹配多个文件,报错
echo "test" > file?.txt # 如果只匹配一个文件,OK
# 8. 单词分割
filename="my file.txt"
echo "test" > $filename # 错误!分割成两个单词
echo "test" > "$filename" # 正确,保持为一个单词
# 如果展开结果是多个单词,Bash 会报错
files="a.txt b.txt"
echo "test" > $files
# bash: $files: ambiguous redirect
# 解决方案:确保只有一个单词
echo "test" > "$files" # 写入名为 "a.txt b.txt" 的文件
# 或者使用循环
for f in $files; do echo "test" > "$f"; done
# Shell 内部可能使用 fd 10 及以上
# 手动使用大编号 fd 时要小心
# 不推荐
exec 10> myfile.txt # 可能与 shell 内部冲突
# 推荐:使用 {varname} 语法
exec {fd}> myfile.txt # 让 shell 分配安全的 fd 编号
# 错误:打开 fd 后忘记关闭
for i in {1..1000}; do
exec {fd}> "/tmp/file$i.txt"
echo "data" >&$fd
# 忘记关闭 fd!
done
# 可能导致 "Too many open files" 错误
# 正确:总是关闭 fd
for i in {1..1000}; do
exec {fd}> "/tmp/file$i.txt"
echo "data" >&$fd
exec {fd}>&- # 关闭
done
# 文件描述符默认被子进程继承
exec 3> shared.txt
(
echo "From subshell" >&3 # 子 shell 可以使用 fd 3
)
# 阻止继承(使用 close-on-exec 标志)
# Bash 本身不直接支持,需要其他手段
#!/bin/bash
# 初始化日志
LOG_DIR="/var/log/myapp"
mkdir -p "$LOG_DIR"
# 打开日志文件描述符
exec {LOG_INFO}>> "$LOG_DIR/info.log"
exec {LOG_ERROR}>> "$LOG_DIR/error.log"
exec {LOG_DEBUG}>> "$LOG_DIR/debug.log"
# 日志函数
log_info() { echo "$(date '+%F %T') [INFO] $*" >&$LOG_INFO; }
log_error() { echo "$(date '+%F %T') [ERROR] $*" >&$LOG_ERROR; }
log_debug() { echo "$(date '+%F %T') [DEBUG] $*" >&$LOG_DEBUG; }
# 清理函数
cleanup() {
exec {LOG_INFO}>&- {LOG_ERROR}>&- {LOG_DEBUG}>&-
}
trap cleanup EXIT
# 使用
log_info "Application started"
log_debug "Initializing components..."
if ! some_operation; then
log_error "Operation failed"
fi
log_info "Application finished"
#!/bin/bash
# 保存原始 stdout 和 stderr
exec {ORIG_STDOUT}>&1
exec {ORIG_STDERR}>&2
# 重定向所有输出到日志
exec 1>> process.log 2>&1
# 进度信息写入原始终端
progress() {
echo "$*" >&$ORIG_STDOUT
}
# 正常输出写入日志
echo "Starting process..."
for i in {1..10}; do
echo "Processing step $i" # 写入日志
progress "Progress: ${i}0%" # 显示在终端
sleep 1
done
echo "Process complete"
progress "Done!"
# 恢复
exec 1>&$ORIG_STDOUT 2>&$ORIG_STDERR
exec {ORIG_STDOUT}>&- {ORIG_STDERR}>&-
#!/bin/bash
# 创建临时文件并打开 fd(文件可以立即删除,fd 仍然有效)
tmpfile=$(mktemp)
exec {tmp_fd}<>"$tmpfile"
rm "$tmpfile" # 删除文件,但 fd 仍然可用
# 写入临时数据
echo "Temporary data line 1" >&$tmp_fd
echo "Temporary data line 2" >&$tmp_fd
# 回到文件开头读取
exec {tmp_fd}<&-
exec {tmp_fd}< /dev/fd/$tmp_fd # 不能直接 seek,需要其他方式
# 更实用的方式:使用进程替换
data=$(cat << 'EOF'
line 1
line 2
line 3
EOF
)
while read line; do
echo "Processing: $line"
done <<< "$data"
#!/bin/bash
# 方法:使用临时文件和 fd
capture_both() {
local cmd="$*"
local stdout_file stderr_file
stdout_file=$(mktemp)
stderr_file=$(mktemp)
eval "$cmd" > "$stdout_file" 2> "$stderr_file"
local exit_code=$?
CAPTURED_STDOUT=$(cat "$stdout_file")
CAPTURED_STDERR=$(cat "$stderr_file")
rm "$stdout_file" "$stderr_file"
return $exit_code
}
# 使用
capture_both ls /exists /nonexistent
echo "Exit code: $?"
echo "Stdout: $CAPTURED_STDOUT"
echo "Stderr: $CAPTURED_STDERR"
# 问题:管道中的循环在子 shell 中运行
count=0
cat file.txt | while read line; do
((count++))
done
echo "$count" # 输出 0!变量修改在子 shell 中丢失
# 解决方案 1:使用进程替换
count=0
while read line; do
((count++))
done < <(cat file.txt)
echo "$count" # 正确的计数
# 解决方案 2:使用 here string
count=0
while read line; do
((count++))
done <<< "$(cat file.txt)"
echo "$count" # 正确的计数
# 解决方案 3:使用 lastpipe 选项(Bash 4.2+)
shopt -s lastpipe
count=0
cat file.txt | while read line; do
((count++))
done
echo "$count" # 正确的计数
# 重定向:直接连接文件和 fd
command < input.txt > output.txt
# 管道:连接两个进程的 stdout 和 stdin
command1 | command2
# 区别:
# - 重定向不创建额外进程
# - 管道两边各有一个进程
# - 重定向的文件需要存在(输入)或可创建(输出)
# 丢弃 stdout
command > /dev/null
# 丢弃 stderr
command 2> /dev/null
# 丢弃所有输出
command > /dev/null 2>&1
command &> /dev/null # 简写
# 不要这样做(创建名为 /dev/null 的普通文件的风险)
command > /dev/null 2> /dev/null # 两次打开,通常 OK,但不必要
Bash 重定向是一个功能强大的系统,核心要点包括:
- 理解文件描述符 0(stdin)、1(stdout)、2(stderr)的概念
- 重定向按从左到右的顺序处理
>覆盖,>>追加2>&1将 stderr 重定向到 stdout 当前指向的位置{varname}语法提供了自动 fd 分配和持久化能力- Here documents 和 here strings 用于内联输入
- 特殊文件
/dev/tcp和/dev/udp提供网络功能 - 使用
exec可以修改当前 shell 的 fd
到此这篇关于Bash重定向完全指南的文章就介绍到这了,更多相关Bash重定向内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!
您可能感兴趣的文章:
- bash shell 重定向输入和输出的例子