linuxutils_misc/
rename.rs1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7 io::{BufRead, Write},
8 path::Path,
9 process::ExitCode,
10};
11
12#[derive(Parser)]
13#[command(
14 name = "rename",
15 about = "Rename files by replacing occurrences of a string in their filenames",
16 override_usage = "rename [options] <expression> <replacement> <file>..."
17)]
18pub struct Args {
19 #[arg(short = 's', long = "symlink")]
21 symlink: bool,
22
23 #[arg(short = 'v', long = "verbose")]
25 verbose: bool,
26
27 #[arg(short = 'n', long = "no-act")]
29 no_act: bool,
30
31 #[arg(short = 'a', long = "all")]
33 all: bool,
34
35 #[arg(short = 'l', long = "last")]
37 last: bool,
38
39 #[arg(short = 'o', long = "no-overwrite")]
41 no_overwrite: bool,
42
43 #[arg(short = 'i', long = "interactive")]
45 interactive: bool,
46
47 expression: String,
49
50 replacement: String,
52
53 #[arg(required = true)]
55 files: Vec<String>,
56}
57
58fn replace_string(
59 input: &str,
60 expression: &str,
61 replacement: &str,
62 all: bool,
63 last: bool,
64) -> String {
65 if expression.is_empty() {
66 if all {
67 let mut result = String::with_capacity(
68 input.len() + replacement.len() * (input.chars().count() + 1),
69 );
70 result.push_str(replacement);
71 for ch in input.chars() {
72 result.push(ch);
73 result.push_str(replacement);
74 }
75 result
76 } else if last {
77 let mut result =
78 String::with_capacity(input.len() + replacement.len());
79 result.push_str(input);
80 result.push_str(replacement);
81 result
82 } else {
83 let mut result =
84 String::with_capacity(input.len() + replacement.len());
85 result.push_str(replacement);
86 result.push_str(input);
87 result
88 }
89 } else if all {
90 input.replace(expression, replacement)
91 } else if last {
92 match input.rfind(expression) {
93 Some(pos) => {
94 let mut result = String::with_capacity(
95 input.len() - expression.len() + replacement.len(),
96 );
97 result.push_str(&input[..pos]);
98 result.push_str(replacement);
99 result.push_str(&input[pos + expression.len()..]);
100 result
101 }
102 None => input.to_string(),
103 }
104 } else {
105 match input.find(expression) {
106 Some(pos) => {
107 let mut result = String::with_capacity(
108 input.len() - expression.len() + replacement.len(),
109 );
110 result.push_str(&input[..pos]);
111 result.push_str(replacement);
112 result.push_str(&input[pos + expression.len()..]);
113 result
114 }
115 None => input.to_string(),
116 }
117 }
118}
119
120fn prompt_overwrite(new_name: &str) -> bool {
121 eprint!("{new_name}: overwrite? ");
122 std::io::stderr().flush().ok();
123 let mut line = String::new();
124 if std::io::stdin().lock().read_line(&mut line).is_err() {
125 return false;
126 }
127 let trimmed = line.trim().to_lowercase();
128 trimmed == "y" || trimmed == "yes"
129}
130
131pub fn run(args: Args) -> ExitCode {
132 let use_full_path =
133 args.expression.contains('/') || args.replacement.contains('/');
134
135 let mut succeeded = 0u64;
136 let mut failed = 0u64;
137 let mut skipped = 0u64;
138
139 for file in &args.files {
140 let path = Path::new(file);
141
142 if args.symlink {
143 let target = match std::fs::read_link(path) {
144 Ok(t) => t,
145 Err(e) => {
146 eprintln!("rename: {file}: not a symlink: {e}");
147 failed += 1;
148 continue;
149 }
150 };
151
152 let target_str = target.to_string_lossy();
153 let new_target = replace_string(
154 &target_str,
155 &args.expression,
156 &args.replacement,
157 args.all,
158 args.last,
159 );
160
161 if new_target == target_str.as_ref() {
162 skipped += 1;
163 continue;
164 }
165
166 let new_target_path = Path::new(&new_target);
167
168 if args.no_overwrite && new_target_path.exists() {
169 if args.verbose {
170 eprintln!(
171 "rename: {file}: not overwriting symlink target `{new_target}'"
172 );
173 }
174 skipped += 1;
175 continue;
176 }
177
178 if args.verbose || args.no_act {
179 println!("`{file}': `{target_str}' -> `{new_target}'");
180 }
181
182 if !args.no_act {
183 if let Err(e) = std::fs::remove_file(path) {
184 eprintln!("rename: {file}: removing symlink failed: {e}");
185 failed += 1;
186 continue;
187 }
188 #[cfg(unix)]
189 {
190 if let Err(e) =
191 std::os::unix::fs::symlink(new_target_path, path)
192 {
193 eprintln!(
194 "rename: {file}: creating symlink failed: {e}"
195 );
196 failed += 1;
197 continue;
198 }
199 }
200 succeeded += 1;
201 }
202 } else {
203 let (dir, name_to_transform) = if use_full_path {
204 (String::new(), file.to_string())
205 } else {
206 let parent = path
207 .parent()
208 .map(|p| p.to_string_lossy().to_string())
209 .unwrap_or_default();
210 let basename = path
211 .file_name()
212 .map(|n| n.to_string_lossy().to_string())
213 .unwrap_or_else(|| file.to_string());
214 (parent, basename)
215 };
216
217 let new_name_part = replace_string(
218 &name_to_transform,
219 &args.expression,
220 &args.replacement,
221 args.all,
222 args.last,
223 );
224
225 if new_name_part == name_to_transform {
226 skipped += 1;
227 continue;
228 }
229
230 let new_path = if !use_full_path && !dir.is_empty() {
231 format!("{dir}/{new_name_part}")
232 } else {
233 new_name_part.clone()
234 };
235
236 let target_path = Path::new(&new_path);
237
238 if target_path.exists() || target_path.symlink_metadata().is_ok() {
239 if args.no_overwrite {
240 if args.verbose {
241 eprintln!("rename: {file}: not overwritten");
242 }
243 skipped += 1;
244 continue;
245 }
246 if args.interactive && !prompt_overwrite(&new_path) {
247 skipped += 1;
248 continue;
249 }
250 }
251
252 if args.verbose || args.no_act {
253 println!("`{file}' -> `{new_path}'");
254 }
255
256 if !args.no_act {
257 if let Err(e) = std::fs::rename(file, &new_path) {
258 eprintln!("rename: {file}: rename failed: {e}");
259 failed += 1;
260 continue;
261 }
262 succeeded += 1;
263 }
264 }
265 }
266
267 if args.no_act {
268 return ExitCode::SUCCESS;
269 }
270
271 let total = succeeded + failed + skipped;
272 if failed == total {
273 ExitCode::from(1)
274 } else if failed > 0 {
275 ExitCode::from(2)
276 } else if succeeded == 0 {
277 ExitCode::from(4)
278 } else {
279 ExitCode::SUCCESS
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_replace_first() {
289 assert_eq!(
290 replace_string("foo.bar.baz", "bar", "qux", false, false),
291 "foo.qux.baz"
292 );
293 }
294
295 #[test]
296 fn test_replace_all() {
297 assert_eq!(
298 replace_string("foo.bar.bar", "bar", "qux", true, false),
299 "foo.qux.qux"
300 );
301 }
302
303 #[test]
304 fn test_replace_last() {
305 assert_eq!(
306 replace_string("foo.bar.bar", "bar", "qux", false, true),
307 "foo.bar.qux"
308 );
309 }
310
311 #[test]
312 fn test_replace_no_match() {
313 assert_eq!(
314 replace_string("foobar", "xyz", "qux", false, false),
315 "foobar"
316 );
317 }
318
319 #[test]
320 fn test_replace_empty_expr() {
321 assert_eq!(replace_string("abc", "", "X", false, false), "Xabc");
322 }
323
324 #[test]
325 fn test_replace_empty_expr_last() {
326 assert_eq!(replace_string("abc", "", "X", false, true), "abcX");
327 }
328
329 #[test]
330 fn test_replace_empty_expr_all() {
331 assert_eq!(replace_string("ab", "", "X", true, false), "XaXbX");
332 }
333}