find/
find.rs

1//! This is not a typical bpaf usage,
2//! but you should be able to replicate command line used by find
3
4use bpaf::*;
5use std::{ffi::OsString, path::PathBuf};
6
7#[derive(Debug, Clone, Default)]
8pub struct Perms {
9    read: bool,
10    write: bool,
11    exec: bool,
12}
13
14#[derive(Debug, Clone)]
15pub enum Perm {
16    All(Perms),
17    Any(Perms),
18    Exact(Perms),
19}
20
21#[derive(Debug, Clone)]
22#[allow(dead_code)]
23pub struct Options {
24    paths: Vec<PathBuf>,
25    exec: Option<Vec<OsString>>,
26    user: Option<String>,
27    perm: Option<Perm>,
28}
29
30// Parses -user xxx
31fn user() -> impl Parser<Option<String>> {
32    // match only literal "-user"
33    let tag = literal("-user").anywhere();
34    let value = positional("USER").help("User name");
35    construct!(tag, value)
36        .adjacent()
37        .map(|pair| pair.1)
38        .optional()
39}
40
41// parsers -exec xxx yyy zzz ;
42fn exec() -> impl Parser<Option<Vec<OsString>>> {
43    let tag = literal("-exec")
44        .help("for every file find finds execute a separate shell command")
45        .anywhere();
46
47    let item = any::<OsString, _, _>("ITEM", |s| (s != ";").then_some(s))
48        .help("command with its arguments, find will replace {} with a file name")
49        .many();
50
51    let endtag = any::<String, _, _>(";", |s| (s == ";").then_some(()))
52        .help("anything after literal \";\" will be considered a regular option again");
53
54    construct!(tag, item, endtag)
55        .adjacent()
56        .map(|triple| triple.1)
57        .optional()
58}
59
60/// parses symbolic permissions `-perm -mode`, `-perm /mode` and `-perm mode`
61fn perm() -> impl Parser<Option<Perm>> {
62    fn parse_mode(input: &str) -> Result<Perms, String> {
63        let mut perms = Perms::default();
64        for c in input.chars() {
65            match c {
66                'r' => perms.read = true,
67                'w' => perms.write = true,
68                'x' => perms.exec = true,
69                _ => return Err(format!("{} is not a valid permission string", input)),
70            }
71        }
72        Ok(perms)
73    }
74
75    let tag = literal("-mode").anywhere();
76
77    // `any` here is used to parse an arbitrary string that can also start with dash (-)
78    // regular positional parser won't work here
79    let mode = any("MODE", Some)
80        .help("(perm | -perm | /perm), where perm is any subset of rwx characters, ex +rw")
81        .parse::<_, _, String>(|s: String| {
82            if let Some(m) = s.strip_prefix('-') {
83                Ok(Perm::All(parse_mode(m)?))
84            } else if let Some(m) = s.strip_prefix('/') {
85                Ok(Perm::Any(parse_mode(m)?))
86            } else {
87                Ok(Perm::Exact(parse_mode(&s)?))
88            }
89        });
90
91    construct!(tag, mode)
92        .adjacent()
93        .map(|pair| pair.1)
94        .optional()
95}
96
97pub fn options() -> OptionParser<Options> {
98    let paths = positional::<PathBuf>("PATH").many();
99
100    construct!(Options {
101        exec(),
102        user(),
103        perm(),
104        paths,
105    })
106    .to_options()
107}
108
109fn main() {
110    println!("{:#?}", options().run());
111}