Simplify subcommand parsing:

- one match statement to return a `Box<dyn Cmd>`
- one array containing all command names
Only two places to register new commands (besides their module), both in
crate::cmd::mod.rs. Also removes `once_cell` crate dependency.

Replace `base64` crate dependency with `data_encoding::BASE64` so that
both base32 and base64 commands use the same crate.
This commit is contained in:
Nathan Fisher 2023-01-06 23:41:02 -05:00
parent 84ede35190
commit fb389fd309
29 changed files with 361 additions and 680 deletions

14
Cargo.lock generated
View File

@ -13,12 +13,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -178,12 +172,6 @@ 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"
@ -215,7 +203,6 @@ name = "shitbox"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"atty", "atty",
"base64",
"clap", "clap",
"clap_complete", "clap_complete",
"clap_complete_nushell", "clap_complete_nushell",
@ -223,7 +210,6 @@ dependencies = [
"data-encoding", "data-encoding",
"hostname", "hostname",
"libc", "libc",
"once_cell",
"termcolor", "termcolor",
] ]

View File

@ -7,7 +7,6 @@ edition = "2021"
[dependencies] [dependencies]
atty = "0.2.14" atty = "0.2.14"
base64 = "0.20.0"
clap = "4.0.29" clap = "4.0.29"
clap_complete = "4.0.6" clap_complete = "4.0.6"
clap_complete_nushell = "0.1.8" clap_complete_nushell = "0.1.8"
@ -15,7 +14,6 @@ clap_mangen = "0.2.5"
data-encoding = "2.3.3" data-encoding = "2.3.3"
hostname = { version = "0.3", features = ["set"] } hostname = { version = "0.3", features = ["set"] }
libc = "0.2.139" libc = "0.2.139"
once_cell = "1.16.0"
termcolor = "1.1.3" termcolor = "1.1.3"
[profile.release] [profile.release]

Binary file not shown.

View File

@ -1,38 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH base32 1 "base32 0.1.0"
.SH NAME
base32 \- Base32 encode/decode data and print to standard output
.SH SYNOPSIS
\fBbase32\fR [\fB\-d\fR|\fB\-\-decode\fR] [\fB\-i\fR|\fB\-\-ignore\-space\fR] [\fB\-w\fR|\fB\-\-wrap\fR] [\fB\-v\fR|\fB\-\-verbose\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIINPUT\fR]
.SH DESCRIPTION
Base32 encode/decode data and print to standard output
.SH OPTIONS
.TP
\fB\-d\fR, \fB\-\-decode\fR
Decode rather than encode
.TP
\fB\-i\fR, \fB\-\-ignore\-space\fR
Ignore whitespace when decoding
.TP
\fB\-w\fR, \fB\-\-wrap\fR [default: 76]
Wrap encoded lines after n characters
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Display a header naming each file
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Do not display header, even with multiple files
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.TP
[\fIINPUT\fR]
The input file to use
.SH VERSION
v0.1.0
.SH AUTHORS
The JeanG3nie <jeang3nie@hitchhiker\-linux.org>

View File

@ -1,24 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH dirname 1 "dirname 0.1.0"
.SH NAME
dirname \- strip last component from file name
.SH SYNOPSIS
\fBdirname\fR [\fB\-z\fR|\fB\-\-zero\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] <\fIname\fR>
.SH DESCRIPTION
strip last component from file name
.SH OPTIONS
.TP
\fB\-z\fR, \fB\-\-zero\fR
end each output line with NUL, not newline
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.TP
<\fIname\fR>
.SH VERSION
v0.1.0

View File

@ -1,26 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH echo 1 "echo 0.1.0"
.SH NAME
echo \- Display a line of text
.SH SYNOPSIS
\fBecho\fR [\fB\-n \fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fISTRING\fR]
.SH DESCRIPTION
Echo the STRING(s) to standard output
.SH OPTIONS
.TP
\fB\-n\fR
Do not output a trailing newline
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information (use `\-h` for a summary)
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.TP
[\fISTRING\fR]
.SH VERSION
v0.1.0
.SH AUTHORS
Nathan Fisher

View File

@ -1,20 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH false 1 "false 0.1.0"
.SH NAME
false \- Does nothing unsuccessfully
.SH SYNOPSIS
\fBfalse\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
.SH DESCRIPTION
Exit with a status code indicating failure
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information (use `\-h` for a summary)
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.SH VERSION
v0.1.0
.SH AUTHORS
Nathan Fisher

View File

