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"
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]]

View File

@ -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

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_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<crate::Path>,
}
#[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<u8> = 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::<String>("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<dyn Error>> {
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::<String>("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<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 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<Path>,
}
pub fn run() {
let args: Vec<String> = 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<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 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<Path>,
}
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<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 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<Path>,
}
#[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<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) {
@ -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 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 <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 struct Hostname {
name: &'static str,
path: Option<Path>,
}
pub fn run(matches: &ArgMatches) {
if let Some(name) = matches.get_one::<String>("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 <jeang3nie@hitchhiker-linux.org>")
.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<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;
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<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 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<Path>,
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
pub fn run(matches: &ArgMatches) {
if let Some(raw) = matches.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);
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<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 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<Path>,
}
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<dyn Error>> {
process::exit(0);
}
fn path(&self) -> Option<Path> {
self.path
}
}

View File

@ -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<String> {
.flatten()
}
pub fn run() {
pub fn run() -> Result<(), Box<dyn Error>> {
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(())
}

View File

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