1use std::fmt;
7use std::fs::{self, File as StdFile, OpenOptions};
8use std::io::{self, Read, Write};
9use std::path::{Path as StdPath, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use chrono::{DateTime as ChronoDateTime, NaiveDate, NaiveDateTime, Utc};
13use uuid::Uuid;
14
15fn format_float(value: f64) -> String {
17 if value.fract() == 0.0 && value.is_finite() {
18 format!("{:.1}", value)
19 } else {
20 format!("{}", value)
21 }
22}
23
24pub trait TypeConverter: fmt::Debug + Send + Sync {
33 type Value;
35
36 fn name(&self) -> &str;
38
39 fn convert(&self, value: &str) -> Result<Self::Value, String>;
43
44 fn get_metavar(&self) -> Option<String> {
48 None
49 }
50
51 fn get_missing_message(&self) -> Option<String> {
53 None
54 }
55
56 fn split_envvar_value(&self, value: &str) -> Vec<String> {
61 value.split_whitespace().map(|s| s.to_string()).collect()
62 }
63
64 fn shell_complete(&self, _incomplete: &str) -> Vec<CompletionItem> {
69 Vec::new()
70 }
71
72 fn is_composite(&self) -> bool {
74 false
75 }
76
77 fn arity(&self) -> usize {
79 1
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct CompletionItem {
86 pub value: String,
88 pub completion_type: String,
90 pub help: Option<String>,
92}
93
94impl CompletionItem {
95 pub fn new(value: impl Into<String>) -> Self {
97 Self {
98 value: value.into(),
99 completion_type: "plain".to_string(),
100 help: None,
101 }
102 }
103
104 pub fn with_type(value: impl Into<String>, completion_type: impl Into<String>) -> Self {
106 Self {
107 value: value.into(),
108 completion_type: completion_type.into(),
109 help: None,
110 }
111 }
112
113 pub fn with_help(mut self, help: impl Into<String>) -> Self {
115 self.help = Some(help.into());
116 self
117 }
118}
119
120#[derive(Debug, Clone, Copy, Default)]
128pub struct StringType;
129
130impl TypeConverter for StringType {
131 type Value = String;
132
133 fn name(&self) -> &str {
134 "TEXT"
135 }
136
137 fn convert(&self, value: &str) -> Result<Self::Value, String> {
138 Ok(value.to_string())
139 }
140
141 fn get_metavar(&self) -> Option<String> {
142 Some("TEXT".to_string())
143 }
144}
145
146pub const STRING: StringType = StringType;
148
149#[derive(Debug, Clone, Copy, Default)]
155pub struct IntType;
156
157impl TypeConverter for IntType {
158 type Value = i64;
159
160 fn name(&self) -> &str {
161 "INTEGER"
162 }
163
164 fn convert(&self, value: &str) -> Result<Self::Value, String> {
165 value
166 .trim()
167 .parse::<i64>()
168 .map_err(|_| format!("'{}' is not a valid integer.", value))
169 }
170
171 fn get_metavar(&self) -> Option<String> {
172 Some("INTEGER".to_string())
173 }
174}
175
176pub const INT: IntType = IntType;
178
179#[derive(Debug, Clone, Copy, Default)]
185pub struct FloatType;
186
187impl TypeConverter for FloatType {
188 type Value = f64;
189
190 fn name(&self) -> &str {
191 "FLOAT"
192 }
193
194 fn convert(&self, value: &str) -> Result<Self::Value, String> {
195 value
196 .trim()
197 .parse::<f64>()
198 .map_err(|_| format!("'{}' is not a valid float.", value))
199 }
200
201 fn get_metavar(&self) -> Option<String> {
202 Some("FLOAT".to_string())
203 }
204}
205
206pub const FLOAT: FloatType = FloatType;
208
209#[derive(Debug, Clone, Copy, Default)]
219pub struct BoolType;
220
221impl BoolType {
222 pub fn str_to_bool(value: &str) -> Option<bool> {
224 match value.trim().to_lowercase().as_str() {
225 "1" | "true" | "yes" | "on" | "t" | "y" => Some(true),
226 "0" | "false" | "no" | "off" | "f" | "n" | "" => Some(false),
227 _ => None,
228 }
229 }
230
231 pub const BOOL_STATES: &'static [&'static str] = &[
233 "", "0", "1", "f", "false", "n", "no", "off", "on", "t", "true", "y", "yes",
234 ];
235}
236
237impl TypeConverter for BoolType {
238 type Value = bool;
239
240 fn name(&self) -> &str {
241 "BOOLEAN"
242 }
243
244 fn convert(&self, value: &str) -> Result<Self::Value, String> {
245 Self::str_to_bool(value).ok_or_else(|| {
246 format!(
247 "'{}' is not a valid boolean. Recognized values: {}",
248 value,
249 Self::BOOL_STATES.join(", ")
250 )
251 })
252 }
253
254 fn get_metavar(&self) -> Option<String> {
255 Some("BOOLEAN".to_string())
256 }
257}
258
259pub const BOOL: BoolType = BoolType;
261
262#[derive(Debug, Clone, Copy, Default)]
268pub struct UuidType;
269
270impl TypeConverter for UuidType {
271 type Value = Uuid;
272
273 fn name(&self) -> &str {
274 "UUID"
275 }
276
277 fn convert(&self, value: &str) -> Result<Self::Value, String> {
278 Uuid::parse_str(value.trim()).map_err(|_| format!("'{}' is not a valid UUID", value))
279 }
280
281 fn get_metavar(&self) -> Option<String> {
282 Some("UUID".to_string())
283 }
284}
285
286pub const UUID: UuidType = UuidType;
288
289#[derive(Debug, Clone, Copy, Default)]
298pub struct UnprocessedType;
299
300impl TypeConverter for UnprocessedType {
301 type Value = String;
302
303 fn name(&self) -> &str {
304 "TEXT"
305 }
306
307 fn convert(&self, value: &str) -> Result<Self::Value, String> {
308 Ok(value.to_string())
309 }
310}
311
312pub const UNPROCESSED: UnprocessedType = UnprocessedType;
314
315#[derive(Debug, Clone, Copy)]
325pub struct IntRange {
326 pub min: Option<i64>,
328 pub max: Option<i64>,
330 pub min_open: bool,
332 pub max_open: bool,
334 pub clamp: bool,
336}
337
338impl Default for IntRange {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344impl IntRange {
345 pub const fn new() -> Self {
347 Self {
348 min: None,
349 max: None,
350 min_open: false,
351 max_open: false,
352 clamp: false,
353 }
354 }
355
356 pub const fn min(mut self, min: i64) -> Self {
358 self.min = Some(min);
359 self
360 }
361
362 pub const fn max(mut self, max: i64) -> Self {
364 self.max = Some(max);
365 self
366 }
367
368 pub const fn range(mut self, min: i64, max: i64) -> Self {
370 self.min = Some(min);
371 self.max = Some(max);
372 self
373 }
374
375 pub const fn min_open(mut self, open: bool) -> Self {
377 self.min_open = open;
378 self
379 }
380
381 pub const fn max_open(mut self, open: bool) -> Self {
383 self.max_open = open;
384 self
385 }
386
387 pub const fn clamp(mut self, clamp: bool) -> Self {
389 self.clamp = clamp;
390 self
391 }
392
393 fn describe_range(&self) -> String {
395 match (self.min, self.max) {
396 (None, None) => "any integer".to_string(),
397 (Some(min), None) => {
398 let op = if self.min_open { ">" } else { ">=" };
399 format!("x{}{}", op, min)
400 }
401 (None, Some(max)) => {
402 let op = if self.max_open { "<" } else { "<=" };
403 format!("x{}{}", op, max)
404 }
405 (Some(min), Some(max)) => {
406 let lop = if self.min_open { "<" } else { "<=" };
407 let rop = if self.max_open { "<" } else { "<=" };
408 format!("{}{lop}x{rop}{}", min, max)
409 }
410 }
411 }
412
413 fn clamp_value(&self, value: i64) -> i64 {
415 let mut result = value;
416 if let Some(min) = self.min {
417 let effective_min = if self.min_open {
419 min.saturating_add(1)
420 } else {
421 min
422 };
423 if result < effective_min {
424 result = effective_min;
425 }
426 }
427 if let Some(max) = self.max {
428 let effective_max = if self.max_open {
430 max.saturating_sub(1)
431 } else {
432 max
433 };
434 if result > effective_max {
435 result = effective_max;
436 }
437 }
438 result
439 }
440}
441
442impl TypeConverter for IntRange {
443 type Value = i64;
444
445 fn name(&self) -> &str {
446 "INTEGER RANGE"
447 }
448
449 fn convert(&self, value: &str) -> Result<Self::Value, String> {
450 let parsed: i64 = value
451 .trim()
452 .parse()
453 .map_err(|_| format!("'{}' is not a valid integer range.", value))?;
454
455 let lt_min = self.min.is_some_and(|min| {
457 if self.min_open {
458 parsed <= min
459 } else {
460 parsed < min
461 }
462 });
463
464 let gt_max = self.max.is_some_and(|max| {
466 if self.max_open {
467 parsed >= max
468 } else {
469 parsed > max
470 }
471 });
472
473 if self.clamp && (lt_min || gt_max) {
474 return Ok(self.clamp_value(parsed));
475 }
476
477 if lt_min || gt_max {
478 return Err(format!(
479 "{} is not in the range {}.",
480 parsed,
481 self.describe_range()
482 ));
483 }
484
485 Ok(parsed)
486 }
487
488 fn get_metavar(&self) -> Option<String> {
489 Some(format!("INTEGER RANGE {}", self.describe_range()))
490 }
491}
492
493#[derive(Debug, Clone, Copy)]
504pub struct FloatRange {
505 pub min: Option<f64>,
507 pub max: Option<f64>,
509 pub min_open: bool,
511 pub max_open: bool,
513 pub clamp: bool,
516}
517
518impl Default for FloatRange {
519 fn default() -> Self {
520 Self::new()
521 }
522}
523
524impl FloatRange {
525 pub const fn new() -> Self {
527 Self {
528 min: None,
529 max: None,
530 min_open: false,
531 max_open: false,
532 clamp: false,
533 }
534 }
535
536 pub fn min(mut self, min: f64) -> Self {
538 self.min = Some(min);
539 self
540 }
541
542 pub fn max(mut self, max: f64) -> Self {
544 self.max = Some(max);
545 self
546 }
547
548 pub fn range(mut self, min: f64, max: f64) -> Self {
550 self.min = Some(min);
551 self.max = Some(max);
552 self
553 }
554
555 pub fn min_open(mut self, open: bool) -> Self {
560 if open && self.clamp {
561 panic!("Clamping is not supported for open bounds");
562 }
563 self.min_open = open;
564 self
565 }
566
567 pub fn max_open(mut self, open: bool) -> Self {
572 if open && self.clamp {
573 panic!("Clamping is not supported for open bounds");
574 }
575 self.max_open = open;
576 self
577 }
578
579 pub fn clamp(mut self, clamp: bool) -> Self {
584 if clamp && (self.min_open || self.max_open) {
585 panic!("Clamping is not supported for open bounds");
586 }
587 self.clamp = clamp;
588 self
589 }
590
591 fn describe_range(&self) -> String {
593 match (self.min, self.max) {
594 (None, None) => "any float".to_string(),
595 (Some(min), None) => {
596 let op = if self.min_open { ">" } else { ">=" };
597 format!("x{}{}", op, format_float(min))
598 }
599 (None, Some(max)) => {
600 let op = if self.max_open { "<" } else { "<=" };
601 format!("x{}{}", op, format_float(max))
602 }
603 (Some(min), Some(max)) => {
604 let lop = if self.min_open { "<" } else { "<=" };
605 let rop = if self.max_open { "<" } else { "<=" };
606 format!("{}{lop}x{rop}{}", format_float(min), format_float(max))
607 }
608 }
609 }
610}
611
612impl TypeConverter for FloatRange {
613 type Value = f64;
614
615 fn name(&self) -> &str {
616 "FLOAT RANGE"
617 }
618
619 fn convert(&self, value: &str) -> Result<Self::Value, String> {
620 let parsed: f64 = value
621 .trim()
622 .parse()
623 .map_err(|_| format!("'{}' is not a valid float range.", value))?;
624
625 let lt_min = self.min.is_some_and(|min| {
627 if self.min_open {
628 parsed <= min
629 } else {
630 parsed < min
631 }
632 });
633
634 let gt_max = self.max.is_some_and(|max| {
636 if self.max_open {
637 parsed >= max
638 } else {
639 parsed > max
640 }
641 });
642
643 if self.clamp {
644 if lt_min {
645 return Ok(self.min.unwrap());
646 }
647 if gt_max {
648 return Ok(self.max.unwrap());
649 }
650 }
651
652 if lt_min || gt_max {
653 return Err(format!(
654 "{} is not in the range {}.",
655 format_float(parsed),
656 self.describe_range()
657 ));
658 }
659
660 Ok(parsed)
661 }
662
663 fn get_metavar(&self) -> Option<String> {
664 Some(format!("FLOAT RANGE {}", self.describe_range()))
665 }
666}
667
668#[derive(Debug, Clone)]
681pub struct DateTimeType {
682 pub formats: Vec<String>,
684}
685
686impl Default for DateTimeType {
687 fn default() -> Self {
688 Self::new()
689 }
690}
691
692impl DateTimeType {
693 pub const DEFAULT_FORMATS: &'static [&'static str] = &[
695 "%Y-%m-%d",
696 "%Y-%m-%dT%H:%M:%S",
697 "%Y-%m-%d %H:%M:%S",
698 "%Y-%m-%dT%H:%M:%S%.f",
699 "%Y-%m-%dT%H:%M:%S%z",
700 "%Y-%m-%dT%H:%M:%S%.f%z",
701 ];
702
703 pub fn new() -> Self {
705 Self {
706 formats: Self::DEFAULT_FORMATS
707 .iter()
708 .map(|s| s.to_string())
709 .collect(),
710 }
711 }
712
713 pub fn with_formats(formats: impl IntoIterator<Item = impl Into<String>>) -> Self {
715 Self {
716 formats: formats.into_iter().map(|s| s.into()).collect(),
717 }
718 }
719}
720
721impl TypeConverter for DateTimeType {
722 type Value = NaiveDateTime;
723
724 fn name(&self) -> &str {
725 "DATETIME"
726 }
727
728 fn convert(&self, value: &str) -> Result<Self::Value, String> {
729 let value = value.trim();
730
731 for format in &self.formats {
733 if let Ok(dt) = NaiveDateTime::parse_from_str(value, format) {
735 return Ok(dt);
736 }
737 if let Ok(date) = NaiveDate::parse_from_str(value, format) {
739 return Ok(date.and_hms_opt(0, 0, 0).unwrap());
740 }
741 if let Ok(dt) = ChronoDateTime::parse_from_str(value, format) {
743 return Ok(dt.with_timezone(&Utc).naive_utc());
744 }
745 }
746
747 Err(format!(
748 "'{}' does not match the formats: {}",
749 value,
750 self.formats.join(", ")
751 ))
752 }
753
754 fn get_metavar(&self) -> Option<String> {
755 Some(format!("[{}]", self.formats.join("|")))
756 }
757}
758
759#[derive(Debug, Clone)]
768pub struct Choice {
769 pub choices: Vec<String>,
771 pub case_sensitive: bool,
773}
774
775impl Choice {
776 pub fn new<I, S>(choices: I) -> Self
778 where
779 I: IntoIterator<Item = S>,
780 S: Into<String>,
781 {
782 Self {
783 choices: choices.into_iter().map(|s| s.into()).collect(),
784 case_sensitive: true,
785 }
786 }
787
788 pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
790 self.case_sensitive = case_sensitive;
791 self
792 }
793
794 fn normalize(&self, value: &str) -> String {
796 if self.case_sensitive {
797 value.to_string()
798 } else {
799 value.to_lowercase()
800 }
801 }
802}
803
804impl TypeConverter for Choice {
805 type Value = String;
806
807 fn name(&self) -> &str {
808 "CHOICE"
809 }
810
811 fn convert(&self, value: &str) -> Result<Self::Value, String> {
812 let normalized_value = self.normalize(value);
813
814 for choice in &self.choices {
815 if self.normalize(choice) == normalized_value {
816 return Ok(choice.clone());
818 }
819 }
820
821 if self.choices.len() == 1 {
822 Err(format!("'{}' is not '{}'.", value, self.choices[0]))
823 } else {
824 let choices_str = self
825 .choices
826 .iter()
827 .map(|c| format!("'{}'", c))
828 .collect::<Vec<_>>()
829 .join(", ");
830 Err(format!("'{}' is not one of {}.", value, choices_str))
831 }
832 }
833
834 fn get_missing_message(&self) -> Option<String> {
835 Some(format!("Choose from:\n\t{}", self.choices.join(",\n\t")))
836 }
837
838 fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem> {
839 let normalized_incomplete = self.normalize(incomplete);
840 self.choices
841 .iter()
842 .filter(|choice| {
843 let normalized_choice = self.normalize(choice);
844 normalized_choice.starts_with(&normalized_incomplete)
845 })
846 .map(|choice| CompletionItem::new(choice.clone()))
847 .collect()
848 }
849
850 fn get_metavar(&self) -> Option<String> {
851 if self.choices.is_empty() {
852 return Some("CHOICE".to_string());
853 }
854 Some(self.choices.join("|"))
855 }
856}
857
858#[derive(Debug, Clone)]
867pub struct PathType {
868 pub exists: bool,
870 pub file_okay: bool,
872 pub dir_okay: bool,
874 pub readable: bool,
876 pub writable: bool,
878 pub executable: bool,
880 pub resolve_path: bool,
882 pub allow_dash: bool,
884}
885
886impl Default for PathType {
887 fn default() -> Self {
888 Self::new()
889 }
890}
891
892impl PathType {
893 pub const fn new() -> Self {
895 Self {
896 exists: false,
897 file_okay: true,
898 dir_okay: true,
899 readable: true,
900 writable: false,
901 executable: false,
902 resolve_path: false,
903 allow_dash: false,
904 }
905 }
906
907 pub const fn exists(mut self, exists: bool) -> Self {
909 self.exists = exists;
910 self
911 }
912
913 pub const fn file_okay(mut self, file_okay: bool) -> Self {
915 self.file_okay = file_okay;
916 self
917 }
918
919 pub const fn dir_okay(mut self, dir_okay: bool) -> Self {
921 self.dir_okay = dir_okay;
922 self
923 }
924
925 pub const fn readable(mut self, readable: bool) -> Self {
927 self.readable = readable;
928 self
929 }
930
931 pub const fn writable(mut self, writable: bool) -> Self {
933 self.writable = writable;
934 self
935 }
936
937 pub const fn executable(mut self, executable: bool) -> Self {
939 self.executable = executable;
940 self
941 }
942
943 pub const fn resolve_path(mut self, resolve_path: bool) -> Self {
945 self.resolve_path = resolve_path;
946 self
947 }
948
949 pub const fn allow_dash(mut self, allow_dash: bool) -> Self {
951 self.allow_dash = allow_dash;
952 self
953 }
954
955 fn type_name(&self) -> &str {
957 if self.file_okay && !self.dir_okay {
958 "File"
959 } else if self.dir_okay && !self.file_okay {
960 "Directory"
961 } else {
962 "Path"
963 }
964 }
965}
966
967impl TypeConverter for PathType {
968 type Value = PathBuf;
969
970 fn name(&self) -> &str {
971 if self.file_okay && !self.dir_okay {
972 "FILE"
973 } else if self.dir_okay && !self.file_okay {
974 "DIRECTORY"
975 } else {
976 "PATH"
977 }
978 }
979
980 fn convert(&self, value: &str) -> Result<Self::Value, String> {
981 if !self.file_okay && !self.dir_okay {
983 return Err("No path is valid (file_okay=false and dir_okay=false)".to_string());
984 }
985
986 if value == "-" {
988 if self.file_okay && self.allow_dash {
989 return Ok(PathBuf::from("-"));
990 }
991 return Err("'-' is not allowed".to_string());
992 }
993
994 let path = if self.resolve_path {
995 match std::fs::canonicalize(value) {
996 Ok(p) => p,
997 Err(_) if !self.exists => {
998 std::env::current_dir()
1001 .map(|cwd| cwd.join(value))
1002 .unwrap_or_else(|_| PathBuf::from(value))
1003 }
1004 Err(_) => return Err(format!("{} '{}' does not exist", self.type_name(), value)),
1005 }
1006 } else {
1007 PathBuf::from(value)
1008 };
1009
1010 if self.exists && !path.exists() {
1012 return Err(format!("{} '{}' does not exist", self.type_name(), value));
1013 }
1014
1015 if path.exists() {
1017 let metadata = std::fs::metadata(&path)
1018 .map_err(|e| format!("Cannot access '{}': {}", value, e))?;
1019
1020 if !self.file_okay && metadata.is_file() {
1022 return Err(format!("{} '{}' is a file", self.type_name(), value));
1023 }
1024 if !self.dir_okay && metadata.is_dir() {
1025 return Err(format!("{} '{}' is a directory", self.type_name(), value));
1026 }
1027
1028 #[cfg(unix)]
1030 {
1031 use std::os::unix::fs::PermissionsExt;
1032 let perms = metadata.permissions();
1033 let mode = perms.mode();
1034
1035 if self.readable && (mode & 0o444) == 0 {
1036 return Err(format!("{} '{}' is not readable", self.type_name(), value));
1037 }
1038 if self.writable && (mode & 0o222) == 0 {
1039 return Err(format!("{} '{}' is not writable", self.type_name(), value));
1040 }
1041 if self.executable && (mode & 0o111) == 0 {
1042 return Err(format!(
1043 "{} '{}' is not executable",
1044 self.type_name(),
1045 value
1046 ));
1047 }
1048 }
1049 }
1050
1051 Ok(path)
1052 }
1053
1054 fn split_envvar_value(&self, value: &str) -> Vec<String> {
1055 std::env::split_paths(value)
1057 .map(|p| p.to_string_lossy().into_owned())
1058 .collect()
1059 }
1060
1061 fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem> {
1062 let completion_type = if self.dir_okay && !self.file_okay {
1063 "dir"
1064 } else {
1065 "file"
1066 };
1067 vec![CompletionItem::with_type(incomplete, completion_type)]
1068 }
1069}
1070
1071#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1077pub enum FileMode {
1078 #[default]
1080 Read,
1081 Write,
1083 Append,
1085 ReadWrite,
1087}
1088
1089impl FileMode {
1090 pub fn parse(s: &str) -> Option<Self> {
1092 match s {
1093 "r" | "rb" => Some(FileMode::Read),
1094 "w" | "wb" => Some(FileMode::Write),
1095 "a" | "ab" => Some(FileMode::Append),
1096 "r+" | "rb+" | "r+b" => Some(FileMode::ReadWrite),
1097 "w+" | "wb+" | "w+b" => Some(FileMode::ReadWrite),
1098 "a+" | "ab+" | "a+b" => Some(FileMode::ReadWrite), _ => None,
1100 }
1101 }
1102
1103 pub fn is_read(&self) -> bool {
1105 matches!(self, FileMode::Read | FileMode::ReadWrite)
1106 }
1107
1108 pub fn is_write(&self) -> bool {
1110 matches!(
1111 self,
1112 FileMode::Write | FileMode::Append | FileMode::ReadWrite
1113 )
1114 }
1115}
1116
1117#[derive(Debug)]
1119enum FileSource {
1120 File(StdFile),
1122 Stdin,
1124 Stdout,
1126}
1127
1128static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
1130
1131#[derive(Debug)]
1137pub struct LazyFile {
1138 path: PathBuf,
1139 mode: FileMode,
1140 source: Option<FileSource>,
1141 is_stdio: bool,
1142 atomic: bool,
1143 temp_path: Option<PathBuf>,
1145}
1146
1147impl LazyFile {
1148 pub fn new(path: PathBuf, mode: FileMode) -> Self {
1150 let is_stdio = path.as_os_str() == "-";
1151 Self {
1152 path,
1153 mode,
1154 source: None,
1155 is_stdio,
1156 atomic: false,
1157 temp_path: None,
1158 }
1159 }
1160
1161 pub fn stdin() -> Self {
1163 Self {
1164 path: PathBuf::from("-"),
1165 mode: FileMode::Read,
1166 source: None,
1167 is_stdio: true,
1168 atomic: false,
1169 temp_path: None,
1170 }
1171 }
1172
1173 pub fn stdout() -> Self {
1175 Self {
1176 path: PathBuf::from("-"),
1177 mode: FileMode::Write,
1178 source: None,
1179 is_stdio: true,
1180 atomic: false,
1181 temp_path: None,
1182 }
1183 }
1184
1185 pub fn atomic(mut self, atomic: bool) -> Self {
1187 self.atomic = atomic;
1188 self
1189 }
1190
1191 pub fn path(&self) -> &StdPath {
1193 &self.path
1194 }
1195
1196 pub fn is_stdio(&self) -> bool {
1198 self.is_stdio
1199 }
1200
1201 fn open(&mut self) -> io::Result<()> {
1203 if self.source.is_some() {
1204 return Ok(());
1205 }
1206
1207 let source = if self.is_stdio {
1208 if self.mode.is_read() {
1209 FileSource::Stdin
1210 } else {
1211 FileSource::Stdout
1212 }
1213 } else if self.atomic && self.mode == FileMode::Write {
1214 let parent = self.path.parent().unwrap_or(StdPath::new("."));
1216 let counter = TEMP_COUNTER.fetch_add(1, Ordering::SeqCst);
1217 let temp_name = format!(
1218 ".{}.tmp.{}",
1219 self.path
1220 .file_name()
1221 .map(|n| n.to_string_lossy())
1222 .unwrap_or_default(),
1223 counter
1224 );
1225 let temp_path = parent.join(&temp_name);
1226 let file = StdFile::create(&temp_path)?;
1227 self.temp_path = Some(temp_path);
1228 FileSource::File(file)
1229 } else {
1230 let file = match self.mode {
1231 FileMode::Read => StdFile::open(&self.path),
1232 FileMode::Write => StdFile::create(&self.path),
1233 FileMode::Append => OpenOptions::new()
1234 .append(true)
1235 .create(true)
1236 .open(&self.path),
1237 FileMode::ReadWrite => OpenOptions::new()
1238 .read(true)
1239 .write(true)
1240 .create(true)
1241 .truncate(false)
1242 .open(&self.path),
1243 }?;
1244 FileSource::File(file)
1245 };
1246 self.source = Some(source);
1247 Ok(())
1248 }
1249
1250 pub fn close(&mut self) -> io::Result<()> {
1258 if let Some(FileSource::File(mut f)) = self.source.take() {
1260 f.flush()?;
1261 }
1263
1264 if let Some(temp_path) = self.temp_path.take() {
1266 fs::rename(&temp_path, &self.path)?;
1267 }
1268
1269 Ok(())
1270 }
1271}
1272
1273impl Drop for LazyFile {
1274 fn drop(&mut self) {
1275 let _ = self.close();
1277 }
1278}
1279
1280impl Read for LazyFile {
1281 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1282 self.open()?;
1283 match self.source.as_mut() {
1284 Some(FileSource::File(f)) => f.read(buf),
1285 Some(FileSource::Stdin) => io::stdin().read(buf),
1286 _ => Err(io::Error::new(
1287 io::ErrorKind::InvalidInput,
1288 "Cannot read from write-only file",
1289 )),
1290 }
1291 }
1292}
1293
1294impl Write for LazyFile {
1295 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1296 self.open()?;
1297 match self.source.as_mut() {
1298 Some(FileSource::File(f)) => f.write(buf),
1299 Some(FileSource::Stdout) => io::stdout().write(buf),
1300 _ => Err(io::Error::new(
1301 io::ErrorKind::InvalidInput,
1302 "Cannot write to read-only file",
1303 )),
1304 }
1305 }
1306
1307 fn flush(&mut self) -> io::Result<()> {
1308 match self.source.as_mut() {
1309 Some(FileSource::File(f)) => f.flush(),
1310 Some(FileSource::Stdout) => io::stdout().flush(),
1311 _ => Ok(()),
1312 }
1313 }
1314}
1315
1316#[derive(Debug, Clone)]
1321pub struct FileType {
1322 pub mode: FileMode,
1324 pub lazy: Option<bool>,
1326 pub atomic: bool,
1328}
1329
1330impl Default for FileType {
1331 fn default() -> Self {
1332 Self::new()
1333 }
1334}
1335
1336impl FileType {
1337 pub const fn new() -> Self {
1339 Self {
1340 mode: FileMode::Read,
1341 lazy: None,
1342 atomic: false,
1343 }
1344 }
1345
1346 pub const fn mode(mut self, mode: FileMode) -> Self {
1348 self.mode = mode;
1349 self
1350 }
1351
1352 pub const fn lazy(mut self, lazy: bool) -> Self {
1354 self.lazy = Some(lazy);
1355 self
1356 }
1357
1358 pub const fn atomic(mut self, atomic: bool) -> Self {
1360 self.atomic = atomic;
1361 self
1362 }
1363
1364 fn resolve_lazy(&self, path: &str) -> bool {
1366 if let Some(lazy) = self.lazy {
1367 return lazy;
1368 }
1369 if path == "-" {
1371 return false;
1372 }
1373 matches!(self.mode, FileMode::Write | FileMode::Append)
1374 }
1375}
1376
1377impl TypeConverter for FileType {
1378 type Value = LazyFile;
1379
1380 fn name(&self) -> &str {
1381 "FILENAME"
1382 }
1383
1384 fn convert(&self, value: &str) -> Result<Self::Value, String> {
1385 if value == "-" {
1387 if matches!(self.mode, FileMode::ReadWrite) {
1389 return Err("'-' (stdin/stdout) cannot be used with read+write mode".to_string());
1390 }
1391 let lazy_file = if self.mode.is_read() {
1392 LazyFile::stdin()
1393 } else {
1394 LazyFile::stdout()
1395 };
1396 return Ok(lazy_file);
1397 }
1398
1399 let path = PathBuf::from(value);
1400
1401 if self.mode.is_read() && !self.resolve_lazy(value) && !path.exists() {
1403 return Err(format!("'{}': No such file or directory", value));
1404 }
1405
1406 let mut lazy_file = LazyFile::new(path, self.mode);
1407 if self.atomic {
1408 lazy_file = lazy_file.atomic(true);
1409 }
1410
1411 if !self.resolve_lazy(value) {
1413 lazy_file
1414 .open()
1415 .map_err(|e| format!("'{}': {}", value, e))?;
1416 }
1417
1418 Ok(lazy_file)
1419 }
1420
1421 fn split_envvar_value(&self, value: &str) -> Vec<String> {
1422 std::env::split_paths(value)
1424 .map(|p| p.to_string_lossy().into_owned())
1425 .collect()
1426 }
1427
1428 fn shell_complete(&self, incomplete: &str) -> Vec<CompletionItem> {
1429 vec![CompletionItem::with_type(incomplete, "file")]
1430 }
1431}
1432
1433pub type BoxedTypeConverter<T> = Box<dyn TypeConverter<Value = T> + Send + Sync>;
1439
1440#[derive(Debug)]
1445pub struct TupleType {
1446 types: Vec<TupleElementType>,
1448}
1449
1450#[derive(Debug, Clone)]
1452enum TupleElementType {
1453 String,
1454 Int,
1455 Float,
1456 Bool,
1457}
1458
1459impl TupleType {
1460 pub fn new<I, S>(types: I) -> Self
1464 where
1465 I: IntoIterator<Item = S>,
1466 S: AsRef<str>,
1467 {
1468 let types = types
1469 .into_iter()
1470 .map(|s| match s.as_ref().to_lowercase().as_str() {
1471 "string" | "str" | "text" => TupleElementType::String,
1472 "int" | "integer" => TupleElementType::Int,
1473 "float" => TupleElementType::Float,
1474 "bool" | "boolean" => TupleElementType::Bool,
1475 _ => TupleElementType::String,
1476 })
1477 .collect();
1478 Self { types }
1479 }
1480
1481 pub fn strings(count: usize) -> Self {
1483 Self {
1484 types: vec![TupleElementType::String; count],
1485 }
1486 }
1487
1488 pub fn ints(count: usize) -> Self {
1490 Self {
1491 types: vec![TupleElementType::Int; count],
1492 }
1493 }
1494}
1495
1496#[derive(Debug, Clone, PartialEq)]
1498pub enum TupleValue {
1499 String(String),
1500 Int(i64),
1501 Float(f64),
1502 Bool(bool),
1503}
1504
1505impl TupleValue {
1506 pub fn as_string(&self) -> Option<&str> {
1508 match self {
1509 TupleValue::String(s) => Some(s),
1510 _ => None,
1511 }
1512 }
1513
1514 pub fn as_int(&self) -> Option<i64> {
1516 match self {
1517 TupleValue::Int(i) => Some(*i),
1518 _ => None,
1519 }
1520 }
1521
1522 pub fn as_float(&self) -> Option<f64> {
1524 match self {
1525 TupleValue::Float(f) => Some(*f),
1526 _ => None,
1527 }
1528 }
1529
1530 pub fn as_bool(&self) -> Option<bool> {
1532 match self {
1533 TupleValue::Bool(b) => Some(*b),
1534 _ => None,
1535 }
1536 }
1537}
1538
1539impl TupleType {
1540 pub fn convert_element(&self, index: usize, value: &str) -> Result<TupleValue, String> {
1545 let element_type = self.types.get(index).ok_or_else(|| {
1546 format!(
1547 "tuple index {} out of bounds (arity {})",
1548 index,
1549 self.types.len()
1550 )
1551 })?;
1552
1553 match element_type {
1554 TupleElementType::String => Ok(TupleValue::String(value.to_string())),
1555 TupleElementType::Int => {
1556 let i: i64 = value
1557 .parse()
1558 .map_err(|_| format!("'{}' is not a valid integer", value))?;
1559 Ok(TupleValue::Int(i))
1560 }
1561 TupleElementType::Float => {
1562 let f: f64 = value
1563 .parse()
1564 .map_err(|_| format!("'{}' is not a valid float", value))?;
1565 Ok(TupleValue::Float(f))
1566 }
1567 TupleElementType::Bool => {
1568 let b = BoolType::str_to_bool(value)
1569 .ok_or_else(|| format!("'{}' is not a valid boolean", value))?;
1570 Ok(TupleValue::Bool(b))
1571 }
1572 }
1573 }
1574
1575 pub fn convert_values(&self, values: &[&str]) -> Result<Vec<TupleValue>, String> {
1580 if values.len() != self.types.len() {
1581 return Err(format!(
1582 "{} values are required, but {} were given",
1583 self.types.len(),
1584 values.len()
1585 ));
1586 }
1587
1588 values
1589 .iter()
1590 .enumerate()
1591 .map(|(i, v)| self.convert_element(i, v))
1592 .collect()
1593 }
1594}
1595
1596impl TypeConverter for TupleType {
1597 type Value = Vec<TupleValue>;
1598
1599 fn name(&self) -> &str {
1600 "TUPLE"
1601 }
1602
1603 fn convert(&self, value: &str) -> Result<Self::Value, String> {
1604 let parts: Vec<&str> = value.split_whitespace().collect();
1609 self.convert_values(&parts)
1610 }
1611
1612 fn get_metavar(&self) -> Option<String> {
1613 let names: Vec<&str> = self
1614 .types
1615 .iter()
1616 .map(|t| match t {
1617 TupleElementType::String => "TEXT",
1618 TupleElementType::Int => "INTEGER",
1619 TupleElementType::Float => "FLOAT",
1620 TupleElementType::Bool => "BOOLEAN",
1621 })
1622 .collect();
1623 Some(format!("<{}>", names.join(" ")))
1624 }
1625
1626 fn is_composite(&self) -> bool {
1627 true
1628 }
1629
1630 fn arity(&self) -> usize {
1631 self.types.len()
1632 }
1633}
1634
1635#[cfg(test)]
1640mod tests {
1641 use super::*;
1642
1643 #[test]
1644 fn test_string_type() {
1645 assert_eq!(STRING.convert("hello").unwrap(), "hello");
1646 assert_eq!(STRING.convert(" spaces ").unwrap(), " spaces ");
1647 assert_eq!(STRING.name(), "TEXT");
1648 }
1649
1650 #[test]
1651 fn test_int_type() {
1652 assert_eq!(INT.convert("42").unwrap(), 42);
1653 assert_eq!(INT.convert("-123").unwrap(), -123);
1654 assert_eq!(INT.convert(" 456 ").unwrap(), 456);
1655 assert!(INT.convert("not a number").is_err());
1656 assert!(INT.convert("3.14").is_err());
1657 }
1658
1659 #[test]
1660 fn test_float_type() {
1661 assert_eq!(FLOAT.convert("3.14").unwrap(), 3.14);
1662 assert_eq!(FLOAT.convert("-2.5").unwrap(), -2.5);
1663 assert_eq!(FLOAT.convert("42").unwrap(), 42.0);
1664 assert!(FLOAT.convert("not a number").is_err());
1665 }
1666
1667 #[test]
1668 fn test_bool_type() {
1669 assert!(BOOL.convert("true").unwrap());
1670 assert!(BOOL.convert("True").unwrap());
1671 assert!(BOOL.convert("TRUE").unwrap());
1672 assert!(BOOL.convert("yes").unwrap());
1673 assert!(BOOL.convert("1").unwrap());
1674 assert!(BOOL.convert("on").unwrap());
1675 assert!(!BOOL.convert("false").unwrap());
1676 assert!(!BOOL.convert("no").unwrap());
1677 assert!(!BOOL.convert("0").unwrap());
1678 assert!(!BOOL.convert("off").unwrap());
1679 assert!(BOOL.convert("maybe").is_err());
1680 }
1681
1682 #[test]
1683 fn test_uuid_type() {
1684 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
1685 let result = UUID.convert(uuid_str).unwrap();
1686 assert_eq!(result.to_string(), uuid_str);
1687 assert!(UUID.convert("not-a-uuid").is_err());
1688 }
1689
1690 #[test]
1691 fn test_int_range() {
1692 let range = IntRange::new().range(0, 100);
1693 assert_eq!(range.convert("50").unwrap(), 50);
1694 assert_eq!(range.convert("0").unwrap(), 0);
1695 assert_eq!(range.convert("100").unwrap(), 100);
1696 assert!(range.convert("-1").is_err());
1697 assert!(range.convert("101").is_err());
1698 }
1699
1700 #[test]
1701 fn test_int_range_open() {
1702 let range = IntRange::new().min(0).max(10).min_open(true).max_open(true);
1703 assert!(range.convert("0").is_err()); assert!(range.convert("10").is_err()); assert_eq!(range.convert("1").unwrap(), 1);
1706 assert_eq!(range.convert("9").unwrap(), 9);
1707 }
1708
1709 #[test]
1710 fn test_int_range_clamp() {
1711 let range = IntRange::new().range(0, 100).clamp(true);
1712 assert_eq!(range.convert("-50").unwrap(), 0);
1713 assert_eq!(range.convert("150").unwrap(), 100);
1714 assert_eq!(range.convert("50").unwrap(), 50);
1715 }
1716
1717 #[test]
1718 fn test_float_range() {
1719 let range = FloatRange::new().range(0.0, 1.0);
1720 assert_eq!(range.convert("0.5").unwrap(), 0.5);
1721 assert_eq!(range.convert("0.0").unwrap(), 0.0);
1722 assert_eq!(range.convert("1.0").unwrap(), 1.0);
1723 assert!(range.convert("-0.1").is_err());
1724 assert!(range.convert("1.1").is_err());
1725 }
1726
1727 #[test]
1728 fn test_datetime_type() {
1729 let dt = DateTimeType::new();
1730
1731 let result = dt.convert("2024-01-15").unwrap();
1733 assert_eq!(result.date().to_string(), "2024-01-15");
1734
1735 let result = dt.convert("2024-01-15T10:30:00").unwrap();
1737 assert_eq!(result.to_string(), "2024-01-15 10:30:00");
1738
1739 let result = dt.convert("2024-01-15 10:30:00").unwrap();
1741 assert_eq!(result.to_string(), "2024-01-15 10:30:00");
1742
1743 assert!(dt.convert("not a date").is_err());
1744 }
1745
1746 #[test]
1747 fn test_choice() {
1748 let choice = Choice::new(["one", "two", "three"]);
1749 assert_eq!(choice.convert("one").unwrap(), "one");
1750 assert_eq!(choice.convert("two").unwrap(), "two");
1751 assert!(choice.convert("four").is_err());
1752 }
1753
1754 #[test]
1755 fn test_choice_case_insensitive() {
1756 let choice = Choice::new(["One", "Two", "Three"]).case_sensitive(false);
1757 assert_eq!(choice.convert("one").unwrap(), "One");
1758 assert_eq!(choice.convert("ONE").unwrap(), "One");
1759 assert_eq!(choice.convert("oNe").unwrap(), "One");
1760 }
1761
1762 #[test]
1763 fn test_choice_shell_complete() {
1764 let choice = Choice::new(["apple", "apricot", "banana"]);
1765 let completions = choice.shell_complete("ap");
1766 assert_eq!(completions.len(), 2);
1767 assert!(completions.iter().any(|c| c.value == "apple"));
1768 assert!(completions.iter().any(|c| c.value == "apricot"));
1769 }
1770
1771 #[test]
1772 fn test_path_type() {
1773 let path = PathType::new();
1774 let result = path.convert("/some/path").unwrap();
1776 assert_eq!(result, PathBuf::from("/some/path"));
1777 }
1778
1779 #[test]
1780 fn test_tuple_type() {
1781 let tuple = TupleType::new(["string", "int", "bool"]);
1782 let result = tuple.convert("hello 42 true").unwrap();
1783 assert_eq!(result.len(), 3);
1784 assert_eq!(result[0].as_string(), Some("hello"));
1785 assert_eq!(result[1].as_int(), Some(42));
1786 assert_eq!(result[2].as_bool(), Some(true));
1787 }
1788
1789 #[test]
1790 fn test_tuple_type_wrong_count() {
1791 let tuple = TupleType::new(["string", "int"]);
1792 assert!(tuple.convert("hello").is_err());
1793 assert!(tuple.convert("hello 42 extra").is_err());
1794 }
1795
1796 #[test]
1797 fn test_unprocessed() {
1798 assert_eq!(UNPROCESSED.convert("raw value").unwrap(), "raw value");
1799 assert_eq!(UNPROCESSED.name(), "TEXT");
1800 }
1801
1802 #[test]
1803 fn test_completion_item() {
1804 let item = CompletionItem::new("test");
1805 assert_eq!(item.value, "test");
1806 assert_eq!(item.completion_type, "plain");
1807 assert!(item.help.is_none());
1808
1809 let item = CompletionItem::with_type("path", "file").with_help("A file path");
1810 assert_eq!(item.value, "path");
1811 assert_eq!(item.completion_type, "file");
1812 assert_eq!(item.help, Some("A file path".to_string()));
1813 }
1814}