Skip to content

Commit

Permalink
feat: ✨ added self-documenting ability
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Sep 4, 2021
1 parent 8891815 commit 00417fd
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 30 deletions.
40 changes: 21 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fn core() -> Result<i32, String> {
// 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.");
Expand Down Expand Up @@ -69,6 +70,12 @@ fn core() -> Result<i32, String> {
// 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
Expand All @@ -90,6 +97,13 @@ fn core() -> Result<i32, String> {
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)
Expand Down
8 changes: 6 additions & 2 deletions src/bonnie.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ This just summarizes the functionality of this command, not the syntax of Bonnie
-i, --init [-t, --template <template-file>] 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.")
Expand Down
14 changes: 9 additions & 5 deletions src/raw_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -270,9 +273,10 @@ enum Command {
Complex {
args: Option<Vec<String>>,
env_vars: Option<Vec<String>>,
subcommands: Option<Scripts>, // Subcommands are fully-fledged commands (mostly)
order: Option<OrderString>, // 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<Scripts>, // Subcommands are fully-fledged commands (mostly)
order: Option<OrderString>, // 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<CommandWrapper>, // This is optional if subcommands are specified
desc: Option<String>, // 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
Expand Down
122 changes: 121 additions & 1 deletion src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) -> Result<String, String> {
// 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::<Vec<&str>>()[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::<Vec<&str>>()[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 {
Expand All @@ -118,6 +190,7 @@ pub struct Command {
pub subcommands: Option<Scripts>, // Subcommands are fully-fledged commands (mostly)
pub order: Option<BonesDirective>, // 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<CommandWrapper>, // If subcommands are provided, a root command is optional
pub description: Option<String>, // This will be rendered in the config's help page
}
impl Command {
// Prepares a command by interpolating everything and resolving shell/tagret logic
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion wiki
Submodule wiki updated from 92ee97 to d6b7d0

0 comments on commit 00417fd

Please sign in to comment.