1use std::{
2 collections::BTreeSet,
3 ffi::OsString,
4 io::{BufReader, Error, ErrorKind, Result},
5 path::PathBuf,
6};
7
8use clap::Args;
9use regex::Regex;
10
11use crate::{file_utils, RunCommand};
12
13static TAG_LINE_PATTERN: &str = r"^\s*tags\s*:\s*[ ]*\[.*\]\s*";
14
15#[derive(Args, Debug)]
17pub struct AddTag {
18 #[clap(required = true)]
20 tags: Vec<String>,
21 #[clap(short, long, parse(from_os_str), value_name = "FILE")]
23 path: PathBuf,
24}
25
26impl RunCommand for AddTag {
28 fn run(&self) {
29 self.add_tag()
30 }
31}
32
33impl AddTag {
35 fn add_tag(&self) {
37 let Self { tags, path } = self;
38
39 let files = file_utils::list_all_files(path);
41 for file in files {
43 println!("Updating file : {}", file.as_os_str().to_str().unwrap());
44 do_add_tag(&tags, &file);
45 }
46 println!("Complete.");
47 }
48}
49
50pub fn do_add_tag(new_tags: &Vec<String>, file: &OsString) {
52 let (line, start, end) = extract_tag_line(&file).unwrap();
53 let new_line = extend_tag(line, new_tags);
54 file_utils::replace_in_file(&file, start, end, new_line).unwrap();
55}
56
57fn extend_tag(old_tag_line: String, new_tags: &Vec<String>) -> String {
69 let mut tags = old_tag_line
70 .split_once("tags")
71 .unwrap()
72 .1
73 .split_once(":")
74 .unwrap()
75 .1
76 .split_once("[")
77 .unwrap()
78 .1
79 .split_once("]")
80 .unwrap()
81 .0
82 .split(",")
83 .map(|s| s.trim().to_string())
84 .collect::<BTreeSet<String>>();
85 tags.extend(new_tags.iter().cloned());
86
87 format!(
88 "tags: [{}]",
89 tags.into_iter().collect::<Vec<String>>().join(", ")
90 )
91}
92
93pub fn extract_tag_line(file: &OsString) -> Result<(String, usize, usize)> {
118 use std::io::BufRead;
119
120 use std::fs::OpenOptions;
121
122 let file = OpenOptions::new()
123 .read(true)
124 .write(true)
125 .create(true)
126 .open(file)
127 .unwrap();
128
129 let re = Regex::new(TAG_LINE_PATTERN).unwrap();
130
131 let mut reader = BufReader::new(file);
132 let (mut i, mut _j) = (0, 0);
133 loop {
134 let mut line = String::new();
135 match reader.read_line(&mut line) {
136 Ok(0) => return Err(Error::new(ErrorKind::Other, "No tag line found")),
137 Ok(size) => {
138 if re.is_match(&line) {
139 _j = i + size;
140 return Ok((String::from(line), i, _j));
141 } else {
142 i += size;
143 }
144 }
145 Err(e) => {
146 eprintln!("{}", e);
147 return Err(e);
148 }
149 }
150 }
151}
152
153#[test]
154fn test_modify_tag() {
155 let line = "tags: [a, b, c]";
156 let new_tags = vec!["d".to_string(), "e".to_string()];
157 let result = extend_tag(line.to_string(), &new_tags);
158 assert_eq!(result, "tags: [a, b, c, d, e]");
159
160 let re = Regex::new(TAG_LINE_PATTERN).unwrap();
161 assert_eq!(re.is_match(line), true);
162}
163
164#[test]
165fn test_regex() {
166 let re = Regex::new(TAG_LINE_PATTERN).unwrap();
167 let line = "tags: [a, b, c]";
168 assert_eq!(re.is_match(line), true);
169 let line = " tags : [ a , b, c ] ";
170 assert_eq!(re.is_match(line), true);
171}