pass_fu/
opt.rs

1use std::{ffi::OsString, iter::once, path::PathBuf};
2
3#[derive(Debug, Clone)]
4pub struct Config {
5    pub name: OsString,
6    pub pass: PathBuf,
7    pub dir: PathBuf,
8    pub find: PathBuf,
9    pub picker: PathBuf,
10    pub picker_args: Vec<OsString>,
11    pub typist: PathBuf,
12    pub typist_args: Vec<OsString>,
13}
14impl Default for Config {
15    fn default() -> Self {
16        Self {
17            name: "pass-fu".into(),
18            dir: "~/.password-store".into(),
19            pass: "/usr/bin/pass".into(),
20            find: "find".into(),
21            picker: "dmenu".into(),
22            picker_args: vec![],
23            typist: "xdotool".into(),
24            typist_args: ["type", "--clearmodifiers", "--file", "-"]
25                .into_iter()
26                .map(|a| a.into())
27                .collect(),
28        }
29    }
30}
31
32#[derive(Debug, Clone)]
33pub struct Opt {
34    pub config: Config,
35    pub command: Command,
36}
37impl AsRef<Opt> for Opt {
38    fn as_ref(&self) -> &Opt {
39        self
40    }
41}
42impl Opt {
43    pub fn parse(
44        args: impl IntoIterator<Item = OsString>,
45        env: impl IntoIterator<Item = (OsString, OsString)>,
46    ) -> Result<Opt, impl std::fmt::Display> {
47        let env = env.into_iter();
48        let mut args = args.into_iter();
49        let mut config = Config::default();
50        config.name = args.next().unwrap_or(config.name);
51
52        let command = Command::parse(args);
53        for (key, val) in env {
54            match key.to_str() {
55                Some("PASSWORD_STORE_DIR") => config.dir = val.into(),
56                _ => {}
57            }
58        }
59        Result::<Opt, &str>::Ok(Self { config, command })
60    }
61}
62
63#[derive(Debug, Clone)]
64pub enum Command {
65    Show(ShowOpt),
66    Other(PassOpt),
67}
68impl Command {
69    fn parse(args: impl IntoIterator<Item = OsString>) -> Command {
70        let mut args = args.into_iter();
71        // unfortunately, pass CLI interface is ambiguous with commands vs paths.
72        // an easy fix would be to demand paths to start with "/" or command to always be
73        // explicitly specified.
74        match args.next() {
75            None => Self::parse_show(args),
76            Some(arg) => match arg.to_str() {
77                None => Self::parse_other(arg, args),
78                Some(cmd) => match cmd {
79                    "--help" | "--version" | "init" | "find" | "grep" | "insert" | "edit"
80                    | "generate" | "rm" | "mv" | "cp" | "git" | "help" | "version" | "ls"                     
81                    // TODO: avoid other extensions?
82                    | "otp" => {
83                        Self::parse_other(arg, args)
84                    }
85                    "show" => Self::parse_show(args),
86                    _positional => Self::parse_show(once(arg).chain(args)),
87                },
88            },
89        }
90    }
91    fn parse_other(command: OsString, args: impl IntoIterator<Item = OsString>) -> Command {
92        Self::Other(PassOpt {
93            command,
94            rest: args.into_iter().collect(),
95        })
96    }
97    fn parse_show(args: impl IntoIterator<Item = OsString>) -> Command {
98        let mut args = args.into_iter();
99        let mut line = String::new();
100        let mut mode = ShowMode::default();
101        let mut path = None;
102        let mut rest = vec![];
103        let mut options_done = false;
104        while let Some(arg) = args.next() {
105            match arg.to_str().and_then(|a| {
106                // extract options with this style:
107                // --option, -o, --option=value, -ovalue
108                // short options are not banged together in pass
109                a.split_once("=")
110                    .or_else(|| Some((a, "")))
111                    .filter(|(o, _)| o.starts_with("--"))
112                    .or_else(|| a.len().checked_sub(2).map(|_| a.split_at(2)))
113                    .filter(|(o, _)| o.starts_with("-"))
114                    .filter(|_| !options_done)
115            }) {
116                Some(("-c", v)) | Some(("--clip", v)) => {
117                    mode = ShowMode::Clip;
118                    line = v.to_owned();
119                }
120                Some(("-q", v)) | Some(("--qrcode", v)) => {
121                    mode = ShowMode::QrCode;
122                    line = v.to_owned();
123                }
124                Some(("-t", v)) | Some(("--type", v)) => {
125                    mode = ShowMode::Type;
126                    line = v.to_owned();
127                }
128                Some(("-o", v)) | Some(("--output", v)) => {
129                    mode = ShowMode::Output;
130                    line = v.to_owned();
131                }
132                Some(("--", "")) => {
133                    options_done = true;
134                    rest.push(arg);
135                }
136                Some(_option) => rest.push(arg),
137                None => {
138                    // no option detected, this is the path
139                    // pass just takes the first and ignores the rest if multiple are specified
140                    path = path.or(Some(PathBuf::from(arg)));
141                }
142            }
143        }
144        Command::Show(ShowOpt {
145            path,
146            line,
147            mode,
148            rest,
149        })
150    }
151}
152
153#[derive(Debug, Default, Clone)]
154pub struct ShowOpt {
155    pub path: Option<PathBuf>,
156    pub line: String,
157    pub mode: ShowMode,
158    pub rest: Vec<OsString>,
159}
160#[derive(Debug, Clone)]
161pub enum ShowMode {
162    Output,
163    Clip,
164    QrCode,
165    Type,
166}
167impl Default for ShowMode {
168    fn default() -> Self {
169        ShowMode::Output
170    }
171}
172#[derive(Debug, Default, Clone)]
173pub struct PassOpt {
174    pub command: OsString,
175    pub rest: Vec<OsString>,
176}