Skip to content

Commit 8ec6cd2

Browse files
committed
fixes #8220
implements the --exchange flag for linux systems with testing clippy checks - added test for testing -T - switched print statement to match GNU exchange format - switched to using the translate! macro instead of get_message took out duplicate uses removed mv-exchange checks in build-gnu.sh cargo fmt swapped RENAME_EXCHANGE with nix renameat Cargo.lock clippy fix formatting, test error messages and reverted build-gnu.sh
1 parent e48c4a7 commit 8ec6cd2

File tree

6 files changed

+274
-1
lines changed

6 files changed

+274
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/mv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ windows-sys = { workspace = true, features = [
4040

4141
[target.'cfg(unix)'.dependencies]
4242
libc = { workspace = true }
43+
nix = { workspace = true, features = ["fs"] }
4344

4445
[[bin]]
4546
name = "mv"

src/uu/mv/locales/en-US.ftl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ mv-error-not-directory = target {$path}: Not a directory
2626
mv-error-target-not-directory = target directory {$path}: Not a directory
2727
mv-error-failed-access-not-directory = failed to access {$path}: Not a directory
2828
mv-error-backup-with-no-clobber = cannot combine --backup with -n/--no-clobber or --update=none-fail
29+
mv-error-exchange-needs-two-files = --exchange requires exactly two files to exchange
30+
mv-error-exchange-conflicts-with-target-directory = --exchange conflicts with --target-directory
31+
mv-error-exchange-conflicts-with-backup = --exchange conflicts with backup options
32+
mv-error-exchange-conflicts-with-update = --exchange conflicts with update options
33+
mv-error-exchange-not-supported = --exchange is not supported on this system
34+
mv-error-exchange-cross-device = --exchange requires both files to be on the same filesystem
2935
mv-error-extra-operand = mv: extra operand {$operand}
3036
mv-error-backup-might-destroy-source = backing up {$target} might destroy source; {$source} not moved
3137
mv-error-will-not-overwrite-just-created = will not overwrite just-created '{$target}' with '{$source}'
@@ -48,6 +54,7 @@ mv-help-verbose = explain what is being done
4854
mv-help-progress = Display a progress bar.
4955
Note: this feature is not supported by GNU coreutils.
5056
mv-help-debug = explain how a file is copied. Implies -v
57+
mv-help-exchange = exchange two files atomically (Linux only)
5158
5259
# Verbose messages
5360
mv-verbose-renamed = renamed {$from} -> {$to}

src/uu/mv/locales/fr-FR.ftl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ mv-error-not-directory = cible {$path} : N'est pas un répertoire
2626
mv-error-target-not-directory = répertoire cible {$path} : N'est pas un répertoire
2727
mv-error-failed-access-not-directory = impossible d'accéder à {$path} : N'est pas un répertoire
2828
mv-error-backup-with-no-clobber = impossible de combiner --backup avec -n/--no-clobber ou --update=none-fail
29+
mv-error-exchange-needs-two-files = --exchange nécessite exactement deux fichiers à échanger
30+
mv-error-exchange-conflicts-with-target-directory = --exchange est en conflit avec --target-directory
31+
mv-error-exchange-conflicts-with-backup = --exchange est en conflit avec les options de sauvegarde
32+
mv-error-exchange-conflicts-with-update = --exchange est en conflit avec les options de mise à jour
33+
mv-error-exchange-not-supported = --exchange n'est pas pris en charge sur ce système
34+
mv-error-exchange-cross-device = --exchange nécessite que les deux fichiers soient sur le même système de fichiers
2935
mv-error-extra-operand = mv : opérande supplémentaire {$operand}
3036
mv-error-backup-might-destroy-source = sauvegarder {$target} pourrait détruire la source ; {$source} non déplacé
3137
mv-error-will-not-overwrite-just-created = ne va pas écraser le fichier qui vient d'être créé '{$target}' avec '{$source}'
@@ -48,6 +54,7 @@ mv-help-verbose = expliquer ce qui est fait
4854
mv-help-progress = Afficher une barre de progression.
4955
Note : cette fonctionnalité n'est pas supportée par GNU coreutils.
5056
mv-help-debug = expliquer comment un fichier est copié. Implique -v
57+
mv-help-exchange = échanger deux fichiers de manière atomique (Linux uniquement)
5158
5259
# Messages verbeux
5360
mv-verbose-renamed = renommé {$from} -> {$to}

src/uu/mv/src/mv.rs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore (ToDO) sourcepath targetpath nushell canonicalized
6+
// spell-checker:ignore (ToDO) sourcepath targetpath nushell canonicalized renameat FDCWD ENOTSUP
77

88
mod error;
99
#[cfg(unix)]
@@ -99,6 +99,9 @@ pub struct Options {
9999

100100
/// `--debug`
101101
pub debug: bool,
102+
103+
/// `--exchange`
104+
pub exchange: bool,
102105
}
103106

104107
impl Default for Options {
@@ -114,6 +117,7 @@ impl Default for Options {
114117
strip_slashes: false,
115118
progress_bar: false,
116119
debug: false,
120+
exchange: false,
117121
}
118122
}
119123
}
@@ -140,6 +144,7 @@ static OPT_VERBOSE: &str = "verbose";
140144
static OPT_PROGRESS: &str = "progress";
141145
static ARG_FILES: &str = "files";
142146
static OPT_DEBUG: &str = "debug";
147+
static OPT_EXCHANGE: &str = "exchange";
143148

144149
#[uucore::main]
145150
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@@ -177,6 +182,34 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
177182
));
178183
}
179184

