rust-tui-p3~> 终端中的各种概念
<~~ 发表日期:2023-10-12 | 本文词数:2251 | 预计阅读时间:12分钟 ~~>

了解一下关于原始模式, 管道, 标准输出等的概念吧

同系列传送门: rust-tui

注意:
这些概念已经尽量简化过了, 因为只是写一个 tui 程序的话不必太过深入, 但可能还是会忍不住扯开去, 见谅

原始模式

你可以观看来自 gnu 官网的原文, 里面有对此的解释

在 POSIX 系统中, 有着两种输入模式: canonical 或者 noncanonical
终端 IO 存在着这两种主要的模式:

  • canonical: 规范模式, 也叫作 cooked-mode, 是最常见的模式
    在该模式下, 会以行为单位, 读取输入(read函数), 在终端输入的字符被保存在缓冲区中
    当输入行结束符(换行/EOF)时, 缓冲区的内容才会被发送到对应的处理程序
    特殊字符不会被无视, 换行符也会像普通字符一样被缓冲区接收, 但 EOF 不会

  • nocanonical: 非规范模式, 也叫作 raw-mode, 也是非常常见的模式
    类似 ERASE 与 KILL 这些特殊的编辑字符会被禁用, 每次输入都会读取, 而不再是基于 line 为单位
    比如 vim, 或者我现在正在使用的 helix-editor, 就处于该模式下, 方便程序捕获并独占你的每份输入, 方便处理

ratatui 中位于 into_raw_mode 的文档, 对于原始模式下的输入的描述如下:

原始模式(raw-mode), 意味着你所输入的标准输入不会被打印/不会显示在屏幕上(它必须由程序手动编写)
此外, 输入不是 canonical 或 buffered 的, 也就是说, 您可以一次读取一个字节

简单来说, 原始模式让我们可以逐一捕获用户的输入, 便于对其进行处理
并且禁用且无视了特殊的字符, 进一步增强了我们程序的权能与对终端的掌控权限
(想想 vim 的操作, 再想想你在 bash 输入命令, 对比一下两者的区别)


文件描述符

在 Linux 系统中, 万物皆文件, 不管是进程也好, 还是你的设备也好, 都是文件, 且有一个对应的文件描述符(file descriptor)
这个文件描述符, 可以看作是系统为了追踪打开的文件而分配的唯一标号, 通过它对文件进行读写

Linux 在启动时, 会创建一个 init 进程, 此时自动创建 3 个特殊的文件描述符, 对应 3 个设备IO文件

3 个特殊的文件描述符:

文件含义描述符
/dev/stdin标准输入0
/dev/stdout标准输出1
/dev/stderr标准错误2

而之后产生的进程, 则都是 init 进程的子进程(一颗多叉树), 子进程会继承父进程的文件描述符
因此, 你打开的 shell(bash, zsh, fish, nushell等), 因为是子进程, 自然会有这三个文件描述符

简单理解:

  • stdin 对应的是键盘, 打字时的信息会输出到 /dev/stdin, stdin 与 stderr 都对应屏幕
  • 虽然 stdout/stderr 都是输出到屏幕, 但前者有缓冲, 后者无缓冲
  • 标准输出的缓冲区有个遇见换行会刷新缓冲区的特性

之后的子进程中所打开的文件, 文件描述符则从 3 开始往上递增


重定向

重定向(redirect), 光听名字就晓得什么意思了, 与网页的重定向一个道理
我们的 tui 程序, 就可以使用 管道(pipeline, 重定向的一种) 来跟更多的其他程序进行交互, 之后会讲

举个例子, 当你执行程序时, 从 stdin(键盘) 获取输入, 成功则将结果输出到 stdout(屏幕), 失败则将报错输出到 stderr(屏幕) 上
这是你没有指定任何重定向时的默认行为, 你会发现 stdout/stderr 都会输出到屏幕上, 极其容易造成混乱

让我们用 rust 来进行一点演示, 首先创建项目并跳转

cargo new --bin demo
cd demo

然后进行编辑:

src/main.rs
fn main() {
    println!("This is a str, which will be outputted to stdout");
    eprintln!("This is a str, which will be outputted to stderr");
}

当你运行 cargo run --quiet, 屏幕上将显示下列内容:

cargo run --quiet
This is a str, which will be outputted to stdout
This is a str, which will be outputted to stderr

println! 对应的应该是我们正常的输出, 而 eprintln! 应该对应一些报错信息, 可是它们同时出现了
这太混乱了, 当向 stdout 与 stderr 大量输出时更是如此, 我们要的是将 "正确的输出" 与 "报错的信息" 分开!

