diff --git a/README.md b/README.md index 1ed7aa4..9eb216d 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,20 @@ Bonnie is a command aliasing tool that allows you to quickly and efficiently define short aliases for long commands that you have to repeatedly run. Here's a quick feature overview: -- ✨ Supports simple key-value aliasing -- ✨ Supports inserting custom arguments into commands -- ✨ Supports interpolating environment variables -- ✨ Supports adding any and all arguments given into a single place -- ✨ Supports using different commands on different operating systems -- ✨ Supports specifying custom shells for individual commands -- ✨ Supports specifying default shells for different operating systems on a per-file basis -- ✨ Supports infinitely nestable subcommands -- ✨ Supports subcommands executed in a certain order based on their exit codes -- ✨ Supports caching large config files after they've been parsed for performance -- ✨ Supports initializing new config files from templates -- ✨ Supports global template -- ✨ Supports debug mode +- ✨ Supports simple key-value aliasing +- ✨ Supports inserting custom arguments into commands +- ✨ Supports interpolating environment variables +- ✨ Supports adding any and all arguments given into a single place +- ✨ Supports using different commands on different operating systems +- ✨ Supports specifying custom shells for individual commands +- ✨ Supports specifying default shells for different operating systems on a per-file basis +- ✨ Supports infinitely nestable subcommands +- ✨ Supports subcommands executed in a certain order based on their exit codes +- ✨ Supports caching large config files after they've been parsed for performance +- ✨ Supports initializing new config files from templates +- ✨ Supports global template +- ✨ Supports debug mode +- ✨ Supports self-documenting configuration files Basically, if you have commands that you routinely run in a project, Bonnie is for you. Bonnie has support for both extremely simple and extremely complex use cases, all while maintaining top-notch performance. @@ -79,12 +80,13 @@ Bonnie was originally intended to move to stable in May 2021, though a full rewr ## Roadmap -* [x] Support default global template in `~/.bonnie/template.toml` -* [x] Support debug mode -* [ ] Support self-documenting configurations -- [ ] Support optional arguments -- [ ] Support giving default values for optional arguments -- [ ] Support piping data into Bonnie scripts with a special opening flag (maybe `%[stdin]`?) +- [x] Support default global template in `~/.bonnie/template.toml` +- [x] Support debug mode +- [ ] Support self-documenting configurations + +* [ ] Support optional arguments +* [ ] Support giving default values for optional arguments +* [ ] Support piping data into Bonnie scripts with a special opening flag (maybe `%[stdin]`?) ## Changelog diff --git a/src/bin/main.rs b/src/bin/main.rs index fbf3173..a00fe78 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -41,6 +41,7 @@ fn core() -> Result { // Check for special arguments let mut should_cache = false; let mut verbose = false; + let mut document = false; if matches!(prog_args.get(0), Some(_)) { if prog_args[0] == "-v" || prog_args[0] == "--version" { writeln!(stdout, "You are currently running Bonnie v{}! You can see the latest release at https://github.com/arctic-hen7/bonnie/releases.", BONNIE_VERSION).expect("Failed to write version."); @@ -69,6 +70,12 @@ fn core() -> Result { // This can be specified with a command following verbose = true; prog_args.remove(0); + + } + // Check if the user wants the configuration's help page (self-documenting) + // TODO 'doc' instead/as well? + else if prog_args[0] == "help" { + document = true; } } // Check if there's a cache we should read from @@ -90,6 +97,13 @@ fn core() -> Result { return Ok(0); } + if document { + // Handle individual commands + let msg = cfg.document(prog_args.get(1).cloned())?; + writeln!(stdout, "{}", msg); + return Ok(0); + } + // Determine which command we're actually running let (command_to_run, command_name, relevant_args) = cfg.get_command_for_args(&prog_args)?; // Get the Bone (item in Bones execution runtime) diff --git a/src/bonnie.toml b/src/bonnie.toml index a56c11b..fa3dc72 100644 --- a/src/bonnie.toml +++ b/src/bonnie.toml @@ -10,8 +10,11 @@ default_shell.targets.windows = { parts = ["cmd", "/C", "{COMMAND}"], delimiter [scripts] # The most basic possible syntax basic = "echo Test" -# This command will have all given arguments inteprolated into it +# This command will have all given arguments interpolated into it echo = "echo \"Message is: '%%'.\"" +# This command is explicitly documented +documented.cmd = "echo \"I'm documented!\"" +documented.desc = "here's some documentation" # This command has required specific arguments and then interpolates everything after that at `%%` append_with_args.cmd = "echo \"Hi %name! Message is: '%%'.\"" append_with_args.args = ["name"] @@ -55,7 +58,8 @@ subcommands.subcommands.multistage_with_interpolation.cmd = [ ] subcommands.subcommands.multistage_with_interpolation.args = ["name"] subcommands.subcommands.multistage_with_interpolation.env_vars = ["GREETING"] -subcommands.subcommands.nested.subcommands.basic = "echo Nested" +subcommands.subcommands.nested.subcommands.basic.cmd = "echo Nested" +subcommands.subcommands.nested.subcommands.basic.desc = "here's a deeply nested description" subcommands.subcommands.nested.order = "basic" # As long as this is given, no problems arise # This command has a series of subcommands that will be executed in a certain order # With this type, subcommands can't take arguments, all arguments must be provided up-front for the execution diff --git a/src/help.rs b/src/help.rs index a3a24d7..7ff5e34 100644 --- a/src/help.rs +++ b/src/help.rs @@ -14,12 +14,13 @@ This just summarizes the functionality of this command, not the syntax of Bonnie -i, --init [-t, --template ] creates a new `bonnie.toml` configuration (or whatever's set in `BONNIE_CONF`), using the specified template file if provided -c, --cache caches the Bonnie configuration file to `.bonnie.cache.json` for performance (this cache must be MANUALLY updated by re-running this command!) +help [command-name] prints the help page for the current Bonnie configuration or for the given command + The expected location of a Bonnie configuration file can be changed from the default `./bonnie.toml` by setting the `BONNIE_CONF` environment variable. The expected location of a Bonnie cache file can be changed from the default `./.bonnie.cache.json` by setting the `BONNIE_CACHE` environment variable. The expected location of your default template can be changed from the default `~/.bonnie/template.toml` by setting the `BONNIE_TEMPLATE` environment variable. -Further information can be found at https://github.com/arctic-hen7/bonnie/wiki. - ", +Further information can be found at https://github.com/arctic-hen7/bonnie/wiki.", version = BONNIE_VERSION ) .expect("Failed to write help page.") diff --git a/src/raw_schema.rs b/src/raw_schema.rs index 729e1c3..215fe8a 100644 --- a/src/raw_schema.rs +++ b/src/raw_schema.rs @@ -145,14 +145,16 @@ impl Config { env_vars: Vec::new(), subcommands: None, order: None, - cmd: Some(raw_command_wrapper.parse()) // In the simple form, a command must be given (no subcommands can be specified) + cmd: Some(raw_command_wrapper.parse()), // In the simple form, a command must be given (no subcommands can be specified) + description: None }, Command::Complex { args, env_vars, subcommands, order, - cmd + cmd, + desc } => schema::Command { // If `order` is defined at the level above, we can't interpolate environment variables from here (has to be done at the level `order` was specified) args: match is_order_defined { @@ -200,7 +202,8 @@ impl Config { Some(cmd) => Some(cmd.parse()), // It's mandatory and not given None => return Err(format!("Error in parsing Bonnie configuration file: if `subcommands` is not specified, `cmd` is mandatory. This error occurred in in the '{}' script/subscript.", script_name)) - } + }, + description: desc.clone() }, }; scripts.insert(script_name.to_string(), command); @@ -270,9 +273,10 @@ enum Command { Complex { args: Option>, env_vars: Option>, - subcommands: Option, // Subcommands are fully-fledged commands (mostly) - order: Option, // If this is specified,subcomands must not specify the `args` property, it may be specified at the top-level of this script as a sibling of `order` + subcommands: Option, // Subcommands are fully-fledged commands (mostly) + order: Option, // If this is specified, subcomands must not specify the `args` property, it may be specified at the top-level of this script as a sibling of `order` cmd: Option, // This is optional if subcommands are specified + desc: Option, // This will be rendered in the config's help page ('description' is overly verbose) }, } type OrderString = String; // A string of as yet undefined syntax that defines the progression between subcommands diff --git a/src/schema.rs b/src/schema.rs index 57acda6..e622de9 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -94,6 +94,78 @@ impl Config { Ok(data) } + // Provides a documentation message for this configuration + // If a single command name is given, only it will be documented + pub fn document(&self, cmd_to_doc: Option) -> Result { + // Handle metadata about the whole file first with a preamble + let mut meta = format!( + "This is the help page for a configuration file. If you'd like help about Bonnie generally, run `bonnie -h` instead. +Version: {}", + self.version, + ); + // Environment variable files + let mut env_files = Vec::new(); + for env_file in &self.env_files { + env_files.push(format!(" {}", env_file)); + } + if !env_files.is_empty() { + meta += &format!( + "\nEnvironment variable files:\n{}", + env_files.join("\n") + ); + } + + let msg; + if let Some(cmd_name) = cmd_to_doc { + let cmd = self.scripts.get(&cmd_name); + let cmd = match cmd { + Some(cmd) => cmd, + None => return Err(format!("Command '{}' not found. You can see all supported commands by running `bonnie help`.", cmd_name)) + }; + msg = cmd.document(&cmd_name); + } else { + // Loop through every command and document it + let mut msgs = Vec::new(); + // Sort the subcommands alphabetically + let mut cmds: Vec<(&String, &Command)> = self.scripts.iter().collect(); + cmds.sort_by(|(name, _), (name2, _)| name.cmp(name2)); + for (cmd_name, cmd) in cmds { + msgs.push( + cmd.document(cmd_name) + ); + } + + msg = msgs.join("\n"); + } + // Space everything out evenly based on the longest command name (thing on the left) + // First, we get the longest command name (thing on the left of where tabs will end up) + // We loop through each line because otherwise subcommands stuff things up + let mut longest_left: usize = 0; + for line in msg.lines() { + // Get the length of the stuff to the left of the tabs placeholder + let left_len = line.split("{TABS}") + .collect::>()[0] + .len(); + if left_len > longest_left { + longest_left = left_len; + } + } + // Now we loop back through each line and add the appropriate amount of space + let mut spaced_msg_lines = Vec::new(); + for line in msg.lines() { + let left_len = line.split("{TABS}") + .collect::>()[0] + .len(); + // We want the longest line to have 4 spaces, then the rest should have (longest - length + 4) spaces + let spaces = " ".repeat(longest_left - left_len + 4); + spaced_msg_lines.push( + line.replace("{TABS}", &spaces) + ); + } + let spaced_msg = spaced_msg_lines.join("\n"); + + Ok(format!("{}\n\n{}", meta, spaced_msg)) + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct DefaultShell { @@ -118,6 +190,7 @@ pub struct Command { pub subcommands: Option, // Subcommands are fully-fledged commands (mostly) pub order: Option, // If this is specified, subcomands must not specify the `args` property, it may be specified at the top-level of this script as a sibling of `order` pub cmd: Option, // If subcommands are provided, a root command is optional + pub description: Option, // This will be rendered in the config's help page } impl Command { // Prepares a command by interpolating everything and resolving shell/tagret logic @@ -306,8 +379,55 @@ impl Command { interpolated } -} + // Gets a documentation message for this command based on its metadata and the `desc` properties + fn document(&self, name: &str) -> String { + let mut msgs = Vec::new(); + // Get the user-given docs (if they exist) + let doc = match &self.description { + Some(desc) => desc.to_string(), + None => String::from("no 'desc' property set") + }; + + // Set up the left side (command name and some arguments info) + let mut left = String::new(); + // Environment variables (before the command name) + for env_var in &self.env_vars { + left += &format!("<{}> ", env_var); + } + // Command name + left += name; + // Arguments (after the command name) + for arg in &self.args { + left += &format!(" <{}>", arg); + } + // Ordered or not + if self.order.is_some() { + left += " (ordered)"; + } + // TODO handle '%%' as `[...]` + // That's a placeholder for a number of tabs that spaces everything evenly + msgs.push(format!("{}{{TABS}}{}", left, doc)); + // Loop through every subcommand and document it + if let Some(subcommands_map) = &self.subcommands { + // Sort the subcommands alphabetically + let mut subcommands_iter: Vec<(&String, &Command)> = subcommands_map.iter().collect(); + subcommands_iter.sort_by(|(name, _), (name2, _)| name.cmp(name2)); + for (cmd_name, cmd) in subcommands_iter { + let subcmd_doc = cmd.document(cmd_name); + msgs.push( + // We add four spaces in front of every line (that way it works recursively for nested subcommands) + format!( + " {}", + subcmd_doc.replace("\n", "\n ") + ) + ); + } + } + + msgs.join("\n") + } +} // This defines how the command runs on different targets #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CommandWrapper { diff --git a/wiki b/wiki index 92ee970..d6b7d04 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 92ee97023f2c8502caa2807c03d5bc51c1a6a31c +Subproject commit d6b7d04d91410da8c2894968d45e552e1319c1f4