Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove the ON::INIT script idea #38

Merged
merged 1 commit into from
Oct 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,30 +336,6 @@ alias wask="mask --maskfile ~/maskfile.md"
wask <subcommand>
~~~

### Subshell environment initialization

Adding the special `ON::INIT` script allows you to hook into the subshell initialization process to inject common environment variables and utilities your commands share. This script only allows shell-based executors (`sh`, `bash`, `zsh`, etc...) and it **must not** be defined as a heading.

**Example:**

```markdown
**ON::INIT**

~~~bash
set -a # Export everything so subprocesses have access
log_info() { echo "🔵 >> $1"; }
log_error() { echo "❌ >> $1"; }
log_success() { echo "✅ >> $1"; }
~~~

## test

~~~bash
log_info "Running tests..."
cargo test || log_error "TESTS FAILED"
~~~
```

### Environment variable utilities

Inside of each script's execution environment, `mask` injects a few environment variable helpers that might come in handy.
Expand Down
31 changes: 2 additions & 29 deletions maskfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ if [[ "$verbose" == "true" ]]; then
extra_args="-- --nocapture --test-threads=1"
fi

log_info "Running tests..."
echo "Running tests..."
if [[ -z "$file" ]]; then
# Run all tests by default
cargo test $extra_args
else
# Tests a specific integration filename
cargo test --test $file $extra_args
fi
log_success "Tests passed!"
echo "Tests passed!"
~~~


Expand Down Expand Up @@ -120,30 +120,3 @@ fi
~~~bash
cargo clippy
~~~







**ON::INIT**

This special script sets up the subshell environment before a command is executed. This is useful for global utilities and helpers.

~~~bash
set -a # Export everything so subprocesses have access
color_reset=$(tput sgr0)
color_blue=$(tput setaf 4)
color_green=$(tput setaf 2)
color_yellow=$(tput setaf 3)
color_red=$(tput setaf 1)
log_info() { echo "$color_blue$1$color_reset"; }
log_success() { echo "$color_green$1$color_reset"; }
log_error() { echo "$color_red$1$color_reset"; }
log_warn() { echo "$color_yellow$1$color_reset"; }
set +a
set -e # Exit on error
# Export this so bash subshells inherit "set -e"
export SHELLOPTS
~~~
63 changes: 4 additions & 59 deletions src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@ use std::process::ExitStatus;

use clap::crate_name;

use crate::command::{Command, Script};
use crate::command::Command;