185+
// Validate exchange flag
186+
if matches.get_flag(OPT_EXCHANGE) {
187+
if files.len() != 2 {
188+
return Err(UUsageError::new(
189+
1,
190+
translate!("--exchange requires exactly two files"),
191+
));
192+
}
193+
if matches.contains_id(OPT_TARGET_DIRECTORY) {
194+
return Err(UUsageError::new(
195+
1,
196+
translate!("--exchange conflicts with --target-directory"),
197+
));
198+
}
199+
if backup_mode != BackupMode::None {
200+
return Err(UUsageError::new(
201+
1,
202+
translate!("--exchange conflicts with backup options"),
203+
));
204+
}
205+
if update_mode != UpdateMode::All {
206+
return Err(UUsageError::new(
207+
1,
208+
translate!("--exchange conflicts with update options"),
209+
));
210+
}
211+
}
212+
180213
let backup_suffix = backup_control::determine_backup_suffix(&matches);
181214

182215
let target_dir = matches
@@ -200,6 +233,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
200233
strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES),
201234
progress_bar: matches.get_flag(OPT_PROGRESS),
202235
debug: matches.get_flag(OPT_DEBUG),
236+
exchange: matches.get_flag(OPT_EXCHANGE),
203237
};
204238

205239
mv(&files[..], &opts)
@@ -296,6 +330,12 @@ pub fn uu_app() -> Command {
296330
.help(translate!("mv-help-debug"))
297331
.action(ArgAction::SetTrue),
298332
)
333+
.arg(
334+
Arg::new(OPT_EXCHANGE)
335+
.long(OPT_EXCHANGE)
336+
.help(translate!("exchange two files"))
337+
.action(ArgAction::SetTrue),
338+
)
299339
}
300340

301341
fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode {
@@ -313,6 +353,52 @@ fn determine_overwrite_mode(matches: &ArgMatches) -> OverwriteMode {
313353
}
314354
}
315355

