有交互CLI——用户流程第一次变成循环

无交互CLI的用户流程是一条直线:输入→输出→结束。有交互CLI打破了这个模式——程序启动后不退出,用户可以反复输入命令,每次输入都得到输出,直到用户主动说「退出」。用户流程从直线变成了循环,这一个变化带来了全新的设计问题。


一、从直线到循环

无交互CLI:

1
2
3
$ grep "hello" file.txt
hello world
$ ← 程序已退出

有交互CLI:

1
2
3
4
5
6
7
8
9
$ tool
> scan file.bin
found at offset 42
> status
running, 3 threads
> scan another.bin
not found
> exit
$ ← 用户主动退出

区别:

1
2
3
4
5
6
7
无交互CLI:
启动 → 执行 → 输出 → 退出
(直线)

有交互CLI:
启动 → [输入 → 执行 → 输出 → 再输入 → 执行 → 输出 → ...] → 退出
(循环)

循环的引入让一切都变了。


二、从直线到循环带来的会话

有交互 CLI 有了「会话」的概念——程序从启动到退出之间的整段时间是一个会话。

1
2
3
4
5
6
7
8
9
会话开始:tool 启动

用户输入命令 1 → 执行 → 输出

用户输入命令 2 → 执行 → 输出

...

用户输入 exit → 会话结束

这是有交互CLI的第一次质变:程序不再执行完就退出,而是持续运行等待下一次输入(会话状态的具体形态与演化见 06,第五节——状态流)。


三、命令循环:用户流程的核心结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart TD
A["等待用户输入"] --> B["解析命令"]
B --> C{"是 exit?"}
C -->|"是"| D["退出程序"]
C -->|"否"| E["执行命令"]
E --> F["输出结果"]
F --> G{"执行成功?"}
G -->|"是"| A
G -->|"否"| H["输出错误信息"]
H --> A

style A fill:#4a9eff,color:#fff
style D fill:#f44,color:#fff
style E fill:#ff9f4a,color:#fff

有交互CLI的用户流程是一个命令循环

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────┐
│ 等待用户输入 │ ← 循环起点
│ ↓ │
│ 解析命令 │
│ ↓ │
│ 执行命令 │
│ ↓ │
│ 输出结果 │
│ ↓ │
│ 是 exit? ──是──→ 退出 │
│ │ 否 │
│ └──→ 回到等待用户输入 │ ← 循环终点
└─────────────────────────────┘

这个循环的代码形态:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
char input[256];
while (1) {
printf("> ");
fgets(input, sizeof(input), stdin);

if (strcmp(input, "exit\n") == 0)
break;

execute(input);
}
return 0;
}

命令循环是有交互CLI的心脏。 所有后续讨论都围绕这个循环展开。


四、用户流程的完整路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
用户流程(完整):

1. 打开终端
2. 输入启动命令(如 tool)
3. 看到提示符 ">" ← 程序在等待
4. 输入命令
5. 看到输出
6. 看到提示符 ">" ← 程序在等待
7. 输入下一个命令
8. 看到输出
9. ...(重复 6-8)
10. 输入 exit
11. 看到程序退出
12. 终端提示符重新出现

和无交互CLI的区别:

1
2
无交互CLI:输入 → 等待 → 输出 → 结束
有交互CLI:输入 → 等待 → 输出 → 提示符 → 输入 → ... → exit → 结束

关键区别:程序不会自动退出,用户掌握退出权。 这意味着:

1
2
3
程序的生命周期 = 用户的会话时间
程序的状态在整个会话期间持续存在
用户可以随时决定「再做一件事」或「结束了」

五、命令解析:用户输入变成程序动作

无交互CLI的参数解析是一次性的——main(argc, argv) 解析一次就结束了。有交互CLI的参数解析是循环的——每次用户输入都要解析。

1
2
3
4
5
6
用户输入 "scan file.bin --verbose"
↓ 解析
Command: scan
Args: {path: "file.bin", verbose: true}
↓ 执行
scan_file("file.bin", true)

命令解析器的结构:

1
2
3
4
5
6
7
8
9
输入字符串
↓ 分词
[命令名, 参数1, 参数2, ...]
↓ 查找命令表
找到对应的处理函数
↓ 调用
执行命令
↓ 返回
回到命令循环

命令表是命令解析器的核心数据结构:

1
2
3
4
5
命令表:
"scan" → handle_scan()
"status" → handle_status()
"help" → handle_help()
"exit" → handle_exit()

如果用户输入了未知命令:

1
2
3
> foobar
unknown command: foobar
>

命令解析是有交互CLI的第二心脏。 第一是命令循环,第二是命令解析。


六、输出模式的变化

无交互CLI的输出是一次性的——程序输出所有结果后就退出了。有交互CLI的输出是分段的——每次命令执行后输出一部分,然后回到等待状态。

1
2
3
4
5
6
7
8
无交互CLI的输出:
全部结果一次性输出
用户看到的是完整输出

有交互CLI的输出:
每次命令输出一部分
用户看到的是增量输出
旧输出可能滚出屏幕

这带来了新的设计问题:

1
2
3
4
1. 输出格式化:如何让每次输出清晰可读?
2. 输出缓冲:是否需要在命令之间清屏?
3. 错误输出:错误信息和正常输出如何区分?
4. 大量输出:如果一次命令产生 1000 行输出怎么办?

七、错误流 / 控制流 / 反馈流的变化

三种流在有交互CLI都发生了第一次质变,跨形态完整展开见 06(控制流第二节、错误流第四节、反馈流第六节):

1
2
3
4
5
6
7
8
错误流:出错 → 输出错误 → 继续运行(无交互:出错 → 退出)
错误不再导致程序退出,程序必须能从错误中恢复、继续处理下一个命令。

控制流:命令循环掌握程序骨架,但「执行什么命令」由用户输入决定
用户是控制流的源头——用户驱动的循环。

反馈流:程序出错 → 用户看到错误 → 输入修正命令 → 同进程继续(无交互:跨进程重来)
反馈流第一次真正实现——用户成为了反馈回路的一部分。

八、退出:用户流程的终点

无交互CLI的退出:程序执行完毕自动退出。

有交互CLI的退出:用户主动决定退出

1
2
> exit
$

有交互CLI的生命周期由用户控制——用户决定什么时候开始,什么时候结束。 无交互CLI的生命周期由程序本身决定——执行完毕就结束(退出流的完整演化见 06 第七节)。


九、有交互CLI的用户流程总结

1
2
3
4
5
6
7
8
9
10
11
有交互CLI的用户流程:

启动 → 提示符 → [输入 → 解析 → 执行 → 输出 → 提示符] × N → exit → 结束

特征(用户流视角):
┌─────────────────────────────────────────┐
│ 命令循环 程序持续运行 │
│ 命令解析 用户输入变成程序动作 │
│ 增量输出 每次命令输出一部分 │
│ 用户控制退出 用户决定什么时候结束 │
└─────────────────────────────────────────┘

有交互CLI相比无交互CLI的核心变化:

维度 无交互CLI 有交互CLI
用户流程 直线 循环
状态 有会话状态
退出 自动 用户主动
错误 终止 恢复继续
反馈 跨进程重新启动 同进程内修正
控制流 程序自己控制 用户驱动循环

(状态/错误/反馈/控制/退出的跨形态演化合并见 06)

从无交互到有交互,一个交互让程序从「一次性工具」变成了「持续服务的会话」。

但有交互CLI仍然是**——纯文本——用户只能输入文字,看不到界面。当程序需要让用户「选择」「浏览」「拖拽」时,纯文本就力不从心了。下一篇看TUI——在文本终端上实现界面交互。