@ -1,38 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH head 1 "head 0.1.0"
.SH NAME
head \- Display first lines of a file
.SH SYNOPSIS
\fBhead\fR [\fB\-c\fR|\fB\-\-bytes\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-v\fR|\fB\-\-verbose\fR] [\fB\-n\fR|\fB\-\-lines\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIFILES\fR]
.SH DESCRIPTION
Print the first 10 lines of each FILE to standard output.
With more than one FILE, precede each with a header giving the file name.
.PP
With no FILE, or when FILE is \-, read standard input.
.SH OPTIONS
.TP
\fB\-c\fR, \fB\-\-bytes\fR
Count bytes instead of lines
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Disable printing a header. Overrides \-c
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Each file is preceded by a header consisting of the string "==> XXX <==" where "XXX" is the name of the file.
.TP
\fB\-n\fR, \fB\-\-lines\fR
Count n number of lines (or bytes if \-c is specified).
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information (use `\-h` for a summary)
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.TP
[\fIFILES\fR]
The input file to use
.SH VERSION
v0.1.0
.SH AUTHORS
Nathan Fisher

View File

@ -1,26 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH hostname 1 "hostname 0.1.0"
.SH NAME
hostname \- Prints the name of the current host. The super\-user can set the host name by supplying an argument.
.SH SYNOPSIS
\fBhostname\fR [\fB\-s\fR|\fB\-\-strip\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fINAME\fR]
.SH DESCRIPTION
Prints the name of the current host. The super\-user can set the host name by supplying an argument.
.SH OPTIONS
.TP
\fB\-s\fR, \fB\-\-strip\fR
Removes any domain information from the printed name.
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.TP
[\fINAME\fR]
name to set
.SH VERSION
v0.1.0
.SH AUTHORS
The JeanG3nie <jeang3nie@hitchhiker\-linux.org>

View File

@ -1,20 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH nologin 1 "nologin 0.1.0"
.SH NAME
nologin \- Denies a user account login ability
.SH SYNOPSIS
\fBnologin\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
.SH DESCRIPTION
Denies a user account login ability
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.SH VERSION
v0.1.0
.SH AUTHORS
Nathan Fisher

View File

@ -1,46 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH bootstrap 1 "bootstrap 0.1.0"
.SH NAME
bootstrap \- Install shitbox into the filesystem
.SH SYNOPSIS
\fBbootstrap\fR [\fB\-p\fR|\fB\-\-prefix\fR] [\fB\-u\fR|\fB\-\-usr\fR] [\fB\-s\fR|\fB\-\-soft\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIsubcommands\fR]
.SH DESCRIPTION
Install symlinks, manpages and shell completions
.SH OPTIONS
.TP
\fB\-p\fR, \fB\-\-prefix\fR [default: /]
The directory path under which to install
.TP
\fB\-u\fR, \fB\-\-usr\fR
Split the installation so that some applets go into /bin | /sbin
while others are placed into /usr/bin | /usr/sbin
.TP
\fB\-s\fR, \fB\-\-soft\fR
Install soft links instead of hardlinks
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information (use `\-h` for a summary)
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.SH SUBCOMMANDS
.TP
bootstrap\-all(1)
Install everything
.TP
bootstrap\-links(1)
Install links for each applet
.TP
bootstrap\-manpages(1)
Install Unix man pages
.TP
bootstrap\-completions(1)
Install shell completions
.TP
bootstrap\-help(1)
Print this message or the help of the given subcommand(s)
.SH VERSION
v0.1.0
.SH AUTHORS
Nathan Fisher

View File

@ -1,52 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH shitbox 1 "shitbox 0.1.0"
.SH NAME
shitbox \- The Harbor Freight multitool of embedded Linux
.SH SYNOPSIS
\fBshitbox\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIsubcommands\fR]
.SH DESCRIPTION
The Harbor Freight multitool of embedded Linux
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.SH SUBCOMMANDS
.TP
shitbox\-base32(1)
Base32 encode/decode data and print to standard output
.TP
shitbox\-bootstrap(1)
Install shitbox into the filesystem
.TP
shitbox\-echo(1)
Display a line of text
.TP
shitbox\-dirname(1)
strip last component from file name
.TP
shitbox\-false(1)
Does nothing unsuccessfully
.TP
shitbox\-head(1)
Display first lines of a file
.TP
shitbox\-nologin(1)
Denies a user account login ability
.TP
shitbox\-hostname(1)
Prints the name of the current host. The super\-user can set the host name by supplying an argument.
.TP
shitbox\-sleep(1)
Suspend execution for an interval of time
.TP
shitbox\-true(1)
Does nothing successfully
.TP
shitbox\-help(1)
Print this message or the help of the given subcommand(s)
.SH VERSION
v0.1.0

View File

@ -1,25 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH sleep 1 "sleep 0.1.0"
.SH NAME
sleep \- Suspend execution for an interval of time
.SH SYNOPSIS
\fBsleep\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] <\fIseconds\fR>
.SH DESCRIPTION
The sleep utility suspends execution for a minimum of the specified number of seconds.
This number must be positive and may contain a decimal fraction.
sleep is commonly used to schedule the execution of other commands
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information (use `\-h` for a summary)
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.TP
<\fIseconds\fR>
The number of seconds to sleep
.SH VERSION
v0.1.0
.SH AUTHORS

View File