pub fn execute_command(
init_script: Script,
cmd: Command,
maskfile_path: String,
) -> Result<ExitStatus> {
pub fn execute_command(cmd: Command, maskfile_path: String) -> Result<ExitStatus> {
if cmd.script.source == "" {
let msg = "Command has no script.";
return Err(Error::new(ErrorKind::Other, msg));
Expand All @@ -24,24 +20,14 @@ pub fn execute_command(
return Err(Error::new(ErrorKind::Other, msg));
}

let mut child;
if init_script.has_script() {
if !validate_init_script(&init_script) {
let msg = "ON::INIT must be a shell-based script executor.";
return Err(Error::new(ErrorKind::Other, msg));
}
child = prepare_command_with_init_script(init_script, &cmd);
} else {
child = prepare_command_without_init_script(&cmd);
}

let mut child = prepare_command(&cmd);
child = add_utility_variables(child, maskfile_path);
child = add_flag_variables(child, &cmd);

child.spawn()?.wait()
}

fn prepare_command_without_init_script(cmd: &Command) -> process::Command {
fn prepare_command(cmd: &Command) -> process::Command {
let executor = cmd.script.executor.clone();
let source = cmd.script.source.clone();

Expand Down Expand Up @@ -76,47 +62,6 @@ fn prepare_command_without_init_script(cmd: &Command) -> process::Command {
}
}

fn prepare_command_with_init_script(init_script: Script, cmd: &Command) -> process::Command {
let executor = cmd.script.executor.clone();

match executor.as_ref() {
"js" | "javascript" => run_with_init_script(&init_script, &cmd, "node -e"),
"py" | "python" => run_with_init_script(&init_script, &cmd, "python -c"),
"rb" | "ruby" => run_with_init_script(&init_script, &cmd, "ruby -e"),
"php" => run_with_init_script(&init_script, &cmd, "php -r"),
// Any other executor that supports -c (sh, bash, zsh, fish, dash, etc...)
_ => run_with_init_script(&init_script, &cmd, &format!("{} -c", executor)),
}
}

fn run_with_init_script(
init_script: &Script,
cmd: &Command,
executor_invocation: &str,
) -> process::Command {
let mut child = process::Command::new(init_script.executor.clone());
// Combine the init script with the command to run
let source = format!(
"{}\n{} \"{}\"",
init_script.source.clone(),
executor_invocation,
"$MASK_CMD_SOURCE"
);
child
.env("MASK_CMD_SOURCE", cmd.script.source.clone())
.arg("-c")
.arg(source);
child
}

// Validate the subshell init script is shell-based
fn validate_init_script(init_script: &Script) -> bool {
match init_script.executor.as_ref() {
"js" | "javascript" | "py" | "python" | "rb" | "ruby" | "php" => false,
_ => true,
}
}

// Add some useful environment variables that scripts can use
fn add_utility_variables(mut child: process::Command, maskfile_path: String) -> process::Command {
let maskfile_path = PathBuf::from(maskfile_path);
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ fn main() {
return;
}

let (root_command, init_script) = mask::parser::build_command_structure(maskfile.unwrap());
let root_command = mask::parser::build_command_structure(maskfile.unwrap());
let matches = build_subcommands(cli_app, &root_command.subcommands).get_matches();
let chosen_cmd = find_command(&matches, &root_command.subcommands)
.expect("SubcommandRequired failed to work");

match execute_command(init_script, chosen_cmd, maskfile_path) {
match execute_command(chosen_cmd, maskfile_path) {
Ok(status) => match status.code() {
Some(code) => std::process::exit(code),
None => return,
Expand Down
65 changes: 14 additions & 51 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@ use pulldown_cmark::{
Options, Parser, Tag,
};

use crate::command::{Command, OptionFlag, RequiredArg, Script};
use crate::command::{Command, OptionFlag, RequiredArg};

// Woof. This is ugly. I'm planning on giving this a rewrite at some point...
// At least we have some decent tests in place.
pub fn build_command_structure(maskfile_contents: String) -> (Command, Script) {
pub fn build_command_structure(maskfile_contents: String) -> Command {
let parser = create_markdown_parser(&maskfile_contents);
let mut commands = vec![];
let mut current_command = Command::new(1);
let mut current_option_flag = OptionFlag::new();
let mut text = "".to_string();
let mut list_level = 0;
let mut init_script = Script::new();
let mut init_script_state = ParserPatternState::new();

for event in parser {
match event {
Expand All @@ -30,11 +28,7 @@ pub fn build_command_structure(maskfile_contents: String) -> (Command, Script) {
current_command = Command::new(heading_level as u8);
}
Tag::CodeBlock(lang_code) => {
if init_script_state.is_open() {
init_script.executor = lang_code.to_string();
} else {
current_command.script.executor = lang_code.to_string();
}
current_command.script.executor = lang_code.to_string();
}
Tag::List(_) => {
// We're in an options list if the current text above it is "OPTIONS"
Expand All @@ -59,12 +53,7 @@ pub fn build_command_structure(maskfile_contents: String) -> (Command, Script) {
current_command.desc = text.clone();
}
Tag::CodeBlock(_) => {
if init_script_state.is_open() {
init_script.source = text.to_string();
init_script_state.closed = true;
} else {
current_command.script.source = text.to_string();
}
current_command.script.source = text.to_string();
}
Tag::List(_) => {
// Don't go lower than zero (for cases where it's a non-OPTIONS list)
Expand All @@ -84,12 +73,8 @@ pub fn build_command_structure(maskfile_contents: String) -> (Command, Script) {
Text(body) => {
text += &body.to_string();

// Look for a shell initialization script
if list_level == 0 && text == "ON::INIT" && !init_script_state.found {
init_script_state.found = true;
}
// Options level 1 is the flag name
else if list_level == 1 {
if list_level == 1 {
current_option_flag.name = text.clone();
}
// Options level 2 is the flag config
Expand Down Expand Up @@ -147,29 +132,7 @@ pub fn build_command_structure(maskfile_contents: String) -> (Command, Script) {
let root_command = all.first().expect("root command must exist");

// The command root and a possible init script
(root_command.clone(), init_script)
}

#[derive(Debug, Clone)]
pub struct ParserPatternState {
// Whether this pattern has been found
pub found: bool,
// Whether this pattern has been closed
pub closed: bool,
}

impl ParserPatternState {
pub fn new() -> Self {
Self {
found: false,
closed: false,
}
}

// Open means we're currently parsing this pattern and haven't closed it yet
pub fn is_open(&self) -> bool {
self.found && !self.closed
}
root_command.clone()
}

fn create_markdown_parser<'a>(maskfile_contents: &'a String) -> Parser<'a> {
Expand Down Expand Up @@ -299,7 +262,7 @@ mod build_command_structure {

#[test]
fn parses_serve_command_name() {
let (tree, _) = build_command_structure(TEST_MASKFILE.to_string());
let tree = build_command_structure(TEST_MASKFILE.to_string());
let serve_command = &tree
.subcommands
.iter()
Expand All @@ -310,7 +273,7 @@ mod build_command_structure {

#[test]
fn parses_serve_command_description() {
let (tree, _) = build_command_structure(TEST_MASKFILE.to_string());
let tree = build_command_structure(TEST_MASKFILE.to_string());
let serve_command = &tree
.subcommands
.iter()
Expand All @@ -321,7 +284,7 @@ mod build_command_structure {

#[test]
fn parses_serve_required_positional_arguments() {
let (tree, _) = build_command_structure(TEST_MASKFILE.to_string());
let tree = build_command_structure(TEST_MASKFILE.to_string());
let serve_command = &tree
.subcommands
.iter()
Expand All @@ -333,7 +296,7 @@ mod build_command_structure {

#[test]
fn parses_serve_command_executor() {
let (tree, _) = build_command_structure(TEST_MASKFILE.to_string());
let tree = build_command_structure(TEST_MASKFILE.to_string());
let serve_command = &tree
.subcommands
.iter()
Expand All @@ -344,7 +307,7 @@ mod build_command_structure {

#[test]
fn parses_serve_command_source_with_tildes() {
let (tree, _) = build_command_structure(TEST_MASKFILE.to_string());
let tree = build_command_structure(TEST_MASKFILE.to_string());
let serve_command = &tree
.subcommands
.iter()
Expand All @@ -358,7 +321,7 @@ mod build_command_structure {

#[test]
fn parses_node_command_source_with_backticks() {
let (tree, _) = build_command_structure(TEST_MASKFILE.to_string());
let tree = build_command_structure(TEST_MASKFILE.to_string());
let node_command = &tree
.subcommands
.iter()
Expand All @@ -372,7 +335,7 @@ mod build_command_structure {

#[test]
fn adds_verbose_optional_flag_to_command_with_script() {
let (tree, _) = build_command_structure(TEST_MASKFILE.to_string());
let tree = build_command_structure(TEST_MASKFILE.to_string());
let node_command = tree
.subcommands
.iter()
Expand All @@ -393,7 +356,7 @@ mod build_command_structure {

#[test]
fn does_not_add_verbose_optional_flag_to_command_with_no_script() {
let (tree, _) = build_command_structure(TEST_MASKFILE.to_string());
let tree = build_command_structure(TEST_MASKFILE.to_string());
let no_script_command = tree
.subcommands
.iter()
Expand Down
Loading