Major API revamp

This commit is contained in:
Nathan Fisher 2022-12-25 18:29:09 -05:00
parent 480960ce2b
commit d190050798
14 changed files with 621 additions and 384 deletions

7
Cargo.lock generated
View File

@ -146,6 +146,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "once_cell"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "6.4.1" version = "6.4.1"
@ -181,6 +187,7 @@ dependencies = [
"clap_complete_nushell", "clap_complete_nushell",
"clap_mangen", "clap_mangen",
"hostname", "hostname",
"once_cell",
] ]
[[package]] [[package]]

View File

@ -11,6 +11,7 @@ clap_complete = "4.0.6"
clap_complete_nushell = "0.1.8" clap_complete_nushell = "0.1.8"
clap_mangen = "0.2.5" clap_mangen = "0.2.5"
hostname = { version = "0.3", features = ["set"] } hostname = { version = "0.3", features = ["set"] }
once_cell = "1.16.0"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1

View File

@ -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(),
_ => {}
}
}

View File

@ -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::{value_parser, Arg, ArgAction, ArgMatches, Command};
use clap_complete::{generate_to, shells, Generator}; use clap_complete::{generate_to, shells, Generator};
use clap_complete_nushell::Nushell; use clap_complete_nushell::Nushell;
use clap_mangen::Man; use clap_mangen::Man;
use std::{ use std::{
fs, io, error::Error,
fs,
io::{self, ErrorKind},
path::{Path, PathBuf}, path::{Path, PathBuf},
process,
}; };
const COMMANDS: [fn() -> clap::Command; 8] = [ pub struct Bootstrap {
cmd::bootstrap::cli, name: &'static str,
cmd::echo::cli, path: Option<crate::Path>,
cmd::r#false::cli, }
cmd::head::cli,
cmd::hostname::cli,
cmd::r#true::cli,
cmd::sleep::cli,
crate::cli::cli,
];
#[must_use] pub const BOOTSTRAP: Bootstrap = Bootstrap {
pub fn cli() -> Command { name: "bootstrap",
Command::new("bootstrap") path: None,
.version(env!("CARGO_PKG_VERSION")) };
.author("Nathan Fisher")
.about("Install shitbox into the filesystem") impl Cmd for Bootstrap {
.long_about("Install symlinks, manpages and shell completions") fn name(&self) -> &str {
.args([ self.name
Arg::new("prefix") }
.help("The directory path under which to install")
.short('p') fn cli(&self) -> clap::Command {
.long("prefix") Command::new(self.name)
.num_args(1) .version(env!("CARGO_PKG_VERSION"))
.default_value("/") .author("Nathan Fisher")
.required(false), .about("Install shitbox into the filesystem")
Arg::new("usr") .long_about("Install symlinks, manpages and shell completions")
.help("Use /usr") .args([
.long_help( Arg::new("prefix")
"Split the installation so that some applets go into /bin | /sbin\n\ .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", while others are placed into /usr/bin | /usr/sbin",
) )
.short('u') .short('u')
.long("usr") .long("usr")
.default_value("true") .default_value("true")
.value_parser(value_parser!(bool)), .value_parser(value_parser!(bool)),
]) ])
.subcommands([ .subcommands([
Command::new("all").about("Install everything"), Command::new("all").about("Install everything"),
Command::new("links") Command::new("links")
.about("Install links for each applet") .about("Install links for each applet")
.arg( .arg(
Arg::new("soft") Arg::new("soft")
.help("Install soft links instead of hardlinks") .help("Install soft links instead of hardlinks")
.short('s') .short('s')
.long("soft"), .long("soft"),
), ),
Command::new("manpages") Command::new("manpages")
.about("Install Unix man pages") .about("Install Unix man pages")
.alias("man"), .alias("man"),
Command::new("completions") Command::new("completions")
.about("Install shell completions") .about("Install shell completions")
.alias("comp") .alias("comp")
.args([ .args([
Arg::new("all") Arg::new("all")
.help("Install completions for all supported shells") .help("Install completions for all supported shells")
.short('a') .short('a')
.long("all") .long("all")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
Arg::new("bash") Arg::new("bash")
.help("Bash shell completions") .help("Bash shell completions")
.short('b') .short('b')
.long("bash") .long("bash")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
Arg::new("fish") Arg::new("fish")
.help("Fish shell completions") .help("Fish shell completions")
.short('f') .short('f')
.long("fish") .long("fish")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
Arg::new("nu") Arg::new("nu")
.help("Nushell completions") .help("Nushell completions")
.short('n') .short('n')
.long("nu") .long("nu")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
Arg::new("pwsh") Arg::new("pwsh")
.help("PowerShell completions") .help("PowerShell completions")
.short('p') .short('p')
.long("pwsh") .long("pwsh")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
]), ]),
]) ])
} }
fn manpage(prefix: &str, f: &dyn Fn() -> Command) -> Result<(), io::Error> { fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box<dyn Error>> {
let cmd = f(); let matches = if let Some(m) = matches {
let fname = match cmd.get_name() { m
"bootstrap" => "shitbox-bootstrap.1".to_string(), } else {
s => format!("{s}.1"), return Err(io::Error::new(ErrorKind::Other, "No input").into());
}; };
let outdir: PathBuf = [prefix, "usr", "share", "man", "man1"].iter().collect(); if let Some(prefix) = matches.get_one::<String>("prefix") {
if !outdir.exists() { let commands: Commands = Commands {
fs::create_dir_all(&outdir)?; items: vec![
} &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &TRUE, &SLEEP, &SHITBOX,
let mut outfile = outdir; ],
outfile.push(fname); };
let man = Man::new(cmd); match matches.subcommand() {
let mut buffer: Vec<u8> = vec![]; Some(("manpages", _matches)) => {
man.render(&mut buffer)?; commands.manpages(prefix)?;
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::<String>("prefix") {
match matches.subcommand() {
Some(("manpages", _matches)) => {
if let Err(e) = manpages(prefix) {
eprintln!("{e}");
process::exit(1);
} }
} Some(("completions", matches)) => {
Some(("completions", matches)) => { commands.completions(prefix, matches)?;
if let Err(e) = completions(prefix, matches) {
eprintln!("{e}");
process::exit(1);
} }
} Some(("all", _matches)) => {
Some(("all", _matches)) => { commands.manpages(prefix)?;
if let Err(e) = manpages(prefix) { commands.completions(prefix, matches)?;
eprintln!("{e}");
process::exit(1);
} }
_ => {}
} }
_ => {}
} }
Ok(())
}
fn path(&self) -> Option<crate::Path> {
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<u8> = 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<PathBuf> {
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)
}
*/

View File

@ -1,42 +1,61 @@
use super::Cmd;
use crate::Path; use crate::Path;
use clap::{Arg, Command}; use clap::{Arg, Command};
use std::env; use std::{env, error::Error};
pub const PATH: Path = Path::Bin; pub struct Echo {
name: &'static str,
#[must_use] path: Option<Path>,
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 fn run() { pub const ECHO: Echo = Echo {
let args: Vec<String> = env::args().collect(); name: "echo",
let idx = match crate::progname() { path: Some(Path::Bin),
Some(s) if s.as_str() == "echo" => 1, };
Some(_) => 2,
None => unreachable!(), impl Cmd for Echo {
}; fn name(&self) -> &str {
let len = args.len(); self.name
let n = len > idx && args[idx] == "-n"; }
let i = if n { idx + 1 } else { idx };
for (index, arg) in args.iter().enumerate().skip(i) { fn cli(&self) -> clap::Command {
if index < len - 1 { Command::new(self.name)
print!("{arg} "); .about("Display a line of text")
} else { .long_about("Echo the STRING(s) to standard output")
print!("{arg}"); .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<dyn Error>> {
let args: Vec<String> = 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<crate::Path> {
self.path
} }
} }

View File

@ -1,18 +1,36 @@
use super::Cmd;
use crate::Path; use crate::Path;
use clap::Command; use clap::Command;
use std::process; use std::{error::Error, process};
pub const PATH: Path = Path::Bin; pub struct False {
name: &'static str,
#[must_use] path: Option<Path>,
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 fn run() { pub const FALSE: False = False {
process::exit(1); 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<dyn Error>> {
process::exit(1);
}
fn path(&self) -> Option<Path> {
self.path
}
} }

View File

@ -1,14 +1,30 @@
use super::Cmd;
use crate::Path; use crate::Path;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use std::fs; use std::{
use std::io::{stdin, Read}; error::Error,
use std::process; fs,
io::{self, stdin, Read},
process,
};
pub const PATH: Path = Path::Bin; pub struct Head {
name: &'static str,
path: Option<Path>,
}
#[must_use] pub const HEAD: Head = Head {
pub fn cli() -> Command { name: "head",
Command::new("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")) .version(env!("CARGO_PKG_VERSION"))
.author("Nathan Fisher") .author("Nathan Fisher")
.about("Display first lines of a file") .about("Display first lines of a file")
@ -45,6 +61,40 @@ pub fn cli() -> Command {
.allow_negative_numbers(false) .allow_negative_numbers(false)
.value_parser(value_parser!(usize)) .value_parser(value_parser!(usize))
]) ])
}
fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box<dyn Error>> {
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::<String>("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<Path> {
self.path
}
} }
fn head(file: &str, count: usize, header: bool, bytes: bool) { 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::<String>("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"),
);
}
}

