第 3 章 · 03 彩色输出与交互输入 本节摘要: 这一节把 CLI 从「能用」推进到「好用」——讲两块「门面功夫」: 彩色输出与交互输入。彩色输出靠的是 ANSI 转义码(一串以 ESC 开头的特殊字符,能让终端显示颜色、粗体、清屏),但好 CLI 的细节不在「能不能上色」,而在「该不该上色」——当输出被管道重定向到文件时,必须自动去色,否则转义码会污染数据。交互输入讲三种常见场景: y/N 确认、密码隐藏输入(关闭终端回显)、选择菜单(编号选择)。本节的核心洞见是一条原则: 交互是脚本化的敌人——好 CLI 只在必要时交互,且永远提供 --yes 跳过交互的选项,保证工具能被脚本无人值守调用。这是「可脚本化优先」原则的具体落地。
本节摘要: 这一节把 CLI 从「能用」推进到「好用」——讲两块「门面功夫」: 彩色输出与交互输入。彩色输出靠的是 ANSI 转义码(一串以 ESC 开头的特殊字符,能让终端显示颜色、粗体、清屏),但好 CLI 的细节不在「能不能上色」,而在「该不该上色」——当输出被管道重定向到文件时,必须自动去色,否则转义码会污染数据。交互输入讲三种常见场景: y/N 确认、密码隐藏输入(关闭终端回显)、选择菜单(编号选择)。本节的核心洞见是一条原则: 交互是脚本化的敌人——好 CLI 只在必要时交互,且永远提供 --yes 跳过交互的选项,保证工具能被脚本无人值守调用。这是「可脚本化优先」原则的具体落地。
内容来源: 基于原索引「Build your own X」Command-Line Tool 域相关条目整理的导读,原始教程为外部资源(如「Build your own grep」「ANSI Escape Sequences」等)。
阅读完本节,你应当能够:
彩色和交互,是 CLI 工具里最有「即时反馈」的两块——你给工具加了颜色,错误信息红色、成功信息绿色,用户立刻觉得「这工具专业」;你加了 y/N 确认,删除前问一句,用户觉得「这工具体贴」。所以新手特别爱加这两块功能。
但这两块也是最容易翻车的地方:
tool > result.txt 把输出存到文件,打开一看全是 \033[31m 这种乱码——转义码混进了数据,文件废了。这两类翻车的根因是同一个: 工具分不清「现在是给人看」还是「现在是被脚本调」。彩色和交互都是「人味儿」很重的功能,适合交互式使用,但不适合脚本化场景。好 CLI 的处理是——根据上下文动态切换:
所以这一节表面讲「怎么上色、怎么做交互」,实际讲的是「怎么让工具既对人友好,又对脚本可靠」——这是上一节「可脚本化优先」原则的具体落地。学会这两块,你的工具才算真正「专业」。
关键概念: 彩色和交互都是「双刃剑」——用对人爽,用错坑脚本。本节的全部技术细节(ANSI 码、isatty 检测、--yes 跳过)都是围绕「如何让这两块功能只在合适的场景生效」展开的。
终端显示颜色、粗体、下划线、清屏,靠的不是什么魔法,而是ANSI 转义码——一串特殊的字符序列,以 ESC 字符(ASCII 27,十六进制 0x1B,通常写作 \033 或 \e)开头,后跟一个 [,再跟一串用分号分隔的参数,最后以一个字母结尾(m 表示设置样式,J 表示清屏,K 表示清行……)。
\033[31m ← 设置前景色为红色 (31 = 红) \033[1m ← 设置粗体 (1 = bold) \033[1;31m ← 粗体 + 红色 (多个参数用分号连) \033[0m ← 重置所有样式 (0 = reset, 必须配对使用!) \033[2J ← 清屏 (J = erase display, 2 = 整屏) \033[2K ← 清当前行 (K = erase line) \033[1;1H ← 光标移到第 1 行第 1 列 (H = position)
常用颜色码: 30 黑、31 红、32 绿、33 黄、34 蓝、35 紫、36 青、37 白;加 10 变背景色(40-47)。粗体 1、暗淡 2、下划线 4、反色 7。
给输出加色的代码极其简单——就是把转义码字符串拼到文本前后:
# 伪代码: 加色函数 RED = "\033[31m" GREEN = "\033[32m" BOLD = "\033[1m" RESET = "\033[0m" def red(text): return RED + text + RESET # 注意: 文本结束后必须 RESET def green(text): return GREEN + text + RESET print(red("错误: 文件不存在")) print(green("成功: 已完成"))
⚠️ 难点预警: ANSI 码最大的坑是「忘记 RESET」。如果你写了
\033[31m错误却没在末尾加\033[0m,那么从这之后所有输出都会变成红色——包括终端提示符、后续命令的输出,直到用户手动 reset 或关掉终端。养成习惯: 每次上色,文本结尾一定配一个 RESET,像 HTML 的开闭标签一样成对出现。
光会写 ANSI 码还不够,真正区分「业余 CLI」和「专业 CLI」的是这一步: 判断输出是不是终端,不是就别上色。
为什么这么重要?考虑这个场景:
$ mytool --stats > result.txt
用户把输出重定向到文件。如果 mytool 不管三七二十一都往输出里塞 ANSI 码,那 result.txt 打开就是这样的乱码:
\033[32m成功: 已完成\033[0m \033[31m错误: 文件不存在\033[0m
转义码混进了数据,文件直接废了——后续如果想用 grep、awk 处理这个文件,全部匹配失败。
解法是在上色前检测输出目标: 如果是终端(交互式使用),上色;如果是文件或管道(被重定向),去色。检测靠的是 isatty(is a TTY)系统调用——它接受一个文件描述符,返回「这个描述符是不是连着终端」。
# 伪代码: 智能 UIColor 函数 import sys def supports_color(): # stdout 是终端, 且环境没禁用颜色 return sys.stdout.isatty() and "NO_COLOR" in environ == False def red(text): if not supports_color(): return text # 不是终端: 原样返回, 不加色 return "\033[31m" + text + "\033[0m"
这里有几个细节值得注意:
sys.stderr.isatty()。NO_COLOR=1 环境变量,所有工具都应该禁用颜色(有些用户在浅色终端上看深色文字费劲)。检测到 NO_COLOR 就强制去色,是好工具的礼貌。--no-color 命令行选项,让用户显式禁色。┌────────────────────────────────────────────────┐ │ 上色决策流程: │ │ │ │ 用户敲 mytool --stats > result.txt │ │ │ │ │ ▼ │ │ isatty(stdout)? ──── 否 ──→ 不上色 (管道/文件)│ │ │ 是 │ │ ▼ │ │ 有 NO_COLOR 环境变量? ── 是 ─→ 不上色 │ │ │ 否 │ │ ▼ │ │ 有 --no-color 选项? ── 是 ─→ 不上色 │ │ │ 否 │ │ ▼ │ │ 上色 ✓ │ └────────────────────────────────────────────────┘
这个决策流程是「智能彩色」的核心。把这套逻辑封装好,你的工具就能「该色时色,不该色时不色」——这是成熟 CLI(grep、ls、git)都遵循的细节。
💡 学习建议: 别小看 isatty 这一步——它是区分「玩具工具」和「生产级工具」的标志。一个不检测 isatty 的工具,在管道场景下会污染数据,用户用一次就再也不用。第一次造 CLI,把这套检测写成工具函数(比如
red()green()都内置检测),一劳永逸。
交互输入有三种常见场景,每种都有标准做法。
场景一: y/N 确认。删除、覆盖、危险操作前,问一句「确定吗?」:
def confirm(prompt, default=False): # default=False 表示默认"否" (大写 N 提示) hint = "[y/N]" if not default else "[Y/n]" answer = input(prompt + " " + hint + " ").strip().lower() if answer == "": return default # 直接回车 = 用默认值 return answer in ["y", "yes"] if confirm("确定要删除 todo #3?", default=False): delete_todo(3)
这里有几个细节: 默认值用大小写提示([y/N] 表示默认否,[Y/n] 表示默认是)、直接回车等于默认值、接受 y/yes/n/no 各种写法。这套细节是 Unix 工具的通用惯例,用户肌肉记忆里就有。
场景二: 密码隐藏输入。输密码时终端不回显(不显示打出的字符),靠的是关闭终端的 echo:
import termios, sys def read_password(prompt): sys.stdout.write(prompt) sys.stdout.flush() # 保存当前终端设置 old = termios.tcgetattr(sys.stdin.fileno()) new = old.copy() new[3] = new[3] & ~termios.ECHO # 关闭回显位 try: termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, new) pwd = sys.stdin.readline().rstrip("\n") finally: termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, old) sys.stdout.write("\n") # 补一个换行 (回显关了, 回车没显示) return pwd password = read_password("密码: ")
这个操作要直接调终端控制(termios 在 Unix、console API 在 Windows),比读普通 input 麻烦。所以实践中通常用现成库(Python 的 getpass、Node 的 readline-async)——但这套机制你得理解: 它靠的是「关闭终端回显」,而不是「把字符替换成星号」(那是另一种实现,更复杂且没必要)。
场景三: 选择菜单。多个选项里选一个,用编号:
def choose(prompt, options): for i, opt in enumerate(options, 1): print(f" {i}. {opt}") while True: answer = input(f"{prompt} (1-{len(options)}): ").strip() if answer.isdigit() and 1 <= int(answer) <= len(options): return options[int(answer) - 1] print("无效选择, 请重试") choice = choose("选择优先级", ["低", "中", "高"])
编号选择菜单适合选项有限(三五个)且选项本身是文字(不好用短选项表达)的场景。
讲完三种交互,必须强调一条贯穿全节的原则: 交互是脚本化的敌人。
为什么?考虑这个场景: 用户写了个脚本,每天凌晨跑 todo cleanup 清理过期任务。如果 cleanup 每次都问「确定要清理 12 条任务吗?(y/N)」,那脚本就会卡在第一个确认上死等输入——而凌晨没人会去敲回车,脚本挂了一整夜。
交互式确认对人友好(防止误操作),但对脚本致命(破坏无人值守)。好 CLI 的处理是:
--yes(默认 yes)、--force(强制不问)、--no-input(禁用所有交互)。这样脚本能用 todo cleanup --yes 无人值守跑。$ todo rm 3 确定要删除 todo #3? [y/N] y ← 交互式: 问 (已删除) $ todo rm 3 --yes ← 脚本化: 不问, 直接删 (已删除)
这条原则是「可脚本化优先」的具体落地——你的工具首要职责是「能被脚本可靠调用」,交互是次要的「人味儿」点缀。两者冲突时,牺牲交互保脚本化。
⚠️ 难点预警: 一个常见反模式是「删除前必须确认,且没提供跳过选项」。这种工具在交互式用着挺好,一旦写进自动化脚本就灾难——脚本作者只能用
echo "y" | todo rm 3这种 hack 喂输入,既丑陋又脆弱(换个问题就失效)。从一开始就加 --yes,别等用户来骂。
掌握了 ANSI、isatty、交互三段,上手时按这个顺序逐层落地:
第一步: 写彩色函数。封装 red/green/yellow/blue 四个函数,内部用 ANSI 码包文本,配对加 RESET。先用最朴素的方式——不管 isatty,直接上色。这一步让你熟悉 ANSI 码的形态,跑通后会发现「忘加 RESET」的各种坑。
第二步: 加 isatty 检测。给彩色函数加一层 if not supports_color(): return text,实现「管道时去色」。同时检测 NO_COLOR 环境变量和 --no-color 选项。这一步让你的工具从「玩具」升级到「生产级」。
第三步: 加 y/N 确认。给危险操作(rm、覆盖)加确认函数,注意默认值用大小写提示、回车等于默认。
第四步: 加 --yes 跳过。任何交互都必须有对应的跳过选项,保证脚本能无人值守。这一步是「可脚本化优先」的硬要求,不能省。
第五步: (可选)加密码输入与选择菜单。只有你的工具真的需要这两块功能时才加——比如造数据库迁移工具需要输密码,造配置生成器需要多选菜单。不需要就别加,别为了炫技堆功能。
💡 学习建议: 彩色和交互都是「锦上添花」的功能,不是核心。先把工具的核心功能(参数解析、业务逻辑、退出码)做扎实,再回来加这两块。新手常犯的错是「花一天调颜色,核心功能还没跑通」——顺序搞反了。
外部有不少「Build your own grep」之类的练习项目,grep 是练习彩色的好题目(匹配到的部分高亮),推荐拿它练手。你可以先做朴素版(不上色),再升级到彩色版(匹配高亮 + isatty 检测),两版对比能让你深刻体会「上色不难,智能上色才难」。
下一节是本章的综合: 我们先把工具「打包分发」——讲脚本语言的包管理分发、编译型语言的跨平台二进制、为什么「单二进制」是分发圣杯;然后把本章学到的参数解析、接口设计、彩色输出、交互输入全部串起来,设计一个完整的 todo CLI(支持 add/list/done/rm 子命令、配置文件、彩色输出),作为全章的综合应用题。