Linux Shell脚本进阶:cat命令重定向与Here Document高级用法详解

cat命令Shell脚本重定向
于 2026-08-04 06:59:18 修改
·本内容遵循CC 4.0 BY-SA版权协议

1. 项目概述:从“猫”说起,一个被低估的文本处理大师

在Linux的世界里,cat命令可能是你最早学会的几个命令之一。它的名字来源于“concatenate”(连接),但它的功能远不止于此。很多新手,甚至一些有经验的用户,都把它简单地当作一个“查看文件内容”的工具,敲个cat file.txt就完事了。这就像把一把瑞士军刀只用来拧螺丝,实在是有点大材小用。今天,我们不聊那些基础的查看操作,而是聚焦于cat命令在文件创建与内容写入方面的几个高级用法:cat > filecat << EOFcat <<- EOF。这三个看似简单的符号组合,背后却藏着Shell脚本编程中关于输入输出重定向和Here Document的大学问。理解它们,不仅能让你写出更健壮、更优雅的Shell脚本,还能在处理配置文件、自动化部署、交互式脚本时游刃有余。无论你是运维工程师、开发者,还是Linux爱好者,搞懂这几个命令的区别,都能让你的命令行操作效率提升一个档次。

2. 核心概念解析:重定向与Here Document

在深入具体命令之前,我们必须先打好两个基础:输出重定向Here Document。这是理解后续所有区别的钥匙。

2.1 输出重定向:>>>

Shell的强大之处在于它能将命令的输入和输出像水流一样进行“重定向”。标准输出(stdout,文件描述符1)默认是打印到终端屏幕的。

  • command > file:这是最基础的重定向。它会把command命令的标准输出覆盖写入到file中。如果file不存在,则创建它;如果存在,其原有内容会被清空。

    • 类比:就像用全新的录像带覆盖录制,旧内容完全消失。
    • 示例echo “Hello” > test.txt 会创建一个test.txt文件,内容只有一行“Hello”。如果再次执行echo “World” > test.txt,文件内容将变成“World”,“Hello”被覆盖了。
  • command >> file:这是追加模式的重定向。它会把command命令的标准输出追加file的末尾。

    • 类比:就像在日记本的最后一页继续写,旧内容完好无损。
    • 示例:先执行echo “Hello” > test.txt,再执行echo “World” >> test.txt,最终文件内容将是两行:“Hello”和“World”。

理解了>cat > file就很好理解了:它启动cat命令,但给它任何文件名参数。此时,cat会进入一个特殊状态——它从标准输入(stdin,通常是键盘)读取数据,直到遇到文件结束符(Ctrl+D)。而>则将cat从标准输入读取到的所有内容,重定向(覆盖写入)到file中。所以,cat > file是一个交互式创建或覆盖文件的命令。

2.2 Here Document:<<EOF 的本质

Here Document,中文常译为“内嵌文档”或“此处文档”,是Shell提供的一种特殊的重定向方式,它允许我们在脚本中直接嵌入一段多行文本,并将其作为命令的标准输入。

其基本语法是:

BASH
command << DELIMITER
text line 1
text line 2
...
DELIMITER

这里的DELIMITER是一个自定义的标记符(常用EOFEND,但可以是任何不包含空格且不在文本中出现的字符串)。Shell会将两个DELIMITER之间的所有行(不包括结尾的DELIMITER本身)作为标准输入传递给前面的command

为什么需要它? 想象一下,你想用cat > file交互式创建一个包含多行内容的文件,你需要在键盘上输入所有行,最后按Ctrl+D。这在脚本里是无法自动化的。Here Document解决了这个问题,它让多行文本的输入变得静态化和可脚本化。

所以,cat << EOF的含义是:将紧随其后的、直到独立一行EOF为止的所有文本,作为输入传递给cat命令。而cat命令默认行为是将输入内容输出到标准输出。因此,整个结构的效果就是在终端上打印出那段内嵌的文本。通常,我们会将它和输出重定向结合使用,即cat << EOF > file,来非交互式地创建或覆盖一个多行文件

