Add `chgrp` applet; Add `args` module for commonly used args to increase

consistency
This commit is contained in:
Nathan Fisher 2023-01-21 18:25:09 -05:00
parent f68ae6df91
commit 9df197a4b9
16 changed files with 322 additions and 204 deletions

44
src/args/mod.rs Normal file
View File

@ -0,0 +1,44 @@
use clap::{Arg, ArgAction};
pub fn verbose() -> Arg {
Arg::new("verbose")
.help("output a diagnostic for every file processed")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue)
}
pub fn header() -> Arg {
Arg::new("HEADER")
.help(
"Each file is preceded by a header consisting of the string \
\"==> XXX <==\" where \"XXX\" is the name of the file.",
)
.short('v')
.long("verbose")
.action(ArgAction::SetTrue)
}
pub fn changes() -> Arg {
Arg::new("changes")
.help("report only when a change is made")
.short('c')
.long("changes")
.conflicts_with("verbose")
.action(ArgAction::SetTrue)
}
pub fn recursive() -> Arg {
Arg::new("recursive")
.help("operate on files and directories recursively")
.short('R')
.long("recursive")
.action(ArgAction::SetTrue)
}
pub fn color() -> Arg {
Arg::new("color")
.short('c')
.long("color")
.value_parser(["always", "ansi", "auto", "never"])
}

View File