@ -1,20 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH true 1 "true 0.1.0"
.SH NAME
true \- Does nothing successfully
.SH SYNOPSIS
\fBtrue\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR]
.SH DESCRIPTION
Exit with a status code indicating success
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help information (use `\-h` for a summary)
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version information
.SH VERSION
v0.1.0
.SH AUTHORS
Nathan Fisher

View File

@ -14,10 +14,14 @@ pub struct Base32 {
path: Option<crate::Path>, path: Option<crate::Path>,
} }
pub const BASE_32: Base32 = Base32 { impl Default for Base32 {
name: "base32", fn default() -> Self {
path: Some(crate::Path::UsrBin), Self {
}; name: "base32",
path: Some(crate::Path::UsrBin),
}
}
}
impl Cmd for Base32 { impl Cmd for Base32 {
fn name(&self) -> &str { fn name(&self) -> &str {
@ -129,7 +133,7 @@ fn decode_base32(mut contents: String, ignore: bool) -> Result<(), Box<dyn Error
} }
let decoded = BASE32.decode(contents.as_bytes())?; let decoded = BASE32.decode(contents.as_bytes())?;
let output = String::from_utf8(decoded)?; let output = String::from_utf8(decoded)?;
println!("{}", output.trim_end()); println!("{}\n", output.trim_end());
Ok(()) Ok(())
} }

View File