356+
/// Atomically exchange two files using renameat2 with `RENAME_EXCHANGE`
357+
#[cfg(target_os = "linux")]
358+
fn exchange_files(path1: &Path, path2: &Path, opts: &Options) -> UResult<()> {
359+
use nix::fcntl::{AT_FDCWD, RenameFlags, renameat2};
360+
361+
// Use renameat2 to atomically exchange the files
362+
match renameat2(
363+
AT_FDCWD,
364+
path1,
365+
AT_FDCWD,
366+
path2,
367+
RenameFlags::RENAME_EXCHANGE,
368+
) {
369+
Ok(()) => {
370+
if opts.verbose {
371+
println!("exchanged '{}' <-> '{}'", path1.display(), path2.display());
372+
}
373+
Ok(())
374+
}
375+
Err(err) => match err {
376+
nix::Error::ENOTSUP | nix::Error::EINVAL => Err(USimpleError::new(
377+
1,
378+
translate!("--exchange is not supported on this filesystem"),
379+
)),
380+
nix::Error::ENOENT => {
381+
let missing_path = if path1.exists() { path2 } else { path1 };
382+
Err(MvError::NoSuchFile(missing_path.display().to_string()).into())
383+
}
384+
nix::Error::EXDEV => Err(USimpleError::new(
385+
1,
386+
translate!("--exchange cannot exchange files across different filesystems"),
387+
)),
388+
_ => Err(USimpleError::new(1, format!("exchange failed: {err}"))),
389+
},
390+
}
391+
}
392+
393+
/// Fallback exchange implementation for non-Linux systems
394+
#[cfg(not(target_os = "linux"))]
395+
fn exchange_files(_path1: &Path, _path2: &Path, _opts: &Options) -> UResult<()> {
396+
Err(USimpleError::new(
397+
1,
398+
translate!("--exchange is not supported on this system"),
399+
))
400+
}
401+
316402
fn parse_paths(files: &[OsString], opts: &Options) -> Vec<PathBuf> {
317403
let paths = files.iter().map(Path::new);
318404

@@ -520,6 +606,17 @@ fn handle_multiple_paths(paths: &[PathBuf], opts: &Options) -> UResult<()> {
520606
pub fn mv(files: &[OsString], opts: &Options) -> UResult<()> {
521607
let paths = parse_paths(files, opts);
522608

609+
// Handle exchange mode
610+
if opts.exchange {
611+
if paths.len() != 2 {
612+
return Err(USimpleError::new(
613+
1,
614+
translate!("--exchange requires exactly two files"),
615+
));
616+
}
617+
return exchange_files(&paths[0], &paths[1], opts);
618+
}
619+
523620
if let Some(ref name) = opts.target_dir {
524621
return move_files_into_dir(&paths, &PathBuf::from(name), opts);
525622
}

tests/by-util/test_mv.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2488,3 +2488,163 @@ fn test_mv_cross_device_permission_denied() {
24882488
set_permissions(other_fs_tempdir.path(), PermissionsExt::from_mode(0o755))
24892489
.expect("Unable to restore directory permissions");
24902490
}
2491+
2492+
// Tests for --exchange flag
2493+
#[test]
2494+
#[cfg(target_os = "linux")]
2495+
fn test_mv_exchange_basic() {
2496+
let (at, mut ucmd) = at_and_ucmd!();
2497+
2498+
at.write("file1", "content1");
2499+
at.write("file2", "content2");
2500+
2501+
ucmd.arg("--exchange").arg("file1").arg("file2").succeeds();
2502+
2503+
// After exchange, file1 should have content2 and file2 should have content1
2504+
assert_eq!(at.read("file1"), "content2");
2505+
assert_eq!(at.read("file2"), "content1");
2506+
}
2507+
2508+
#[test]
2509+
#[cfg(target_os = "linux")]
2510+
fn test_mv_exchange_verbose() {
2511+
let (at, mut ucmd) = at_and_ucmd!();
2512+
2513+
at.write("file1", "content1");
2514+
at.write("file2", "content2");
2515+
2516+
ucmd.arg("--exchange")
2517+
.arg("--verbose")
2518+
.arg("file1")
2519+
.arg("file2")
2520+
.succeeds()
2521+
.stdout_contains("exchanged 'file1' <-> 'file2'");
2522+
}
2523+
2524+
#[test]
2525+
fn test_mv_exchange_wrong_number_of_args() {
2526+
let (at, mut ucmd) = at_and_ucmd!();
2527+
2528+
at.write("file1", "content1");
2529+
2530+
ucmd.arg("--exchange")
2531+
.arg("file1")
2532+
.fails()
2533+
.stderr_contains("requires at least 2 values");
2534+
}
2535+
2536+
#[test]
2537+
fn test_mv_exchange_three_files() {
2538+
let (at, mut ucmd) = at_and_ucmd!();
2539+
2540+
at.write("file1", "content1");
2541+
at.write("file2", "content2");
2542+
at.write("file3", "content3");
2543+
2544+
ucmd.arg("--exchange")
2545+
.arg("file1")
2546+
.arg("file2")
2547+
.arg("file3")
2548+
.fails()
2549+
.stderr_contains("--exchange requires exactly two files");
2550+
}
2551+
2552+
#[test]
2553+
fn test_mv_exchange_conflicts_with_target_directory() {
2554+
let (at, mut ucmd) = at_and_ucmd!();
2555+
2556+
at.write("file1", "content1");
2557+
at.write("file2", "content2");
2558+
at.mkdir("dir");
2559+
2560+
ucmd.arg("--exchange")
2561+
.arg("--target-directory")
2562+
.arg("dir")
2563+
.arg("file1")
2564+
.arg("file2")
2565+
.fails()
2566+
.stderr_contains("--exchange conflicts with --target-directory");
2567+
}
2568+
2569+
#[test]
2570+
fn test_mv_exchange_conflicts_with_backup() {
2571+
let (at, mut ucmd) = at_and_ucmd!();
2572+
2573+
at.write("file1", "content1");
2574+
at.write("file2", "content2");
2575+
2576+
ucmd.arg("--exchange")
2577+
.arg("--backup")
2578+
.arg("file1")
2579+
.arg("file2")
2580+
.fails()
2581+
.stderr_contains("--exchange conflicts with backup options");
2582+
}
2583+
2584+
#[test]
2585+
#[cfg(target_os = "linux")]
2586+
fn test_mv_exchange_missing_file() {
2587+
let (at, mut ucmd) = at_and_ucmd!();
2588+
2589+
at.write("file1", "content1");
2590+
// file2 doesn't exist
2591+
2592+
ucmd.arg("--exchange")
2593+
.arg("file1")
2594+
.arg("file2")
2595+
.fails()
2596+
.stderr_contains("cannot stat file2: No such file or directory");
2597+
}
2598+
2599+
#[test]
2600+
#[cfg(not(target_os = "linux"))]
2601+
fn test_mv_exchange_missing_file() {
2602+
let (at, mut ucmd) = at_and_ucmd!();
2603+
2604+
at.write("file1", "content1");
2605+
// file2 doesn't exist
2606+
2607+
ucmd.arg("--exchange")
2608+
.arg("file1")
2609+
.arg("file2")
2610+
.fails()
2611+
.stderr_contains("--exchange is not supported on this system");
2612+
}
2613+
2614+
#[test]
2615+
#[cfg(not(target_os = "linux"))]
2616+
fn test_mv_exchange_not_supported() {
2617+
let (at, mut ucmd) = at_and_ucmd!();
2618+
2619+
at.write("file1", "content1");
2620+
at.write("file2", "content2");
2621+
2622+
ucmd.arg("--exchange")
2623+
.arg("file1")
2624+
.arg("file2")
2625+
.fails()
2626+
.stderr_contains("--exchange is not supported on this system");
2627+
}
2628+
2629+
#[test]
2630+
#[cfg(target_os = "linux")]
2631+
fn test_mv_exchange_with_no_target_directory() {
2632+
let (at, mut ucmd) = at_and_ucmd!();
2633+
2634+
at.mkdir("d1");
2635+
at.mkdir("d2");
2636+
at.write("d1/file1", "content1");
2637+
at.write("d2/file2", "content2");
2638+
2639+
ucmd.arg("-T")
2640+
.arg("--exchange")
2641+
.arg("d1")
2642+
.arg("d2")
2643+
.succeeds();
2644+
2645+
// after exchange, d1 should contain file2 and d2 should contain file1
2646+
assert_eq!(at.read("d1/file2"), "content2");
2647+
assert_eq!(at.read("d2/file1"), "content1");
2648+
assert!(!at.file_exists("d1/file1"));
2649+
assert!(!at.file_exists("d2/file2"));
2650+
}

0 commit comments

Comments
 (0)