code_it_later_rs/
args.rs

1//! The arguments of codeitlater are using
2
3use clap::Parser;
4use std::{
5    ffi::OsString,
6    fs::File,
7    io::{BufRead, BufReader},
8};
9
10/// Command Line Args
11#[derive(Default, Parser, Debug)]
12#[command(author = "ccQpein", version, about)]
13pub struct Args {
14    /// What are the filetypes you want to scan.
15    #[arg(short, long)]
16    pub(crate) filetypes: Vec<OsString>,
17
18    /// The folder name should ignored
19    #[arg(short = 'x', long = "ignore-dir")]
20    pub(crate) ignore_dirs: Vec<OsString>,
21
22    /// Keywords
23    #[arg(short, long)]
24    pub(crate) keywords: Option<Vec<String>>,
25
26    /// Expand dictionary json file path
27    #[arg(short, long)]
28    pub(crate) jsonx: Option<String>,
29
30    /// Files/Dirs input directly
31    #[arg(value_name = "files/dirs", default_value = ".")]
32    pub(crate) targets: Vec<String>,
33
34    /// Delete the crumbs
35    #[arg(short = 'D', long = "del")]
36    pub(crate) delete: bool,
37
38    /// Restore the crumbs back to normal comment
39    #[arg(short = 'R', long = "restore")]
40    pub(crate) restore: bool,
41
42    /// Format command after delete crumbs
43    #[arg(long = "fmt")]
44    pub(crate) fmt_command: Option<String>,
45
46    /// Output format: json, list
47    #[arg(short = 'O', long = "output-format")]
48    pub(crate) output_format: Option<String>,
49
50    /// Show all ignored crumbs
51    #[arg(long = "show-ignored", default_value = "false")]
52    pub(crate) show_ignore: bool,
53
54    /// Config file location, default value it "."
55    #[arg(short = 'C', long = "config", default_value = ".")]
56    pub(crate) config_location: String,
57}
58
59impl Args {
60    /// cover this args with other, self values totally rewrotten by other
61    /// if both of args have same fields. Except ignore dirs, they are merged
62    pub fn cover(&mut self, mut other: Self) {
63        if other.filetypes.len() != 0 {
64            self.filetypes = other.filetypes
65        }
66
67        if other.ignore_dirs.len() != 0 {
68            self.ignore_dirs.append(&mut other.ignore_dirs)
69        }
70
71        if other.keywords.is_some() {
72            self.keywords = other.keywords
73        }
74
75        if other.jsonx.is_some() {
76            self.jsonx = other.jsonx
77        }
78
79        if other.targets.len() != 0 {
80            self.targets = other.targets;
81        }
82
83        if other.delete {
84            self.delete = other.delete
85        }
86
87        if other.restore {
88            self.restore = other.restore
89        }
90
91        if other.fmt_command.is_some() {
92            self.fmt_command = other.fmt_command
93        }
94
95        if other.output_format.is_some() {
96            self.output_format = other.output_format
97        }
98
99        self.show_ignore = other.show_ignore
100    }
101
102    pub fn fmt_command(&self) -> Option<&String> {
103        self.fmt_command.as_ref()
104    }
105
106    pub fn config_location(&self) -> String {
107        self.config_location.to_string()
108    }
109}
110
111fn split_space_exclude_those_in_inner_string(s: &str) -> Result<Vec<String>, String> {
112    let mut result = vec![];
113    let mut buf = vec![];
114    let mut in_string = false;
115
116    for b in s.bytes() {
117        if b == b' ' && !in_string {
118            if !buf.is_empty() {
119                result.push(String::from_utf8(buf).map_err(|e| e.to_string())?);
120                buf = vec![];
121            }
122        } else {
123            if b == b'"' {
124                in_string ^= true;
125                continue;
126            }
127            buf.push(b);
128        }
129    }
130
131    if !buf.is_empty() {
132        result.push(String::from_utf8(buf).map_err(|e| e.to_string())?);
133    }
134
135    Ok(result)
136}
137
138fn read_config_raw_content<R: BufRead>(content: R) -> Vec<String> {
139    let buf_reader = BufReader::new(content);
140    let mut a = vec!["codeitlater".to_string()];
141    a.append(
142        &mut buf_reader
143            .lines()
144            .filter_map(|l| {
145                let ll = l.unwrap();
146                if ll.is_empty() {
147                    None
148                } else {
149                    Some(split_space_exclude_those_in_inner_string(&ll).unwrap())
150                }
151            })
152            .flatten()
153            .collect::<Vec<String>>(),
154    );
155    a
156}
157
158pub fn parse_from_current_path_config(config_folder: String) -> Option<Args> {
159    match File::open(config_folder + "/.codeitlater") {
160        Ok(f) => Some(Args::parse_from(read_config_raw_content(BufReader::new(f)))),
161        Err(_e) => {
162            //println!("{}", _e.to_string());
163            None
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use clap::Parser;
172
173    #[test]
174    fn test_parse_from_iter() {
175        let args = vec!["codeitlater", "-x", "dd"];
176        assert_eq!(Args::parse_from(args).ignore_dirs, vec!["dd"]);
177
178        let args = vec!["codeitlater", "-x", "dd", "-x", "ff"];
179        assert_eq!(Args::parse_from(args).ignore_dirs, vec!["dd", "ff"]);
180
181        // if there are some options, "--" is required
182        let args = vec!["codeitlater", "-x", "dd", "--", "a", "b", "c"];
183        assert_eq!(
184            Args::parse_from(args).targets,
185            vec!["a".to_string(), "b".to_string(), "c".to_string()]
186        );
187
188        let args = vec!["codeitlater", "--", "a", "b", "c"];
189        assert_eq!(
190            Args::parse_from(args).targets,
191            vec!["a".to_string(), "b".to_string(), "c".to_string()]
192        );
193
194        let args = vec!["codeitlater", "a", "b", "c"];
195        assert_eq!(
196            Args::parse_from(args).targets,
197            vec!["a".to_string(), "b".to_string(), "c".to_string()]
198        );
199
200        let args = vec!["codeitlater", "--del", "--", "a", "b", "c"];
201        assert_eq!(
202            Args::parse_from(args).targets,
203            vec!["a".to_string(), "b".to_string(), "c".to_string()]
204        );
205
206        let args = vec!["codeitlater", "-x", "dd", "-x", "ff", "-D", "a", "b", "c"];
207        let args = Args::parse_from(args);
208        assert_eq!(
209            args.targets,
210            vec!["a".to_string(), "b".to_string(), "c".to_string()]
211        );
212        assert_eq!(args.delete, true);
213        assert_eq!(args.ignore_dirs, vec!["dd", "ff"]);
214
215        let args = vec![
216            "codeitlater",
217            "-x",
218            "dd",
219            "-x",
220            "ff",
221            "-D",
222            "a",
223            "b",
224            "c",
225            "-R",
226        ];
227        let args = Args::parse_from(args);
228        assert_eq!(
229            args.targets,
230            vec!["a".to_string(), "b".to_string(), "c".to_string()]
231        );
232        assert_eq!(args.delete, true);
233        assert_eq!(args.ignore_dirs, vec!["dd", "ff"]);
234        assert_eq!(args.restore, true);
235    }
236
237    #[test]
238    fn test_read_current_path_config() {
239        let content = "
240-x target
241
242-k    TODO"
243            .as_bytes();
244        //dbg!(read_config_raw_content(content));
245        assert_eq!(
246            vec!["codeitlater", "-x", "target", "-k", "TODO"],
247            read_config_raw_content(content)
248        );
249    }
250
251    /// fmt command is the shell command, so it has to be string
252    #[test]
253    fn test_parse_the_fmt_string() {
254        let args = vec!["codeitlater", "--fmt", "aaa bbb"];
255        assert_eq!(Args::parse_from(args).fmt_command.unwrap(), "aaa bbb");
256
257        let args = vec!["codeitlater", "--fmt", r#""cargo fmt""#];
258        assert_eq!(
259            Args::parse_from(args).fmt_command.unwrap(),
260            r#""cargo fmt""#
261        )
262    }
263}