Skip to content

Commit 10f43ec

Browse files
authored
Merge pull request #7129 from sylvestre/chgrp
chgrp: add option --from
2 parents ceb0785 + 4c3e9c8 commit 10f43ec

File tree

3 files changed

+240
-32
lines changed

3 files changed

+240
-32
lines changed

src/uu/chgrp/src/chgrp.rs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,26 @@ use std::os::unix::fs::MetadataExt;
1919
const ABOUT: &str = help_about!("chgrp.md");
2020
const USAGE: &str = help_usage!("chgrp.md");
2121

22-
fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<GidUidOwnerFilter> {
23-
let mut raw_group: String = String::new();
22+
fn parse_gid_from_str(group: &str) -> Result<u32, String> {
23+
if let Some(gid_str) = group.strip_prefix(':') {
24+
// Handle :gid format
25+
gid_str
26+
.parse::<u32>()
27+
.map_err(|_| format!("invalid group id: '{}'", gid_str))
28+
} else {
29+
// Try as group name first
30+
match entries::grp2gid(group) {
31+
Ok(g) => Ok(g),
32+
// If group name lookup fails, try parsing as raw number
33+
Err(_) => group
34+
.parse::<u32>()
35+
.map_err(|_| format!("invalid group: '{}'", group)),
36+
}
37+
}
38+
}
39+
40+
fn get_dest_gid(matches: &ArgMatches) -> UResult<(Option<u32>, String)> {
41+
let mut raw_group = String::new();
2442
let dest_gid = if let Some(file) = matches.get_one::<String>(options::REFERENCE) {
2543
fs::metadata(file)
2644
.map(|meta| {
@@ -38,22 +56,38 @@ fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<GidUidOwnerFilter> {
3856
if group.is_empty() {
3957
None
4058
} else {
41-
match entries::grp2gid(group) {
59+
match parse_gid_from_str(group) {
4260
Ok(g) => Some(g),
43-
_ => {
44-
return Err(USimpleError::new(
45-
1,
46-
format!("invalid group: {}", group.quote()),
47-
))
48-
}
61+
Err(e) => return Err(USimpleError::new(1, e)),
62+
}
63+
}
64+
};
65+
Ok((dest_gid, raw_group))
66+
}
67+
68+
fn parse_gid_and_uid(matches: &ArgMatches) -> UResult<GidUidOwnerFilter> {
69+
let (dest_gid, raw_group) = get_dest_gid(matches)?;
70+
71+
// Handle --from option
72+
let filter = if let Some(from_group) = matches.get_one::<String>(options::FROM) {
73+
match parse_gid_from_str(from_group) {
74+
Ok(g) => IfFrom::Group(g),
75+
Err(_) => {
76+
return Err(USimpleError::new(
77+
1,
78+
format!("invalid user: '{}'", from_group),
79+
))
4980
}
5081
}
82+
} else {
83+
IfFrom::All
5184
};
85+
5286
Ok(GidUidOwnerFilter {
5387
dest_gid,
5488
dest_uid: None,
5589
raw_owner: raw_group,
56-
filter: IfFrom::All,
90+
filter,
5791
})
5892
}
5993

@@ -120,6 +154,12 @@ pub fn uu_app() -> Command {
120154
.value_hint(clap::ValueHint::FilePath)
121155
.help("use RFILE's group rather than specifying GROUP values"),
122156
)
157+
.arg(
158+
Arg::new(options::FROM)
159+
.long(options::FROM)
160+
.value_name("GROUP")
161+
.help("change the group only if its current group matches GROUP"),
162+
)
123163
.arg(
124164
Arg::new(options::RECURSIVE)
125165
.short('R')

src/uucore/src/lib/features/perms.rs

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -446,29 +446,21 @@ impl ChownExecutor {
446446

447447
fn print_verbose_ownership_retained_as(&self, path: &Path, uid: u32, gid: Option<u32>) {
448448
if self.verbosity.level == VerbosityLevel::Verbose {
449-
match (self.dest_uid, self.dest_gid, gid) {
450-
(Some(_), Some(_), Some(gid)) => {
451-
println!(
452-
"ownership of {} retained as {}:{}",
453-
path.quote(),
454-
entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()),
455-
entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
456-
);
457-
}
449+
let ownership = match (self.dest_uid, self.dest_gid, gid) {
450+
(Some(_), Some(_), Some(gid)) => format!(
451+
"{}:{}",
452+
entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()),
453+
entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string())
454+
),
458455
(None, Some(_), Some(gid)) => {
459-
println!(
460-
"ownership of {} retained as {}",
461-
path.quote(),
462-
entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()),
463-
);
464-
}
465-
(_, _, _) => {
466-
println!(
467-
"ownership of {} retained as {}",
468-
path.quote(),
469-
entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()),
470-
);
456+
entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string())
471457
}
458+
_ => entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()),
459+
};
460+
if self.verbosity.groups_only {
461+
println!("group of {} retained as {}", path.quote(), ownership);
462+
} else {
463+
println!("ownership of {} retained as {}", path.quote(), ownership);
472464
}
473465
}
474466
}

tests/by-util/test_chgrp.rs

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ fn test_invalid_group() {
5656
}
5757

5858
#[test]
59-
fn test_1() {
59+
fn test_error_1() {
6060
if getegid() != 0 {
6161
new_ucmd!().arg("bin").arg(DIR).fails().stderr_contains(
6262
// linux fails with "Operation not permitted (os error 1)"
@@ -417,3 +417,179 @@ fn test_traverse_symlinks() {
417417
);
418418
}
419419
}
420+
421+
#[test]
422+
#[cfg(not(target_vendor = "apple"))]
423+
fn test_from_option() {
424+
use std::os::unix::fs::MetadataExt;
425+
let scene = TestScenario::new(util_name!());
426+
let at = &scene.fixtures;
427+
let groups = nix::unistd::getgroups().unwrap();
428+
// Skip test if we don't have at least two different groups to work with
429+
if groups.len() < 2 {
430+
return;
431+
}
432+
let (first_group, second_group) = (groups[0], groups[1]);
433+
434+
at.touch("test_file");
435+
scene
436+
.ucmd()
437+
.arg(first_group.to_string())
438+
.arg("test_file")
439+
.succeeds();
440+
441+
// Test successful group change with --from
442+
scene
443+
.ucmd()
444+
.arg("--from")
445+
.arg(first_group.to_string())
446+
.arg(second_group.to_string())
447+
.arg("test_file")
448+
.succeeds()
449+
.no_stderr();
450+
451+
// Verify the group was changed
452+
let new_gid = at.plus("test_file").metadata().unwrap().gid();
453+
assert_eq!(new_gid, second_group.as_raw());
454+
455+
scene
456+
.ucmd()
457+
.arg("--from")
458+
.arg(first_group.to_string())
459+
.arg(first_group.to_string())
460+
.arg("test_file")
461+
.succeeds()
462+
.no_stderr();
463+
464+
let unchanged_gid = at.plus("test_file").metadata().unwrap().gid();
465+
assert_eq!(unchanged_gid, second_group.as_raw());
466+
}
467+
468+
#[test]
469+
#[cfg(not(any(target_os = "android", target_os = "macos")))]
470+
fn test_from_with_invalid_group() {
471+
let (at, mut ucmd) = at_and_ucmd!();
472+
at.touch("test_file");
473+
#[cfg(not(target_os = "android"))]
474+
let err_msg = "chgrp: invalid user: 'nonexistent_group'\n";
475+
#[cfg(target_os = "android")]
476+
let err_msg = "chgrp: invalid user: 'staff'\n";
477+
478+
ucmd.arg("--from")
479+
.arg("nonexistent_group")
480+
.arg("staff")
481+
.arg("test_file")
482+
.fails()
483+
.stderr_is(err_msg);
484+
}
485+
486+
#[test]
487+
#[cfg(not(target_vendor = "apple"))]
488+
fn test_verbosity_messages() {
489+
let scene = TestScenario::new(util_name!());
490+
let at = &scene.fixtures;
491+
let groups = nix::unistd::getgroups().unwrap();
492+
// Skip test if we don't have at least one group to work with
493+
if groups.is_empty() {
494+
return;
495+
}
496+
497+
at.touch("ref_file");
498+
at.touch("target_file");
499+
500+
scene
501+
.ucmd()
502+
.arg("-v")
503+
.arg("--reference=ref_file")
504+
.arg("target_file")
505+
.succeeds()
506+
.stderr_contains("group of 'target_file' retained as ");
507+
}
508+
509+
#[test]
510+
#[cfg(not(target_vendor = "apple"))]
511+
fn test_from_with_reference() {
512+
use std::os::unix::fs::MetadataExt;
513+
let scene = TestScenario::new(util_name!());
514+
let at = &scene.fixtures;
515+
let groups = nix::unistd::getgroups().unwrap();
516+
if groups.len() < 2 {
517+
return;
518+
}
519+
let (first_group, second_group) = (groups[0], groups[1]);
520+
521+
at.touch("ref_file");
522+
at.touch("test_file");
523+
524+
scene
525+
.ucmd()
526+
.arg(first_group.to_string())
527+
.arg("test_file")
528+
.succeeds();
529+
530+
scene
531+
.ucmd()
532+
.arg(second_group.to_string())
533+
.arg("ref_file")
534+
.succeeds();
535+
536+
// Test --from with --reference
537+
scene
538+
.ucmd()
539+
.arg("--from")
540+
.arg(first_group.to_string())
541+
.arg("--reference=ref_file")
542+
.arg("test_file")
543+
.succeeds()
544+
.no_stderr();
545+
546+
let new_gid = at.plus("test_file").metadata().unwrap().gid();
547+
let ref_gid = at.plus("ref_file").metadata().unwrap().gid();
548+
assert_eq!(new_gid, ref_gid);
549+
}
550+
551+
#[test]
552+
#[cfg(not(target_vendor = "apple"))]
553+
fn test_numeric_group_formats() {
554+
use std::os::unix::fs::MetadataExt;
555+
let scene = TestScenario::new(util_name!());
556+
let at = &scene.fixtures;
557+
558+
let groups = nix::unistd::getgroups().unwrap();
559+
if groups.len() < 2 {
560+
return;
561+
}
562+
let (first_group, second_group) = (groups[0], groups[1]);
563+
564+
at.touch("test_file");
565+
566+
scene
567+
.ucmd()
568+
.arg(first_group.to_string())
569+
.arg("test_file")
570+
.succeeds();
571+
572+
// Test :gid format in --from
573+
scene
574+
.ucmd()
575+
.arg(format!("--from=:{}", first_group.as_raw()))
576+
.arg(second_group.to_string())
577+
.arg("test_file")
578+
.succeeds()
579+
.no_stderr();
580+
581+
let new_gid = at.plus("test_file").metadata().unwrap().gid();
582+
assert_eq!(new_gid, second_group.as_raw());
583+
584+
// Test :gid format in target group
585+
scene
586+
.ucmd()
587+
.arg(format!("--from={}", second_group.as_raw()))
588+
.arg(format!(":{}", first_group.as_raw()))
589+
.arg("test_file")
590+
.succeeds()
591+
.no_stderr();
592+
593+
let final_gid = at.plus("test_file").metadata().unwrap().gid();
594+
assert_eq!(final_gid, first_group.as_raw());
595+
}

0 commit comments

Comments
 (0)