- 开发工具
【免费下载链接】pterm
✨ PTerm is a modern Go module to easily beautify console output. Featuring charts, progressbars, tables, trees, text input, select menus and much more 🚀 It's completely configurable and 100% cross-platform compatible.
导读
本文围绕 PTerm 官方示例 interactive_textinput/password 展开,讲解如何通过InteractiveTextInputPrinter的WithMask()方法,将普通文本输入框改造成不回显真实字符的密码输入控件。读完本文,你将掌握 Mask 掩码的完整用法、底层渲染原理、键盘交互行为与安全注意事项,并能在自己的 Go CLI 项目中直接落地一个带口令输入的交互界面。
一、示例全景:一段代码完成密码输入
示例 main.go 的完整代码非常精简,全文如下:
package main import "github.com/pterm/pterm" func main() { // WithMask echoes the given string instead of the typed characters, // which turns the text input into a password prompt. passwordInput := pterm.DefaultInteractiveTextInput.WithMask("*") result, _ := passwordInput.Show("Enter your password") // Never log passwords in a real application, this is just a demo. logger := pterm.DefaultLogger logger.Info("Password received", logger.Args("password", result)) }这段代码只用两个步骤就完成了密码输入:
- 开启掩码:
pterm.DefaultInteractiveTextInput.WithMask("*")返回一个*InteractiveTextInputPrinter,用户每输入一个字符,屏幕上回显的不是该字符本身,而是一个*号。 - 显示提示并获取结果:
Show("Enter your password")在终端渲染出提示语Enter your password并进入键盘监听循环;用户按回车后,方法返回真正输入的内容(而非掩码串)。
运行方式也很简单,进入示例目录直接执行即可:
cd _examples/interactive_textinput/password go run main.go说明:示例末尾用
pterm.DefaultLogger打印了结果,其logger.Args("password", result)会按「键-值」成对方式把参数组装成结构化日志字段(见 logger.go)。注释里特意强调:真实应用中绝不要记录密码,这里仅为演示。
二、掩码背后的实现:Mask 字段与 WithMask
要理解WithMask的语义,需要先看InteractiveTextInputPrinter的定义(interactive_textinput_printer.go):
type InteractiveTextInputPrinter struct { TextStyle *Style DefaultText string DefaultValue string Delimiter string MultiLine bool Mask string OnInterruptFunc func() // ... 内部状态字段 }其中Mask字段就是控制回显方式的关键,其默认值在DefaultInteractiveTextInput中为空字符串(interactive_textinput_printer.go):
var DefaultInteractiveTextInput = InteractiveTextInputPrinter{ DefaultText: "Input text", Delimiter: ": ", TextStyle: &ThemeDefault.PrimaryStyle, Mask: "", }而WithMask是一个典型的「复制式」链式方法——它基于当前实例复制一份并修改Mask字段后返回新指针,不会污染默认实例(interactive_textinput_printer.go):
func (p InteractiveTextInputPrinter) WithMask(mask string) *InteractiveTextInputPrinter { p.Mask = mask return &p }关键机制:Mask语义是「用给定字符串替换每个已输入字符的回显」。也就是说传入"*"时每个字符显示一个星号;如果传入"●",每个字符就会显示一个圆点;传入"?"则显示问号。用户可以按需选择掩码符号。
三、底层渲染原理:updateArea 的掩码替换逻辑
掩码不是输入时逐字符处理的,而是在每次重绘输入区域时统一替换,核心逻辑位于 interactive_textinput_printer.go 的updateArea方法:
func (p InteractiveTextInputPrinter) updateArea(area *cursor.Area) string { // ... areaText := p.text for i, s := range p.input { if i < len(p.input)-1 { areaText += s + "\n" } else { areaText += s } } if p.Mask != "" { areaText = p.text + strings.Repeat(p.Mask, internal.GetStringMaxWidth(areaText)-internal.GetStringMaxWidth(p.text)) } area.Update(areaText) // ... 光标定位逻辑 return areaText }可以拆解出以下三点:
- 渲染与存储分离:用户输入的原始文本始终保存在
p.input切片里,掩码只影响「显示层」。 - 按宽度重复掩码:
strings.Repeat(p.Mask, ...)根据已输入内容的显示宽度(internal.GetStringMaxWidth计算)重复掩码字符串,保证回显的掩码数量与字符宽度一致。这解决了中文、全角字符等宽字符与掩码宽度对齐的问题。 - 提示语保留:掩码替换只发生在输入内容上,
p.text(提示语 + 分隔符)原样保留,所以Enter your password: ****这样的完整画面才能正确渲染。
整套交互基于cursor.NewArea()创建的实时区域(live area):每次按键后area.Update原地重绘,而不是追加新行,因此输入过程中终端画面是平滑刷新的。
四、测试验证:掩码渲染与真实返回值
仓库测试 interactive_textinput_printer_test.go 专门为掩码行为写了断言,是理解WithMask语义最直接的证据:
func TestInteractiveTextInputPrinter_MaskRendersMaskButReturnsText(t *testing.T) { var result string output := captureUserFacingStdout(t, func() { simulateKeys(t, "abc", keys.Enter) result = showTextInput(t, pterm.DefaultInteractiveTextInput.WithMask("*")) }) assert.Equal(t, "abc", result, "the returned text must be the real input, not the mask") assert.Contains(t, stripANSI(output), "***", "the rendered input must be masked") assert.NotContains(t, output, "abc", "the real input must never be rendered") }该测试同时验证了两条核心契约:
- 返回值是真实输入:模拟键入
abc后Show返回"abc",而不是"***"; - 屏幕上是掩码:渲染输出包含
***且绝不包含abc,保证真实输入不会泄露到屏幕上。
从源码结构看,这也意味着密码输入的回显保护是库层面保证的——只要设置了Mask,正常渲染路径就不会输出原始字符。
五、键盘交互与中断处理
Show()通过atomicgo.dev/keyboard的keyboard.Listen进入按键监听(interactive_textinput_printer.go),与密码场景相关的主要行为有:
| 按键 | 单行模式行为 |
|---|---|
Enter | 提交输入,Show返回结果 |
Backspace | 删除光标前一个字符 |
Delete | 删除光标处字符;若尚未输入且存在DefaultValue,则清空默认值 |
Ctrl+C | 触发取消信号(见下文) |
Left/Right | 在输入内容内移动插入位置 |
其中Ctrl+C的处理值得一提:Show开头会通过internal.NewCancelationSignal(p.OnInterruptFunc)注册取消回调(cancelation_signal.go),按下Ctrl+C时如果设置了OnInterruptFunc则调用它,否则默认以退出码 1 退出进程(测试 interactive_textinput_printer_test.go 验证了该默认行为)。对密码输入而言,中断意味着用户放弃了本次输入,默认直接退出通常是符合直觉的行为。
六、与其他选项组合:打造完整的输入交互
WithMask可以自由地与InteractiveTextInputPrinter的其他链式方法组合,满足更复杂的表单场景:
// 带默认值 + 掩码 + 自定义分隔符与提示文本 passwordInput := pterm.DefaultInteractiveTextInput. WithMask("●"). WithDefaultValue("initial"). WithDelimiter(" > "). WithTextStyle(pterm.NewStyle(pterm.FgCyan)). WithOnInterruptFunc(func() { fmt.Println("用户取消了输入") }) result, err := passwordInput.Show("Admin password")各选项说明(字段定义见 interactive_textinput_printer.go,设置方法见 L39-L79):
WithDefaultValue(value string):预填默认值。按回车直接提交默认值;一旦开始键入则从默认值基础上编辑;按Delete可清空后重新输入(行为均有测试覆盖,见 interactive_textinput_printer_test.go)。WithDelimiter(delimiter string):设置提示语与输入区之间的分隔符,默认": "。WithTextStyle(style *Style):设置提示语样式,默认ThemeDefault.PrimaryStyle。WithMultiLine(multiLine ...bool):切换多行模式——回车换行、Tab提交。注意密码场景一般保持单行;多行模式下掩码逻辑同样生效,但建议密码输入始终使用单行模式。WithOnInterruptFunc(exitFunc func()):自定义Ctrl+C时的行为。
链式方法均返回副本而非修改原对象,所以可以像上面这样把
DefaultInteractiveTextInput当作模板反复派生,各副本互不影响。
七、安全实践:掩码不等于加密
结合示例注释与实现,使用密码输入时需要明确几点:
- 掩码只防「肩窥」:
WithMask解决的是屏幕回显问题,防止旁人看到你输入的密码字符;它不改变输入内容本身,返回值仍是明文。 - 绝对不要记录密码:示例里打印密码仅为演示
logger.Args的用法,注释明确警告「Never log passwords in a real application」。真实应用中应直接消费result用于鉴权,并尽快从内存中清理。 - 结合
pterm.DisableOutput()做静默模式:如果应用需要--quiet等全局静默开关,可以用 pterm.go 中的DisableOutput()/EnableOutput()统一控制所有 PTerm 输出。 - 默认值慎用于密码:
WithDefaultValue会把默认值显示在输入区中,与掩码叠加时会先以掩码形式回显默认值;若默认值本身是敏感内容,建议密码输入时不设置默认值。
八、运行演示与 CI 自动化
仓库为该示例提供了两套运行入口:
- 手动运行:
go run main.go会启动交互式输入,等待真实键盘输入(参见 _examples/test.sh 中批量验证示例的模式)。 - CI 自动化演示:ci.go 检测到环境变量
CI=true时,会用keyboard.SimulateKeyPress模拟键入Hello, World!并自动按回车,实现无人值守运行,用于录制示例动画:
if os.Getenv("CI") == "true" { go func() { time.Sleep(time.Second) input := "Hello, World!" for _, r := range input { if r == '\n' { keyboard.SimulateKeyPress(keys.Enter) } else { keyboard.SimulateKeyPress(r) } time.Sleep(time.Millisecond * 250) } keyboard.SimulateKeyPress(keys.Enter) }() }如果你想在本地复现这段演示,可设置CI=true后运行(会看到自动键入并被掩码回显的完整过程)。这也是库内交互类打印机的通用测试模式:通过keyboard.SimulateKeyPress模拟按键流驱动Show()返回(见 interactive_test.go)。
九、相关示例与延伸阅读
掩码输入是 PTerm 交互文本输入家族的一个分支,围绕同一打印器还有其他三个示例(汇总于 _examples/interactive_textinput/README.md):
| 示例 | 路径 | 核心能力 |
|---|---|---|
| 基础输入 | _examples/interactive_textinput/demo/main.go | 单行默认行为,回车提交 |
| 默认值 | _examples/interactive_textinput/default-value/main.go | WithDefaultValue预填建议值 |
| 多行 | _examples/interactive_textinput/multi-line/main.go | WithMultiLine,回车换行、Tab 提交 |
| 密码 | _examples/interactive_textinput/password/main.go | WithMask掩码回显 |
若需要更深层的定制,可以继续阅读打印器完整实现 interactive_textinput_printer.go(含全部链式方法与按键处理)以及配套测试 interactive_textinput_printer_test.go(覆盖默认值、掩码、多行、中断等 8 组场景)。结合这些源码,你可以在自己的 CLI 工具中构建包含用户名、密码、确认密码等完整表单的交互式引导流程。
- 开发工具
【免费下载链接】pterm
✨ PTerm is a modern Go module to easily beautify console output. Featuring charts, progressbars, tables, trees, text input, select menus and much more 🚀 It's completely configurable and 100% cross-platform compatible.
相关推荐
PTerm 交互式文本输入入门:用 InteractiveTextInput 在终端收集用户输入
PTerm 交互式文本输入入门:用 InteractiveTextInput 在终端收集用户输入 本指南以 PTerm 官方示例 interactive_tex
开发工具PasscodeInput 密码输入框实战指南:ant-design-mobile 验证码/密码脱敏输入组件的完整使用与源码解析
PasscodeInput 密码输入框实战指南:ant design mobile 验证码/密码脱敏输入组件的完整使用与源码解析 本文聚焦 ant design
UI组件前端移动开发blendOS终极指南:如何无缝融合所有Linux发行版、Android和Web应用
blendOS终极指南:如何无缝融合所有Linux发行版、Android和Web应用 blendOS是一款终极Linux操作系统,它能够无缝融合所有Linux发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考