@ -1,4 +1,5 @@
use super::Cmd;
use crate::args;
use clap::{value_parser, Arg, ArgAction, Command};
use data_encoding::BASE32;
use std::{
@ -16,41 +17,7 @@ impl Cmd for Base32 {
Command::new("base32")
.author("Nathan Fisher")
.about("Base32 encode/decode data and print to standard output")
.args([
Arg::new("INPUT")
.help("The input file to use")
.num_args(1..),
Arg::new("DECODE")
.help("Decode rather than encode")
.short('d')
.long("decode")
.action(ArgAction::SetTrue),
Arg::new("IGNORE")
.help("Ignore whitespace when decoding")
.short('i')
.long("ignore-space")
.action(ArgAction::SetTrue),
Arg::new("WRAP")
.help("Wrap encoded lines after n characters")
.short('w')
.long("wrap")
.value_parser(value_parser!(usize))
.default_value("76"),
Arg::new("color")
.short('c')
.long("color")
.value_parser(["always", "ansi", "auto", "never"]),
Arg::new("VERBOSE")
.help("Display a header naming each file")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
Arg::new("QUIET")
.help("Do not display header, even with multiple files")
.short('q')
.long("quiet")
.action(ArgAction::SetTrue),
])
.args(args())
}
fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box<dyn std::error::Error>> {
@ -75,7 +42,7 @@ impl Cmd for Base32 {
_ => ColorChoice::Never,
};
for (index, file) in files.into_iter().enumerate() {
if { len > 1 || matches.get_flag("VERBOSE") } && !matches.get_flag("QUIET") {
if { len > 1 || matches.get_flag("verbose") } && !matches.get_flag("QUIET") {
let mut stdout = StandardStream::stdout(color);
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
match index {
@ -109,6 +76,37 @@ impl Cmd for Base32 {
}
}
pub fn args() -> [Arg; 7] {
[
Arg::new("INPUT")
.help("The input file to use")
.num_args(1..),
Arg::new("DECODE")
.help("Decode rather than encode")
.short('d')
.long("decode")
.action(ArgAction::SetTrue),
Arg::new("IGNORE")
.help("Ignore whitespace when decoding")
.short('i')
.long("ignore-space")
.action(ArgAction::SetTrue),
Arg::new("WRAP")
.help("Wrap encoded lines after n characters")
.short('w')
.long("wrap")
.value_parser(value_parser!(usize))
.default_value("76"),
args::color(),
args::verbose(),
Arg::new("QUIET")
.help("Do not display header, even with multiple files")
.short('q')
.long("quiet")
.action(ArgAction::SetTrue),
]
}
fn decode_base32(mut contents: String, ignore: bool) -> Result<(), Box<dyn Error>> {
if ignore {
contents.retain(|c| !c.is_whitespace());

View File

@ -1,5 +1,5 @@
use super::Cmd;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use super::{base32::args, Cmd};
use clap::{ArgMatches, Command};
use data_encoding::BASE64;
use std::{
error::Error,
@ -16,41 +16,7 @@ impl Cmd for Base64 {
Command::new("base64")
.author("Nathan Fisher")
.about("Base64 encode/decode data and print to standard output")
.args([
Arg::new("INPUT")
.help("The input file to use")
.num_args(0..),
Arg::new("DECODE")
.help("Decode rather than encode")
.short('d')
.long("decode")
.action(ArgAction::SetTrue),
Arg::new("IGNORE")
.help("Ignore whitespace when decoding")
.short('i')
.long("ignore-space")
.action(ArgAction::SetTrue),
Arg::new("WRAP")
.help("Wrap encoded lines after n characters")
.short('w')
.long("wrap")
.default_value("76")
.value_parser(value_parser!(usize)),
Arg::new("color")
.short('c')
.long("color")
.value_parser(["always", "ansi", "auto", "never"]),
Arg::new("VERBOSE")
.help("Display a header naming each file")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
Arg::new("QUIET")
.help("Do not display header, even with multiple files")
.short('q')
.long("quiet")
.action(ArgAction::SetTrue),
])
.args(args())
}
fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box<dyn Error>> {

View File

@ -1,5 +1,8 @@
use super::{Cmd, Feedback};
use crate::mode::{Mode, Parser};
use crate::{
args,
mode::{Mode, Parser},
};
use clap::{Arg, ArgAction, Command};
use std::{
error::Error,
@ -20,32 +23,17 @@ impl Cmd for Chmod {
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.args([
Arg::new("verbose")
.short('v')
.long("verbose")
.help("output a diagnostic for every file processed")
.action(ArgAction::SetTrue),
Arg::new("changes")
.short('c')
.long("changes")
.help("like verbose but report only when a change is made")
.conflicts_with("verbose")
.action(ArgAction::SetTrue),
args::verbose(),
args::changes(),
Arg::new("quiet")
.short('f')
.long("silent")
.visible_alias("quiet")
.help("suppress most error messages")
.action(ArgAction::SetTrue),
Arg::new("recursive")
.short('R')
.long("recursive")
.visible_short_alias('r')
.help("change files and directories recursively")
.action(ArgAction::SetTrue),
args::recursive(),
Arg::new("mode")
.value_name("MODE")
.value_delimiter(',')
.num_args(1)
.required(true),
Arg::new("file")

158
src/cmd/chown/chgrp.rs Normal file
View File

@ -0,0 +1,158 @@
use super::{Group, Recurse, Traversal};
use crate::cmd::{Cmd, Feedback};
use crate::pw;
use clap::{Arg, ArgGroup, Command, ValueHint};
use std::{
error::Error,
fs::File,
io,
os::{fd::AsRawFd, unix::prelude::MetadataExt},
path::PathBuf,
};
use walkdir::{DirEntry, WalkDir};
#[derive(Debug, Default)]
pub struct Chgrp;
impl Cmd for Chgrp {
fn cli(&self) -> clap::Command {
Command::new("chgrp")
.about("change group ownership")
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.arg(
Arg::new("group")
.value_name("GROUP")
.value_hint(ValueHint::Username)
.num_args(1)
.required(true),
)
.args(super::args())
.group(
ArgGroup::new("links")
.args(["cli-traverse", "full-traverse", "no-traverse"])
.requires("recursive")
.multiple(false),
)
}
fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box<dyn std::error::Error>> {
let Some(matches) = matches else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "no input")));
};
let recurse = Recurse::from_matches(matches);
let feedback = Feedback::from_matches(matches);
let group = if let Some(grp) = matches.get_one::<String>("group") {
let gid = pw::get_gid_for_groupname(grp)
.ok_or(io::Error::new(io::ErrorKind::Other, "cannot get gid"))?;
Group { name: grp, gid }
} else {
return Err(Box::new(io::Error::new(
io::ErrorKind::Other,
"no user specified",
)));
};
if let Some(files) = matches.get_many::<String>("file") {
for f in files {
let action = Action {
path: PathBuf::from(f),
group: group.clone(),
feedback,
};
if let Some(r) = recurse {
if action.path.is_dir() {
action.recurse(recurse)?;
} else if action.path.is_symlink() {
if r.traversal != Traversal::NoLinks {
action.recurse(recurse)?;
}
}
}
action.apply()?;
}
}
Ok(())
}
fn path(&self) -> Option<crate::Path> {
Some(crate::Path::Bin)
}
}
#[derive(Debug)]
struct Action<'a> {
path: PathBuf,
group: Group<'a>,
feedback: Option<Feedback>,
}
impl Action<'_> {
fn apply(&self) -> Result<(), Box<dyn Error>> {
let fd = File::open(&self.path)?;
let meta = fd.metadata()?;
let uid = meta.uid();
let gid = meta.gid();
unsafe {
if libc::fchown(fd.as_raw_fd(), uid, self.group.gid) != 0 {
return Err(io::Error::last_os_error().into());
}
}
drop(fd);
if let Some(feedback) = self.feedback {
match feedback {
Feedback::Full => {
if self.group.gid != gid {
self.display_changes(gid)?;
} else {
self.display_retained();
}
}
Feedback::Changes => {
if self.group.gid != gid {
self.display_changes(gid)?;
}
}
}
}
Ok(())
}
fn display_changes(&self, gid: u32) -> Result<(), std::str::Utf8Error> {
let groupname = pw::get_groupname_for_gid(gid)?;
println!(
"{} changed from {groupname} to {}",
&self.path.display(),
&self.group.name
);
Ok(())
}
fn display_retained(&self) {
println!("{} retained as {}", &self.path.display(), &self.group.name);
}
fn into_child(&self, entry: DirEntry) -> Result<Self, Box<dyn Error>> {
let path = entry.path().to_path_buf();
Ok(Self {
path,
group: self.group.clone(),
feedback: self.feedback,
})
}
fn recurse(&self, recurse: Option<Recurse>) -> Result<(), Box<dyn Error>> {
let walker = WalkDir::new(&self.path)
.max_open(1)
.same_file_system(recurse.map_or(false, |x| x.same_filesystem))
.follow_links(recurse.map_or(false, |x| match x.traversal {
Traversal::FullLinks => true,
_ => false,
}));
for entry in walker {
let entry = entry?;
let action = self.into_child(entry)?;
action.apply()?;
}
Ok(())
}
}

View File

@ -1,5 +1,5 @@
use super::{Cmd, Feedback};
use crate::pw;
use crate::{args, pw};
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueHint};
use std::{
error::Error,
@ -10,6 +10,9 @@ use std::{
};
use walkdir::{DirEntry, WalkDir};
mod chgrp;
pub use chgrp::Chgrp;
#[derive(Debug, Default)]
pub struct Chown;
@ -19,54 +22,14 @@ impl Cmd for Chown {
.about("change file owner and group")
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.args([
.arg(
Arg::new("user")
.value_name("OWNER[:GROUP]")
.value_hint(ValueHint::Username)
.num_args(1)
.required(true),
Arg::new("changes")
.help("report only when a change is made")
.short('c')
.long("changes")
.conflicts_with("verbose")
.action(ArgAction::SetTrue),
Arg::new("verbose")
.help("output a diagnostic for every file processed")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
Arg::new("recursive")
.help("operate on files and directories recursively")
.short('R')
.long("recursive")
.action(ArgAction::SetTrue),
Arg::new("cli-traverse")
.help(
"if a command line argument is a symbolic link to a directory, traverse it",
)
.short('H')
.action(ArgAction::SetTrue),
Arg::new("full-traverse")
.help("traverse every symbolic link encountered in a directory")
.short('L')
.action(ArgAction::SetTrue),
Arg::new("no-traverse")
.help("do not traverse any symbolic links (default)")
.short('P')
.action(ArgAction::SetTrue),
Arg::new("same-filesystem")
.help("do not cross filesystem boundaries (requires recursive)")
.short('s')
.requires("recursive")
.long("same-filesystem")
.action(ArgAction::SetTrue),
Arg::new("file")
.value_name("FILE")
.value_hint(ValueHint::AnyPath)
.num_args(1..)
.required(true),
])
)
.args(args())
.group(
ArgGroup::new("links")
.args(["cli-traverse", "full-traverse", "no-traverse"])
@ -127,6 +90,37 @@ impl Cmd for Chown {
}
}
fn args() -> [Arg; 8] {
[
args::changes(),
args::verbose(),
args::recursive(),
Arg::new("cli-traverse")
.help("if a command line argument is a symbolic link to a directory, traverse it")
.short('H')
.action(ArgAction::SetTrue),
Arg::new("full-traverse")
.help("traverse every symbolic link encountered in a directory")
.short('L')
.action(ArgAction::SetTrue),
Arg::new("no-traverse")
.help("do not traverse any symbolic links (default)")
.short('P')
.action(ArgAction::SetTrue),
Arg::new("same-filesystem")
.help("do not cross filesystem boundaries (requires recursive)")
.short('s')
.requires("recursive")
.long("same-filesystem")
.action(ArgAction::SetTrue),
Arg::new("file")
.value_name("FILE")
.value_hint(ValueHint::AnyPath)
.num_args(1..)
.required(true),
]
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Traversal {
CliLinks,

View File

@ -1,5 +1,5 @@
use super::Cmd;
use crate::Path;
use crate::{args, Path};
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use std::{
env,
@ -20,7 +20,7 @@ impl Cmd for Head {
.long_about(
"Print the first 10 lines of each FILE to standard output.\n\
With more than one FILE, precede each with a header giving the file name.\n\n\
With no FILE, or when FILE is -, read standard input."
With no FILE, or when FILE is -, read standard input.",
)
.args([
Arg::new("FILES")
@ -31,16 +31,12 @@ impl Cmd for Head {
.short('c')
.long("bytes")
.action(ArgAction::SetTrue),
Arg::new("QUIET")
Arg::new("QUIET")
.help("Disable printing a header. Overrides -c")
.short('q')
.long("quiet")
.action(ArgAction::SetTrue),
Arg::new("HEADER")
.help("Each file is preceded by a header consisting of the string \"==> XXX <==\" where \"XXX\" is the name of the file.")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
args::header(),
Arg::new("LINES")
.help("Count n number of lines (or bytes if -c is specified).")
.short('n')
@ -48,15 +44,12 @@ impl Cmd for Head {
.allow_negative_numbers(false)
.conflicts_with("num")
.value_parser(value_parser!(usize)),
Arg::new("color")
.short('C')
.long("color")
.value_parser(["always", "ansi", "auto", "never"]),
args::color(),
Arg::new("num")
.short('1')
.short_aliases(['2', '3', '4', '5', '6', '7', '8', '9'])
.hide(true)
.action(ArgAction::Append)
.action(ArgAction::Append),
])
}

View File

@ -1,5 +1,6 @@
use super::Cmd;
use clap::{Arg, ArgAction, Command};
use crate::args;
use clap::{Arg, Command};
use std::{fs, io};
#[derive(Debug, Default)]
@ -14,10 +15,7 @@ impl Cmd for Link {
.args([
Arg::new("file1").required(true).index(1),
Arg::new("file2").required(true).index(2),
Arg::new("verbose")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
args::verbose(),
])
}

View File

@ -1,6 +1,6 @@
use super::Cmd;
use crate::mode::Parser;
use clap::{Arg, ArgAction, ArgMatches, Command};
use crate::{args, mode::Parser};
use clap::{Arg, ArgMatches, Command};
use std::{error::Error, ffi::CString, io};
#[derive(Debug, Default)]
@ -30,11 +30,7 @@ impl Cmd for MkFifo {
.long("mode")
.value_name("MODE")
.num_args(1),
Arg::new("verbose")
.help("print a diagnostic for every pipe created")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
args::verbose(),
Arg::new("file").num_args(1..).required(true),
])
}

View File

@ -1,6 +1,9 @@
use super::Cmd;
use crate::mode::{get_umask, Parser};
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use crate::{
args,
mode::{get_umask, Parser},
};
use clap::{value_parser, Arg, ArgMatches, Command};
use std::{convert::Infallible, error::Error, ffi::CString, io, str::FromStr};
#[derive(Debug, Default)]
@ -19,10 +22,7 @@ impl Cmd for MkNod {
.help("set file permission bits to MODE, not a=rw - umask")
.value_name("MODE")
.num_args(1),
Arg::new("verbose")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
args::verbose(),
Arg::new("file").value_name("NAME").required(true),
Arg::new("type")
.value_name("TYPE")

View File

@ -1,7 +1,7 @@
use clap::ArgMatches;
use std::{error::Error, fmt};
mod base32;
pub mod base32;
mod base64;
mod basename;
mod bootstrap;
@ -68,6 +68,7 @@ pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
"basename" => Some(Box::new(basename::Basename::default())),
"bootstrap" => Some(Box::new(bootstrap::Bootstrap::default())),
"chmod" => Some(Box::new(chmod::Chmod::default())),
"chgrp" => Some(Box::new(chown::Chgrp::default())),
"chown" => Some(Box::new(chown::Chown::default())),
"clear" => Some(Box::new(clear::Clear::default())),
"cut" => Some(Box::new(cut::Cut::default())),
@ -100,12 +101,13 @@ pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
}
}
pub static COMMANDS: [&str; 34] = [
pub static COMMANDS: [&str; 35] = [
"base32",
"base64",
"basename",
"bootstrap",
"chmod",
"chgrp",
"chown",
"clear",
"cut",

View File

@ -1,5 +1,6 @@
use super::Cmd;
use clap::{Arg, ArgAction, Command};
use crate::args;
use clap::{Arg, Command};
use std::{
fs::File,
io::{self, BufRead, BufReader, ErrorKind, Write},
@ -15,15 +16,8 @@ impl Cmd for Rev {
.about("reverse lines characterwise")
.author("Nathan Fisher")
.args([
Arg::new("verbose")
.short('v')
.long("verbose")
.help("print a header between each file")
.action(ArgAction::SetTrue),
Arg::new("color")
.short('c')
.long("color")
.value_parser(["always", "ansi", "auto", "never"]),
args::header(),
args::color(),
Arg::new("file")
.help("if file is '-' read from stdin")
.num_args(0..),
@ -51,7 +45,7 @@ impl Cmd for Rev {
_ => ColorChoice::Never,
};
for (index, file) in files.into_iter().enumerate() {
if matches.get_flag("verbose") {
if matches.get_flag("header") {
let mut stdout = StandardStream::stdout(color);
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
match index {

View File

@ -1,5 +1,5 @@
use super::Cmd;
use crate::fs::FileType;
use crate::{args, fs::FileType};
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueHint};
use std::{
error::Error,
@ -46,17 +46,8 @@ impl Cmd for Rm {
.long("force")
.help("ignore nonexistent files and arguments, never prompt")
.action(ArgAction::SetTrue),
Arg::new("recursive")
.short('r')
.long("recursive")
.visible_short_alias('R')
.help("remove directories and their contents recursively")
.action(ArgAction::SetTrue),
Arg::new("verbose")
.short('v')
.long("verbose")
.help("explain what is being done")
.action(ArgAction::SetTrue),
args::recursive(),
args::verbose(),
Arg::new("file")
.value_name("FILE")
.value_hint(ValueHint::AnyPath)

View File

@ -1,4 +1,5 @@
use super::Cmd;
use crate::args;
use clap::{Arg, ArgAction, Command, ValueHint};
use std::{error::Error, fs, io, path::Path};
@ -22,11 +23,7 @@ impl Cmd for Rmdir {
.value_name("DIRECTORY")
.value_hint(ValueHint::DirPath)
.required(true),
Arg::new("verbose")
.help("output a diagnostic for every directory processed")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
args::verbose(),
])
}

View File

@ -1,5 +1,6 @@
use super::Cmd;
use clap::{Arg, ArgAction, Command};
use crate::args;
use clap::{Arg, Command};
use std::{ffi::CString, io, process};
#[derive(Debug, Default)]
@ -12,11 +13,7 @@ impl Cmd for Unlink {
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.args([
Arg::new("verbose")
.help("display user feedback upon success")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
args::verbose(),
Arg::new("file")
.value_name("FILE")
.required(true)

View File

@ -1,13 +1,15 @@
#![warn(clippy::all, clippy::pedantic)]
use std::{env, path::PathBuf, process, string::ToString};
pub mod args;
mod cmd;
pub use cmd::Cmd;
pub mod fs;
pub mod math;
pub mod mode;
pub mod pw;
pub use cmd::Cmd;
/// Defines the location relative to the binary where a command will be installed
#[derive(Debug, Clone, Copy)]
pub enum Path {