3. 三剑客详解:语法、行为与实战场景

现在,让我们把镜头对准今天的三位主角,逐一拆解。

3.1 cat > file:交互式单行/多行写入

这是最直接的文件写入方式。

  • 语法cat > filename

  • 行为

    1. 执行命令后,光标会停在新的一行,等待你输入。此时终端没有任何提示符,这常常让新手困惑。
    2. 你可以输入任意多行文本。每按一次回车,就输入一行。
    3. 输入完成后,在新的一行开头,按下 Ctrl + D(代表输入结束,EOF)。此时,你输入的所有内容会被一次性保存到filename指定的文件中。
    4. 如果文件已存在,其原有内容会被完全覆盖
  • 实战场景与示例

    • 快速创建一个小配置文件:当你需要临时创建一个只有几行内容的文件,又不想动用vimnano时。
    BASH
    # 假设我们需要创建一个简单的nginx服务器配置片段
    cat > my_site.conf << ‘EOF‘ # 注意,这里其实已经结合了Here Document,纯cat > file 是交互式的
    server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    }
    EOF
    # 更纯粹的交互式示例:下面这个操作完全依赖键盘输入
    # $ cat > memo.txt
    # 这是一个临时备忘录。 # (你输入的第一行)
    # 第二行内容。 # (你输入的第二行)
    # (然后你按下 Ctrl+D)
    # 操作完成,文件已保存。
  • 注意事项

    警告:cat > file是一个“沉默的杀手”。它不会询问“文件已存在,是否覆盖?”。如果你不小心对一个重要文件执行了此命令,并在输入Ctrl+D前发现了错误,可以立即按Ctrl+C终止命令,原文件不会被修改。但一旦按下了Ctrl+D,数据就覆水难收了。对于重要操作,一个良好的习惯是先备份,或者使用set -o noclobber(Bash中)来防止意外覆盖,此时需要用>|来强制覆盖。

3.2 cat << EOF:静态多行文本块传递

