diff --git a/Cargo.lock b/Cargo.lock index 9833d75..550927e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + [[package]] name = "os_str_bytes" version = "6.4.1" @@ -181,6 +187,7 @@ dependencies = [ "clap_complete_nushell", "clap_mangen", "hostname", + "once_cell", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9342f32..e6dc5df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ clap_complete = "4.0.6" clap_complete_nushell = "0.1.8" clap_mangen = "0.2.5" hostname = { version = "0.3", features = ["set"] } +once_cell = "1.16.0" [profile.release] codegen-units = 1 diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 1b56810..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::cmd::{bootstrap, echo, head, hostname, r#false, r#true, sleep}; -use clap::Command; - -pub fn cli() -> Command { - Command::new("shitbox") - .about("The Harbor Freight multitool of embedded Linux") - .version(env!("CARGO_PKG_VERSION")) - .arg_required_else_help(true) - .subcommands([ - bootstrap::cli(), - echo::cli(), - r#false::cli(), - head::cli(), - hostname::cli(), - sleep::cli(), - r#true::cli(), - ]) -} - -pub fn run() { - let matches = cli().get_matches(); - match matches.subcommand() { - Some(("echo", _matches)) => echo::run(), - Some(("false", _matches)) => r#false::run(), - Some(("head", matches)) => head::run(matches), - Some(("hostname", matches)) => hostname::run(matches), - Some(("sleep", matches)) => sleep::run(matches), - Some(("true", _matches)) => r#true::run(), - _ => {} - } -} diff --git a/src/cmd/bootstrap/mod.rs b/src/cmd/bootstrap/mod.rs index 46956f0..24a27b8 100644 --- a/src/cmd/bootstrap/mod.rs +++ b/src/cmd/bootstrap/mod.rs @@ -1,198 +1,249 @@ -use crate::cmd; +use super::{Cmd, ECHO, FALSE, HEAD, HOSTNAME, SHITBOX, SLEEP, TRUE}; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use clap_complete::{generate_to, shells, Generator}; use clap_complete_nushell::Nushell; use clap_mangen::Man; use std::{ - fs, io, + error::Error, + fs, + io::{self, ErrorKind}, path::{Path, PathBuf}, - process, }; -const COMMANDS: [fn() -> clap::Command; 8] = [ - cmd::bootstrap::cli, - cmd::echo::cli, - cmd::r#false::cli, - cmd::head::cli, - cmd::hostname::cli, - cmd::r#true::cli, - cmd::sleep::cli, - crate::cli::cli, -]; +pub struct Bootstrap { + name: &'static str, + path: Option, +} -#[must_use] -pub fn cli() -> Command { - Command::new("bootstrap") - .version(env!("CARGO_PKG_VERSION")) - .author("Nathan Fisher") - .about("Install shitbox into the filesystem") - .long_about("Install symlinks, manpages and shell completions") - .args([ - Arg::new("prefix") - .help("The directory path under which to install") - .short('p') - .long("prefix") - .num_args(1) - .default_value("/") - .required(false), - Arg::new("usr") - .help("Use /usr") - .long_help( - "Split the installation so that some applets go into /bin | /sbin\n\ +pub const BOOTSTRAP: Bootstrap = Bootstrap { + name: "bootstrap", + path: None, +}; + +impl Cmd for Bootstrap { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .version(env!("CARGO_PKG_VERSION")) + .author("Nathan Fisher") + .about("Install shitbox into the filesystem") + .long_about("Install symlinks, manpages and shell completions") + .args([ + Arg::new("prefix") + .help("The directory path under which to install") + .short('p') + .long("prefix") + .num_args(1) + .default_value("/") + .required(false), + Arg::new("usr") + .help("Use /usr") + .long_help( + "Split the installation so that some applets go into /bin | /sbin\n\ while others are placed into /usr/bin | /usr/sbin", - ) - .short('u') - .long("usr") - .default_value("true") - .value_parser(value_parser!(bool)), - ]) - .subcommands([ - Command::new("all").about("Install everything"), - Command::new("links") - .about("Install links for each applet") - .arg( - Arg::new("soft") - .help("Install soft links instead of hardlinks") - .short('s') - .long("soft"), - ), - Command::new("manpages") - .about("Install Unix man pages") - .alias("man"), - Command::new("completions") - .about("Install shell completions") - .alias("comp") - .args([ - Arg::new("all") - .help("Install completions for all supported shells") - .short('a') - .long("all") - .action(ArgAction::SetTrue), - Arg::new("bash") - .help("Bash shell completions") - .short('b') - .long("bash") - .action(ArgAction::SetTrue), - Arg::new("fish") - .help("Fish shell completions") - .short('f') - .long("fish") - .action(ArgAction::SetTrue), - Arg::new("nu") - .help("Nushell completions") - .short('n') - .long("nu") - .action(ArgAction::SetTrue), - Arg::new("pwsh") - .help("PowerShell completions") - .short('p') - .long("pwsh") - .action(ArgAction::SetTrue), - ]), - ]) -} + ) + .short('u') + .long("usr") + .default_value("true") + .value_parser(value_parser!(bool)), + ]) + .subcommands([ + Command::new("all").about("Install everything"), + Command::new("links") + .about("Install links for each applet") + .arg( + Arg::new("soft") + .help("Install soft links instead of hardlinks") + .short('s') + .long("soft"), + ), + Command::new("manpages") + .about("Install Unix man pages") + .alias("man"), + Command::new("completions") + .about("Install shell completions") + .alias("comp") + .args([ + Arg::new("all") + .help("Install completions for all supported shells") + .short('a') + .long("all") + .action(ArgAction::SetTrue), + Arg::new("bash") + .help("Bash shell completions") + .short('b') + .long("bash") + .action(ArgAction::SetTrue), + Arg::new("fish") + .help("Fish shell completions") + .short('f') + .long("fish") + .action(ArgAction::SetTrue), + Arg::new("nu") + .help("Nushell completions") + .short('n') + .long("nu") + .action(ArgAction::SetTrue), + Arg::new("pwsh") + .help("PowerShell completions") + .short('p') + .long("pwsh") + .action(ArgAction::SetTrue), + ]), + ]) + } -fn manpage(prefix: &str, f: &dyn Fn() -> Command) -> Result<(), io::Error> { - let cmd = f(); - let fname = match cmd.get_name() { - "bootstrap" => "shitbox-bootstrap.1".to_string(), - s => format!("{s}.1"), - }; - let outdir: PathBuf = [prefix, "usr", "share", "man", "man1"].iter().collect(); - if !outdir.exists() { - fs::create_dir_all(&outdir)?; - } - let mut outfile = outdir; - outfile.push(fname); - let man = Man::new(cmd); - let mut buffer: Vec = vec![]; - man.render(&mut buffer)?; - fs::write(&outfile, buffer)?; - println!(" {}", outfile.display()); - Ok(()) -} - -fn manpages(prefix: &str) -> Result<(), io::Error> { - println!("Generating Unix man pages:"); - COMMANDS.iter().try_for_each(|cmd| manpage(prefix, &cmd))?; - Ok(()) -} - -fn generate_completions( - outdir: &Path, - f: &dyn Fn() -> Command, - gen: impl Generator, -) -> Result<(), io::Error> { - let cmd = f(); - let name = cmd.get_name(); - let mut cmd = cmd.clone(); - if !outdir.exists() { - fs::create_dir_all(&outdir)?; - } - let path = generate_to(gen, &mut cmd, name, outdir)?; - println!(" {}", path.display()); - Ok(()) -} - -fn completions(prefix: &str, matches: &ArgMatches) -> Result<(), io::Error> { - println!("Generating completions:"); - if matches.get_flag("bash") || matches.get_flag("all") { - let outdir: PathBuf = [prefix, "share", "bash-completion", "completion"] - .iter() - .collect(); - COMMANDS - .iter() - .try_for_each(|cmd| generate_completions(&outdir, &cmd, shells::Bash))?; - } - if matches.get_flag("fish") || matches.get_flag("all") { - let outdir: PathBuf = [prefix, "share", "fish", "completions"].iter().collect(); - COMMANDS - .iter() - .try_for_each(|cmd| generate_completions(&outdir, &cmd, shells::Fish))?; - } - if matches.get_flag("nu") || matches.get_flag("all") { - let outdir: PathBuf = [prefix, "share", "nu", "completions"].iter().collect(); - COMMANDS - .iter() - .try_for_each(|cmd| generate_completions(&outdir, &cmd, Nushell))?; - } - if matches.get_flag("pwsh") || matches.get_flag("all") { - let outdir: PathBuf = [prefix, "share", "pwsh", "completions"].iter().collect(); - COMMANDS - .iter() - .try_for_each(|cmd| generate_completions(&outdir, &cmd, shells::PowerShell))?; - } - if matches.get_flag("zsh") || matches.get_flag("all") { - let outdir: PathBuf = [prefix, "share", "zsh", "site-functions"].iter().collect(); - COMMANDS - .iter() - .try_for_each(|cmd| generate_completions(&outdir, &cmd, shells::Zsh))?; - } - Ok(()) -} - -pub fn run(matches: &ArgMatches) { - if let Some(prefix) = matches.get_one::("prefix") { - match matches.subcommand() { - Some(("manpages", _matches)) => { - if let Err(e) = manpages(prefix) { - eprintln!("{e}"); - process::exit(1); + fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { + let matches = if let Some(m) = matches { + m + } else { + return Err(io::Error::new(ErrorKind::Other, "No input").into()); + }; + if let Some(prefix) = matches.get_one::("prefix") { + let commands: Commands = Commands { + items: vec![ + &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &TRUE, &SLEEP, &SHITBOX, + ], + }; + match matches.subcommand() { + Some(("manpages", _matches)) => { + commands.manpages(prefix)?; } - } - Some(("completions", matches)) => { - if let Err(e) = completions(prefix, matches) { - eprintln!("{e}"); - process::exit(1); + Some(("completions", matches)) => { + commands.completions(prefix, matches)?; } - } - Some(("all", _matches)) => { - if let Err(e) = manpages(prefix) { - eprintln!("{e}"); - process::exit(1); + Some(("all", _matches)) => { + commands.manpages(prefix)?; + commands.completions(prefix, matches)?; } + _ => {} } - _ => {} } + Ok(()) + } + + fn path(&self) -> Option { + self.path } } + +struct Commands<'a> { + items: Vec<&'a dyn Cmd>, +} + +impl<'a> Commands<'a> { + fn manpages(&self, prefix: &str) -> Result<(), io::Error> { + println!("Generating Unix man pages:"); + self.items + .iter() + .try_for_each(|cmd| Self::manpage(prefix, cmd))?; + Ok(()) + } + + fn manpage(prefix: &str, cmd: &&dyn Cmd) -> Result<(), io::Error> { + let command = cmd.cli(); + let fname = match cmd.name() { + "bootstrap" => "shitbox-bootstrap.1".to_string(), + s => format!("{s}.1"), + }; + let outdir: PathBuf = [prefix, "usr", "share", "man", "man1"].iter().collect(); + if !outdir.exists() { + fs::create_dir_all(&outdir)?; + } + let mut outfile = outdir; + outfile.push(fname); + let man = Man::new(command); + let mut buffer: Vec = vec![]; + man.render(&mut buffer)?; + fs::write(&outfile, buffer)?; + println!(" {}", outfile.display()); + Ok(()) + } + + fn completion(outdir: &Path, cmd: &&dyn Cmd, gen: impl Generator) -> Result<(), io::Error> { + let name = cmd.name(); + let mut cmd = cmd.cli(); + if !outdir.exists() { + fs::create_dir_all(outdir)?; + } + let path = generate_to(gen, &mut cmd, name, outdir)?; + println!(" {}", path.display()); + Ok(()) + } + + fn completions(&self, prefix: &str, matches: &ArgMatches) -> Result<(), io::Error> { + println!("Generating completions:"); + if matches.get_flag("bash") || matches.get_flag("all") { + let outdir: PathBuf = [prefix, "share", "bash-completion", "completion"] + .iter() + .collect(); + self.items + .iter() + .try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Bash))?; + } + if matches.get_flag("fish") || matches.get_flag("all") { + let outdir: PathBuf = [prefix, "share", "fish", "completions"].iter().collect(); + self.items + .iter() + .try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Fish))?; + } + if matches.get_flag("nu") || matches.get_flag("all") { + let outdir: PathBuf = [prefix, "share", "nu", "completions"].iter().collect(); + self.items + .iter() + .try_for_each(|cmd| Self::completion(&outdir, cmd, Nushell))?; + } + if matches.get_flag("pwsh") || matches.get_flag("all") { + let outdir: PathBuf = [prefix, "share", "pwsh", "completions"].iter().collect(); + self.items + .iter() + .try_for_each(|cmd| Self::completion(&outdir, cmd, shells::PowerShell))?; + } + if matches.get_flag("zsh") || matches.get_flag("all") { + let outdir: PathBuf = [prefix, "share", "zsh", "site-functions"].iter().collect(); + self.items + .iter() + .try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Zsh))?; + } + Ok(()) + } +} + +/* +fn get_path(prefix: &str, name: &str, usr: bool) -> Option { + let mut path = PathBuf::from(prefix); + let binpath = match name { + "bootstrap" => Bootstrap::path(), + "echo" => Echo::path(), + "false" => False::path(), + "head" => Head::path(), + "hostname" => Hostname::path(), + "true" => True::path(), + "sleep" => Sleep::path(), + "shitbox" => Shitbox::path(), + _ => todo!(), + }; + match binpath { + Some(crate::Path::Bin) => path.push("bin"), + Some(crate::Path::Sbin) => path.push("sbin"), + Some(crate::Path::UsrBin) => { + if usr { + path.push("usr"); + } + path.push("bin"); + } + Some(crate::Path::UsrSbin) => { + if usr { + path.push("usr"); + } + path.push("sbin"); + } + None => return None, + } + path.push(name); + Some(path) +} +*/ diff --git a/src/cmd/echo/mod.rs b/src/cmd/echo/mod.rs index 2a37d3c..099e840 100644 --- a/src/cmd/echo/mod.rs +++ b/src/cmd/echo/mod.rs @@ -1,42 +1,61 @@ +use super::Cmd; use crate::Path; use clap::{Arg, Command}; -use std::env; +use std::{env, error::Error}; -pub const PATH: Path = Path::Bin; - -#[must_use] -pub fn cli() -> Command { - Command::new("echo") - .about("Display a line of text") - .long_about("Echo the STRING(s) to standard output") - .version(env!("CARGO_PKG_VERSION")) - .author("Nathan Fisher") - .args([ - Arg::new("inline") - .short('n') - .help("Do not output a trailing newline"), - Arg::new("STRING").num_args(1..), - ]) +pub struct Echo { + name: &'static str, + path: Option, } -pub fn run() { - let args: Vec = env::args().collect(); - let idx = match crate::progname() { - Some(s) if s.as_str() == "echo" => 1, - Some(_) => 2, - None => unreachable!(), - }; - let len = args.len(); - let n = len > idx && args[idx] == "-n"; - let i = if n { idx + 1 } else { idx }; - for (index, arg) in args.iter().enumerate().skip(i) { - if index < len - 1 { - print!("{arg} "); - } else { - print!("{arg}"); +pub const ECHO: Echo = Echo { + name: "echo", + path: Some(Path::Bin), +}; + +impl Cmd for Echo { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .about("Display a line of text") + .long_about("Echo the STRING(s) to standard output") + .version(env!("CARGO_PKG_VERSION")) + .author("Nathan Fisher") + .args([ + Arg::new("inline") + .short('n') + .help("Do not output a trailing newline"), + Arg::new("STRING").num_args(1..), + ]) + } + + fn run(&self, _matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + let args: Vec = env::args().collect(); + let idx = match crate::progname() { + Some(s) if s.as_str() == self.name() => 1, + Some(_) => 2, + None => unreachable!(), + }; + let len = args.len(); + let n = len > idx && args[idx] == "-n"; + let i = if n { idx + 1 } else { idx }; + for (index, arg) in args.iter().enumerate().skip(i) { + if index < len - 1 { + print!("{arg} "); + } else { + print!("{arg}"); + } } + if !n { + println!(); + } + Ok(()) } - if !n { - println!(); + + fn path(&self) -> Option { + self.path } } diff --git a/src/cmd/false/mod.rs b/src/cmd/false/mod.rs index fedfb50..bf1a861 100644 --- a/src/cmd/false/mod.rs +++ b/src/cmd/false/mod.rs @@ -1,18 +1,36 @@ +use super::Cmd; use crate::Path; use clap::Command; -use std::process; +use std::{error::Error, process}; -pub const PATH: Path = Path::Bin; - -#[must_use] -pub fn cli() -> Command { - Command::new("false") - .about("Does nothing unsuccessfully") - .long_about("Exit with a status code indicating failure") - .author("Nathan Fisher") - .version(env!("CARGO_PKG_VERSION")) +pub struct False { + name: &'static str, + path: Option, } -pub fn run() { - process::exit(1); +pub const FALSE: False = False { + name: "false", + path: Some(Path::Bin), +}; + +impl Cmd for False { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .about("Does nothing unsuccessfully") + .long_about("Exit with a status code indicating failure") + .author("Nathan Fisher") + .version(env!("CARGO_PKG_VERSION")) + } + + fn run(&self, _matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + process::exit(1); + } + + fn path(&self) -> Option { + self.path + } } diff --git a/src/cmd/head/mod.rs b/src/cmd/head/mod.rs index 3db8e50..c436350 100644 --- a/src/cmd/head/mod.rs +++ b/src/cmd/head/mod.rs @@ -1,14 +1,30 @@ +use super::Cmd; use crate::Path; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; -use std::fs; -use std::io::{stdin, Read}; -use std::process; +use std::{ + error::Error, + fs, + io::{self, stdin, Read}, + process, +}; -pub const PATH: Path = Path::Bin; +pub struct Head { + name: &'static str, + path: Option, +} -#[must_use] -pub fn cli() -> Command { - Command::new("head") +pub const HEAD: Head = Head { + name: "head", + path: Some(Path::Bin), +}; + +impl Cmd for Head { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> Command { + Command::new(self.name) .version(env!("CARGO_PKG_VERSION")) .author("Nathan Fisher") .about("Display first lines of a file") @@ -45,6 +61,40 @@ pub fn cli() -> Command { .allow_negative_numbers(false) .value_parser(value_parser!(usize)) ]) + } + + fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { + let matches = if let Some(m) = matches { + m + } else { + return Err(io::Error::new(io::ErrorKind::Other, "No input").into()); + }; + let files = match matches.get_many::("FILES") { + Some(c) => c.map(std::string::ToString::to_string).collect(), + None => vec!["-".to_string()], + }; + let header = + !matches.get_flag("QUIET") && { files.len() > 1 || matches.get_flag("HEADER") }; + for (index, file) in files.into_iter().enumerate() { + if index == 1 && header { + println!(); + } + head( + &file, + match matches.get_one("LINES") { + Some(c) => *c, + None => 10, + }, + header, + matches.get_flag("BYTES"), + ); + } + Ok(()) + } + + fn path(&self) -> Option { + self.path + } } fn head(file: &str, count: usize, header: bool, bytes: bool) { @@ -90,25 +140,3 @@ fn head(file: &str, count: usize, header: bool, bytes: bool) { } } } - -pub fn run(matches: &ArgMatches) { - let files = match matches.get_many::("FILES") { - Some(c) => c.map(std::string::ToString::to_string).collect(), - None => vec!["-".to_string()], - }; - let header = !matches.get_flag("QUIET") && { files.len() > 1 || matches.get_flag("HEADER") }; - for (index, file) in files.into_iter().enumerate() { - if index == 1 && header { - println!(); - } - head( - &file, - match matches.get_one("LINES") { - Some(c) => *c, - None => 10, - }, - header, - matches.get_flag("BYTES"), - ); - } -} diff --git a/src/cmd/hostname/mod.rs b/src/cmd/hostname/mod.rs index 893680b..0464e4f 100644 --- a/src/cmd/hostname/mod.rs +++ b/src/cmd/hostname/mod.rs @@ -1,55 +1,67 @@ +use super::Cmd; use crate::Path; use clap::{Arg, ArgAction, ArgMatches, Command}; -use std::process; +use std::{error::Error, io}; -pub const PATH: Path = Path::Bin; - -#[must_use] -pub fn cli() -> Command { - Command::new("hostname") - .version(env!("CARGO_PKG_VERSION")) - .author("The JeanG3nie ") - .about("Prints the name of the current host. The super-user can set the host name by supplying an argument.") - .arg( - Arg::new("NAME") - .help("name to set") - ) - .arg( - Arg::new("STRIP") - .help("Removes any domain information from the printed name.") - .short('s') - .long("strip") - .action(ArgAction::SetTrue) - ) +pub struct Hostname { + name: &'static str, + path: Option, } -pub fn run(matches: &ArgMatches) { - if let Some(name) = matches.get_one::("NAME") { - if let Err(e) = hostname::set(name) { - eprintln!("{e}"); - process::exit(1); - } - } else { - match hostname::get() { - Ok(hostname) => { - if matches.get_flag("STRIP") { - println!( - "{}", - if let Some(s) = hostname.to_string_lossy().split('.').next() { - s - } else { - eprintln!("hostname: missing operand"); - process::exit(1); - } - ); - } else { - println!("{}", hostname.to_string_lossy()); - } - } - Err(e) => { - eprintln!("{e}"); - process::exit(1); +pub const HOSTNAME: Hostname = Hostname { + name: "hostname", + path: Some(Path::Bin), +}; + +impl Cmd for Hostname { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .version(env!("CARGO_PKG_VERSION")) + .author("The JeanG3nie ") + .about("Prints the name of the current host. The super-user can set the host name by supplying an argument.") + .args([ + Arg::new("NAME") + .help("name to set"), + Arg::new("STRIP") + .help("Removes any domain information from the printed name.") + .short('s') + .long("strip") + .action(ArgAction::SetTrue) + ]) + } + + fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { + let matches = matches.unwrap(); + if let Some(name) = matches.get_one::("NAME") { + hostname::set(name)?; + Ok(()) + } else { + let hostname = hostname::get()?; + if matches.get_flag("STRIP") { + println!( + "{}", + if let Some(s) = hostname.to_string_lossy().split('.').next() { + s + } else { + return Err(io::Error::new( + io::ErrorKind::Other, + "hostname: missing operand", + ) + .into()); + } + ); + } else { + println!("{}", hostname.to_string_lossy()); } + Ok(()) } } + + fn path(&self) -> Option { + self.path + } } diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 95c0f89..ea1b04d 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,3 +1,6 @@ +use clap::ArgMatches; +use std::error::Error; + pub mod bootstrap; mod cat; mod chmod; @@ -16,6 +19,25 @@ mod mv; mod pwd; mod rm; mod rmdir; +pub mod shitbox; pub mod sleep; mod sync; pub mod r#true; + +pub use { + self::hostname::{Hostname, HOSTNAME}, + bootstrap::{Bootstrap, BOOTSTRAP}, + echo::{Echo, ECHO}, + head::{Head, HEAD}, + r#false::{False, FALSE}, + r#true::{True, TRUE}, + shitbox::{Shitbox, SHITBOX}, + sleep::{Sleep, SLEEP}, +}; + +pub trait Cmd { + fn name(&self) -> &str; + fn cli(&self) -> clap::Command; + fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box>; + fn path(&self) -> Option; +} diff --git a/src/cmd/shitbox/mod.rs b/src/cmd/shitbox/mod.rs new file mode 100644 index 0000000..4411bf0 --- /dev/null +++ b/src/cmd/shitbox/mod.rs @@ -0,0 +1,60 @@ +use super::{Cmd, BOOTSTRAP, ECHO, FALSE, HEAD, HOSTNAME, SLEEP, TRUE}; +use clap::Command; +use std::{ + error::Error, + io::{self, ErrorKind}, +}; + +pub struct Shitbox { + name: &'static str, + path: Option, +} + +pub const SHITBOX: Shitbox = Shitbox { + name: "shitbox", + path: None, +}; + +impl Cmd for Shitbox { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .about("The Harbor Freight multitool of embedded Linux") + .version(env!("CARGO_PKG_VERSION")) + .arg_required_else_help(true) + .subcommands([ + BOOTSTRAP.cli(), + ECHO.cli(), + FALSE.cli(), + HEAD.cli(), + HOSTNAME.cli(), + SLEEP.cli(), + TRUE.cli(), + ]) + } + + fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + let matches = if let Some(m) = matches { + m + } else { + return Err(Box::new(io::Error::new(ErrorKind::Other, "No input"))); + }; + match matches.subcommand() { + Some(("echo", _matches)) => ECHO.run(None)?, + Some(("false", _matches)) => FALSE.run(None)?, + Some(("head", matches)) => HEAD.run(Some(matches))?, + Some(("hostname", matches)) => HOSTNAME.run(Some(matches))?, + Some(("sleep", matches)) => SLEEP.run(Some(matches))?, + Some(("true", _matches)) => TRUE.run(None)?, + _ => unimplemented!(), + } + Ok(()) + } + + fn path(&self) -> Option { + self.path + } +} diff --git a/src/cmd/sleep/mod.rs b/src/cmd/sleep/mod.rs index b3055c3..30ce5e5 100644 --- a/src/cmd/sleep/mod.rs +++ b/src/cmd/sleep/mod.rs @@ -1,37 +1,56 @@ +use super::Cmd; use crate::Path; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; -use std::{env, thread, time::Duration}; +use std::{env, error::Error, thread, time::Duration}; -pub const PATH: Path = Path::Bin; - -#[must_use] -pub fn cli() -> Command { - Command::new("sleep") - .about("Suspend execution for an interval of time") - .long_about( - "The sleep utility suspends execution for a minimum of the specified number of seconds.\n\ - This number must be positive and may contain a decimal fraction.\n\ - sleep is commonly used to schedule the execution of other commands" - ) - .version(env!("CARGO_PKG_VERSION")) - .author(env!("CARGO_PKG_AUTHORS")) - .arg( - Arg::new("seconds") - .help("The number of seconds to sleep") - .num_args(1) - .allow_negative_numbers(false) - .value_parser(value_parser!(f64)) - .required(true) - .action(ArgAction::Set) - ) +pub struct Sleep { + name: &'static str, + path: Option, } -#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] -pub fn run(matches: &ArgMatches) { - if let Some(raw) = matches.get_one::("seconds") { - let seconds = *raw as u64; - let nanos = ((raw % 1.0) * 10e-9) as u32; - let s = Duration::new(seconds, nanos); - thread::sleep(s); +pub const SLEEP: Sleep = Sleep { + name: "sleep", + path: Some(Path::Bin), +}; + +impl Cmd for Sleep { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .about("Suspend execution for an interval of time") + .long_about( + "The sleep utility suspends execution for a minimum of the specified number of seconds.\n\ + This number must be positive and may contain a decimal fraction.\n\ + sleep is commonly used to schedule the execution of other commands" + ) + .version(env!("CARGO_PKG_VERSION")) + .author(env!("CARGO_PKG_AUTHORS")) + .arg( + Arg::new("seconds") + .help("The number of seconds to sleep") + .num_args(1) + .allow_negative_numbers(false) + .value_parser(value_parser!(f64)) + .required(true) + .action(ArgAction::Set) + ) + } + + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { + if let Some(raw) = matches.unwrap().get_one::("seconds") { + let seconds = *raw as u64; + let nanos = ((raw % 1.0) * 10e-9) as u32; + let s = Duration::new(seconds, nanos); + thread::sleep(s); + } + Ok(()) + } + + fn path(&self) -> Option { + self.path } } diff --git a/src/cmd/true/mod.rs b/src/cmd/true/mod.rs index 0b5b64c..3744e2b 100644 --- a/src/cmd/true/mod.rs +++ b/src/cmd/true/mod.rs @@ -1,18 +1,36 @@ +use super::Cmd; use crate::Path; use clap::Command; -use std::process; +use std::{error::Error, process}; -pub const PATH: Path = Path::Bin; - -#[must_use] -pub fn cli() -> Command { - Command::new("true") - .about("Does nothing successfully") - .long_about("Exit with a status code indicating success") - .version(env!("CARGO_PKG_VERSION")) - .author("Nathan Fisher") +pub struct True { + name: &'static str, + path: Option, } -pub fn run() { - process::exit(0); +pub const TRUE: True = True { + name: "true", + path: Some(Path::Bin), +}; + +impl Cmd for True { + fn name(&self) -> &str { + self.name + } + + fn cli(&self) -> clap::Command { + Command::new(self.name) + .about("Does nothing successfully") + .long_about("Exit with a status code indicating success") + .version(env!("CARGO_PKG_VERSION")) + .author("Nathan Fisher") + } + + fn run(&self, _matches: Option<&clap::ArgMatches>) -> Result<(), Box> { + process::exit(0); + } + + fn path(&self) -> Option { + self.path + } } diff --git a/src/lib.rs b/src/lib.rs index 089f2bc..80ac1dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,10 @@ #![warn(clippy::all, clippy::pedantic)] -use std::{env, path::PathBuf, string::ToString}; +use std::{env, error::Error, path::PathBuf, string::ToString}; -mod cli; pub mod cmd; +use cmd::{Cmd, ECHO, FALSE, HEAD, HOSTNAME, SHITBOX, SLEEP, TRUE}; +#[derive(Debug, Clone, Copy)] pub enum Path { Bin, Sbin, @@ -23,17 +24,24 @@ pub fn progname() -> Option { .flatten() } -pub fn run() { +pub fn run() -> Result<(), Box> { if let Some(progname) = progname() { match progname.as_str() { - "echo" => cmd::echo::run(), - "false" => cmd::r#false::run(), - "head" => cmd::head::run(&cmd::head::cli().get_matches()), - "hostname" => cmd::hostname::run(&cmd::hostname::cli().get_matches()), - "true" => cmd::r#true::run(), - "shitbox" => cli::run(), - "sleep" => cmd::sleep::run(&cmd::sleep::cli().get_matches()), + "echo" => ECHO.run(None)?, + "false" => FALSE.run(None)?, + "head" => { + HEAD.run(Some(&HEAD.cli().get_matches()))?; + } + "hostname" => HOSTNAME.run(Some(&HOSTNAME.cli().get_matches()))?, + "true" => TRUE.run(None)?, + "shitbox" => { + SHITBOX.run(Some(&SHITBOX.cli().get_matches()))?; + } + "sleep" => { + SLEEP.run(Some(&SLEEP.cli().get_matches()))?; + } _ => unimplemented!(), } } + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 8b68cd6..3d413d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,8 @@ +use std::process; + fn main() { - shitbox::run(); + if let Err(e) = shitbox::run() { + eprintln!("{e}"); + process::exit(1); + } }