查找与动态模式匹配的文件

Find Files that Match a Dynamic Pattern

我希望能够解析目录中的所有文件,以找到具有与用户提供的模式匹配的最大时间戳的文件。

即如果用户运行

$ search /foo/bar/baz.txt

并且目录/foo/bar/包含文件baz.001.txtbaz.002.txtbaz.003.txt,那么结果应该是baz.003.txt

目前我正在构建 PathBuf。 用它来构建 Regex。 然后在目录中查找与表达式匹配的所有文件。 但是感觉对于一个相对简单的问题来说,这是一个很大的工作量。

fn find(foo: &str) -> Result<Vec<String>, Box<dyn Error>> {
    let mut files = vec![];

    let mut path = PathBuf::from(foo);
    let base = path.parent().unwrap().to_str().unwrap();
    let file_name = path.file_stem().unwrap().to_str().unwrap();
    let extension = path.extension().unwrap().to_str().unwrap();
    let pattern = format!("{}\.\d{{3}}\.{}", file_name, extension);
    let expression = Regex::new(&pattern).unwrap();

    let objects: Vec<String> = fs::read_dir(&base)
        .unwrap()
        .map(|entry| {
            entry
                .unwrap()
                .path()
                .file_name()
                .unwrap()
                .to_str()
                .unwrap()
                .to_owned()
        })
        .collect();
    for object in objects.iter() {
        if expression.is_match(object) {
            files.push(String::from(object));
        }
    }

    Ok(files)
}

有没有更简单的方法来获取文件路径、生成模式并找到所有匹配的文件?

Rust 并不是真正适用于快速而肮脏的解决方案的语言。相反,它强烈地鼓励优雅的解决方案,所有极端情况都得到妥善处理。这通常不会导致极短的解决方案,但您可以避免过多依赖外部包装箱的样板文件,这些包装箱会包含大量代码。假设您还没有出现“library-wide”错误,我会这样做。

fn find(foo: &str) -> Result<Vec<String>, FindError> {
    let path = PathBuf::from(foo);
    let base = path
        .parent()
        .ok_or(FindError::InvalidBaseFile)?
        .to_str()
        .ok_or(FindError::OsStringNotUtf8)?;
    let file_name = path
        .file_stem()
        .ok_or(FindError::InvalidFileName)?
        .to_str()
        .ok_or(FindError::OsStringNotUtf8)?;
    let file_extension = path
        .extension()
        .ok_or(FindError::NoFileExtension)?
        .to_str()
        .ok_or(FindError::OsStringNotUtf8)?;
    let pattern = format!(r"{}\.\d{{3}}\.{}", file_name, file_extension);
    let expression = Regex::new(&pattern)?;
    Ok(
        fs::read_dir(&base)?
            .map(|entry| Ok(
                entry?
                .path()
                .file_name()
                .ok_or(FindError::InvalidFileName)?
                .to_str()
                .ok_or(FindError::OsStringNotUtf8)?
                .to_string()
            ))
            .collect::<Result<Vec<_>, FindError>>()?
            .into_iter()
            .filter(|file_name| expression.is_match(&file_name))
            .collect()
    )
}

FindError 的简单定义可以通过 thiserror 箱子实现:

use thiserror::Error;

#[derive(Error, Debug)]
enum FindError {
    #[error(transparent)]
    RegexError(#[from] regex::Error),
    #[error("File name has no extension")]
    NoFileExtension,
    #[error("Not a valid file name")]
    InvalidFileName,
    #[error("No valid base file")]
    InvalidBaseFile,
    #[error("An OS string is not valid utf-8")]
    OsStringNotUtf8,
    #[error(transparent)]
    IoError(#[from] std::io::Error),
}

编辑

正如@Masklinn 所指出的,您可以轻松检索文件的主干和扩展名。它会导致 less-well 处理错误(并且一些特殊情况,例如没有扩展名的隐藏文件处理不当),但整体代码不那么冗长。供您根据需要选择。

fn find(foo: &str) -> Result<Vec<String>, FindError> {
    let (file_name, file_extension) = foo
        .rsplit_one('.')
        .ok_or(FindError::NoExtension)?;
  ... // the rest is unchanged
}

您可能也需要适应 FindError。你也可以摆脱 ok_or 的情况,如果你真的不关心它,只需用 .unwrap_or((foo, "")) 替换它(但是这会产生令人惊讶的结果......)。