1use 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}