这是Here Document的标准形式,也是脚本中最常用的。

  • 语法cat << EOF

  • 行为

    1. Shell会识别<< EOF,并开始读取后续行。
    2. 读取的所有行,直到遇到一个顶格书写(即该行开头没有任何空格或制表符)的EOF为止,都会作为cat命令的输入。
    3. 文本块中的内容(包括缩进)会原封不动地传递给cat
    4. 通常与重定向联用:cat << EOF > file,将文本块直接写入文件。
  • 实战场景与示例

    • 在脚本中生成配置文件:这是最经典的用途,比如安装软件后自动生成默认配置。
    BASH
    #!/bin/bash
    CONFIG_FILE=“/etc/myapp/config.yaml”
    cat << EOF > $CONFIG_FILE
    # MyApp 配置文件
    # 生成时间:$(date)
    database:
    host: localhost
    port: 3306
    name: mydb
    logging:
    level: info
    file: /var/log/myapp.log
    EOF
    echo “配置文件已生成至 $CONFIG_FILE
    • 向命令传递多行参数:有些命令接受来自标准输入的多行数据,比如mail命令。
    BASH
    cat << EOF | mail -s “系统报告” admin@example.com
    尊敬的管理员:
    以下是服务器 $(hostname) 在 $(date) 的状态报告:
    - 磁盘使用率:$(df -h / | tail -1)
    - 内存使用率:$(free -m | awk ‘NR==2{printf “%s/%sMB (%.2f%%)\n”, $3,$2,$3*100/$2 }’)
    请及时查看。
    EOF
  • 核心细节

    • 标记符(EOF)的选择EOF只是一个约定俗成的标记,你可以使用任何字符串,如ENDCONFIG_END等,只要保证开始和结束的标记完全一致即可。标记符应选择那些不会在文本块内容中出现的字符串。
    • 引号的作用:如果开始标记被引号包围,如cat << ‘EOF’,那么Here Document内的所有字符(包括$\`)都会被当作普通字符,变量和命令替换将不会进行。如果不用引号或使用双引号cat << “EOF”,那么Here Document内的变量(如$variable)和命令替换(如$(command))会被Shell解析并替换。这是一个非常重要的区别!
    BASH
    name=“World”
    # 变量会被扩展
    cat << EOF
    Hello, $name!
    EOF
    # 输出:Hello, World!
     
    # 变量不会被扩展
    cat << ‘EOF‘
    Hello, $name!
    EOF
    # 输出:Hello, $name!

3.3 cat <<- EOF:忽略前导制表符的增强版

这是Here Document的一个变体,多了一个减号-

  • 语法cat <<- EOF

  • 行为

    1. 行为与<< EOF基本一致。
    2. 关键区别在于:在寻找结束标记EOF时,Shell会忽略EOF前行首的制表符(Tab)。同时,在输出给命令的文本中,每一行开头所有的制表符(Tab)也会被自动删除
    3. 重要:它只忽略制表符(Tab),不忽略空格(Space)。这是一个常见的坑。
  • 实战场景与示例

    • 编写格式美观的Shell脚本:为了让脚本中的Here Document部分与周围的代码缩进保持一致,提高可读性。
    BASH
    #!/bin/bash
    function create_config() {
    local config_file=$1
    # 使用 <<- EOF 可以让结束标记与函数体缩进对齐
    cat <<- EOF > “$config_file”
    # 注意:这里的缩进必须是Tab键,不是空格
    server {
    listen 80;
    server_name ${config_file%.conf};
    # 这里的缩进也会在输出时被去除
    location / {
    root /var/www/${config_file%.conf};
    }
    }
    EOF # 这个EOF前面可以有一个Tab缩进
    }
    create_config “mysite.conf”

    在上面的脚本中,EOF和它前面的代码都使用了Tab缩进,使得脚本结构非常清晰。如果不使用<<-EOF必须顶格写,会破坏代码块的视觉一致性。

  • 注意事项

    关键区别:Tab vs Space<<-只剥除行首的制表符(Tab)。如果你在编辑器里把缩进设置成了空格(比如4个空格),那么<<-将完全不起作用,结束标记EOF前的空格会导致Shell找不到结束符,脚本会一直等待输入,直到遇到一个顶格的EOF。因此,在使用此特性时,务必确认你的编辑器对Here Document部分使用的是真正的Tab键进行缩进。你可以用cat -A查看文件,Tab会显示为^I

4. 对比总结与决策指南

为了更直观地理解,我们将三者的核心特性总结如下表:

特性 cat > file cat << EOF cat <<- EOF
核心机制 标准输入重定向 Here Document (标准形式) Here Document (忽略前导Tab)
输入方式 交互式键盘输入(以Ctrl+D结束) 脚本内静态文本块 脚本内静态文本块
结束标记 Ctrl+D (EOF字符) 顶格写的EOF 可前带Tab的EOF
文本缩进处理 保留所有输入,包括缩进 完全保留文本块内的所有格式 删除每行行首的所有制表符(Tab)
主要用途 临时手动创建文件 脚本中生成保留格式的静态内容 脚本中生成内容,同时保持脚本自身缩进美观
变量扩展 输入时由Shell解析(如果支持) 默认扩展变量/命令,可用‘EOF‘禁用 << EOF,默认扩展,可用<<- ‘EOF‘禁用
脚本可读性 不适用于脚本 结束标记必须顶格,可能破坏缩进 结束标记可缩进,提升脚本整体可读性

如何选择?

  1. 在命令行终端进行一次性操作:使用 cat > file。简单直接,输入完按Ctrl+D就走。
  2. 在Shell脚本中创建复杂、多行的配置文件或文本
    • 如果文本内容不需要和脚本代码保持相同的缩进格式,或者你不在乎脚本中有一块“顶格”的代码,用 cat << EOF > file
    • 如果你追求脚本的整洁和可读性,希望Here Document部分与周围代码的缩进对齐,请务必使用 cat <<- EOF > file,并确保使用Tab键进行缩进。
  3. 需要文本中的变量被替换:使用不带引号的EOF(即<< EOF<<- EOF)。
  4. 需要文本内容原样输出,不进行变量替换(例如,要生成一个包含$PATH字样的脚本),使用带单引号的EOF(即<< ‘EOF‘<<- ‘EOF‘)。

5. 高级技巧与避坑指南

掌握了基本用法,我们来看看一些更深入的技巧和实践中容易踩的坑。

5.1 嵌套与转义:当EOF出现在文本中时

如果你的文本内容本身就包含EOF这个字符串怎么办?Shell会把它误认为是结束标记。

  • 解决方案1:使用不常见的标记符。这是最简单的方法。

    BASH
    cat << _MY_CONFIG_END_
    This line has the word EOF in it, but it‘s not the delimiter.
    The delimiter is _MY_CONFIG_END_.
    _MY_CONFIG_END_
  • 解决方案2:对标记符进行引用(推荐)。使用<< ‘EOF‘<< “EOF”,虽然这主要控制变量扩展,但标记符本身也更“严格”。

    • 更稳妥的做法是,如果文本内容复杂,直接选用一个极不可能出现的随机字符串作为标记。

5.2 与管道和其他命令的组合

Here Document的输出可以轻松地通过管道(|)传递给其他命令进行进一步处理,这是其强大灵活性的体现。

BASH
# 生成一个多行列表,然后排序
cat << EOF | sort
banana
apple
cherry
date
EOF
# 输出:
# apple
# banana
# cherry
# date
 
# 生成一个SQL查询语句(变量已替换),然后通过mysql客户端执行
DB_NAME=“mydb”
TABLE_NAME=“users”
cat << EOF | mysql -u root -p$DB_PASSWORD
USE $DB_NAME;
SELECT * FROM $TABLE_NAME WHERE active = 1 LIMIT 10;
EOF

5.3 常见错误排查

  1. 错误:syntax error: unexpected end of file

    • 原因:这是最常见的问题。Shell没有找到结束标记EOF
    • 排查
      • 检查结束标记EOF是否顶格书写(对于<< EOF)。
      • 检查EOF前后是否有空格。例如EOF (后面有空格)或 EOF(前面有空格)都不行。
      • 对于<<- EOF,检查EOF前是否是Tab,如果是空格,同样会报错。
      • 检查开始标记<< EOF和结束标记EOF是否完全一致(大小写、字符)。
  2. 错误:Here Document内容中的变量没有被替换

    • 原因:开始标记被引用了,如<< ‘EOF‘
    • 解决:如果希望变量扩展,去掉引号。如果希望部分变量扩展,部分不扩展,可能需要改用<< EOF,并对不希望扩展的$进行转义(\$)。
  3. 使用<<-时,缩进没有被去除

    • 原因:缩进使用的是空格,而不是制表符(Tab)。
    • 解决:将编辑器中对Here Document区域的缩进设置为真正的Tab键。在Vim中,可以用:set list查看,Tab会显示为^I。在VSCode中,注意状态栏显示的是“Spaces:4”还是“Tab Size:4”,如果是Spaces,需要切换为Tab。
  4. cat > file操作后,文件是空的

    • 原因:你输入内容后,没有在新的一行按Ctrl+D,而是按了Ctrl+C或其他方式终止了命令。Ctrl+D必须在新行开头按下才表示输入结束。
    • 操作习惯:输入完所有内容后,按一次回车到新行,然后按Ctrl+D

6. 真实场景综合应用案例

让我们通过一个综合的、贴近实际的例子,把今天学的知识串起来。假设我们要写一个自动化部署脚本,用于在服务器上配置一个Web应用。

BASH
# !/bin/bash
# deploy_webapp.sh
set -e # 遇到错误立即退出
 
APP_NAME=“my_web_app”
USER=“www-data”
INSTALL_DIR=“/opt/$APP_NAME
NGINX_CONF_DIR=“/etc/nginx/sites-available”
 
# 1. 创建应用目录(这里用到了简单的命令和变量)
echo “正在创建应用目录…”
sudo mkdir -p “$INSTALL_DIR
sudo chown -R $USER:$USER$INSTALL_DIR
 
# 2. 生成应用配置文件 (使用 <<- EOF 保持脚本缩进美观)
# 注意:下面的缩进必须是Tab
CONFIG_FILE=“$INSTALL_DIR/config.ini”
cat <<- EOF | sudo tee “$CONFIG_FILE” > /dev/null
# $APP_NAME 配置文件
[database]
host = localhost
port = 3306
name = ${APP_NAME}_db
user = app_user
# 密码应从安全存储中获取,此处仅为示例
password = $(openssl rand -base64 12)
 
[server]
port = 8080
log_level = INFO
log_file = /var/log/${APP_NAME}.log
EOF
echo “应用配置文件已生成:$CONFIG_FILE
sudo chmod 600 “$CONFIG_FILE# 配置文件通常设置严格权限
 
# 3. 生成Nginx站点配置 (使用 << ‘EOF‘ 防止变量过早扩展)
# 这里我们不想在生成配置时扩展$host变量,因为它是Nginx内部的变量。
NGINX_CONF=“$NGINX_CONF_DIR/$APP_NAME
cat << ‘EOF‘ | sudo tee “$NGINX_CONF” > /dev/null
server {
listen 80;
server_name myapp.example.com;
client_max_body_size 10M;
 
location / {
proxy_pass http://127.0.0.1:8080; # 这里的端口对应上面config.ini里的port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
 
location /static/ {
alias /opt/my_web_app/static/;
expires 30d;
}
}
EOF
echo “Nginx配置文件已生成:$NGINX_CONF
 
# 4. 启用Nginx配置(创建符号链接)
sudo ln -sf “$NGINX_CONF” “/etc/nginx/sites-enabled/”
sudo nginx -t && sudo systemctl reload nginx
echo “Nginx配置已重载。”
 
# 5. 创建一个简单的启动脚本 (使用 cat > file 的变体,但通过echo和Here Document结合)
SERVICE_FILE=“/etc/systemd/system/$APP_NAME.service”
sudo bash -c “cat > ‘$SERVICE_FILE’ << ‘EOF’
[Unit]
Description=My Web Application Service
After=network.target
 
[Service]
Type=simple
User=$USER
WorkingDirectory=$INSTALL_DIR
ExecStart=/usr/bin/python3 app.py
Restart=on-failure
Environment=“CONFIG_PATH=$INSTALL_DIR/config.ini”
 
[Install]
WantedBy=multi-user.target
EOF”
echo “Systemd服务文件已创建:$SERVICE_FILE
sudo systemctl daemon-reload
sudo systemctl enable –now “$APP_NAME.service”
 
echo “部署完成!应用服务已启动。”

在这个脚本中,我们综合运用了:

  • <<- EOF:用于生成config.ini,使脚本结构清晰。
  • << ‘EOF‘:用于生成Nginx配置,确保Nginx变量(如$host)不被当前Shell解析。
  • 通过sudo tee来提升权限写入文件,这是一种比在cat整个命令前加sudo更安全、更清晰的做法。
  • 使用了变量替换、命令替换$(…),展示了Here Document与Shell功能的完美结合。

理解cat在重定向和Here Document中的这些细微差别,绝非纸上谈兵。它直接关系到你写的脚本是否健壮、可读、可维护。下次当你需要在脚本中生成文件时,不妨停下来想一想:我是要原样输出,还是要替换变量?我是否需要保持脚本的缩进美观?想清楚这几个问题,你就能从catEOF和那一堆符号中,选出最得心应手的那把“瑞士军刀”。