大家好,我是硅上观道。这篇文章我们将接着上篇文章谈错误处理,使用交互式输入方式获取用户输入的正确文件路径。

欢迎查看这个系列的上一篇文章,也欢迎关注这个系列专栏,我将更新更多精彩内容。


上篇文章发布后,在和一位网友的讨论中,对方建议我考虑在用户输入错误文件路径后使用交互式的方式获取新的路径信息。

这种方式不仅方便了用户不必重复输入命令调整参数,也使得整个软件的执行逻辑更流畅丝滑。

我听了觉得很有道理,这就来研究一下怎么实现这个功能。

事实上,如果只是简单读取用户输入信息,完全可以在不使用第三方库的前提下完成。

但是,考虑到实现的简洁性、可拓展性以及可视化效果,我决定还是用一个社区内还在维护、文档相对更好的库inquire来实现这个功能。

这是这个库的项目地址

好,那么我先上代码:

use anyhow::Result;
use clap::Parser;
use inquire::Text;

#[derive(Parser)]
struct Cli {
    pattern: String,
    path: std::path::PathBuf,
}

fn main() -> Result<()> {
    let args = Cli::parse();

    let content = std::fs::read_to_string(&args.path).or_else(|e| {
        eprintln!("Failed to read {}: {}", args.path.display(), e);
        prompt_for_file_interactively()
    })?;

    content
        .lines()
        .filter(|line| line.contains(&args.pattern))
        .for_each(|line| println!("{line}"));

    Ok(())
}

/// Prompts user to enter a file path interactively until successful read or cancellation
fn prompt_for_file_interactively() -> Result<String> {
    loop {
        let input_path = Text::new("Enter file path (ESC to cancel):")
            .prompt()
            .map_err(|_| anyhow::anyhow!("Operation cancelled by user"))?;

        match std::fs::read_to_string(&input_path) {
            Ok(content) => return Ok(content),
            Err(e) => eprintln!("Error reading file: {}: {}", input_path, e),
        }
    }
}

你可能会发现相比上一版做了一些比较大的调整。下面,我来详细说说每个调整的位置。

在深入细节前,请确认安装inquireanyhow库。

cargo add inquire anyhow

1. or_else函数和eprintln!

这回,我们在文件没办法读入的时候,不是简单地直接上抛错误结束main函数。相反的,我们在这里再加入了一个or_else函数。

这个函数只有在前面文件读取出现错误的时候才执行。如果的确出错,那么将会执行这两行代码:

eprintln!("Failed to read {}: {}", args.path.display(), e);
prompt_for_file_interactively()

也即,我们通过eprintln!宏先打印出错误,然后再调用prompt_for_file_interactively()函数。

这里为什么我们使用eprintln!而不是一般的println!呢?

在绝大多数操作系统上,有两种输出流,stdoutstderr。其中,stdout一般用于软件的实际输出,而stderr是专门用于打印错误信息的输出流,独立于stdout存在。

在这里,eprintln!打印的信息就是通过stderr。相比于把错误信息通过println!通过stdout打印,现在这种方式更有利于用户将错误集中保存到文件,或是导入到其他软件进行分析。

当然,这里的重头戏在于prompt_for_file_interactively()函数。


2. 交互式获取用户输入新路径

这里,我们将交互式输入部分的逻辑抽象到了prompt_for_file_interactively()函数中,我们来看看这个函数:

loop {
	let input_path = Text::new("Enter file path (ESC to cancel):")
		.prompt()
		.map_err(|_| anyhow::anyhow!("Operation cancelled by user"))?;

	match std::fs::read_to_string(&input_path) {
		Ok(content) => return Ok(content),
		Err(e) => eprintln!("Error reading file: {}: {}", input_path, e),
	}
}

我们使用一个loop来让这个循环重复进行。

首先,我们通过input_path来获取新的输入路径。这里,我们直接使用了inquire库中导入的Text结构体,通过简单的prompt函数就可以直接获取输入的字符串信息。

需要注意的是,有可能用户不想输入新路径,而是想直接退出。inquire帮我们实现了用户在按下ESC键后退出的功能,可以看到这里我们做了map_error来处理用户按下退出键后触发的inquire::Error(这里的anyhow我们有机会再解释)。

也就是说,出现这类错误在处理后我们直接上抛,退出loop,直接返回函数。

反之,如果用户的确输入了东西,那么我们就检查一下这次的路径是否合法。

如果读取文件无问题,直接返回Ok(content)退出循环和函数;如果还是不对,那就再eprintln!打印出问题,循环重来一遍。

是不是非常清晰的逻辑?


3. 查询内容逻辑方面的小调整

此外,我还做了一个小调整,这是原版的代码:

for line in content.lines() {
	if line.contains(&args.pattern) {
		println!("{}", line);
	}
}

这是现在修改后更富有函数式风格的代码:

content
	.lines()
	.filter(|line| line.contains(&args.pattern))
	.for_each(|line| println!("{line}"));

实际上,无论是使用过程式的循环,还是函数式的函数链式调用,只要能完成任务都没问题。这里改成函数式风格,很大程度上只是为了展示这种写法,且我自己更喜欢而已。

简单来说,这里就是在content.lines()后,filter过滤出符合条件的行,在对过滤后的内容逐行打印出。


4. 结果

我们最后来看一下这样设计的效果。

如果直接输入了正确的结果,那么当然是继续能运行的:

正确命令仍然能正常运行

如果是错误的呢?会成为现在这个样子:

输入错误路径后的反应

我们可以选择按下 ESC 直接退出:

按下ESC可以退出

如果我们继续输入,可能输入的路径仍然是错的:

再次输入错误路径

直至我们输入正确的路径:

反复询问直至完全正确

好啦,这就是我们第四篇文章的全部内容。

细心的读者可能注意到了代码中的anyhow库,同时可能也会觉得需要再丰富一下目参数设计、可视化等更高级的功能。这些内容,我们将会在专栏后续文章中慢慢讲。


希望这篇博客能帮助到你,也欢迎关注交流。愿与君共勉!