1use std::collections::HashMap;
5
6use fslite_core::{
7 ContentQuery, CopyOptions, CreateOptions, FindQuery, LinkTarget, MoveOptions, MutationOptions,
8 NodeKind, PageRequest, ReadOptions, RemoveOptions, Revision, StatOptions, TouchOptions,
9 TrashId, TreeOptions, VirtualPath, WriteOptions,
10};
11
12use crate::Command;
13use crate::lexer::{LexError, Token, tokenize};
14
15#[derive(Debug, Eq, PartialEq)]
17pub enum ParseError {
18 Lex(LexError),
19 UnknownVerb(String),
20 MissingArgument {
21 verb: &'static str,
22 name: &'static str,
23 },
24 InvalidArgument {
25 verb: &'static str,
26 name: &'static str,
27 reason: String,
28 },
29 UnknownFlag {
30 verb: &'static str,
31 flag: String,
32 },
33 TooManyArguments {
41 verb: &'static str,
42 expected: usize,
43 actual: usize,
44 },
45}
46
47impl From<LexError> for ParseError {
48 fn from(err: LexError) -> Self {
49 ParseError::Lex(err)
50 }
51}
52
53struct Args {
54 positionals: Vec<String>,
55 flags: HashMap<String, Option<String>>,
56}
57
58impl Args {
59 fn positional(
60 &self,
61 verb: &'static str,
62 index: usize,
63 name: &'static str,
64 ) -> Result<&str, ParseError> {
65 self.positionals
66 .get(index)
67 .map(String::as_str)
68 .ok_or(ParseError::MissingArgument { verb, name })
69 }
70
71 fn has_flag(&self, name: &str) -> bool {
72 self.flags.contains_key(name)
73 }
74
75 fn flag_value(&self, name: &str) -> Option<&str> {
76 self.flags.get(name).and_then(|v| v.as_deref())
77 }
78
79 fn check_known_flags(&self, verb: &'static str, known: &[&str]) -> Result<(), ParseError> {
80 for flag in self.flags.keys() {
81 if !known.contains(&flag.as_str()) {
82 return Err(ParseError::UnknownFlag {
83 verb,
84 flag: flag.clone(),
85 });
86 }
87 }
88 Ok(())
89 }
90
91 fn check_positional_arity(
99 &self,
100 verb: &'static str,
101 expected: usize,
102 ) -> Result<(), ParseError> {
103 if self.positionals.len() > expected {
104 return Err(ParseError::TooManyArguments {
105 verb,
106 expected,
107 actual: self.positionals.len(),
108 });
109 }
110 Ok(())
111 }
112
113 fn expected_revision(&self, verb: &'static str) -> Result<Option<Revision>, ParseError> {
114 match self.flag_value("expected-revision") {
115 None => Ok(None),
116 Some(raw) => {
117 let value: u64 = raw.parse().map_err(|_| ParseError::InvalidArgument {
118 verb,
119 name: "expected-revision",
120 reason: "must be a non-negative integer".into(),
121 })?;
122 Revision::new(value)
123 .ok_or(ParseError::InvalidArgument {
124 verb,
125 name: "expected-revision",
126 reason: "must be nonzero".into(),
127 })
128 .map(Some)
129 }
130 }
131 }
132
133 fn page(&self, verb: &'static str) -> Result<PageRequest, ParseError> {
134 let mut page = PageRequest::default();
135 if let Some(cursor) = self.flag_value("cursor") {
136 page = page.cursor(Some(cursor.to_string()));
137 }
138 if let Some(limit) = self.flag_value("limit") {
139 let limit: u32 = limit.parse().map_err(|_| ParseError::InvalidArgument {
140 verb,
141 name: "limit",
142 reason: "must be a non-negative integer".into(),
143 })?;
144 page = page.limit(limit);
145 }
146 Ok(page)
147 }
148}
149
150fn split(tokens: Vec<Token>) -> (Vec<String>, HashMap<String, Option<String>>) {
151 let mut positionals = Vec::new();
152 let mut flags = HashMap::new();
153 for token in tokens {
154 match token {
155 Token::Word(word) => positionals.push(word),
156 Token::Flag { name, value } => {
157 flags.insert(name, value);
158 }
159 }
160 }
161 (positionals, flags)
162}
163
164fn parse_path(
165 verb: &'static str,
166 name: &'static str,
167 raw: &str,
168) -> Result<VirtualPath, ParseError> {
169 VirtualPath::parse(raw).map_err(|e| ParseError::InvalidArgument {
170 verb,
171 name,
172 reason: e.message().to_string(),
173 })
174}
175
176pub fn parse(line: &str) -> Result<Command, ParseError> {
178 let tokens = tokenize(line)?;
179 let mut iter = tokens.into_iter();
180 let verb_token = iter.next().ok_or(ParseError::MissingArgument {
181 verb: "<line>",
182 name: "verb",
183 })?;
184 let verb = match verb_token {
185 Token::Word(w) => w,
186 Token::Flag { name, .. } => return Err(ParseError::UnknownVerb(format!("--{name}"))),
187 };
188 let (positionals, flags) = split(iter.collect());
189 let args = Args { positionals, flags };
190
191 match verb.as_str() {
192 "usage" => {
193 args.check_known_flags("usage", &[])?;
194 args.check_positional_arity("usage", 0)?;
195 Ok(Command::WorkspaceUsage)
196 }
197
198 "stat" => {
199 args.check_known_flags("stat", &["no-follow"])?;
200 args.check_positional_arity("stat", 1)?;
201 let path = parse_path("stat", "path", args.positional("stat", 0, "path")?)?;
202 let options = StatOptions::default().follow_symlinks(!args.has_flag("no-follow"));
203 Ok(Command::Stat { path, options })
204 }
205
206 "exists" => {
207 args.check_known_flags("exists", &["no-follow"])?;
208 args.check_positional_arity("exists", 1)?;
209 let path = parse_path("exists", "path", args.positional("exists", 0, "path")?)?;
210 let options = StatOptions::default().follow_symlinks(!args.has_flag("no-follow"));
211 Ok(Command::Exists { path, options })
212 }
213
214 "ls" => {
215 args.check_known_flags("ls", &["cursor", "limit"])?;
216 args.check_positional_arity("ls", 1)?;
217 let path = parse_path("ls", "path", args.positional("ls", 0, "path")?)?;
218 Ok(Command::ReadDir {
219 path,
220 page: args.page("ls")?,
221 })
222 }
223
224 "tree" => {
225 args.check_known_flags("tree", &["max-depth", "follow-symlinks", "cursor", "limit"])?;
226 args.check_positional_arity("tree", 1)?;
227 let path = parse_path("tree", "path", args.positional("tree", 0, "path")?)?;
228 let max_depth = args
229 .flag_value("max-depth")
230 .map(|v| {
231 v.parse().map_err(|_| ParseError::InvalidArgument {
232 verb: "tree",
233 name: "max-depth",
234 reason: "must be a non-negative integer".into(),
235 })
236 })
237 .transpose()?;
238 let options = TreeOptions::default()
239 .max_depth(max_depth)
240 .follow_symlinks(args.has_flag("follow-symlinks"));
241 Ok(Command::Tree {
242 path,
243 options,
244 page: args.page("tree")?,
245 })
246 }
247
248 "mkdir" => {
249 args.check_known_flags("mkdir", &["parents", "exist-ok", "expected-revision"])?;
250 args.check_positional_arity("mkdir", 1)?;
251 let path = parse_path("mkdir", "path", args.positional("mkdir", 0, "path")?)?;
252 let options = CreateOptions::default()
253 .parents(args.has_flag("parents"))
254 .exist_ok(args.has_flag("exist-ok"))
255 .expected_revision(args.expected_revision("mkdir")?);
256 Ok(Command::Mkdir { path, options })
257 }
258
259 "cat" => {
260 args.check_known_flags("cat", &["range", "no-follow"])?;
261 args.check_positional_arity("cat", 1)?;
262 let path = parse_path("cat", "path", args.positional("cat", 0, "path")?)?;
263 let range = args
264 .flag_value("range")
265 .map(|raw| {
266 let (start, end) = raw.split_once('-').ok_or(ParseError::InvalidArgument {
267 verb: "cat",
268 name: "range",
269 reason: "expected START-END".into(),
270 })?;
271 let start: u64 = start.parse().map_err(|_| ParseError::InvalidArgument {
272 verb: "cat",
273 name: "range",
274 reason: "invalid start".into(),
275 })?;
276 let end: u64 = end.parse().map_err(|_| ParseError::InvalidArgument {
277 verb: "cat",
278 name: "range",
279 reason: "invalid end".into(),
280 })?;
281 Ok::<_, ParseError>(fslite_core::ByteRange::new(start, end))
282 })
283 .transpose()?;
284 let options = ReadOptions::default()
285 .range(range)
286 .follow_symlinks(!args.has_flag("no-follow"));
287 Ok(Command::Read { path, options })
288 }
289
290 "write" => {
291 args.check_known_flags("write", &["text", "no-create", "expected-revision"])?;
292 args.check_positional_arity("write", 1)?;
293 let path = parse_path("write", "path", args.positional("write", 0, "path")?)?;
294 let bytes = args
295 .flag_value("text")
296 .map(|s| s.as_bytes().to_vec())
297 .ok_or(ParseError::MissingArgument {
298 verb: "write",
299 name: "--text (or another payload source)",
300 })?;
301 let options = WriteOptions::default()
302 .create(!args.has_flag("no-create"))
303 .expected_revision(args.expected_revision("write")?);
304 Ok(Command::Write {
305 path,
306 bytes,
307 options,
308 })
309 }
310
311 "write-at" => {
312 args.check_known_flags(
313 "write-at",
314 &["offset", "text", "no-create", "expected-revision"],
315 )?;
316 args.check_positional_arity("write-at", 1)?;
317 let path = parse_path("write-at", "path", args.positional("write-at", 0, "path")?)?;
318 let offset: u64 = args
319 .flag_value("offset")
320 .ok_or(ParseError::MissingArgument {
321 verb: "write-at",
322 name: "--offset",
323 })?
324 .parse()
325 .map_err(|_| ParseError::InvalidArgument {
326 verb: "write-at",
327 name: "offset",
328 reason: "must be a non-negative integer".into(),
329 })?;
330 let bytes = args
331 .flag_value("text")
332 .map(|s| s.as_bytes().to_vec())
333 .ok_or(ParseError::MissingArgument {
334 verb: "write-at",
335 name: "--text",
336 })?;
337 let options = WriteOptions::default()
338 .create(!args.has_flag("no-create"))
339 .expected_revision(args.expected_revision("write-at")?);
340 Ok(Command::WriteAt {
341 path,
342 offset,
343 bytes,
344 options,
345 })
346 }
347
348 "append" => {
349 args.check_known_flags("append", &["text", "expected-revision"])?;
350 args.check_positional_arity("append", 1)?;
351 let path = parse_path("append", "path", args.positional("append", 0, "path")?)?;
352 let bytes = args
353 .flag_value("text")
354 .map(|s| s.as_bytes().to_vec())
355 .ok_or(ParseError::MissingArgument {
356 verb: "append",
357 name: "--text",
358 })?;
359 let options =
360 WriteOptions::default().expected_revision(args.expected_revision("append")?);
361 Ok(Command::Append {
362 path,
363 bytes,
364 options,
365 })
366 }
367
368 "truncate" => {
369 args.check_known_flags("truncate", &["length", "expected-revision"])?;
370 args.check_positional_arity("truncate", 1)?;
371 let path = parse_path("truncate", "path", args.positional("truncate", 0, "path")?)?;
372 let length: u64 = args
373 .flag_value("length")
374 .ok_or(ParseError::MissingArgument {
375 verb: "truncate",
376 name: "--length",
377 })?
378 .parse()
379 .map_err(|_| ParseError::InvalidArgument {
380 verb: "truncate",
381 name: "length",
382 reason: "must be a non-negative integer".into(),
383 })?;
384 let options =
385 MutationOptions::default().expected_revision(args.expected_revision("truncate")?);
386 Ok(Command::Truncate {
387 path,
388 length,
389 options,
390 })
391 }
392
393 "touch" => {
394 args.check_known_flags("touch", &["no-create", "expected-revision"])?;
395 args.check_positional_arity("touch", 1)?;
396 let path = parse_path("touch", "path", args.positional("touch", 0, "path")?)?;
397 let options = TouchOptions::default()
398 .create(!args.has_flag("no-create"))
399 .expected_revision(args.expected_revision("touch")?);
400 Ok(Command::Touch { path, options })
401 }
402
403 "cp" => {
404 args.check_known_flags("cp", &["recursive", "overwrite", "expected-revision"])?;
405 args.check_positional_arity("cp", 2)?;
406 let from = parse_path("cp", "from", args.positional("cp", 0, "from")?)?;
407 let to = parse_path("cp", "to", args.positional("cp", 1, "to")?)?;
408 let options = CopyOptions::default()
409 .recursive(args.has_flag("recursive"))
410 .overwrite(args.has_flag("overwrite"))
411 .expected_revision(args.expected_revision("cp")?);
412 Ok(Command::Copy { from, to, options })
413 }
414
415 "mv" => {
416 args.check_known_flags("mv", &["overwrite", "expected-revision"])?;
417 args.check_positional_arity("mv", 2)?;
418 let from = parse_path("mv", "from", args.positional("mv", 0, "from")?)?;
419 let to = parse_path("mv", "to", args.positional("mv", 1, "to")?)?;
420 let options = MoveOptions::default()
421 .overwrite(args.has_flag("overwrite"))
422 .expected_revision(args.expected_revision("mv")?);
423 Ok(Command::Move { from, to, options })
424 }
425
426 "rm" => {
427 args.check_known_flags("rm", &["recursive", "expected-revision"])?;
428 args.check_positional_arity("rm", 1)?;
429 let path = parse_path("rm", "path", args.positional("rm", 0, "path")?)?;
430 let options = RemoveOptions::default()
431 .recursive(args.has_flag("recursive"))
432 .expected_revision(args.expected_revision("rm")?);
433 Ok(Command::Remove { path, options })
434 }
435
436 "ln" => {
437 args.check_known_flags("ln", &["parents", "exist-ok", "expected-revision"])?;
438 args.check_positional_arity("ln", 2)?;
439 let target_raw = args.positional("ln", 0, "target")?;
440 let link_raw = args.positional("ln", 1, "link")?;
441 let target =
442 LinkTarget::parse(target_raw).map_err(|e| ParseError::InvalidArgument {
443 verb: "ln",
444 name: "target",
445 reason: e.message().to_string(),
446 })?;
447 let link = parse_path("ln", "link", link_raw)?;
448 let options = CreateOptions::default()
449 .parents(args.has_flag("parents"))
450 .exist_ok(args.has_flag("exist-ok"))
451 .expected_revision(args.expected_revision("ln")?);
452 Ok(Command::Symlink {
453 target,
454 link,
455 options,
456 })
457 }
458
459 "readlink" => {
460 args.check_known_flags("readlink", &[])?;
461 args.check_positional_arity("readlink", 1)?;
462 let path = parse_path("readlink", "path", args.positional("readlink", 0, "path")?)?;
463 Ok(Command::ReadLink { path })
464 }
465
466 "trash" => {
467 args.check_known_flags("trash", &["expected-revision"])?;
468 args.check_positional_arity("trash", 1)?;
469 let path = parse_path("trash", "path", args.positional("trash", 0, "path")?)?;
470 let options =
471 MutationOptions::default().expected_revision(args.expected_revision("trash")?);
472 Ok(Command::Trash { path, options })
473 }
474
475 "trash-ls" => {
476 args.check_known_flags("trash-ls", &["cursor", "limit"])?;
477 args.check_positional_arity("trash-ls", 0)?;
478 Ok(Command::ListTrash {
479 page: args.page("trash-ls")?,
480 })
481 }
482
483 "restore" => {
484 args.check_known_flags("restore", &["to", "expected-revision"])?;
485 args.check_positional_arity("restore", 1)?;
486 let raw_id = args.positional("restore", 0, "trash-id")?;
487 let trash = TrashId::parse(raw_id).map_err(|_| ParseError::InvalidArgument {
488 verb: "restore",
489 name: "trash-id",
490 reason: "not a valid id".into(),
491 })?;
492 let destination = args
493 .flag_value("to")
494 .map(|raw| parse_path("restore", "to", raw))
495 .transpose()?;
496 let options =
497 MutationOptions::default().expected_revision(args.expected_revision("restore")?);
498 Ok(Command::Restore {
499 trash,
500 destination,
501 options,
502 })
503 }
504
505 "purge" => {
506 args.check_known_flags("purge", &[])?;
507 args.check_positional_arity("purge", 1)?;
508 let raw_id = args.positional("purge", 0, "trash-id")?;
509 let trash = TrashId::parse(raw_id).map_err(|_| ParseError::InvalidArgument {
510 verb: "purge",
511 name: "trash-id",
512 reason: "not a valid id".into(),
513 })?;
514 Ok(Command::Purge { trash })
515 }
516
517 "setattr" => {
518 args.check_known_flags("setattr", &["value", "expected-revision"])?;
519 args.check_positional_arity("setattr", 2)?;
520 let path = parse_path("setattr", "path", args.positional("setattr", 0, "path")?)?;
521 let key = args.positional("setattr", 1, "key")?.to_string();
522 let value = args
523 .flag_value("value")
524 .map(|s| s.as_bytes().to_vec())
525 .ok_or(ParseError::MissingArgument {
526 verb: "setattr",
527 name: "--value",
528 })?;
529 let options =
530 MutationOptions::default().expected_revision(args.expected_revision("setattr")?);
531 Ok(Command::SetAttribute {
532 path,
533 key,
534 value,
535 options,
536 })
537 }
538
539 "rmattr" => {
540 args.check_known_flags("rmattr", &["expected-revision"])?;
541 args.check_positional_arity("rmattr", 2)?;
542 let path = parse_path("rmattr", "path", args.positional("rmattr", 0, "path")?)?;
543 let key = args.positional("rmattr", 1, "key")?.to_string();
544 let options =
545 MutationOptions::default().expected_revision(args.expected_revision("rmattr")?);
546 Ok(Command::RemoveAttribute { path, key, options })
547 }
548
549 "glob" => {
550 args.check_known_flags("glob", &["cursor", "limit"])?;
551 args.check_positional_arity("glob", 1)?;
552 let pattern = args.positional("glob", 0, "pattern")?.to_string();
553 Ok(Command::Glob {
554 pattern,
555 page: args.page("glob")?,
556 })
557 }
558
559 "find" => {
560 args.check_known_flags(
561 "find",
562 &[
563 "name-contains",
564 "kind",
565 "min-size",
566 "max-size",
567 "modified-after",
568 "modified-before",
569 "cursor",
570 "limit",
571 ],
572 )?;
573 args.check_positional_arity("find", 1)?;
574 let root = parse_path("find", "root", args.positional("find", 0, "root")?)?;
575 let kind = args
576 .flag_value("kind")
577 .map(|k| match k {
578 "file" => Ok(NodeKind::File),
579 "directory" => Ok(NodeKind::Directory),
580 "symlink" => Ok(NodeKind::Symlink),
581 other => Err(ParseError::InvalidArgument {
582 verb: "find",
583 name: "kind",
584 reason: format!("unknown kind `{other}`"),
585 }),
586 })
587 .transpose()?;
588 let min_logical_size = args
589 .flag_value("min-size")
590 .map(|v| {
591 v.parse().map_err(|_| ParseError::InvalidArgument {
592 verb: "find",
593 name: "min-size",
594 reason: "must be a non-negative integer".into(),
595 })
596 })
597 .transpose()?;
598 let max_logical_size = args
599 .flag_value("max-size")
600 .map(|v| {
601 v.parse().map_err(|_| ParseError::InvalidArgument {
602 verb: "find",
603 name: "max-size",
604 reason: "must be a non-negative integer".into(),
605 })
606 })
607 .transpose()?;
608 let modified_after_ms = args
609 .flag_value("modified-after")
610 .map(|v| {
611 v.parse().map_err(|_| ParseError::InvalidArgument {
612 verb: "find",
613 name: "modified-after",
614 reason: "must be an integer".into(),
615 })
616 })
617 .transpose()?;
618 let modified_before_ms = args
619 .flag_value("modified-before")
620 .map(|v| {
621 v.parse().map_err(|_| ParseError::InvalidArgument {
622 verb: "find",
623 name: "modified-before",
624 reason: "must be an integer".into(),
625 })
626 })
627 .transpose()?;
628 let query = FindQuery::default()
629 .root(root)
630 .name_contains(args.flag_value("name-contains").map(str::to_string))
631 .kind(kind)
632 .min_logical_size(min_logical_size)
633 .max_logical_size(max_logical_size)
634 .modified_after_ms(modified_after_ms)
635 .modified_before_ms(modified_before_ms);
636 Ok(Command::Find {
637 query,
638 page: args.page("find")?,
639 })
640 }
641
642 "grep" => {
643 args.check_known_flags("grep", &["cursor", "limit"])?;
644 args.check_positional_arity("grep", 2)?;
645 let root = parse_path("grep", "root", args.positional("grep", 0, "root")?)?;
646 let needle = args.positional("grep", 1, "needle")?.as_bytes().to_vec();
647 let query = ContentQuery::default().root(root).needle(needle);
648 Ok(Command::SearchContent {
649 query,
650 page: args.page("grep")?,
651 })
652 }
653
654 "changes" => {
655 args.check_known_flags("changes", &["after", "cursor", "limit"])?;
656 args.check_positional_arity("changes", 0)?;
657 let after = args
658 .flag_value("after")
659 .map(|raw| fslite_core::ChangeCursor::new(raw.to_string()));
660 Ok(Command::Changes {
661 after,
662 page: args.page("changes")?,
663 })
664 }
665
666 "batch" => {
667 args.check_known_flags("batch", &["file"])?;
668 args.check_positional_arity("batch", 0)?;
669 let file = args.flag_value("file").ok_or(ParseError::MissingArgument {
670 verb: "batch",
671 name: "--file",
672 })?;
673 let contents =
674 std::fs::read_to_string(file).map_err(|e| ParseError::InvalidArgument {
675 verb: "batch",
676 name: "file",
677 reason: e.to_string(),
678 })?;
679 let operations: Vec<fslite_core::BatchOperation> = serde_json::from_str(&contents)
680 .map_err(|e| ParseError::InvalidArgument {
681 verb: "batch",
682 name: "file",
683 reason: e.to_string(),
684 })?;
685 Ok(Command::Batch(operations))
686 }
687
688 other => Err(ParseError::UnknownVerb(other.to_string())),
689 }
690}