use super::{Cmd, Commands}; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_complete::shells; use clap_complete_nushell::Nushell; use std::{ error::Error, fs, io::{self, ErrorKind}, path::PathBuf, }; #[derive(Debug)] pub struct Bootstrap { name: &'static str, path: Option, } pub const BOOTSTRAP: Bootstrap = Bootstrap { name: "bootstrap", path: None, }; impl Bootstrap { fn all() -> clap::Command { 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") .conflicts_with_all(["bash", "fish", "nu", "pwsh", "zsh"]) .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 links() -> clap::Command { 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), ) } fn manpages() -> clap::Command { Command::new("manpages") .about("Install Unix man pages") .alias("man") } fn completions() -> clap::Command { Command::new("completions") .about("Install shell completions") .alias("comp") .args([ Arg::new("all") .help("Install completions for all supported shells") .short('a') .long("all") .exclusive(true) .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), ]) } } 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([ Self::all(), Self::links(), Self::manpages(), Self::completions(), ]) } fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { let Some(matches) = matches else { return Err(io::Error::new(ErrorKind::Other, "No input").into()); }; if let Some(prefix) = matches.get_one::("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); 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 } } 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>; } 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))?; 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(()) } }