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 pub transactions: bool,
23 pub transactions_file: Option<String>,
24 pub commit_mode: Option<String>,
25 pub retention: Option<String>,
26 pub vc: bool,
27 pub vc_file: Option<String>,
28 pub branch: Option<String>,
29 pub branch_from: Option<i64>,
30 pub checkout: Option<String>,
31 pub tag: Option<String>,
32 pub list_branches: bool,
33 pub list_tags: bool,
34 pub show_log: bool,
35}
36
37impl Default for Cli {
38 fn default() -> Self {
39 Self {
40 db: DEFAULT_DATABASE_FILENAME.to_string(),
41 query: None,
42 query_arg: None,
43 trace: false,
44 auto_create_missing_references: false,
45 structure: None,
46 before: false,
47 changes: false,
48 after: false,
49 lino_input: None,
50 lino_output: None,
51 transactions: false,
52 transactions_file: None,
53 commit_mode: None,
54 retention: None,
55 vc: false,
56 vc_file: None,
57 branch: None,
58 branch_from: None,
59 checkout: None,
60 tag: None,
61 list_branches: false,
62 list_tags: false,
63 show_log: false,
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum CliCommand {
70 Run(Box<Cli>),
71 Help,
72 Version,
73}
74
75impl Cli {
76 pub fn transactions_requested(&self) -> bool {
78 self.transactions
79 || self.transactions_file.is_some()
80 || self.commit_mode.is_some()
81 || self.retention.is_some()
82 || self.show_log
83 || self.vc_requested()
84 }
85
86 pub fn vc_requested(&self) -> bool {
88 self.vc
89 || self.vc_file.is_some()
90 || self.branch.is_some()
91 || self.branch_from.is_some()
92 || self.checkout.is_some()
93 || self.tag.is_some()
94 || self.list_branches
95 || self.list_tags
96 }
97
98 pub fn parse() -> Result<CliCommand> {
99 lino_arguments::init();
100 Self::parse_from(env::args_os())
101 }
102
103 pub fn parse_from<I, T>(args: I) -> Result<CliCommand>
104 where
105 I: IntoIterator<Item = T>,
106 T: Into<OsString>,
107 {
108 let mut cli = Cli::default();
109 let mut args = args
110 .into_iter()
111 .map(|arg| arg.into().to_string_lossy().into_owned())
112 .peekable();
113
114 let _program = args.next();
115
116 while let Some(arg) = args.next() {
117 if let Some(value) = inline_value(&arg, &["--db", "--data-source", "--data"]) {
118 cli.db = value.to_string();
119 continue;
120 }
121 if let Some(value) = inline_value(&arg, &["--query", "--apply", "--do"]) {
122 cli.query = Some(value.to_string());
123 continue;
124 }
125 if let Some(value) = inline_value(&arg, &["--structure"]) {
126 cli.structure = Some(parse_link_id("--structure", value)?);
127 continue;
128 }
129 if let Some(value) = inline_value(&arg, &["--trace"]) {
130 cli.trace = parse_bool("--trace", value)?;
131 continue;
132 }
133 if let Some(value) = inline_value(&arg, &["--auto-create-missing-references"]) {
134 cli.auto_create_missing_references =
135 parse_bool("--auto-create-missing-references", value)?;
136 continue;
137 }
138 if let Some(value) = inline_value(&arg, &["--before"]) {
139 cli.before = parse_bool("--before", value)?;
140 continue;
141 }
142 if let Some(value) = inline_value(&arg, &["--changes"]) {
143 cli.changes = parse_bool("--changes", value)?;
144 continue;
145 }
146 if let Some(value) = inline_value(&arg, &["--after", "--links"]) {
147 cli.after = parse_bool("--after", value)?;
148 continue;
149 }
150 if let Some(value) = inline_value(&arg, &["--out", "--lino-output", "--export"]) {
151 cli.lino_output = Some(value.to_string());
152 continue;
153 }
154 if let Some(value) = inline_value(&arg, &["--in", "--lino-input", "--import"]) {
155 cli.lino_input = Some(value.to_string());
156 continue;
157 }
158 if let Some(value) = inline_value(&arg, &["--transactions"]) {
159 cli.transactions = parse_bool("--transactions", value)?;
160 continue;
161 }
162 if let Some(value) = inline_value(&arg, &["--transactions-file"]) {
163 cli.transactions_file = Some(value.to_string());
164 continue;
165 }
166 if let Some(value) = inline_value(&arg, &["--commit-mode"]) {
167 cli.commit_mode = Some(value.to_string());
168 continue;
169 }
170 if let Some(value) = inline_value(&arg, &["--retention"]) {
171 cli.retention = Some(value.to_string());
172 continue;
173 }
174 if let Some(value) = inline_value(&arg, &["--vc"]) {
175 cli.vc = parse_bool("--vc", value)?;
176 continue;
177 }
178 if let Some(value) = inline_value(&arg, &["--vc-file"]) {
179 cli.vc_file = Some(value.to_string());
180 continue;
181 }
182 if let Some(value) = inline_value(&arg, &["--branch"]) {
183 cli.branch = Some(value.to_string());
184 continue;
185 }
186 if let Some(value) = inline_value(&arg, &["--branch-from"]) {
187 cli.branch_from = Some(parse_seq("--branch-from", value)?);
188 continue;
189 }
190 if let Some(value) = inline_value(&arg, &["--checkout"]) {
191 cli.checkout = Some(value.to_string());
192 continue;
193 }
194 if let Some(value) = inline_value(&arg, &["--tag"]) {
195 cli.tag = Some(value.to_string());
196 continue;
197 }
198 if let Some(value) = inline_value(&arg, &["--list-branches"]) {
199 cli.list_branches = parse_bool("--list-branches", value)?;
200 continue;
201 }
202 if let Some(value) = inline_value(&arg, &["--list-tags"]) {
203 cli.list_tags = parse_bool("--list-tags", value)?;
204 continue;
205 }
206 if let Some(value) = inline_value(&arg, &["--log"]) {
207 cli.show_log = parse_bool("--log", value)?;
208 continue;
209 }
210
211 match arg.as_str() {
212 "-h" | "--help" => return Ok(CliCommand::Help),
213 "-V" | "--version" => return Ok(CliCommand::Version),
214 "-d" | "--db" | "--data-source" | "--data" => {
215 cli.db = next_value(&mut args, &arg)?;
216 }
217 "-q" | "--query" | "--apply" | "--do" => {
218 cli.query = Some(next_value(&mut args, &arg)?);
219 }
220 "-t" | "--trace" => {
221 cli.trace = next_bool_value(&mut args, true)?;
222 }
223 "--auto-create-missing-references" => {
224 cli.auto_create_missing_references = next_bool_value(&mut args, true)?;
225 }
226 "-s" | "--structure" => {
227 let value = next_value(&mut args, &arg)?;
228 cli.structure = Some(parse_link_id(&arg, &value)?);
229 }
230 "-b" | "--before" => {
231 cli.before = next_bool_value(&mut args, true)?;
232 }
233 "-c" | "--changes" => {
234 cli.changes = next_bool_value(&mut args, true)?;
235 }
236 "-a" | "--after" | "--links" => {
237 cli.after = next_bool_value(&mut args, true)?;
238 }
239 "--out" | "--lino-output" | "--export" => {
240 cli.lino_output = Some(next_value(&mut args, &arg)?);
241 }
242 "--in" | "--lino-input" | "--import" => {
243 cli.lino_input = Some(next_value(&mut args, &arg)?);
244 }
245 "--transactions" => {
246 cli.transactions = next_bool_value(&mut args, true)?;
247 }
248 "--transactions-file" => {
249 cli.transactions_file = Some(next_value(&mut args, &arg)?);
250 }
251 "--commit-mode" => {
252 cli.commit_mode = Some(next_value(&mut args, &arg)?);
253 }
254 "--retention" => {
255 cli.retention = Some(next_value(&mut args, &arg)?);
256 }
257 "--vc" => {
258 cli.vc = next_bool_value(&mut args, true)?;
259 }
260 "--vc-file" => {
261 cli.vc_file = Some(next_value(&mut args, &arg)?);
262 }
263 "--branch" => {
264 cli.branch = Some(next_value(&mut args, &arg)?);
265 }
266 "--branch-from" => {
267 let value = next_value(&mut args, &arg)?;
268 cli.branch_from = Some(parse_seq(&arg, &value)?);
269 }
270 "--checkout" => {
271 cli.checkout = Some(next_value(&mut args, &arg)?);
272 }
273 "--tag" => {
274 cli.tag = Some(next_value(&mut args, &arg)?);
275 }
276 "--list-branches" => {
277 cli.list_branches = next_bool_value(&mut args, true)?;
278 }
279 "--list-tags" => {
280 cli.list_tags = next_bool_value(&mut args, true)?;
281 }
282 "--log" => {
283 cli.show_log = next_bool_value(&mut args, true)?;
284 }
285 "--" => {
286 for value in args.by_ref() {
287 set_positional_query(&mut cli, value)?;
288 }
289 break;
290 }
291 value if value.starts_with('-') => {
292 bail!("unknown option '{value}'");
293 }
294 value => {
295 set_positional_query(&mut cli, value.to_string())?;
296 }
297 }
298 }
299
300 Ok(CliCommand::Run(Box::new(cli)))
301 }
302
303 pub fn print_help() {
304 print!("{}", Self::help_text());
305 }
306
307 pub fn help_text() -> &'static str {
308 concat!(
309 "LiNo CLI Tool for managing links data store\n\n",
310 "Usage: clink [OPTIONS] [QUERY]\n\n",
311 "Arguments:\n",
312 " [QUERY] LiNo query for CRUD operation\n\n",
313 "Options:\n",
314 " -d, --db <DB>, --data-source <DB>, --data <DB>\n",
315 " Path to the links database file [default: db.links]\n",
316 " -q, --query <QUERY>, --apply <QUERY>, --do <QUERY>\n",
317 " LiNo query for CRUD operation\n",
318 " -t, --trace\n",
319 " Enable trace (verbose output)\n",
320 " --auto-create-missing-references\n",
321 " Create missing numeric and named references as self-referential point links\n",
322 " -s, --structure <STRUCTURE>\n",
323 " ID of the link to format its structure\n",
324 " -b, --before\n",
325 " Print the state of the database before applying changes\n",
326 " -c, --changes\n",
327 " Print the changes applied by the query\n",
328 " -a, --after, --links\n",
329 " Print the state of the database after applying changes\n",
330 " --in <IN>, --lino-input <IN>, --import <IN>\n",
331 " Read and import a LiNo file into the database\n",
332 " --out <OUT>, --lino-output <OUT>, --export <OUT>\n",
333 " Write the complete database as a LiNo file\n",
334 " --transactions\n",
335 " Enable the transactions layer (default log path: <db>.transitions.links)\n",
336 " --transactions-file <FILE>\n",
337 " Path to the transitions log store (implies --transactions)\n",
338 " --commit-mode <MODE>\n",
339 " Choose 'sync' or 'async' commits (default: sync, implies --transactions)\n",
340 " --retention <SPEC>\n",
341 " Log retention policy: 'infinite', 'sized:<n>', or 'chunked:<n>:<dir>'\n",
342 " (implies --transactions)\n",
343 " --vc\n",
344 " Enable the version-control decorator (implies --transactions)\n",
345 " --vc-file <FILE>\n",
346 " Path to the version-control branches store\n",
347 " (default: <db>.versioncontrol.links)\n",
348 " --branch <NAME>\n",
349 " Switch to a branch (creating it if --branch-from is also passed).\n",
350 " Implies --vc.\n",
351 " --branch-from <SEQ>\n",
352 " When creating a branch with --branch, fork from this sequence point\n",
353 " --checkout <POINT>\n",
354 " Time-travel to a specific transition sequence or named tag.\n",
355 " Implies --vc.\n",
356 " --tag <NAME[=SEQ]>\n",
357 " Create a tag at current head or at the given sequence point.\n",
358 " Implies --vc.\n",
359 " --list-branches\n",
360 " List version-control branches and exit\n",
361 " --list-tags\n",
362 " List version-control tags and exit\n",
363 " --log\n",
364 " Print the transitions log and exit (implies --transactions)\n",
365 " -h, --help\n",
366 " Print help\n",
367 " -V, --version\n",
368 " Print version\n",
369 )
370 }
371
372 pub fn version_text() -> String {
373 format!("clink {}", env!("CARGO_PKG_VERSION"))
374 }
375}
376
377fn inline_value<'a>(arg: &'a str, names: &[&str]) -> Option<&'a str> {
378 names.iter().find_map(|name| {
379 arg.strip_prefix(name)
380 .and_then(|rest| rest.strip_prefix('='))
381 })
382}
383
384fn next_value<I>(args: &mut I, option: &str) -> Result<String>
385where
386 I: Iterator<Item = String>,
387{
388 args.next()
389 .ok_or_else(|| anyhow::anyhow!("missing value for option '{option}'"))
390}
391
392fn next_bool_value<I>(args: &mut std::iter::Peekable<I>, default: bool) -> Result<bool>
393where
394 I: Iterator<Item = String>,
395{
396 if let Some(value) = args.peek().and_then(|value| bool_literal(value)) {
397 args.next();
398 Ok(value)
399 } else {
400 Ok(default)
401 }
402}
403
404fn parse_bool(option: &str, value: &str) -> Result<bool> {
405 bool_literal(value)
406 .ok_or_else(|| anyhow::anyhow!("invalid boolean value '{value}' for {option}"))
407}
408
409fn bool_literal(value: &str) -> Option<bool> {
410 match value.to_ascii_lowercase().as_str() {
411 "true" | "1" | "yes" | "on" => Some(true),
412 "false" | "0" | "no" | "off" => Some(false),
413 _ => None,
414 }
415}
416
417fn parse_link_id(option: &str, value: &str) -> Result<u32> {
418 value
419 .parse()
420 .map_err(|_| anyhow::anyhow!("invalid link id '{value}' for {option}"))
421}
422
423fn parse_seq(option: &str, value: &str) -> Result<i64> {
424 value
425 .parse()
426 .map_err(|_| anyhow::anyhow!("invalid sequence value '{value}' for {option}"))
427}
428
429fn set_positional_query(cli: &mut Cli, value: String) -> Result<()> {
430 if cli.query_arg.is_some() {
431 bail!("unexpected extra positional argument '{value}'");
432 }
433
434 cli.query_arg = Some(value);
435 Ok(())
436}