use super::{Cmd, ECHO, FALSE, HEAD, HOSTNAME, SHITBOX, SLEEP, TRUE, NOLOGIN}; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_complete::{generate_to, shells, Generator}; use clap_complete_nushell::Nushell; use clap_mangen::Man; use std::{ error::Error, fs, io::{self, ErrorKind}, path::{Path, PathBuf}, }; pub struct Bootstrap { name: &'static str, path: Option, } 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") .action(ArgAction::SetTrue), Arg::new("soft") .help("Install soft links instead of hardlinks") .short('s') .long("soft") .action(ArgAction::SetTrue), ]) .subcommands([ Command::new("all").about("Install everything") .args([ Arg::new("soft") .help("Install soft links instead of hardlinks") .short('s') .long("soft") .action(ArgAction::SetTrue), 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), Arg::new("zsh") .help("Zshell completions") .short('z') .long("zsh") .action(ArgAction::SetTrue), ]), Command::new("links") .about("Install links for each applet") .arg( Arg::new("soft") .help("Install soft links instead of hardlinks") .short('s') .long("soft") .action(ArgAction::SetTrue), ), 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), Arg::new("zsh") .help("Zshell completions") .short('z') .long("zsh") .action(ArgAction::SetTrue), ]), ]) } fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { let matches = if let Some(m) = matches { m } else { return Err(io::Error::new(ErrorKind::Other, "No input").into()); }; if let Some(prefix) = matches.get_one::("prefix") { let commands: Commands = Commands { items: vec![ &BOOTSTRAP, &ECHO, &FALSE, &HEAD, &HOSTNAME, &NOLOGIN, &TRUE, &SLEEP, &SHITBOX, ], }; let usr = matches.get_flag("usr"); if let Some(progpath) = crate::progpath() { let mut outpath = PathBuf::from(prefix); outpath.push("bin"); println!("Installing binary:"); if !outpath.exists() { fs::create_dir_all(&outpath)?; println!(" mkdir: {}", outpath.display()); } outpath.push(env!("CARGO_PKG_NAME")); fs::copy(&progpath, &outpath)?; println!(" install: {} -> {}", progpath.display(), outpath.display()); } match matches.subcommand() { Some(("links", matches)) => { commands.links(prefix, usr, matches)?; } Some(("manpages", _matches)) => { commands.manpages(prefix)?; } Some(("completions", matches)) => { commands.completions(prefix, matches)?; } Some(("all", matches)) => { commands.links(prefix, usr, matches)?; commands.manpages(prefix)?; commands.completions(prefix, matches)?; } _ => {} } } Ok(()) } fn path(&self) -> Option { self.path } } struct Commands<'a> { items: Vec<&'a dyn Cmd>, } impl<'a> Commands<'a> { fn manpages(&self, prefix: &str) -> Result<(), io::Error> { println!("Generating Unix man pages:"); self.items .iter() .try_for_each(|cmd| Self::manpage(prefix, cmd))?; Ok(()) } fn manpage(prefix: &str, cmd: &&dyn Cmd) -> Result<(), io::Error> { let command = cmd.cli(); let fname = match cmd.name() { "bootstrap" => "shitbox-bootstrap.1".to_string(), s => format!("{s}.1"), }; let outdir: PathBuf = [prefix, "usr", "share", "man", "man1"].iter().collect(); if !outdir.exists() { fs::create_dir_all(&outdir)?; } let mut outfile = outdir; outfile.push(fname); let man = Man::new(command); let mut buffer: Vec = vec![]; man.render(&mut buffer)?; fs::write(&outfile, buffer)?; println!(" {}", outfile.display()); Ok(()) } fn completion(outdir: &Path, cmd: &&dyn Cmd, gen: impl Generator) -> Result<(), io::Error> { let name = cmd.name(); let mut cmd = cmd.cli(); if !outdir.exists() { fs::create_dir_all(outdir)?; } let path = generate_to(gen, &mut cmd, name, outdir)?; println!(" {}", path.display()); Ok(()) } fn completions(&self, prefix: &str, matches: &ArgMatches) -> Result<(), io::Error> { println!("Generating completions:"); if matches.get_flag("bash") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "bash-completion", "completion"] .iter() .collect(); self.items .iter() .try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Bash))?; } if matches.get_flag("fish") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "fish", "completions"].iter().collect(); self.items .iter() .try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Fish))?; } if matches.get_flag("nu") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "nu", "completions"].iter().collect(); self.items .iter() .try_for_each(|cmd| Self::completion(&outdir, cmd, Nushell))?; } if matches.get_flag("pwsh") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "pwsh", "completions"].iter().collect(); self.items .iter() .try_for_each(|cmd| Self::completion(&outdir, cmd, shells::PowerShell))?; } if matches.get_flag("zsh") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "zsh", "site-functions"].iter().collect(); self.items .iter() .try_for_each(|cmd| Self::completion(&outdir, cmd, shells::Zsh))?; } Ok(()) } fn links(&self, prefix: &str, usr: bool, cmd: &ArgMatches) -> Result<(), Box> { 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)?; 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(()) } }