diff --git a/src/args/mod.rs b/src/args/mod.rs new file mode 100644 index 0000000..9d9d14a --- /dev/null +++ b/src/args/mod.rs @@ -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"]) +} diff --git a/src/cmd/base32/mod.rs b/src/cmd/base32/mod.rs index 63cc400..e2bd8bf 100644 --- a/src/cmd/base32/mod.rs +++ b/src/cmd/base32/mod.rs @@ -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> { @@ -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> { if ignore { contents.retain(|c| !c.is_whitespace()); diff --git a/src/cmd/base64/mod.rs b/src/cmd/base64/mod.rs index 924f6a8..38bcb39 100644 --- a/src/cmd/base64/mod.rs +++ b/src/cmd/base64/mod.rs @@ -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> { diff --git a/src/cmd/chmod/mod.rs b/src/cmd/chmod/mod.rs index 051e5f7..960c6b1 100644 --- a/src/cmd/chmod/mod.rs +++ b/src/cmd/chmod/mod.rs @@ -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") diff --git a/src/cmd/chown/chgrp.rs b/src/cmd/chown/chgrp.rs new file mode 100644 index 0000000..92cf1a6 --- /dev/null +++ b/src/cmd/chown/chgrp.rs @@ -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> { + 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::("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::("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 { + Some(crate::Path::Bin) + } +} + +#[derive(Debug)] +struct Action<'a> { + path: PathBuf, + group: Group<'a>, + feedback: Option, +} + +impl Action<'_> { + fn apply(&self) -> Result<(), Box> { + 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> { + let path = entry.path().to_path_buf(); + Ok(Self { + path, + group: self.group.clone(), + feedback: self.feedback, + }) + } + + fn recurse(&self, recurse: Option) -> Result<(), Box> { + 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(()) + } +} diff --git a/src/cmd/chown/mod.rs b/src/cmd/chown/mod.rs index 46815c8..f8722f6 100644 --- a/src/cmd/chown/mod.rs +++ b/src/cmd/chown/mod.rs @@ -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, diff --git a/src/cmd/head/mod.rs b/src/cmd/head/mod.rs index 8ae0625..8b61a42 100644 --- a/src/cmd/head/mod.rs +++ b/src/cmd/head/mod.rs @@ -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), ]) } diff --git a/src/cmd/link/mod.rs b/src/cmd/link/mod.rs index 69aaf45..a97f147 100644 --- a/src/cmd/link/mod.rs +++ b/src/cmd/link/mod.rs @@ -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(), ]) } diff --git a/src/cmd/mkfifo/mod.rs b/src/cmd/mkfifo/mod.rs index ee69fd0..0a62d23 100644 --- a/src/cmd/mkfifo/mod.rs +++ b/src/cmd/mkfifo/mod.rs @@ -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), ]) } diff --git a/src/cmd/mknod/mod.rs b/src/cmd/mknod/mod.rs index 6836ef8..f6d5c5e 100644 --- a/src/cmd/mknod/mod.rs +++ b/src/cmd/mknod/mod.rs @@ -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") diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index f25746a..bcca642 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -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> { "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> { } } -pub static COMMANDS: [&str; 34] = [ +pub static COMMANDS: [&str; 35] = [ "base32", "base64", "basename", "bootstrap", "chmod", + "chgrp", "chown", "clear", "cut", diff --git a/src/cmd/rev/mod.rs b/src/cmd/rev/mod.rs index 24fb903..2b94b7d 100644 --- a/src/cmd/rev/mod.rs +++ b/src/cmd/rev/mod.rs @@ -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 { diff --git a/src/cmd/rm/mod.rs b/src/cmd/rm/mod.rs index 3bf6b7d..7c53863 100644 --- a/src/cmd/rm/mod.rs +++ b/src/cmd/rm/mod.rs @@ -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) diff --git a/src/cmd/rmdir/mod.rs b/src/cmd/rmdir/mod.rs index ee6d7d6..7d9bc65 100644 --- a/src/cmd/rmdir/mod.rs +++ b/src/cmd/rmdir/mod.rs @@ -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(), ]) } diff --git a/src/cmd/unlink/mod.rs b/src/cmd/unlink/mod.rs index 1c78d29..02e2b8c 100644 --- a/src/cmd/unlink/mod.rs +++ b/src/cmd/unlink/mod.rs @@ -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) diff --git a/src/lib.rs b/src/lib.rs index 589a8b8..33e682f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 {