1use std::env;
9use std::error::Error;
10use std::ffi::OsStr;
11use std::fs;
12use std::io;
13use std::path::*;
14use std::time::Instant;
15
16use clap::{App, AppSettings, Arg, ArgGroup};
17
18use ignore::WalkBuilder;
19
20use inflector::Inflector;
21
22pub struct Config {
23 target_dir: PathBuf,
24 naming_convention: String,
25 recursive: bool,
26 include_dir: bool,
27 quiet: bool,
28 text: bool,
29}
30
31impl Config {
32 pub fn new() -> Result<Config, Box<dyn Error>> {
33 let matches = App::new("stdrename")
34 .version("v1.3.0")
35 .author("Gabriel Lacroix <lacroixgabriel@gmail.com>")
36 .about("This small utility is designed to rename all files in a folder according to a specified naming convention (camelCase, snake_case, kebab-case, etc.).")
37 .usage("stdrename [FLAGS] <convention> [TARGET]")
38 .setting(AppSettings::ArgRequiredElseHelp)
39 .setting(AppSettings::DeriveDisplayOrder)
40 .arg(
41 Arg::with_name("TARGET")
42 .help("Specifies a different target directory")
43 .required(false)
44 .index(1),
45 )
46 .arg(
47 Arg::with_name("camelCase")
48 .help("Uses the camelCase naming convention")
49 .short("c")
50 .long("camel"),
51 )
52 .arg(
53 Arg::with_name("kebab-case")
54 .help("Uses the kebab-case naming convention")
55 .short("k")
56 .long("kebab"),
57 )
58 .arg(
59 Arg::with_name("PascalCase")
60 .help("Uses the PascalCase naming convention")
61 .short("p")
62 .long("pascal"),
63 )
64 .arg(
65 Arg::with_name("SCREAMING_SNAKE_CASE")
66 .help("Uses the SCREAMING_SNAKE_CASE naming convention")
67 .long("screaming"),
68 )
69 .arg(
70 Arg::with_name("Sentence case")
71 .help("Uses the Sentence case naming convention")
72 .short("S")
73 .long("sentence"),
74 )
75 .arg(
76 Arg::with_name("snake_case")
77 .help("Uses the snake_case naming convention")
78 .short("s")
79 .long("snake"),
80 )
81 .arg(
82 Arg::with_name("Title Case")
83 .help("Uses the Title Case naming convention")
84 .short("T")
85 .long("title"),
86 )
87 .arg(
88 Arg::with_name("Train-Case")
89 .help("Uses the Train-Case naming convention")
90 .short("t")
91 .long("train"),
92 )
93 .group(
94 ArgGroup::with_name("convention")
95 .required(true)
96 .args(&["camelCase","kebab-case","PascalCase","SCREAMING_SNAKE_CASE","Sentence case","snake_case","Title Case","Train-Case"]),
97 )
98 .arg(
99 Arg::with_name("recursive")
100 .help("Makes renaming recursive, renaming files in subfolders as well")
101 .short("r")
102 .long("recursive"),
103 )
104 .arg(
105 Arg::with_name("directories")
106 .help("Renames directories as well")
107 .short("D")
108 .long("dir")
109 )
110 .arg(
111 Arg::with_name("text")
112 .help("Reads lines from stdin and translates them to the given convention in stdout until the first empty line")
113 .long("text")
114 )
115 .arg(
116 Arg::with_name("quiet")
117 .help("Suppress output")
118 .short("q")
119 .long("quiet")
120 )
121 .after_help("Full documentation available here: https://github.com/Gadiguibou/stdrename")
122 .get_matches();
123
124 let target_dir = match matches.value_of("TARGET") {
125 Some(dir) => PathBuf::from(dir),
126 None => env::current_dir()?,
127 };
128
129 let naming_convention = {
130 if matches.is_present("camelCase") {
131 "camelCase"
132 } else if matches.is_present("kebab-case") {
133 "kebab-case"
134 } else if matches.is_present("PascalCase") {
135 "PascalCase"
136 } else if matches.is_present("SCREAMING_SNAKE_CASE") {
137 "SCREAMING_SNAKE_CASE"
138 } else if matches.is_present("Sentence case") {
139 "Sentence_case"
140 } else if matches.is_present("snake_case") {
141 "snake_case"
142 } else if matches.is_present("Title Case") {
143 "Title_Case"
144 } else if matches.is_present("Train-Case") {
145 "Train-Case"
146 } else {
147 unreachable!()
148 }
149 }
150 .to_owned();
151
152 let recursive = matches.is_present("recursive");
153 let include_dir = matches.is_present("directories");
154 let quiet = matches.is_present("quiet");
155 let text = matches.is_present("text");
156
157 Ok(Config {
158 target_dir,
159 naming_convention,
160 recursive,
161 include_dir,
162 quiet,
163 text,
164 })
165 }
166}
167
168pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
169 let start_time = Instant::now();
170
171 let mut files_renamed: u64 = 0;
172
173 if config.text {
175 let stdin = io::stdin();
176
177 loop {
178 let mut input = String::new();
179
180 let len = stdin.read_line(&mut input)?;
181
182 if len == 0 || input.trim().is_empty() {
183 let running_time: f32 = start_time.elapsed().as_micros() as f32 / 1_000_000f32;
184
185 if !config.quiet {
186 println!(
187 "{} names translated in {} s. See you next time!\n(^ _ ^)/",
188 files_renamed, running_time
189 )
190 };
191
192 return Ok(());
193 } else {
194 let translation = change_naming_convention(
195 &PathBuf::from(input.trim()),
196 &config.naming_convention,
197 )?;
198 println!("{}", translation);
199 files_renamed += 1;
200 }
201 }
202 }
203
204 let mut walk_builder = WalkBuilder::new(&config.target_dir);
205
206 walk_builder
207 .max_depth(if !config.recursive { Some(1) } else { None })
208 .require_git(true);
209
210 #[cfg(unix)]
215 if let Some(home_path) = env::var_os("HOME") {
216 let config_location = format!("{}/.config/stdrename/ignore", home_path.to_string_lossy());
217 if PathBuf::from(&config_location).is_file() {
218 if let Some(e) = walk_builder.add_ignore(Path::new(&config_location)) {
219 eprintln!("Error parsing global config file: {}", e);
220 }
221 }
222 }
223 #[cfg(windows)]
224 if let Some(user_profile) = env::var_os("USERPROFILE") {
225 let config_location = format!(
226 "{}\\AppData\\Local\\stdrename\\ignore",
227 user_profile.to_string_lossy()
228 );
229 if PathBuf::from(&config_location).is_file() {
230 if let Some(e) = walk_builder.add_ignore(Path::new(&config_location)) {
231 eprintln!("Error parsing global config file: {}", e);
232 }
233 }
234 }
235
236 for entry in walk_builder.build() {
237 let entry = entry?;
238
239 let path = entry.path();
240
241 if !config.include_dir && !path.is_file() || path.eq(&config.target_dir) {
245 continue;
246 }
247
248 let new_name = change_naming_convention(&path, &config.naming_convention)?;
249 let new_path = path
250 .parent()
251 .ok_or("can't find path parent")?
252 .join(new_name);
253 if path != new_path {
254 fs::rename(&path, &new_path)?;
255 files_renamed += 1;
256 }
257 }
258 let running_time: f32 = start_time.elapsed().as_micros() as f32 / 1_000_000f32;
259
260 if !config.quiet {
261 println!(
262 "{} files renamed in {} s. See you next time!\n(^ _ ^)/",
263 files_renamed, running_time
264 )
265 };
266
267 Ok(())
268}
269
270pub fn change_naming_convention(
271 path_to_file: &Path,
272 new_naming_convention: &str,
273) -> Result<String, Box<dyn Error>> {
274 let file_stem = path_to_file
275 .file_stem()
276 .unwrap_or_else(|| OsStr::new(""))
277 .to_str()
278 .ok_or_else(|| {
279 format!(
280 "couldn't convert file stem of {:?} to valid Unicode",
281 path_to_file
282 )
283 })?;
284
285 let file_extension = path_to_file
286 .extension()
287 .unwrap_or_else(|| OsStr::new(""))
288 .to_str()
289 .ok_or_else(|| {
290 format!(
291 "couldn't convert file extension of {:?} to valid Unicode",
292 path_to_file
293 )
294 })?;
295
296 let file_stem = match new_naming_convention {
297 "camelCase" => file_stem.to_camel_case(),
298 "kebab-case" => file_stem.to_kebab_case(),
299 "PascalCase" => file_stem.to_pascal_case(),
300 "SCREAMING_SNAKE_CASE" => file_stem.to_screaming_snake_case(),
301 "Sentence_case" => file_stem.to_sentence_case(),
302 "snake_case" => file_stem.to_snake_case(),
303 "Title_Case" => file_stem.to_title_case(),
304 "Train-Case" => file_stem.to_train_case(),
305 _ => return Err(From::from("naming convention not found")),
306 };
307
308 if file_stem.is_empty() {
309 Ok(format!(".{}", file_extension))
310 } else if file_extension.is_empty() {
311 Ok(file_stem)
312 } else {
313 Ok(format!("{}.{}", file_stem, file_extension))
314 }
315}