View File

@ -1,55 +1,67 @@
use super::Cmd;
use crate::Path; use crate::Path;
use clap::{Arg, ArgAction, ArgMatches, Command}; use clap::{Arg, ArgAction, ArgMatches, Command};
use std::process; use std::{error::Error, io};
pub const PATH: Path = Path::Bin; pub struct Hostname {
name: &'static str,
#[must_use] path: Option<Path>,
pub fn cli() -> Command {
Command::new("hostname")
.version(env!("CARGO_PKG_VERSION"))
.author("The JeanG3nie <jeang3nie@hitchhiker-linux.org>")
.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 fn run(matches: &ArgMatches) { pub const HOSTNAME: Hostname = Hostname {
if let Some(name) = matches.get_one::<String>("NAME") { name: "hostname",
if let Err(e) = hostname::set(name) { path: Some(Path::Bin),
eprintln!("{e}"); };
process::exit(1);
} impl Cmd for Hostname {
} else { fn name(&self) -> &str {
match hostname::get() { self.name
Ok(hostname) => { }
if matches.get_flag("STRIP") {
println!( fn cli(&self) -> clap::Command {
"{}", Command::new(self.name)
if let Some(s) = hostname.to_string_lossy().split('.').next() { .version(env!("CARGO_PKG_VERSION"))
s .author("The JeanG3nie <jeang3nie@hitchhiker-linux.org>")
} else { .about("Prints the name of the current host. The super-user can set the host name by supplying an argument.")
eprintln!("hostname: missing operand"); .args([
process::exit(1); Arg::new("NAME")
} .help("name to set"),
); Arg::new("STRIP")
} else { .help("Removes any domain information from the printed name.")
println!("{}", hostname.to_string_lossy()); .short('s')
} .long("strip")
} .action(ArgAction::SetTrue)
Err(e) => { ])
eprintln!("{e}"); }
process::exit(1);
fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box<dyn Error>> {
let matches = matches.unwrap();
if let Some(name) = matches.get_one::<String>("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<crate::Path> {
self.path
}
} }

View File

@ -1,3 +1,6 @@
use clap::ArgMatches;
use std::error::Error;
pub mod bootstrap; pub mod bootstrap;
mod cat; mod cat;
mod chmod; mod chmod;
@ -16,6 +19,25 @@ mod mv;
mod pwd; mod pwd;
mod rm; mod rm;
mod rmdir; mod rmdir;
pub mod shitbox;
pub mod sleep; pub mod sleep;
mod sync; mod sync;
pub mod r#true; 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<dyn Error>>;
fn path(&self) -> Option<crate::Path>;
}

60
src/cmd/shitbox/mod.rs Normal file
View File

@ -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<crate::Path>,
}
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<dyn Error>> {
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<crate::Path> {
self.path
}
}

View File

@ -1,37 +1,56 @@
use super::Cmd;
use crate::Path; use crate::Path;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; 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; pub struct Sleep {
name: &'static str,
#[must_use] path: Option<Path>,
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)
)
} }
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] pub const SLEEP: Sleep = Sleep {
pub fn run(matches: &ArgMatches) { name: "sleep",
if let Some(raw) = matches.get_one::<f64>("seconds") { path: Some(Path::Bin),
let seconds = *raw as u64; };
let nanos = ((raw % 1.0) * 10e-9) as u32;
let s = Duration::new(seconds, nanos); impl Cmd for Sleep {
thread::sleep(s); 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<dyn Error>> {
if let Some(raw) = matches.unwrap().get_one::<f64>("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<Path> {
self.path
} }
} }

View File

@ -1,18 +1,36 @@
use super::Cmd;
use crate::Path; use crate::Path;
use clap::Command; use clap::Command;
use std::process; use std::{error::Error, process};
pub const PATH: Path = Path::Bin; pub struct True {
name: &'static str,
#[must_use] path: Option<Path>,
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 fn run() { pub const TRUE: True = True {
process::exit(0); 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<dyn Error>> {
process::exit(0);
}
fn path(&self) -> Option<Path> {
self.path
}
} }

View File

@ -1,9 +1,10 @@
#![warn(clippy::all, clippy::pedantic)] #![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; pub mod cmd;
use cmd::{Cmd, ECHO, FALSE, HEAD, HOSTNAME, SHITBOX, SLEEP, TRUE};
#[derive(Debug, Clone, Copy)]
pub enum Path { pub enum Path {
Bin, Bin,
Sbin, Sbin,
@ -23,17 +24,24 @@ pub fn progname() -> Option<String> {
.flatten() .flatten()
} }
pub fn run() { pub fn run() -> Result<(), Box<dyn Error>> {
if let Some(progname) = progname() { if let Some(progname) = progname() {
match progname.as_str() { match progname.as_str() {
"echo" => cmd::echo::run(), "echo" => ECHO.run(None)?,
"false" => cmd::r#false::run(), "false" => FALSE.run(None)?,
"head" => cmd::head::run(&cmd::head::cli().get_matches()), "head" => {
"hostname" => cmd::hostname::run(&cmd::hostname::cli().get_matches()), HEAD.run(Some(&HEAD.cli().get_matches()))?;
"true" => cmd::r#true::run(), }
"shitbox" => cli::run(), "hostname" => HOSTNAME.run(Some(&HOSTNAME.cli().get_matches()))?,
"sleep" => cmd::sleep::run(&cmd::sleep::cli().get_matches()), "true" => TRUE.run(None)?,
"shitbox" => {
SHITBOX.run(Some(&SHITBOX.cli().get_matches()))?;
}
"sleep" => {
SLEEP.run(Some(&SLEEP.cli().get_matches()))?;
}
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
Ok(())
} }

View File

@ -1,3 +1,8 @@
use std::process;
fn main() { fn main() {
shitbox::run(); if let Err(e) = shitbox::run() {
eprintln!("{e}");
process::exit(1);
}
} }