@ -1,6 +1,6 @@
use super::Cmd; use super::Cmd;
use base64::{decode, encode};
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; use clap::{value_parser, Arg, ArgAction, ArgMatches, Command};
use data_encoding::BASE64;
use std::{ use std::{
error::Error, error::Error,
fs, fs,
@ -14,10 +14,14 @@ pub struct Base64 {
path: Option<crate::Path>, path: Option<crate::Path>,
} }
pub const BASE_64: Base64 = Base64 { impl Default for Base64 {
name: "base64", fn default() -> Self {
path: Some(crate::Path::UsrBin), Self {
}; name: "base64",
path: Some(crate::Path::UsrBin),
}
}
}
impl Cmd for Base64 { impl Cmd for Base64 {
fn name(&self) -> &str { fn name(&self) -> &str {
@ -125,14 +129,15 @@ fn decode_base64(mut contents: String, ignore: bool) -> Result<(), Box<dyn Error
} else { } else {
contents = contents.replace('\n', ""); contents = contents.replace('\n', "");
} }
let decoded = decode(&contents)?.clone(); let decoded = BASE64.decode(&contents.as_bytes())?;
let output = String::from_utf8(decoded)?; let output = String::from_utf8(decoded)?;
println!("{}", output.trim_end()); println!("{}\n", output.trim_end());
Ok(()) Ok(())
} }
fn encode_base64(contents: &str, wrap: usize) { fn encode_base64(contents: &str, wrap: usize) {
encode(contents.as_bytes()) BASE64
.encode(contents.as_bytes())
.chars() .chars()
.collect::<Vec<char>>() .collect::<Vec<char>>()
.chunks(wrap) .chunks(wrap)

View File

@ -1,12 +1,14 @@
use super::{Cmd, Commands}; use super::{Cmd, COMMANDS};
use clap::{Arg, ArgAction, ArgMatches, Command}; use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_complete::shells; use clap_complete::{generate_to, shells};
use clap_complete_nushell::Nushell; use clap_complete_nushell::Nushell;
use clap_mangen::Man;
use std::{ use std::{
error::Error, error::Error,
fs, fs,
io::{self, ErrorKind}, io::{self, ErrorKind},
path::PathBuf, os::unix::fs::symlink,
path::{Path, PathBuf},
}; };
#[derive(Debug)] #[derive(Debug)]
@ -15,10 +17,14 @@ pub struct Bootstrap {
path: Option<crate::Path>, path: Option<crate::Path>,
} }
pub const BOOTSTRAP: Bootstrap = Bootstrap { impl Default for Bootstrap {
name: "bootstrap", fn default() -> Self {
path: None, Self {
}; name: "bootstrap",
path: None,
}
}
}
impl Bootstrap { impl Bootstrap {
fn all() -> clap::Command { fn all() -> clap::Command {
@ -167,9 +173,6 @@ impl Cmd for Bootstrap {
return Err(io::Error::new(ErrorKind::Other, "No input").into()); return Err(io::Error::new(ErrorKind::Other, "No input").into());
}; };
if let Some(prefix) = matches.get_one::<String>("prefix") { if let Some(prefix) = matches.get_one::<String>("prefix") {
let commands = super::COMMANDS
.get()
.ok_or_else(|| io::Error::new(ErrorKind::Other, "Cannot get commands list"))?;
let usr = matches.get_flag("usr"); let usr = matches.get_flag("usr");
if let Some(progpath) = crate::progpath() { if let Some(progpath) = crate::progpath() {
let mut outpath = PathBuf::from(prefix); let mut outpath = PathBuf::from(prefix);
@ -189,18 +192,18 @@ impl Cmd for Bootstrap {
} }
match matches.subcommand() { match matches.subcommand() {
Some(("links", matches)) => { Some(("links", matches)) => {
commands.links(prefix, usr, matches)?; links(prefix, usr, matches)?;
} }
Some(("manpages", _matches)) => { Some(("manpages", _matches)) => {
commands.manpages(prefix)?; manpages(prefix)?;
} }
Some(("completions", matches)) => { Some(("completions", matches)) => {
commands.completions(prefix, matches)?; completions(prefix, matches)?;
} }
Some(("all", matches)) => { Some(("all", matches)) => {
commands.links(prefix, usr, matches)?; links(prefix, usr, matches)?;
commands.manpages(prefix)?; manpages(prefix)?;
commands.completions(prefix, matches)?; completions(prefix, matches)?;
} }
_ => {} _ => {}
} }
@ -213,59 +216,178 @@ impl Cmd for Bootstrap {
} }
} }
pub trait Bootstrappable { pub trait BootstrapCmd {
fn manpages(&self, prefix: &str) -> Result<(), io::Error>; fn completion(&self, outdir: &Path, gen: &str) -> Result<(), io::Error>;
fn completions(&self, prefix: &str, matches: &ArgMatches) -> Result<(), io::Error>; fn linkpath(&self, prefix: &str, usr: bool) -> Option<PathBuf>;
fn links(&self, prefix: &str, usr: bool, cmd: &ArgMatches) -> Result<(), Box<dyn Error>>; fn link(&self, prefix: &str, usr: bool, soft: bool) -> Result<(), Box<dyn Error>>;
fn manpage(&self, prefix: &str) -> Result<(), io::Error>;
} }
impl<'a> Bootstrappable for Commands<'a> { impl BootstrapCmd for dyn Cmd {
fn manpages(&self, prefix: &str) -> Result<(), io::Error> { fn completion(&self, outdir: &Path, gen: &str) -> Result<(), io::Error> {
println!("Generating Unix man pages:"); let name = self.name();
self.items.iter().try_for_each(|cmd| cmd.manpage(prefix))?; let mut cmd = self.cli();
if !outdir.exists() {
fs::create_dir_all(outdir)?;
}
let path = match gen {
"bash" => generate_to(shells::Bash, &mut cmd, name, outdir)?,
"fish" => generate_to(shells::Fish, &mut cmd, name, outdir)?,
"nu" => generate_to(Nushell, &mut cmd, name, outdir)?,
"pwsh" => generate_to(shells::PowerShell, &mut cmd, name, outdir)?,
"zsh" => generate_to(shells::Zsh, &mut cmd, name, outdir)?,
_ => unimplemented!(),
};
println!(" {}", path.display());
Ok(()) Ok(())
} }
fn completions(&self, prefix: &str, matches: &ArgMatches) -> Result<(), io::Error> { fn linkpath(&self, prefix: &str, usr: bool) -> Option<PathBuf> {
println!("Generating completions:"); let mut path = PathBuf::from(prefix);
if matches.get_flag("bash") || matches.get_flag("all") { let binpath = self.path();
let outdir: PathBuf = [prefix, "share", "bash-completion", "completion"] match binpath {
.iter() Some(crate::Path::Bin) => path.push("bin"),
.collect(); Some(crate::Path::Sbin) => path.push("sbin"),
self.items Some(crate::Path::UsrBin) => {
.iter() if usr {
.try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Bash))?; path.push("usr");
}
path.push("bin");
}
Some(crate::Path::UsrSbin) => {
if usr {
path.push("usr");
}
path.push("sbin");
}
None => return None,
} }
if matches.get_flag("fish") || matches.get_flag("all") { path.push(self.name());
let outdir: PathBuf = [prefix, "share", "fish", "completions"].iter().collect(); Some(path)
self.items }
.iter()
.try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Fish))?; fn link(&self, prefix: &str, usr: bool, soft: bool) -> Result<(), Box<dyn Error>> {
} if let Some(linkpath) = self.linkpath(prefix, usr) {
if matches.get_flag("nu") || matches.get_flag("all") { if soft {
let outdir: PathBuf = [prefix, "share", "nu", "completions"].iter().collect(); let binpath = match self.path().unwrap() {
self.items crate::Path::Bin => "shitbox",
.iter() crate::Path::Sbin => "../bin/shitbox",
.try_for_each(|cmd| Self::completion(&outdir, cmd, Nushell))?; crate::Path::UsrBin => {
} if usr {
if matches.get_flag("pwsh") || matches.get_flag("all") { "../../bin/shitbox"
let outdir: PathBuf = [prefix, "share", "pwsh", "completions"].iter().collect(); } else {
self.items "shitbox"
.iter() }
.try_for_each(|cmd| Self::completion(&outdir, cmd, shells::PowerShell))?; }
} crate::Path::UsrSbin => {
if matches.get_flag("zsh") || matches.get_flag("all") { if usr {
let outdir: PathBuf = [prefix, "share", "zsh", "site-functions"].iter().collect(); "../../bin/shitbox"
self.items } else {
.iter() "../bin/shitbox"
.try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Zsh))?; }
}
};
symlink(binpath, &linkpath)?;
println!(" symlink: {binpath} -> {}", linkpath.display());
} else {
let mut binpath = PathBuf::from(prefix);
binpath.push("bin");
binpath.push(env!("CARGO_PKG_NAME"));
fs::hard_link(&binpath, &linkpath)?;
println!(" link: {} -> {}", binpath.display(), linkpath.display());
}
} }
Ok(()) Ok(())
} }
fn links(&self, prefix: &str, usr: bool, cmd: &ArgMatches) -> Result<(), Box<dyn Error>> { fn manpage(&self, prefix: &str) -> Result<(), io::Error> {
println!("Generating links:"); let command = self.cli();
let mut binpath = PathBuf::from(prefix); let fname = match self.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 manpages(prefix: &str) -> Result<(), io::Error> {
println!("Generating Unix man pages:");
COMMANDS
.iter()
.try_for_each(|cmd| crate::cmd::get(cmd).unwrap().manpage(prefix))?;
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| {
let cmd = crate::cmd::get(cmd).unwrap();
cmd.completion(&outdir, "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| {
let cmd = crate::cmd::get(cmd).unwrap();
cmd.completion(&outdir, "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| {
let cmd = crate::cmd::get(cmd).unwrap();
cmd.completion(&outdir, "nu")
})?;
}
if matches.get_flag("pwsh") || matches.get_flag("all") {
let outdir: PathBuf = [prefix, "share", "pwsh", "completions"].iter().collect();
COMMANDS.iter().try_for_each(|cmd| {
let cmd = crate::cmd::get(cmd).unwrap();
cmd.completion(&outdir, "pwsh")
})?;
}
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| {
let cmd = crate::cmd::get(cmd).unwrap();
cmd.completion(&outdir, "zsh")
})?;
}
Ok(())
}
fn links(prefix: &str, usr: bool, cmd: &ArgMatches) -> Result<(), Box<dyn Error>> {
println!("Generating links:");
let mut binpath = PathBuf::from(prefix);
binpath.push("bin");
if !binpath.exists() {
fs::create_dir_all(&binpath)?;
println!(" mkdir: {}", binpath.display());
}
binpath.pop();
binpath.push("sbin");
if !binpath.exists() {
fs::create_dir_all(&binpath)?;
println!(" mkdir: {}", binpath.display());
}
if usr {
binpath.pop();
binpath.push("usr");
binpath.push("bin"); binpath.push("bin");
if !binpath.exists() { if !binpath.exists() {
fs::create_dir_all(&binpath)?; fs::create_dir_all(&binpath)?;
@ -277,25 +399,11 @@ impl<'a> Bootstrappable for Commands<'a> {
fs::create_dir_all(&binpath)?; fs::create_dir_all(&binpath)?;
println!(" mkdir: {}", binpath.display()); println!(" mkdir: {}", binpath.display());
} }
if usr {
binpath.pop();
binpath.push("usr");
binpath.push("bin");
if !binpath.exists() {
fs::create_dir_all(&binpath)?;
println!(" mkdir: {}", binpath.display());
}
binpath.pop();
binpath.push("sbin");
if !binpath.exists() {
fs::create_dir_all(&binpath)?;
println!(" mkdir: {}", binpath.display());
}
}
let soft = cmd.get_flag("soft");
self.items
.iter()
.try_for_each(|cmd| cmd.link(prefix, usr, soft))?;
Ok(())
} }
let soft = cmd.get_flag("soft");
COMMANDS.iter().try_for_each(|c| {
let cmd = crate::cmd::get(c).unwrap();
cmd.link(prefix, usr, soft)
})?;
Ok(())
} }

