#[cfg(not(unix))]
fn main() {
eprintln!("chgrp: only available on Unix");
std::process::exit(1);
}
#[cfg(unix)]
use std::process;
#[cfg(unix)]
const TOOL_NAME: &str = "chgrp";
#[cfg(unix)]
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(unix)]
fn main() {
coreutils_rs::common::reset_sigpipe();
let args: Vec<String> = std::env::args().skip(1).collect();
let mut config = coreutils_rs::chgrp::ChgrpConfig::default();
let mut reference: Option<String> = None;
let mut positional: Vec<String> = Vec::new();
let mut saw_dashdash = false;
let mut no_preserve_root = false;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if saw_dashdash {
positional.push(arg.clone());
i += 1;
continue;
}
match arg.as_str() {
"--help" => {
print_help();
return;
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
return;
}
"-c" | "--changes" => config.changes = true,
"-f" | "--silent" | "--quiet" => config.silent = true,
"-v" | "--verbose" => config.verbose = true,
"--dereference" => config.no_dereference = false,
"-h" | "--no-dereference" => config.no_dereference = true,
"--preserve-root" => config.preserve_root = true,
"--no-preserve-root" => no_preserve_root = true,
"-R" | "--recursive" => config.recursive = true,
"-H" => {
config.symlink_follow = coreutils_rs::chown::SymlinkFollow::CommandLine;
}
"-L" => {
config.symlink_follow = coreutils_rs::chown::SymlinkFollow::Always;
}
"-P" => {
config.symlink_follow = coreutils_rs::chown::SymlinkFollow::Never;
}
"--" => saw_dashdash = true,
s if s.starts_with("--reference=") => {
reference = Some(s["--reference=".len()..].to_string());
}
s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => {
for ch in s[1..].chars() {
match ch {
'c' => config.changes = true,
'f' => config.silent = true,
'v' => config.verbose = true,
'h' => config.no_dereference = true,
'R' => config.recursive = true,
'H' => {
config.symlink_follow = coreutils_rs::chown::SymlinkFollow::CommandLine;
}
'L' => {
config.symlink_follow = coreutils_rs::chown::SymlinkFollow::Always;
}
'P' => {
config.symlink_follow = coreutils_rs::chown::SymlinkFollow::Never;
}
_ => {
eprintln!("{}: invalid option -- '{}'", TOOL_NAME, ch);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
}
}
_ => positional.push(arg.clone()),
}
i += 1;
}
if no_preserve_root {
config.preserve_root = false;
}
let (gid, file_start) = if let Some(ref rfile) = reference {
match coreutils_rs::chown::get_reference_ids(std::path::Path::new(rfile)) {
Ok((_u, g)) => (g, 0),
Err(e) => {
eprintln!(
"{}: failed to get attributes of '{}': {}",
TOOL_NAME,
rfile,
coreutils_rs::common::io_error_msg(&e)
);
process::exit(1);
}
}
} else {
if positional.is_empty() {
eprintln!("{}: missing operand", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
let group_spec = &positional[0];
if group_spec.is_empty() {
let files = &positional[1..];
if files.is_empty() {
eprintln!("{}: missing operand after '{}'", TOOL_NAME, group_spec);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
let mut errors = 0;
for file in files {
let path = std::path::Path::new(file);
let exists = if config.no_dereference {
std::fs::symlink_metadata(path).is_ok()
} else {
std::fs::metadata(path).is_ok()
};
if !exists {
if !config.silent {
eprintln!(
"{}: cannot access '{}': No such file or directory",
TOOL_NAME, file
);
}
errors += 1;
}
}
if errors > 0 {
process::exit(1);
}
return;
}
match coreutils_rs::chown::resolve_group(group_spec) {
Some(g) => (g, 1),
None => {
eprintln!("{}: invalid group: '{}'", TOOL_NAME, group_spec);
process::exit(1);
}
}
};
let files = &positional[file_start..];
if files.is_empty() {
eprintln!(
"{}: missing operand after '{}'",
TOOL_NAME,
positional.first().map(|s| s.as_str()).unwrap_or("")
);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
let mut errors = 0;
for file in files {
let path = std::path::Path::new(file);
if config.recursive {
errors += coreutils_rs::chgrp::chgrp_recursive(path, gid, &config, true, TOOL_NAME);
} else {
match coreutils_rs::chgrp::chgrp_file(path, gid, &config) {
Ok(_) => {}
Err(e) => {
if !config.silent {
eprintln!(
"{}: changing group of '{}': {}",
TOOL_NAME,
file,
coreutils_rs::common::io_error_msg(&e)
);
}
errors += 1;
}
}
}
}
if errors > 0 {
process::exit(1);
}
}
#[cfg(unix)]
fn print_help() {
println!("Usage: {} [OPTION]... GROUP FILE...", TOOL_NAME);
println!(" or: {} [OPTION]... --reference=RFILE FILE...", TOOL_NAME);
println!("Change the group of each FILE to GROUP.");
println!("With --reference, change the group of each FILE to that of RFILE.");
println!();
println!(" -c, --changes like verbose but report only when a change is made");
println!(" -f, --silent, --quiet suppress most error messages");
println!(" -v, --verbose output a diagnostic for every file processed");
println!(" --dereference affect the referent of each symbolic link (default)");
println!(" -h, --no-dereference affect symbolic links instead of any referenced file");
println!(" --no-preserve-root do not treat '/' specially (the default)");
println!(" --preserve-root fail to operate recursively on '/'");
println!(" --reference=RFILE use RFILE's group rather than specifying a GROUP value");
println!(" -R, --recursive operate on files and directories recursively");
println!();
println!("The following options modify how a hierarchy is traversed when -R is specified:");
println!(" -H if a command line argument is a symbolic link to a");
println!(" directory, traverse it");
println!(" -L traverse every symbolic link to a directory encountered");
println!(" -P do not traverse any symbolic links (default)");
println!();
println!(" --help display this help and exit");
println!(" --version output version information and exit");
}
#[cfg(test)]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fchgrp");
Command::new(path)
}
#[test]
#[cfg(unix)]
fn test_chgrp_matches_gnu_errors_missing_operand() {
let output = cmd().output().unwrap();
assert_ne!(output.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("missing operand"));
let gnu = Command::new("chgrp").output();
if let Ok(gnu) = gnu {
assert_ne!(gnu.status.code(), Some(0));
}
}
#[test]
#[cfg(unix)]
fn test_chgrp_matches_gnu_errors_missing_file() {
#[cfg(target_os = "macos")]
let group = "wheel";
#[cfg(not(target_os = "macos"))]
let group = "root";
let output = cmd().arg(group).output().unwrap();
assert_ne!(output.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("missing operand"), "stderr was: {}", stderr);
}
#[test]
#[cfg(unix)]
fn test_chgrp_matches_gnu_errors_invalid_group() {
let output = cmd()
.args(["nonexistent_group_xyz_99999", "/tmp/nofile"])
.output()
.unwrap();
assert_ne!(output.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("invalid group"), "stderr was: {}", stderr);
}
#[test]
#[cfg(unix)]
fn test_chgrp_preserve_root() {
#[cfg(target_os = "macos")]
let group = "wheel";
#[cfg(not(target_os = "macos"))]
let group = "root";
let output = cmd()
.args(["--preserve-root", "-R", group, "/"])
.output()
.unwrap();
assert_ne!(output.status.code(), Some(0));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("dangerous to operate recursively on '/'"),
"stderr was: {}",
stderr
);
}
#[test]
#[cfg(unix)]
fn test_chgrp_nonexistent_file() {
#[cfg(target_os = "macos")]
let group = "wheel";
#[cfg(not(target_os = "macos"))]
let group = "root";
let output = cmd()
.args([group, "/nonexistent_file_xyz_99999"])
.output()
.unwrap();
assert_ne!(output.status.code(), Some(0));
}
#[test]
#[cfg(unix)]
fn test_chgrp_verbose() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "data").unwrap();
use std::os::unix::fs::MetadataExt;
let meta = std::fs::metadata(&file).unwrap();
let gid = meta.gid();
let output = cmd()
.args(["-v", &gid.to_string(), file.to_str().unwrap()])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
#[cfg(unix)]
fn test_chgrp_reference() {
let dir = tempfile::tempdir().unwrap();
let ref_file = dir.path().join("ref.txt");
let target = dir.path().join("target.txt");
std::fs::write(&ref_file, "ref").unwrap();
std::fs::write(&target, "target").unwrap();
let output = cmd()
.args([
&format!("--reference={}", ref_file.display()),
target.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
#[cfg(unix)]
fn test_chgrp_recursive() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("sub");
std::fs::create_dir(&sub).unwrap();
std::fs::write(sub.join("f.txt"), "data").unwrap();
use std::os::unix::fs::MetadataExt;
let gid = std::fs::metadata(dir.path()).unwrap().gid();
let output = cmd()
.args(["-R", &gid.to_string(), dir.path().to_str().unwrap()])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
#[cfg(unix)]
fn test_chgrp_empty_group_spec() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "data").unwrap();
let output = cmd().args(["", file.to_str().unwrap()]).output().unwrap();
let code = output.status.code().unwrap();
assert!(code == 0 || code == 1);
}
}