Skip to main content

link_cli/
cli.rs

1//! Command-line argument parsing for the `clink` binary.
2
3use anyhow::{bail, Result};
4use std::env;
5use std::ffi::OsString;
6
7const DEFAULT_DATABASE_FILENAME: &str = "db.links";
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Cli {
11    pub db: String,
12    pub query: Option<String>,
13    pub query_arg: Option<String>,
14    pub trace: bool,
15    pub auto_create_missing_references: bool,
16    pub structure: Option<u32>,
17    pub before: bool,
18    pub changes: bool,
19    pub after: bool,
20    pub lino_input: Option<String>,
21    pub lino_output: Option<String>,
22}
23
24impl Default for Cli {
25    fn default() -> Self {
26        Self {
27            db: DEFAULT_DATABASE_FILENAME.to_string(),
28            query: None,
29            query_arg: None,
30            trace: false,
31            auto_create_missing_references: false,
32            structure: None,
33            before: false,
34            changes: false,
35            after: false,
36            lino_input: None,
37            lino_output: None,
38        }
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum CliCommand {
44    Run(Cli),
45    Help,
46    Version,
47}
48
49impl Cli {
50    pub fn parse() -> Result<CliCommand> {
51        lino_arguments::init();
52        Self::parse_from(env::args_os())
53    }
54
55    pub fn parse_from<I, T>(args: I) -> Result<CliCommand>
56    where
57        I: IntoIterator<Item = T>,
58        T: Into<OsString>,
59    {
60        let mut cli = Cli::default();
61        let mut args = args
62            .into_iter()
63            .map(|arg| arg.into().to_string_lossy().into_owned())
64            .peekable();
65
66        let _program = args.next();
67
68        while let Some(arg) = args.next() {
69            if let Some(value) = inline_value(&arg, &["--db", "--data-source", "--data"]) {
70                cli.db = value.to_string();
71                continue;
72            }
73            if let Some(value) = inline_value(&arg, &["--query", "--apply", "--do"]) {
74                cli.query = Some(value.to_string());
75                continue;
76            }
77            if let Some(value) = inline_value(&arg, &["--structure"]) {
78                cli.structure = Some(parse_link_id("--structure", value)?);
79                continue;
80            }
81            if let Some(value) = inline_value(&arg, &["--trace"]) {
82                cli.trace = parse_bool("--trace", value)?;
83                continue;
84            }
85            if let Some(value) = inline_value(&arg, &["--auto-create-missing-references"]) {
86                cli.auto_create_missing_references =
87                    parse_bool("--auto-create-missing-references", value)?;
88                continue;
89            }
90            if let Some(value) = inline_value(&arg, &["--before"]) {
91                cli.before = parse_bool("--before", value)?;
92                continue;
93            }
94            if let Some(value) = inline_value(&arg, &["--changes"]) {
95                cli.changes = parse_bool("--changes", value)?;
96                continue;
97            }
98            if let Some(value) = inline_value(&arg, &["--after", "--links"]) {
99                cli.after = parse_bool("--after", value)?;
100                continue;
101            }
102            if let Some(value) = inline_value(&arg, &["--out", "--lino-output", "--export"]) {
103                cli.lino_output = Some(value.to_string());
104                continue;
105            }
106            if let Some(value) = inline_value(&arg, &["--in", "--lino-input", "--import"]) {
107                cli.lino_input = Some(value.to_string());
108                continue;
109            }
110
111            match arg.as_str() {
112                "-h" | "--help" => return Ok(CliCommand::Help),
113                "-V" | "--version" => return Ok(CliCommand::Version),
114                "-d" | "--db" | "--data-source" | "--data" => {
115                    cli.db = next_value(&mut args, &arg)?;
116                }
117                "-q" | "--query" | "--apply" | "--do" => {
118                    cli.query = Some(next_value(&mut args, &arg)?);
119                }
120                "-t" | "--trace" => {
121                    cli.trace = next_bool_value(&mut args, true)?;
122                }
123                "--auto-create-missing-references" => {
124                    cli.auto_create_missing_references = next_bool_value(&mut args, true)?;
125                }
126                "-s" | "--structure" => {
127                    let value = next_value(&mut args, &arg)?;
128                    cli.structure = Some(parse_link_id(&arg, &value)?);
129                }
130                "-b" | "--before" => {
131                    cli.before = next_bool_value(&mut args, true)?;
132                }
133                "-c" | "--changes" => {
134                    cli.changes = next_bool_value(&mut args, true)?;
135                }
136                "-a" | "--after" | "--links" => {
137                    cli.after = next_bool_value(&mut args, true)?;
138                }
139                "--out" | "--lino-output" | "--export" => {
140                    cli.lino_output = Some(next_value(&mut args, &arg)?);
141                }
142                "--in" | "--lino-input" | "--import" => {
143                    cli.lino_input = Some(next_value(&mut args, &arg)?);
144                }
145                "--" => {
146                    for value in args.by_ref() {
147                        set_positional_query(&mut cli, value)?;
148                    }
149                    break;
150                }
151                value if value.starts_with('-') => {
152                    bail!("unknown option '{value}'");
153                }
154                value => {
155                    set_positional_query(&mut cli, value.to_string())?;
156                }
157            }
158        }
159
160        Ok(CliCommand::Run(cli))
161    }
162
163    pub fn print_help() {
164        print!("{}", Self::help_text());
165    }
166
167    pub fn help_text() -> &'static str {
168        concat!(
169            "LiNo CLI Tool for managing links data store\n\n",
170            "Usage: clink [OPTIONS] [QUERY]\n\n",
171            "Arguments:\n",
172            "  [QUERY]  LiNo query for CRUD operation\n\n",
173            "Options:\n",
174            "  -d, --db <DB>, --data-source <DB>, --data <DB>\n",
175            "          Path to the links database file [default: db.links]\n",
176            "  -q, --query <QUERY>, --apply <QUERY>, --do <QUERY>\n",
177            "          LiNo query for CRUD operation\n",
178            "  -t, --trace\n",
179            "          Enable trace (verbose output)\n",
180            "      --auto-create-missing-references\n",
181            "          Create missing numeric and named references as self-referential point links\n",
182            "  -s, --structure <STRUCTURE>\n",
183            "          ID of the link to format its structure\n",
184            "  -b, --before\n",
185            "          Print the state of the database before applying changes\n",
186            "  -c, --changes\n",
187            "          Print the changes applied by the query\n",
188            "  -a, --after, --links\n",
189            "          Print the state of the database after applying changes\n",
190            "      --in <IN>, --lino-input <IN>, --import <IN>\n",
191            "          Read and import a LiNo file into the database\n",
192            "      --out <OUT>, --lino-output <OUT>, --export <OUT>\n",
193            "          Write the complete database as a LiNo file\n",
194            "  -h, --help\n",
195            "          Print help\n",
196            "  -V, --version\n",
197            "          Print version\n",
198        )
199    }
200
201    pub fn version_text() -> String {
202        format!("clink {}", env!("CARGO_PKG_VERSION"))
203    }
204}
205
206fn inline_value<'a>(arg: &'a str, names: &[&str]) -> Option<&'a str> {
207    names.iter().find_map(|name| {
208        arg.strip_prefix(name)
209            .and_then(|rest| rest.strip_prefix('='))
210    })
211}
212
213fn next_value<I>(args: &mut I, option: &str) -> Result<String>
214where
215    I: Iterator<Item = String>,
216{
217    args.next()
218        .ok_or_else(|| anyhow::anyhow!("missing value for option '{option}'"))
219}
220
221fn next_bool_value<I>(args: &mut std::iter::Peekable<I>, default: bool) -> Result<bool>
222where
223    I: Iterator<Item = String>,
224{
225    if let Some(value) = args.peek().and_then(|value| bool_literal(value)) {
226        args.next();
227        Ok(value)
228    } else {
229        Ok(default)
230    }
231}
232
233fn parse_bool(option: &str, value: &str) -> Result<bool> {
234    bool_literal(value)
235        .ok_or_else(|| anyhow::anyhow!("invalid boolean value '{value}' for {option}"))
236}
237
238fn bool_literal(value: &str) -> Option<bool> {
239    match value.to_ascii_lowercase().as_str() {
240        "true" | "1" | "yes" | "on" => Some(true),
241        "false" | "0" | "no" | "off" => Some(false),
242        _ => None,
243    }
244}
245
246fn parse_link_id(option: &str, value: &str) -> Result<u32> {
247    value
248        .parse()
249        .map_err(|_| anyhow::anyhow!("invalid link id '{value}' for {option}"))
250}
251
252fn set_positional_query(cli: &mut Cli, value: String) -> Result<()> {
253    if cli.query_arg.is_some() {
254        bail!("unexpected extra positional argument '{value}'");
255    }
256
257    cli.query_arg = Some(value);
258    Ok(())
259}