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

View File

@ -7,7 +7,6 @@ edition = "2021"
[dependencies]
atty = "0.2.14"
base64 = "0.20.0"
clap = "4.0.29"
clap_complete = "4.0.6"
clap_complete_nushell = "0.1.8"
@ -15,7 +14,6 @@ clap_mangen = "0.2.5"
data-encoding = "2.3.3"
hostname = { version = "0.3", features = ["set"] }
libc = "0.2.139"
once_cell = "1.16.0"
termcolor = "1.1.3"
[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>,
}
pub const BASE_32: Base32 = Base32 {
name: "base32",
path: Some(crate::Path::UsrBin),
};
impl Default for Base32 {
fn default() -> Self {
Self {
name: "base32",
path: Some(crate::Path::UsrBin),
}
}
}
impl Cmd for Base32 {
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 output = String::from_utf8(decoded)?;
println!("{}", output.trim_end());
println!("{}\n", output.trim_end());
Ok(())
}

View File

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

View File

@ -1,12 +1,14 @@
use super::{Cmd, Commands};
use super::{Cmd, COMMANDS};
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_complete::shells;
use clap_complete::{generate_to, shells};
use clap_complete_nushell::Nushell;
use clap_mangen::Man;
use std::{
error::Error,
fs,
io::{self, ErrorKind},
path::PathBuf,
os::unix::fs::symlink,
path::{Path, PathBuf},
};
#[derive(Debug)]
@ -15,10 +17,14 @@ pub struct Bootstrap {
path: Option<crate::Path>,
}
pub const BOOTSTRAP: Bootstrap = Bootstrap {
name: "bootstrap",
path: None,
};
impl Default for Bootstrap {
fn default() -> Self {
Self {
name: "bootstrap",
path: None,
}
}
}
impl Bootstrap {
fn all() -> clap::Command {
@ -167,9 +173,6 @@ impl Cmd for Bootstrap {
return Err(io::Error::new(ErrorKind::Other, "No input").into());
};
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");
if let Some(progpath) = crate::progpath() {
let mut outpath = PathBuf::from(prefix);
@ -189,18 +192,18 @@ impl Cmd for Bootstrap {
}
match matches.subcommand() {
Some(("links", matches)) => {
commands.links(prefix, usr, matches)?;
links(prefix, usr, matches)?;
}
Some(("manpages", _matches)) => {
commands.manpages(prefix)?;
manpages(prefix)?;
}
Some(("completions", matches)) => {
commands.completions(prefix, matches)?;
completions(prefix, matches)?;
}
Some(("all", matches)) => {
commands.links(prefix, usr, matches)?;
commands.manpages(prefix)?;
commands.completions(prefix, matches)?;
links(prefix, usr, matches)?;
manpages(prefix)?;
completions(prefix, matches)?;
}
_ => {}
}
@ -213,59 +216,178 @@ impl Cmd for Bootstrap {
}
}
pub trait Bootstrappable {
fn manpages(&self, prefix: &str) -> Result<(), io::Error>;
fn completions(&self, prefix: &str, matches: &ArgMatches) -> Result<(), io::Error>;
fn links(&self, prefix: &str, usr: bool, cmd: &ArgMatches) -> Result<(), Box<dyn Error>>;
pub trait BootstrapCmd {
fn completion(&self, outdir: &Path, gen: &str) -> Result<(), io::Error>;
fn linkpath(&self, prefix: &str, usr: bool) -> Option<PathBuf>;
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> {
fn manpages(&self, prefix: &str) -> Result<(), io::Error> {
println!("Generating Unix man pages:");
self.items.iter().try_for_each(|cmd| cmd.manpage(prefix))?;
impl BootstrapCmd for dyn Cmd {
fn completion(&self, outdir: &Path, gen: &str) -> Result<(), io::Error> {
let name = self.name();
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(())
}
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))?;
fn linkpath(&self, prefix: &str, usr: bool) -> Option<PathBuf> {
let mut path = PathBuf::from(prefix);
let binpath = self.path();
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,
}
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))?;
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 links(&self, prefix: &str, usr: bool, cmd: &ArgMatches) -> Result<(), Box<dyn Error>> {
println!("Generating links:");
let mut binpath = PathBuf::from(prefix);
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(())
}
}
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");
if !binpath.exists() {
fs::create_dir_all(&binpath)?;
@ -277,25 +399,11 @@ impl<'a> Bootstrappable for Commands<'a> {
fs::create_dir_all(&binpath)?;
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>,
}
pub const DIRNAME: Dirname = Dirname {
name: "dirname",
path: Some(crate::Path::UsrBin),
};
impl Default for Dirname {
fn default() -> Self {
Self {
name: "dirname",
path: Some(crate::Path::UsrBin),
}
}
}
impl Cmd for Dirname {
fn name(&self) -> &str {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,5 @@
use clap::ArgMatches;
use clap_complete::{generate_to, Generator};
use clap_mangen::Man;
use once_cell::sync::OnceCell;
use std::{
error::Error,
fmt, fs, io,
os::unix::fs::symlink,
path::{Path, PathBuf},
};
use std::{error::Error, fmt};
pub mod base32;
pub mod base64;
@ -38,123 +30,50 @@ mod sync;
pub mod r#true;
pub use {
self::base64::{Base64, BASE_64},
self::hostname::{Hostname, HOSTNAME},
base32::{Base32, BASE_32},
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},
self::hostname::Hostname, base32::Base32, base64::Base64, bootstrap::Bootstrap,
dirname::Dirname, echo::Echo, factor::Factor, head::Head, mountpoint::Mountpoint,
nologin::Nologin, r#false::False, r#true::True, shitbox::Shitbox, 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 {
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>;
}
fn linkpath(&self, prefix: &str, usr: bool) -> Option<PathBuf> {
let mut path = PathBuf::from(prefix);
let binpath = self.path();
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(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 fn get(name: &str) -> Option<Box<dyn Cmd>> {
match name {
"base64" => Some(Box::new(Base64::default())),
"base32" => Some(Box::new(Base32::default())),
"bootstrap" => Some(Box::new(Bootstrap::default())),
"dirname" => Some(Box::new(Dirname::default())),
"echo" => Some(Box::new(Echo::default())),
"factor" => Some(Box::new(Factor::default())),
"false" => Some(Box::new(False::default())),
"head" => Some(Box::new(Head::default())),
"mountpoint" => Some(Box::new(Mountpoint::default())),
"nologin" => Some(Box::new(Nologin::default())),
"shitbox" => Some(Box::new(Shitbox::default())),
"sleep" => Some(Box::new(Sleep::default())),
"true" => Some(Box::new(True::default())),
_ => None,
}
}
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>,
}
pub const MOUNTPOINT: Mountpoint = Mountpoint {
name: "mountpoint",
path: Some(crate::Path::Bin),
};
impl Default for Mountpoint {
fn default() -> Self {
Self {
name: "mountpoint",
path: Some(crate::Path::Bin),
}
}
}
impl Cmd for Mountpoint {
fn name(&self) -> &str {
@ -38,7 +42,7 @@ impl Cmd for Mountpoint {
failure: incorrect invocation, permissions or system error\
\n 32\n \
failure: the directory is not a mountpoint, or device is not a \
block device on --devno"
block device on --devno",
)
.args([
Arg::new("fs-devno")

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,8 @@
#![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 use cmd::{
Cmd, Commands, BASE_32, BASE_64, BOOTSTRAP, DIRNAME, ECHO, FACTOR, FALSE, HEAD, HOSTNAME,
MOUNTPOINT, NOLOGIN, SHITBOX, SLEEP, TRUE,
};
pub use cmd::Cmd;
pub mod math;
#[derive(Debug, Clone, Copy)]
@ -37,50 +34,16 @@ pub fn progpath() -> Option<PathBuf> {
}
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() {
match progname.as_str() {
"base32" => BASE_32.run(Some(&BASE_32.cli().get_matches()))?,
"base64" => BASE_64.run(Some(&BASE_64.cli().get_matches()))?,
"dirname" => DIRNAME.run(Some(&DIRNAME.cli().get_matches()))?,
"echo" => ECHO.run(None)?,
"false" => FALSE.run(None)?,
"factor" => FACTOR.run(Some(&FACTOR.cli().get_matches()))?,
"head" => {
HEAD.run(Some(&HEAD.cli().get_matches()))?;
if let Some(command) = cmd::get(&progname) {
let cli = command.cli();
if let Err(e) = command.run(Some(&cli.get_matches())) {
eprintln!("{progname}: Error: {e}");
process::exit(1);
}
"hostname" => HOSTNAME.run(Some(&HOSTNAME.cli().get_matches()))?,
"mountpoint" => MOUNTPOINT.run(Some(&MOUNTPOINT.cli().get_matches()))?,
"nologin" => NOLOGIN.run(None)?,
"true" => TRUE.run(None)?,
"shitbox" => {
SHITBOX.run(Some(&SHITBOX.cli().get_matches()))?;
}
"sleep" => {
SLEEP.run(Some(&SLEEP.cli().get_matches()))?;
}
_ => unimplemented!(),
} else {
eprintln!("shitbox: Error: unknown command {progname}");
process::exit(1);
}
}
Ok(())