1use crate::protocol::ArityRange;
4use anyhow::{bail, Result};
5use std::env;
6use std::ffi::OsString;
7
8const DEFAULT_DATABASE_FILENAME: &str = "db.links";
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Cli {
12 pub db: String,
13 pub query: Option<String>,
14 pub query_arg: Option<String>,
15 pub trace: bool,
16 pub auto_create_missing_references: bool,
17 pub structure: Option<u32>,
18 pub before: bool,
19 pub changes: bool,
20 pub after: bool,
21 pub lino_input: Option<String>,
22 pub lino_output: Option<String>,
23 pub binary_input: Option<String>,
24 pub binary_output: Option<String>,
25 pub always: bool,
26 pub once: bool,
27 pub never: bool,
28 pub triggers: bool,
29 pub triggers_file: Option<String>,
30 pub embed_triggers: bool,
31 pub transactions: bool,
32 pub transactions_file: Option<String>,
33 pub commit_mode: Option<String>,
34 pub retention: Option<String>,
35 pub vc: bool,
36 pub vc_file: Option<String>,
37 pub branch: Option<String>,
38 pub branch_from: Option<i64>,
39 pub checkout: Option<String>,
40 pub tag: Option<String>,
41 pub list_branches: bool,
42 pub list_tags: bool,
43 pub show_log: bool,
44 pub serve: Option<String>,
45 pub connect: Option<String>,
46 pub protocol: Option<String>,
47 pub external_references: bool,
48 pub arity: ArityRange,
49 pub packed_widths: bool,
50}
51
52impl Default for Cli {
53 fn default() -> Self {
54 Self {
55 db: DEFAULT_DATABASE_FILENAME.to_string(),
56 query: None,
57 query_arg: None,
58 trace: false,
59 auto_create_missing_references: false,
60 structure: None,
61 before: false,
62 changes: false,
63 after: false,
64 lino_input: None,
65 lino_output: None,
66 binary_input: None,
67 binary_output: None,
68 always: false,
69 once: false,
70 never: false,
71 triggers: false,
72 triggers_file: None,
73 embed_triggers: false,
74 transactions: false,
75 transactions_file: None,
76 commit_mode: None,
77 retention: None,
78 vc: false,
79 vc_file: None,
80 branch: None,
81 branch_from: None,
82 checkout: None,
83 tag: None,
84 list_branches: false,
85 list_tags: false,
86 show_log: false,
87 serve: None,
88 connect: None,
89 protocol: None,
90 external_references: false,
91 arity: ArityRange::DOUBLETS,
92 packed_widths: false,
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum CliCommand {
99 Run(Box<Cli>),
100 Help,
101 Version,
102}
103
104impl Cli {
105 pub fn transactions_requested(&self) -> bool {
107 self.transactions
108 || self.transactions_file.is_some()
109 || self.commit_mode.is_some()
110 || self.retention.is_some()
111 || self.show_log
112 || self.vc_requested()
113 }
114
115 pub fn trigger_command_count(&self) -> usize {
118 usize::from(self.always) + usize::from(self.once) + usize::from(self.never)
119 }
120
121 pub fn persistent_transformations_requested(&self) -> bool {
127 self.always
128 || self.once
129 || self.never
130 || self.triggers
131 || self.embed_triggers
132 || self.triggers_file.is_some()
133 }
134
135 pub fn vc_requested(&self) -> bool {
137 self.vc
138 || self.vc_file.is_some()
139 || self.branch.is_some()
140 || self.branch_from.is_some()
141 || self.checkout.is_some()
142 || self.tag.is_some()
143 || self.list_branches
144 || self.list_tags
145 }
146
147 pub fn parse() -> Result<CliCommand> {
148 lino_arguments::init();
149 Self::parse_from(env::args_os())
150 }
151
152 pub fn parse_from<I, T>(args: I) -> Result<CliCommand>
153 where
154 I: IntoIterator<Item = T>,
155 T: Into<OsString>,
156 {
157 let mut cli = Cli::default();
158 let mut args = args
159 .into_iter()
160 .map(|arg| arg.into().to_string_lossy().into_owned())
161 .peekable();
162
163 let _program = args.next();
164
165 while let Some(arg) = args.next() {
166 if let Some(value) = inline_value(&arg, &["--db", "--data-source", "--data"]) {
167 cli.db = value.to_string();
168 continue;
169 }
170 if let Some(value) = inline_value(&arg, &["--query", "--apply", "--do"]) {
171 cli.query = Some(value.to_string());
172 continue;
173 }
174 if let Some(value) = inline_value(&arg, &["--structure"]) {
175 cli.structure = Some(parse_link_id("--structure", value)?);
176 continue;
177 }
178 if let Some(value) = inline_value(&arg, &["--trace"]) {
179 cli.trace = parse_bool("--trace", value)?;
180 continue;
181 }
182 if let Some(value) = inline_value(&arg, &["--auto-create-missing-references"]) {
183 cli.auto_create_missing_references =
184 parse_bool("--auto-create-missing-references", value)?;
185 continue;
186 }
187 if let Some(value) = inline_value(&arg, &["--before"]) {
188 cli.before = parse_bool("--before", value)?;
189 continue;
190 }
191 if let Some(value) = inline_value(&arg, &["--changes"]) {
192 cli.changes = parse_bool("--changes", value)?;
193 continue;
194 }
195 if let Some(value) = inline_value(&arg, &["--after", "--links"]) {
196 cli.after = parse_bool("--after", value)?;
197 continue;
198 }
199 if let Some(value) = inline_value(&arg, &["--out", "--lino-output", "--export"]) {
200 cli.lino_output = Some(value.to_string());
201 continue;
202 }
203 if let Some(value) = inline_value(&arg, &["--in", "--lino-input", "--import"]) {
204 cli.lino_input = Some(value.to_string());
205 continue;
206 }
207 if let Some(value) = inline_value(
208 &arg,
209 &["--export-binary", "--binary-output", "--binary-out"],
210 ) {
211 cli.binary_output = Some(value.to_string());
212 continue;
213 }
214 if let Some(value) =
215 inline_value(&arg, &["--import-binary", "--binary-input", "--binary-in"])
216 {
217 cli.binary_input = Some(value.to_string());
218 continue;
219 }
220 if let Some(value) = inline_value(&arg, &["--always"]) {
221 cli.always = parse_bool("--always", value)?;
222 continue;
223 }
224 if let Some(value) = inline_value(&arg, &["--once"]) {
225 cli.once = parse_bool("--once", value)?;
226 continue;
227 }
228 if let Some(value) = inline_value(&arg, &["--never"]) {
229 cli.never = parse_bool("--never", value)?;
230 continue;
231 }
232 if let Some(value) = inline_value(&arg, &["--triggers"]) {
233 cli.triggers = parse_bool("--triggers", value)?;
234 continue;
235 }
236 if let Some(value) = inline_value(&arg, &["--triggers-file"]) {
237 cli.triggers_file = Some(value.to_string());
238 continue;
239 }
240 if let Some(value) = inline_value(&arg, &["--embed-triggers"]) {
241 cli.embed_triggers = parse_bool("--embed-triggers", value)?;
242 continue;
243 }
244 if let Some(value) = inline_value(&arg, &["--transactions"]) {
245 cli.transactions = parse_bool("--transactions", value)?;
246 continue;
247 }
248 if let Some(value) = inline_value(&arg, &["--transactions-file"]) {
249 cli.transactions_file = Some(value.to_string());
250 continue;
251 }
252 if let Some(value) = inline_value(&arg, &["--commit-mode"]) {
253 cli.commit_mode = Some(value.to_string());
254 continue;
255 }
256 if let Some(value) = inline_value(&arg, &["--retention"]) {
257 cli.retention = Some(value.to_string());
258 continue;
259 }
260 if let Some(value) = inline_value(&arg, &["--vc"]) {
261 cli.vc = parse_bool("--vc", value)?;
262 continue;
263 }
264 if let Some(value) = inline_value(&arg, &["--vc-file"]) {
265 cli.vc_file = Some(value.to_string());
266 continue;
267 }
268 if let Some(value) = inline_value(&arg, &["--branch"]) {
269 cli.branch = Some(value.to_string());
270 continue;
271 }
272 if let Some(value) = inline_value(&arg, &["--branch-from"]) {
273 cli.branch_from = Some(parse_seq("--branch-from", value)?);
274 continue;
275 }
276 if let Some(value) = inline_value(&arg, &["--checkout"]) {
277 cli.checkout = Some(value.to_string());
278 continue;
279 }
280 if let Some(value) = inline_value(&arg, &["--tag"]) {
281 cli.tag = Some(value.to_string());
282 continue;
283 }
284 if let Some(value) = inline_value(&arg, &["--list-branches"]) {
285 cli.list_branches = parse_bool("--list-branches", value)?;
286 continue;
287 }
288 if let Some(value) = inline_value(&arg, &["--list-tags"]) {
289 cli.list_tags = parse_bool("--list-tags", value)?;
290 continue;
291 }
292 if let Some(value) = inline_value(&arg, &["--log"]) {
293 cli.show_log = parse_bool("--log", value)?;
294 continue;
295 }
296 if let Some(value) = inline_value(&arg, &["--serve"]) {
297 cli.serve = Some(value.to_string());
298 continue;
299 }
300 if let Some(value) = inline_value(&arg, &["--connect"]) {
301 cli.connect = Some(value.to_string());
302 continue;
303 }
304 if let Some(value) = inline_value(&arg, &["--protocol"]) {
305 cli.protocol = Some(value.to_string());
306 continue;
307 }
308 if let Some(value) = inline_value(&arg, &["--external-references"]) {
309 cli.external_references = parse_bool("--external-references", value)?;
310 continue;
311 }
312 if let Some(value) = inline_value(&arg, &["--arity"]) {
313 cli.arity = parse_arity(value)?;
314 continue;
315 }
316 if let Some(value) = inline_value(&arg, &["--packed-widths"]) {
317 cli.packed_widths = parse_bool("--packed-widths", value)?;
318 continue;
319 }
320
321 match arg.as_str() {
322 "-h" | "--help" => return Ok(CliCommand::Help),
323 "-V" | "--version" => return Ok(CliCommand::Version),
324 "-d" | "--db" | "--data-source" | "--data" => {
325 cli.db = next_value(&mut args, &arg)?;
326 }
327 "-q" | "--query" | "--apply" | "--do" => {
328 cli.query = Some(next_value(&mut args, &arg)?);
329 }
330 "-t" | "--trace" => {
331 cli.trace = next_bool_value(&mut args, true)?;
332 }
333 "--auto-create-missing-references" => {
334 cli.auto_create_missing_references = next_bool_value(&mut args, true)?;
335 }
336 "-s" | "--structure" => {
337 let value = next_value(&mut args, &arg)?;
338 cli.structure = Some(parse_link_id(&arg, &value)?);
339 }
340 "-b" | "--before" => {
341 cli.before = next_bool_value(&mut args, true)?;
342 }
343 "-c" | "--changes" => {
344 cli.changes = next_bool_value(&mut args, true)?;
345 }
346 "-a" | "--after" | "--links" => {
347 cli.after = next_bool_value(&mut args, true)?;
348 }
349 "--out" | "--lino-output" | "--export" => {
350 cli.lino_output = Some(next_value(&mut args, &arg)?);
351 }
352 "--in" | "--lino-input" | "--import" => {
353 cli.lino_input = Some(next_value(&mut args, &arg)?);
354 }
355 "--export-binary" | "--binary-output" | "--binary-out" => {
356 cli.binary_output = Some(next_value(&mut args, &arg)?);
357 }
358 "--import-binary" | "--binary-input" | "--binary-in" => {
359 cli.binary_input = Some(next_value(&mut args, &arg)?);
360 }
361 "--always" => {
362 cli.always = next_bool_value(&mut args, true)?;
363 }
364 "--once" => {
365 cli.once = next_bool_value(&mut args, true)?;
366 }
367 "--never" => {
368 cli.never = next_bool_value(&mut args, true)?;
369 }
370 "--triggers" => {
371 cli.triggers = next_bool_value(&mut args, true)?;
372 }
373 "--triggers-file" => {
374 cli.triggers_file = Some(next_value(&mut args, &arg)?);
375 }
376 "--embed-triggers" => {
377 cli.embed_triggers = next_bool_value(&mut args, true)?;
378 }
379 "--transactions" => {
380 cli.transactions = next_bool_value(&mut args, true)?;
381 }
382 "--transactions-file" => {
383 cli.transactions_file = Some(next_value(&mut args, &arg)?);
384 }
385 "--commit-mode" => {
386 cli.commit_mode = Some(next_value(&mut args, &arg)?);
387 }
388 "--retention" => {
389 cli.retention = Some(next_value(&mut args, &arg)?);
390 }
391 "--vc" => {
392 cli.vc = next_bool_value(&mut args, true)?;
393 }
394 "--vc-file" => {
395 cli.vc_file = Some(next_value(&mut args, &arg)?);
396 }
397 "--branch" => {
398 cli.branch = Some(next_value(&mut args, &arg)?);
399 }
400 "--branch-from" => {
401 let value = next_value(&mut args, &arg)?;
402 cli.branch_from = Some(parse_seq(&arg, &value)?);
403 }
404 "--checkout" => {
405 cli.checkout = Some(next_value(&mut args, &arg)?);
406 }
407 "--tag" => {
408 cli.tag = Some(next_value(&mut args, &arg)?);
409 }
410 "--list-branches" => {
411 cli.list_branches = next_bool_value(&mut args, true)?;
412 }
413 "--list-tags" => {
414 cli.list_tags = next_bool_value(&mut args, true)?;
415 }
416 "--log" => {
417 cli.show_log = next_bool_value(&mut args, true)?;
418 }
419 "--serve" => {
420 cli.serve = Some(next_value(&mut args, &arg)?);
421 }
422 "--connect" => {
423 cli.connect = Some(next_value(&mut args, &arg)?);
424 }
425 "--protocol" => {
426 cli.protocol = Some(next_value(&mut args, &arg)?);
427 }
428 "--external-references" => {
429 cli.external_references = next_bool_value(&mut args, true)?;
430 }
431 "--arity" => {
432 cli.arity = parse_arity(&next_value(&mut args, &arg)?)?;
433 }
434 "--packed-widths" => {
435 cli.packed_widths = next_bool_value(&mut args, true)?;
436 }
437 "--" => {
438 for value in args.by_ref() {
439 set_positional_query(&mut cli, value)?;
440 }
441 break;
442 }
443 value if value.starts_with('-') => {
444 bail!("unknown option '{value}'");
445 }
446 value => {
447 set_positional_query(&mut cli, value.to_string())?;
448 }
449 }
450 }
451
452 Ok(CliCommand::Run(Box::new(cli)))
453 }
454
455 pub fn print_help() {
456 print!("{}", Self::help_text());
457 }
458
459 pub fn help_text() -> &'static str {
460 concat!(
461 "LiNo CLI Tool for managing links data store\n\n",
462 "Usage: clink [OPTIONS] [QUERY]\n\n",
463 "Arguments:\n",
464 " [QUERY] LiNo query for CRUD operation\n\n",
465 "Options:\n",
466 " -d, --db <DB>, --data-source <DB>, --data <DB>\n",
467 " Path to the links database file [default: db.links]\n",
468 " -q, --query <QUERY>, --apply <QUERY>, --do <QUERY>\n",
469 " LiNo query for CRUD operation\n",
470 " -t, --trace\n",
471 " Enable trace (verbose output)\n",
472 " --auto-create-missing-references\n",
473 " Create missing numeric and named references as self-referential point links\n",
474 " -s, --structure <STRUCTURE>\n",
475 " ID of the link to format its structure\n",
476 " -b, --before\n",
477 " Print the state of the database before applying changes\n",
478 " -c, --changes\n",
479 " Print the changes applied by the query\n",
480 " -a, --after, --links\n",
481 " Print the state of the database after applying changes\n",
482 " --in <IN>, --lino-input <IN>, --import <IN>\n",
483 " Read and import a LiNo file into the database\n",
484 " --out <OUT>, --lino-output <OUT>, --export <OUT>\n",
485 " Write the complete database as a LiNo file\n",
486 " --import-binary <PATH>, --binary-input <PATH>, --binary-in <PATH>\n",
487 " Read and import a store archive in binary links notation\n",
488 " --export-binary <PATH>, --binary-output <PATH>, --binary-out <PATH>\n",
489 " Write the complete database, names included, as a store archive in\n",
490 " binary links notation\n",
491 " --always\n",
492 " Store the query as an always-on persistent transformation trigger\n",
493 " --once\n",
494 " Store the query as a persistent transformation trigger that deletes\n",
495 " itself after it fires\n",
496 " --never\n",
497 " Remove stored persistent transformation triggers matching the query\n",
498 " --triggers\n",
499 " Enable persistent transformation triggers for this command\n",
500 " --triggers-file <FILE>\n",
501 " Path to the persistent transformation trigger links database\n",
502 " (default: <db>.triggers.links)\n",
503 " --embed-triggers\n",
504 " Store persistent transformation triggers directly in the main links\n",
505 " database\n",
506 " --transactions\n",
507 " Enable the transactions layer (default log path: <db>.transitions.links)\n",
508 " --transactions-file <FILE>\n",
509 " Path to the transitions log store (implies --transactions)\n",
510 " --commit-mode <MODE>\n",
511 " Choose 'sync' or 'async' commits (default: sync, implies --transactions)\n",
512 " --retention <SPEC>\n",
513 " Log retention policy: 'infinite', 'sized:<n>', or 'chunked:<n>:<dir>'\n",
514 " (implies --transactions)\n",
515 " --vc\n",
516 " Enable the version-control decorator (implies --transactions)\n",
517 " --vc-file <FILE>\n",
518 " Path to the version-control branches store\n",
519 " (default: <db>.versioncontrol.links)\n",
520 " --branch <NAME>\n",
521 " Switch to a branch (creating it if --branch-from is also passed).\n",
522 " Implies --vc.\n",
523 " --branch-from <SEQ>\n",
524 " When creating a branch with --branch, fork from this sequence point\n",
525 " --checkout <POINT>\n",
526 " Time-travel to a specific transition sequence or named tag.\n",
527 " Implies --vc.\n",
528 " --tag <NAME[=SEQ]>\n",
529 " Create a tag at current head or at the given sequence point.\n",
530 " Implies --vc.\n",
531 " --list-branches\n",
532 " List version-control branches and exit\n",
533 " --list-tags\n",
534 " List version-control tags and exit\n",
535 " --log\n",
536 " Print the transitions log and exit (implies --transactions)\n",
537 " --serve <ADDR>\n",
538 " Serve the database over TCP (e.g. 127.0.0.1:7878) until stopped;\n",
539 " each message may use the text or the binary LiNo protocol\n",
540 " --connect <ADDR>\n",
541 " Send the query to a clink server instead of opening a database;\n",
542 " an empty query reads every link\n",
543 " --protocol <PROTOCOL>\n",
544 " 'text' or 'binary' for --connect (default: text); 'text', 'binary'\n",
545 " or 'any' for --serve (default: any)\n",
546 " --external-references\n",
547 " Binary protocol: send numbers and characters as external references\n",
548 " --arity <RANGE>\n",
549 " Binary protocol: link lengths to use, 'n', 'min..max' or 'min..'\n",
550 " (default: 2, doublets only; e.g. 2..3 adds triplets, 1.. any length)\n",
551 " --packed-widths\n",
552 " Binary protocol: give each section of links the narrowest reference\n",
553 " width it needs instead of one width for the whole packet\n",
554 " -h, --help\n",
555 " Print help\n",
556 " -V, --version\n",
557 " Print version\n",
558 )
559 }
560
561 pub fn version_text() -> String {
562 format!("clink {}", env!("CARGO_PKG_VERSION"))
563 }
564}
565
566fn parse_arity(value: &str) -> Result<ArityRange> {
567 value
568 .trim()
569 .parse()
570 .map_err(|error| anyhow::anyhow!("invalid value for '--arity': {error}"))
571}
572
573fn inline_value<'a>(arg: &'a str, names: &[&str]) -> Option<&'a str> {
574 names.iter().find_map(|name| {
575 arg.strip_prefix(name)
576 .and_then(|rest| rest.strip_prefix('='))
577 })
578}
579
580fn next_value<I>(args: &mut I, option: &str) -> Result<String>
581where
582 I: Iterator<Item = String>,
583{
584 args.next()
585 .ok_or_else(|| anyhow::anyhow!("missing value for option '{option}'"))
586}
587
588fn next_bool_value<I>(args: &mut std::iter::Peekable<I>, default: bool) -> Result<bool>
589where
590 I: Iterator<Item = String>,
591{
592 if let Some(value) = args.peek().and_then(|value| bool_literal(value)) {
593 args.next();
594 Ok(value)
595 } else {
596 Ok(default)
597 }
598}
599
600fn parse_bool(option: &str, value: &str) -> Result<bool> {
601 bool_literal(value)
602 .ok_or_else(|| anyhow::anyhow!("invalid boolean value '{value}' for {option}"))
603}
604
605fn bool_literal(value: &str) -> Option<bool> {
606 match value.to_ascii_lowercase().as_str() {
607 "true" | "1" | "yes" | "on" => Some(true),
608 "false" | "0" | "no" | "off" => Some(false),
609 _ => None,
610 }
611}
612
613fn parse_link_id(option: &str, value: &str) -> Result<u32> {
614 value
615 .parse()
616 .map_err(|_| anyhow::anyhow!("invalid link id '{value}' for {option}"))
617}
618
619fn parse_seq(option: &str, value: &str) -> Result<i64> {
620 value
621 .parse()
622 .map_err(|_| anyhow::anyhow!("invalid sequence value '{value}' for {option}"))
623}
624
625fn set_positional_query(cli: &mut Cli, value: String) -> Result<()> {
626 if cli.query_arg.is_some() {
627 bail!("unexpected extra positional argument '{value}'");
628 }
629
630 cli.query_arg = Some(value);
631 Ok(())
632}