// 安全研究 · 2025-06-07

命令行中的“隐形符号”:从 - 标准输入到 -- 选项终止符

1. 引言:被忽视的命令行符号

在 Linux 命令行中,有一些符号看起来“不像命令”,却扮演着极其关键的角色。它们不是可执行文件,也不是内置命令,而是 Shell 和命令行工具共同约定的特殊标记。

本文聚焦其中最核心的两个:

符号 名称 核心功能
- 单横线(标准输入/输出占位符) 表示“数据从这里流进来”或“数据从这里流出去”
-- 双横线(选项终止符) 表示“选项到此结束,之后都是普通参数”

这些符号的巧妙之处在于——它们不需要被“实现”,只需要被“遵守”。任何遵循 POSIX 标准的命令行工具,都会默认支持这些约定。

2. -:标准输入/输出的“通配符”

2.1 核心理念:位置即含义

在命令行中,- 不是一个“选项”,而是一个位置占位符。当工具在预期“文件名”的位置看到 - 时,它会理解为:

  • 如果是从文件读取数据 → 从标准输入(stdin)读取
  • 如果是向文件写入数据 → 向标准输出(stdout)写入
graph LR
    A["命令参数位置"] --> B{"参数是 - ?"}
    B -->|"读取操作"| C["从 stdin 读取"]
    B -->|"写入操作"| D["向 stdout 写入"]
    B -->|"其他操作"| E["字面文件名 '-'"]

2.2 典型使用场景

场景一:压缩/解压时的流式处理

# 将 tar 输出重定向到 gzip(而非写入文件)
tar -cf - ./directory | gzip > archive.tar.gz
#     ↑ 这里 - 表示“输出到 stdout”

# 从标准输入读取,解压到当前目录
gzip -dc archive.tar.gz | tar -xf -
#                                 ↑ 这里 - 表示“从 stdin 读取”

场景二:用 diff 比较文件与标准输入

# 比较文件与管道输出
echo "new content" | diff file.txt -
# 将 file.txt 与 stdin 的内容进行比较

# 比较命令输出与文件
find . -type f | sort | diff sorted_files.txt -

场景三:替换标准文件操作

# 将 echo 的输出写入文件(- 通常代表 stdout,但这里 cat 会自动处理)
echo "hello" | cat - > output.txt
# 等同于:echo "hello" > output.txt

# 合并文件与标准输入
cat file.txt - <<< "additional line"
# 先输出 file.txt,再输出 "additional line"

场景四:程序间管道连接

# 第一步:生成数据
generate_data > data.txt

# 可以替换为:
generate_data | process_data | tee output.txt
# 不需要中间文件,纯流式处理

2.3 - 的变体

写法 含义 示例
- 标准输入/输出 cat -
/dev/stdin 标准输入设备文件 cat /dev/stdin
/dev/stdout 标准输出设备文件 echo "hi" > /dev/stdout
/dev/fd/0 文件描述符 0 cat /dev/fd/0

在某些需要文件路径的场景(如 docker run -v),/dev/stdin 比 - 更可靠。

3. --:选项边界的“界碑”

3.1 问题起源:当文件名以 - 开头

普通用户几乎不会创建以 - 开头的文件,但在以下场景中非常常见:

  • 从不可信来源下载的文件(攻击者可能命名为 -rf)
  • 程序生成的临时文件(如 Git 的 .git/refs/heads/-d)
  • 测试数据中的特殊命名
# 创建一个名为 -f 的文件
touch -- -f
# 或
touch ./-f

# 尝试读取它
cat -f
# 错误:-f 被解析为 cat 的选项(期待后续参数)

3.2 解决方案:-- 终止选项解析

-- 告诉命令行解析器:“后面的所有内容,无论是否以 - 开头,都视为普通参数,不要再作为选项处理”。

# 正确删除 -f 文件
rm -- -f

# 正确查看 -f 文件内容
cat -- -f

# 正确查找名为 -name 的文件
grep -- "-pattern" file.txt

3.3 -- 在其他场景的应用

场景一:避免参数冲突:

# grep 搜索 " -v" 字符串(而非使用 -v 选项)
grep -- " -v" file.txt

场景二:明确参数边界(脚本安全):

#!/bin/bash
# 使用 -- 保护用户输入
rm -- "$user_input"   # 即使 user_input = "-rf /",也会被当作文件名处理

场景三:传递给子命令(常见于包装脚本):

# 将所有参数传递给 ssh
ssh user@host -- "$@"

4. 易混淆概念的辨析

4.1 - 不是重定向

# 重定向:改变数据流向
command > file      # stdout → file
command < file      # stdin ← file

# - 占位符:告诉工具从 stdin 读取
command -          # 等价于 command /dev/stdin

重定向是 Shell 的语法,- 是工具的约定。

4.2 - 与 -- 的不同层次

特性 - (stdin/stdout) -- (选项终止)
作用对象 数据内容 命令行参数解析
处理层级 应用程序内部 命令行解析器(Shell / getopt)
实现方式 工具自身需实现 getopt() 库原生支持
典型配合 管道链 参数边界保护

4.3 - 在非 GNU 工具中的差异

并非所有工具都支持 - 作为 stdin/stdout 的占位符。例如 cp:

cp - file.txt
# 错误:cp 不将 - 识别为标准输入

此时应使用 /dev/stdin:

cat file.txt | cp /dev/stdin copy.txt   # 部分系统支持

5. 实战组合:- 与 -- 的协同

5.1 从压缩包中提取特定文件并处理

tar -czf - -- ./src | ssh user@host "tar -xzf - -C /dest"
#      ↑ -- 确保 ./src 不被解析为选项       ↑ - 从 stdin 读取

5.2 安全处理用户输入的文件名

#!/bin/bash
# 假设用户可能输入以 - 开头的文件名
while IFS= read -r file; do
    # -- 确保 file 中的 - 不会被当作选项
    # - 作为 cat 的输入占位符
    cat -- "$file" | process_command -
done < filelist.txt

6. 总结:小符号,大作用

符号 一句话总结 记忆口诀
- “我不在乎文件名,给我数据流就行” 用 - 接水管
-- “后面的都别解析了,全是普通参数” 用 -- 划界限

这两个符号的设计体现了 Unix 哲学的精髓:简单的约定,通用的理解。

  • - 是工具作者需要“主动支持”的功能——写工具时,检查参数是否为 -,如果是则切换到 stdin/stdout
  • -- 是 getopt() 类库“免费提供”的能力——任何使用标准选项解析的命令行工具都自动支持

理解它们,意味着:

  • 在管道链中可以写出更简洁的命令(tar -cf -)
  • 在脚本中可以安全处理任意文件名(rm -- "$var")
  • 在编写工具时,可以遵循这些“隐形约定”,让自己写的命令更符合 Unix 生态的预期

原文 https://blog.csdn.net/2301_79518550/article/details/148503080