请对比以下三条命令(在bash中):

  • cargo run --quiet: 全部输出到屏幕
This is a str, which will be outputted to stdout
This is a str, which will be outputted to stderr
  • cargo run --quiet > output.log: 进行重定向, 将内容输出到 output.log 文件中, 默认只重定向 stdout
This is a str, which will be outputted to stderr
  • cargo run --quiet 1> output.log: 指定了文件描述符为 1, 重定向 stdout, 等价于默认行为
This is a str, which will be outputted to stderr
  • cargo run --quiet 2> error.log: 指定了文件描述符为 2, 重定向 stderr, 因此 stderr 的存到了 error.log
This is a str, which will be outputted to stdout

简单地了解下就差不多了, 其实还有很多内容, 不过懒得展开了, 知道两者区别就够了

本节最后使用 nushell(类似bash/zsh的现代shell) 再展示一下:

  • do {cargo run --quiet} | complete:
┌───────────┬──────────────────────────────────────────────────┐
│ stdout    │ This is a str, which will be outputted to stdout │
│           │                                                  │
│ stderr    │ This is a str, which will be outputted to stderr │
│           │                                                  │
│ exit_code │ 0                                                │
└───────────┴──────────────────────────────────────────────────┘

nushell 可以直接帮我们结构化这段信息, 因此我放上来方便小伙伴们理解

再来一个例子, 当使用 cat 命令时传入一个不存在的文件:

  • do {cat non-exist.txt} | complete:
┌───────────┬───────────────────────────────────────────────┐
│ stdout    │                                               │
│           │                                               │
│ stderr    │ cat: non-exist.txt: No such file or directory │
│           │                                               │
│ exit_code │ 1                                             │
└───────────┴───────────────────────────────────────────────┘

而如果使用 bash, 自然就无法这么简单地得到如此结构化的数据了:

cat: non-exist.txt: No such file or directory

管道

此处以 bash 中的管道(pipeline)为例
管道命令的操作符时 "|", 形式为 command_1 | command_2, 表示将前者的输出作为后者的输入, 即 stdout -> stdin
使用管道时, 两个命令会依次被调用,

值得注意的有两点:

  • 管道只接收代表着正确的输出, 也就是 stdout 作为后一个命令的输入, 因此无法接受 stderr 的输出
  • 管道后面的命令必须能够接收 stdin 才行

来点例子, 假设存在这么一个文件 README.md:

  • cat README.md | wc -m: 当前者命令的输出为 stdout 时, 成功通过管道传递, 屏幕上不显示前者命令的结果, 因为已被传走
57
  • cat REDM.m | wc -m: 当前者命令的输出为 stderr 时, 无法通过管道传递, 屏幕上仍然显示, 且由于 stdin 为空, 后者输出为 0
cat: REDM.m: No such file or directory
0

再来点例子:

  • cat README.md | ls: 即使前者命令为正确的 stdout, 因为 ls 命令不支持 stdin, 屏幕上不显示前者的结果, 因为数据被丢弃了
Documents  Downloads  Music  Pictures  Videos  Trash

管道与重定向的区别:

  • 重定向是发生在一个进程内的, 而管道则产生了两个进程来执行两个命令
  • 重定向的 ">", 这个箭头指向的地方必须是一个文件, 所以右边得是文件, 同理 "<" 左边得是文件

管道在 tui 程序中的常见用途:

假设我们的 tui 程序, 想在执行的时候将结果交给外部的命令, 我们就可以通过管道来支持这一行为

比如, 我们做了一个超级小的终端文件浏览器demo, 类似 ranger/nnn/xplr/yazi, 在选择文件按下回车后, 能获得其 path
假设这个程序叫作 file_chooser, 我们想将获取的路径传递给其他程序, 类似这样:

file_chooser | vim

获取的文件路径, 通过管道传递给了 vim, 然后可以在 vim 中编辑这个文件

但请注意, 我们的 file_chooser 是 tui 程序, 也是一堆字符所组成的, 如果你让这些字符(ui), 渲染输出到了 stdout 中......
那通过管道传递的, 可就不止是你真正想传递给外部的文件路径了! 还有你的一大堆 ui 字符!

真是可怕啊......不过解决办法非常简单, 你只要在 stderr 中渲染你的 ui 界面就阔以了
所以我才在 p2 中提过一嘴, 为什么那个模板是使用了 stderr, 而不是使用 stdout


上一篇: p2~> 基础架构