View File

@ -8,10 +8,14 @@ pub struct Dirname {
path: Option<crate::Path>, path: Option<crate::Path>,
} }
pub const DIRNAME: Dirname = Dirname { impl Default for Dirname {
name: "dirname", fn default() -> Self {
path: Some(crate::Path::UsrBin), Self {
}; name: "dirname",
path: Some(crate::Path::UsrBin),
}
}
}
impl Cmd for Dirname { impl Cmd for Dirname {
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -9,10 +9,14 @@ pub struct Echo {
path: Option<Path>, path: Option<Path>,
} }
pub const ECHO: Echo = Echo { impl Default for Echo {
name: "echo", fn default() -> Self {
path: Some(Path::Bin), Self {
}; name: "echo",
path: Some(crate::Path::Bin),
}
}
}
impl Cmd for Echo { impl Cmd for Echo {
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -11,10 +11,14 @@ pub struct Factor {
path: Option<crate::Path>, path: Option<crate::Path>,
} }
pub const FACTOR: Factor = Factor { impl Default for Factor {
name: "factor", fn default() -> Self {
path: Some(crate::Path::UsrBin), Self {
}; name: "factor",
path: Some(crate::Path::UsrBin),
}
}
}
impl Cmd for Factor { impl Cmd for Factor {
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -9,10 +9,14 @@ pub struct False {
path: Option<Path>, path: Option<Path>,
} }
pub const FALSE: False = False { impl Default for False {
name: "false", fn default() -> Self {
path: Some(Path::Bin), Self {
}; name: "false",
path: Some(crate::Path::Bin),
}
}
}
impl Cmd for False { impl Cmd for False {
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -15,10 +15,14 @@ pub struct Head {
path: Option<Path>, path: Option<Path>,
} }
pub const HEAD: Head = Head { impl Default for Head {
name: "head", fn default() -> Self {
path: Some(Path::Bin), Self {
}; name: "head",
path: Some(crate::Path::Bin),
}
}
}
impl Cmd for Head { impl Cmd for Head {
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -1,13 +1,5 @@
use clap::ArgMatches; use clap::ArgMatches;
use clap_complete::{generate_to, Generator}; use std::{error::Error, fmt};
use clap_mangen::Man;
use once_cell::sync::OnceCell;
use std::{
error::Error,
fmt, fs, io,
os::unix::fs::symlink,
path::{Path, PathBuf},
};
pub mod base32; pub mod base32;
pub mod base64; pub mod base64;
@ -38,123 +30,50 @@ mod sync;
pub mod r#true; pub mod r#true;
pub use { pub use {
self::base64::{Base64, BASE_64}, self::hostname::Hostname, base32::Base32, base64::Base64, bootstrap::Bootstrap,
self::hostname::{Hostname, HOSTNAME}, dirname::Dirname, echo::Echo, factor::Factor, head::Head, mountpoint::Mountpoint,
base32::{Base32, BASE_32}, nologin::Nologin, r#false::False, r#true::True, shitbox::Shitbox, sleep::Sleep,
bootstrap::{Bootstrap, BOOTSTRAP},
dirname::{Dirname, DIRNAME},
echo::{Echo, ECHO},
factor::{Factor, FACTOR},
head::{Head, HEAD},
mountpoint::{Mountpoint, MOUNTPOINT},
nologin::{Nologin, NOLOGIN},
r#false::{False, FALSE},
r#true::{True, TRUE},
shitbox::{Shitbox, SHITBOX},
sleep::{Sleep, SLEEP},
}; };
#[derive(Debug)]
pub struct Commands<'a> {
pub items: Vec<&'a dyn Cmd>,
}
pub static COMMANDS: OnceCell<Commands> = OnceCell::new();
impl<'a> Commands<'a> {
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(())
}
}
pub trait Cmd: fmt::Debug + Sync { pub trait Cmd: fmt::Debug + Sync {
fn name(&self) -> &str; fn name(&self) -> &str;
fn cli(&self) -> clap::Command; fn cli(&self) -> clap::Command;
fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box<dyn Error>>; fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box<dyn Error>>;
fn path(&self) -> Option<crate::Path>; fn path(&self) -> Option<crate::Path>;
}
fn linkpath(&self, prefix: &str, usr: bool) -> Option<PathBuf> { pub fn get(name: &str) -> Option<Box<dyn Cmd>> {
let mut path = PathBuf::from(prefix); match name {
let binpath = self.path(); "base64" => Some(Box::new(Base64::default())),
match binpath { "base32" => Some(Box::new(Base32::default())),
Some(crate::Path::Bin) => path.push("bin"), "bootstrap" => Some(Box::new(Bootstrap::default())),
Some(crate::Path::Sbin) => path.push("sbin"), "dirname" => Some(Box::new(Dirname::default())),
Some(crate::Path::UsrBin) => { "echo" => Some(Box::new(Echo::default())),
if usr { "factor" => Some(Box::new(Factor::default())),
path.push("usr"); "false" => Some(Box::new(False::default())),
} "head" => Some(Box::new(Head::default())),
path.push("bin"); "mountpoint" => Some(Box::new(Mountpoint::default())),
} "nologin" => Some(Box::new(Nologin::default())),
Some(crate::Path::UsrSbin) => { "shitbox" => Some(Box::new(Shitbox::default())),
if usr { "sleep" => Some(Box::new(Sleep::default())),
path.push("usr"); "true" => Some(Box::new(True::default())),
} _ => None,
path.push("sbin");
}
None => return None,
}
path.push(self.name());
Some(path)
}
fn link(&self, prefix: &str, usr: bool, soft: bool) -> Result<(), Box<dyn Error>> {
if let Some(linkpath) = self.linkpath(prefix, usr) {
if soft {
let binpath = match self.path().unwrap() {
crate::Path::Bin => "shitbox",
crate::Path::Sbin => "../bin/shitbox",
crate::Path::UsrBin => {
if usr {
"../../bin/shitbox"
} else {
"shitbox"
}
}
crate::Path::UsrSbin => {
if usr {
"../../bin/shitbox"
} else {
"../bin/shitbox"
}
}
};
symlink(binpath, &linkpath)?;
println!(" symlink: {binpath} -> {}", linkpath.display());
} else {
let mut binpath = PathBuf::from(prefix);
binpath.push("bin");
binpath.push(env!("CARGO_PKG_NAME"));
fs::hard_link(&binpath, &linkpath)?;
println!(" link: {} -> {}", binpath.display(), linkpath.display());
}
}
Ok(())
}
fn manpage(&self, prefix: &str) -> Result<(), io::Error> {
let command = self.cli();
let fname = match self.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(())
} }
} }
pub static COMMANDS: [&'static str; 14] = [
"base32",
"base64",
"bootstrap",
"dirname",
"echo",
"false",
"factor",
"head",
"hostname",
"mountpoint",
"nologin",
"true",
"sleep",
"shitbox",
];

View File

@ -15,10 +15,14 @@ pub struct Mountpoint {
path: Option<crate::Path>, path: Option<crate::Path>,
} }
pub const MOUNTPOINT: Mountpoint = Mountpoint { impl Default for Mountpoint {
name: "mountpoint", fn default() -> Self {
path: Some(crate::Path::Bin), Self {
}; name: "mountpoint",
path: Some(crate::Path::Bin),
}
}
}
impl Cmd for Mountpoint { impl Cmd for Mountpoint {
fn name(&self) -> &str { fn name(&self) -> &str {
@ -38,7 +42,7 @@ impl Cmd for Mountpoint {
failure: incorrect invocation, permissions or system error\ failure: incorrect invocation, permissions or system error\
\n 32\n \ \n 32\n \
failure: the directory is not a mountpoint, or device is not a \ failure: the directory is not a mountpoint, or device is not a \
block device on --devno" block device on --devno",
) )
.args([ .args([
Arg::new("fs-devno") Arg::new("fs-devno")

View File

@ -8,10 +8,14 @@ pub struct Nologin {
path: Option<crate::Path>, path: Option<crate::Path>,
} }
pub const NOLOGIN: Nologin = Nologin { impl Default for Nologin {
name: "nologin", fn default() -> Self {
path: Some(crate::Path::Sbin), Self {
}; name: "nologin",
path: Some(crate::Path::Sbin),
}
}
}
impl Cmd for Nologin { impl Cmd for Nologin {
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -1,11 +1,9 @@
use super::{ use super::{Cmd, COMMANDS};
Cmd, BASE_32, BASE_64, BOOTSTRAP, DIRNAME, ECHO, FACTOR, FALSE, HEAD, HOSTNAME, MOUNTPOINT,
NOLOGIN, SLEEP, TRUE,
};
use clap::Command; use clap::Command;
use std::{ use std::{
error::Error, error::Error,
io::{self, ErrorKind}, io::{self, ErrorKind},
process,
}; };
#[derive(Debug)] #[derive(Debug)]
@ -14,6 +12,15 @@ pub struct Shitbox {
path: Option<crate::Path>, path: Option<crate::Path>,
} }
impl Default for Shitbox {
fn default() -> Self {
Self {
name: "shitbox",
path: None,
}
}
}
pub const SHITBOX: Shitbox = Shitbox { pub const SHITBOX: Shitbox = Shitbox {
name: "shitbox", name: "shitbox",
path: None, path: None,
@ -25,6 +32,18 @@ impl Cmd for Shitbox {
} }
fn cli(&self) -> clap::Command { fn cli(&self) -> clap::Command {
let subcommands: Vec<Command> = {
let mut s = vec![];
for c in COMMANDS {
if c == "shitbox" {
continue;
}
if let Some(cmd) = crate::cmd::get(c) {
s.push(cmd.cli());
}
}
s
};
Command::new(self.name) Command::new(self.name)
.about("The Harbor Freight multitool of embedded Linux") .about("The Harbor Freight multitool of embedded Linux")
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
@ -32,42 +51,20 @@ impl Cmd for Shitbox {
.arg_required_else_help(true) .arg_required_else_help(true)
.subcommand_value_name("APPLET") .subcommand_value_name("APPLET")
.subcommand_help_heading("APPLETS") .subcommand_help_heading("APPLETS")
.subcommands([ .subcommands(&subcommands)
BASE_32.cli(),
BASE_64.cli(),
BOOTSTRAP.cli(),
DIRNAME.cli(),
ECHO.cli(),
FALSE.cli(),
FACTOR.cli(),
HEAD.cli(),
MOUNTPOINT.cli(),
NOLOGIN.cli(),
HOSTNAME.cli(),
SLEEP.cli(),
TRUE.cli(),
])
} }
fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box<dyn Error>> { fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box<dyn Error>> {
let Some(matches) = matches else { let Some(matches) = matches else {
return Err(Box::new(io::Error::new(ErrorKind::Other, "No input"))); return Err(Box::new(io::Error::new(ErrorKind::Other, "No input")));
}; };
match matches.subcommand() { if let Some((name, matches)) = matches.subcommand() {
Some(("base32", matches)) => BASE_32.run(Some(matches))?, if let Some(command) = crate::cmd::get(name) {
Some(("base64", matches)) => BASE_64.run(Some(matches))?, if let Err(e) = command.run(Some(matches)) {
Some(("bootstrap", matches)) => BOOTSTRAP.run(Some(matches))?, eprintln!("Error: {name}: {e}");
Some(("dirname", matches)) => DIRNAME.run(Some(matches))?, process::exit(1);
Some(("echo", _matches)) => ECHO.run(None)?, }
Some(("factor", matches)) => FACTOR.run(Some(matches))?, }
Some(("false", _matches)) => FALSE.run(None)?,
Some(("head", matches)) => HEAD.run(Some(matches))?,
Some(("mountpoint", matches)) => MOUNTPOINT.run(Some(matches))?,
Some(("nologin", _matches)) => NOLOGIN.run(None)?,
Some(("hostname", matches)) => HOSTNAME.run(Some(matches))?,
Some(("sleep", matches)) => SLEEP.run(Some(matches))?,
Some(("true", _matches)) => TRUE.run(None)?,
_ => unimplemented!(),
} }
Ok(()) Ok(())
} }

View File

@ -9,10 +9,14 @@ pub struct Sleep {
path: Option<Path>, path: Option<Path>,
} }
pub const SLEEP: Sleep = Sleep { impl Default for Sleep {
name: "sleep", fn default() -> Self {
path: Some(Path::Bin), Self {
}; name: "sleep",
path: Some(crate::Path::Bin),
}
}
}
impl Cmd for Sleep { impl Cmd for Sleep {
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -9,10 +9,14 @@ pub struct True {
path: Option<Path>, path: Option<Path>,
} }
pub const TRUE: True = True { impl Default for True {
name: "true", fn default() -> Self {
path: Some(Path::Bin), Self {
}; name: "true",
path: Some(crate::Path::Bin),
}
}
}
impl Cmd for True { impl Cmd for True {
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -1,11 +1,8 @@
#![warn(clippy::all, clippy::pedantic)] #![warn(clippy::all, clippy::pedantic)]
use std::{env, error::Error, path::PathBuf, string::ToString}; use std::{env, error::Error, path::PathBuf, process, string::ToString};
pub mod cmd; pub mod cmd;
pub use cmd::{ pub use cmd::Cmd;
Cmd, Commands, BASE_32, BASE_64, BOOTSTRAP, DIRNAME, ECHO, FACTOR, FALSE, HEAD, HOSTNAME,
MOUNTPOINT, NOLOGIN, SHITBOX, SLEEP, TRUE,
};
pub mod math; pub mod math;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -37,50 +34,16 @@ pub fn progpath() -> Option<PathBuf> {
} }
pub fn run() -> Result<(), Box<dyn Error>> { pub fn run() -> Result<(), Box<dyn Error>> {
if cmd::COMMANDS.get().is_none() {
cmd::COMMANDS
.set(Commands {
items: vec![
&BASE_32,
&BASE_64,
&BOOTSTRAP,
&DIRNAME,
&ECHO,
&FALSE,
&FACTOR,
&HEAD,
&HOSTNAME,
&MOUNTPOINT,
&NOLOGIN,
&TRUE,
&SLEEP,
&SHITBOX,
],
})
.expect("Cannot register commands");
}
if let Some(progname) = progname() { if let Some(progname) = progname() {
match progname.as_str() { if let Some(command) = cmd::get(&progname) {
"base32" => BASE_32.run(Some(&BASE_32.cli().get_matches()))?, let cli = command.cli();
"base64" => BASE_64.run(Some(&BASE_64.cli().get_matches()))?, if let Err(e) = command.run(Some(&cli.get_matches())) {
"dirname" => DIRNAME.run(Some(&DIRNAME.cli().get_matches()))?, eprintln!("{progname}: Error: {e}");
"echo" => ECHO.run(None)?, process::exit(1);
"false" => FALSE.run(None)?,
"factor" => FACTOR.run(Some(&FACTOR.cli().get_matches()))?,
"head" => {
HEAD.run(Some(&HEAD.cli().get_matches()))?;
} }
"hostname" => HOSTNAME.run(Some(&HOSTNAME.cli().get_matches()))?, } else {
"mountpoint" => MOUNTPOINT.run(Some(&MOUNTPOINT.cli().get_matches()))?, eprintln!("shitbox: Error: unknown command {progname}");
"nologin" => NOLOGIN.run(None)?, process::exit(1);
"true" => TRUE.run(None)?,
"shitbox" => {
SHITBOX.run(Some(&SHITBOX.cli().get_matches()))?;
}
"sleep" => {
SLEEP.run(Some(&SLEEP.cli().get_matches()))?;
}
_ => unimplemented!(),
} }
} }
Ok(()) Ok(())