Skip to content

Commit

Permalink
Merge pull request #36 from jakedeichert/on-init-script
Browse files Browse the repository at this point in the history
Add support for an ON::INIT script which initializes subshell environments
  • Loading branch information
jacobdeichert authored Oct 6, 2019
2 parents f8f3843 + 3a4dc46 commit ac965d9
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 71 deletions.
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,7 @@ Valid lang codes: py, python

~~~python
import os

name = os.getenv("name", "WORLD")

print("Hello, " + name + "!")
~~~

Expand All @@ -232,7 +230,6 @@ Valid lang codes: rb, ruby

~~~ruby
name = ENV["name"] || "WORLD"

puts "Hello, #{name}!"
~~~

Expand All @@ -243,7 +240,6 @@ puts "Hello, #{name}!"

~~~php
$name = getenv("name") ?: "WORLD";

echo "Hello, " . $name . "!\n";
~~~
```
Expand Down Expand Up @@ -341,6 +337,30 @@ alias wask="mask --maskfile ~/maskfile.md"
wask <subcommand>
~~~

### Subshell environment initialization script

When specifying the special `ON::INIT` script, you can hook into the subshell initialization process to inject common helpers 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
56 changes: 42 additions & 14 deletions maskfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* flags: -w --watch
* desc: Rebuild on file change

~~~sh
~~~bash
if [[ $watch == "true" ]]; then
watchexec --exts rs --restart "cargo run -- $maskfile_command"
else
Expand All @@ -33,7 +33,7 @@ fi

> Build a release version of mask
~~~sh
~~~bash
cargo build --release
~~~

Expand All @@ -54,27 +54,28 @@ cargo install --force --path .
> Run all tests
**OPTIONS**
* pattern
* flags: -p --pattern
* file
* flags: -f --file
* type: string
* desc: Test only a specific file pattern
* desc: Only run tests from a specific filename

~~~sh
~~~bash
extra_args=""

if [[ "$verbose" == "true" ]]; then
# Run tests linearly and make logs visible in output
extra_args="-- --nocapture --test-threads=1"
fi

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


Expand All @@ -87,7 +88,7 @@ fi

> Update the cargo dependencies
~~~sh
~~~bash
cargo update
~~~

Expand All @@ -102,7 +103,7 @@ cargo update
* flags: -c --check
* desc: Show which files are not formatted correctly

~~~sh
~~~bash
if [[ $check == "true" ]]; then
cargo fmt --all -- --check
else
Expand All @@ -116,6 +117,33 @@ fi

> Lint the project with clippy
~~~sh
~~~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
~~~
31 changes: 24 additions & 7 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ pub struct Command {
pub cmd_level: u8,
pub name: String,
pub desc: String,
// The executor to run the source with
pub executor: String, // shell, node, ruby, python, etc...
// The script source to execute
pub source: String,
pub script: Script,
pub subcommands: Vec<Command>,
pub required_args: Vec<RequiredArg>,
pub option_flags: Vec<OptionFlag>,
Expand All @@ -18,8 +15,7 @@ impl Command {
cmd_level,
name: "".to_string(),
desc: "".to_string(),
executor: "".to_string(),
source: "".to_string(),
script: Script::new(),
subcommands: vec![],
required_args: vec![],
option_flags: vec![],
Expand All @@ -28,7 +24,7 @@ impl Command {

pub fn build(mut self) -> Self {
// Auto add common flags like verbose for commands that have a script source
if !self.source.is_empty() {
if !self.script.source.is_empty() {
self.option_flags.push(OptionFlag {
name: "verbose".to_string(),
desc: "Sets the level of verbosity".to_string(),
Expand All @@ -44,6 +40,27 @@ impl Command {
}
}

#[derive(Debug, Clone)]
pub struct Script {
// The executor to run the source with
pub executor: String, // shell, node, ruby, python, etc...
// The script source to execute
pub source: String,
}

impl Script {
pub fn new() -> Self {
Self {
executor: "".to_string(),
source: "".to_string(),
}
}

pub fn has_script(&self) -> bool {
self.source != ""
}
}

#[derive(Debug, Clone)]
pub struct RequiredArg {
pub name: String,
Expand Down
116 changes: 93 additions & 23 deletions src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,62 +1,116 @@
use std::fs::canonicalize;
use std::io::Result;
use std::io::{Error, ErrorKind};
use std::path::{Path, PathBuf};
use std::process;
use std::process::ExitStatus;

use clap::crate_name;

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

pub fn execute_command(cmd: Command, maskfile_path: String) -> Result<ExitStatus> {
let mut child = match cmd.executor.as_ref() {
pub fn execute_command(
init_script: Script,
cmd: Command,
maskfile_path: String,
) -> Result<ExitStatus> {
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);
}

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 {
let executor = cmd.script.executor.clone();
let source = cmd.script.source.clone();

match executor.as_ref() {
"js" | "javascript" => {
let mut child = process::Command::new("node");
child.arg("-e").arg(cmd.source);
let mut child;
child = process::Command::new("node");
child.arg("-e").arg(source);
child
}
"py" | "python" => {
let mut child = process::Command::new("python");
child.arg("-c").arg(cmd.source);
child.arg("-c").arg(source);
child
}
"rb" | "ruby" => {
let mut child = process::Command::new("ruby");
child.arg("-e").arg(cmd.source);
child.arg("-e").arg(source);
child
}
"php" => {
let mut child = process::Command::new("php");
child.arg("-r").arg(cmd.source);
child.arg("-r").arg(source);
child
}
"bash" | "zsh" | "fish" => {
let mut child = process::Command::new(cmd.executor);
child.arg("-c").arg(cmd.source);
let mut child = process::Command::new(executor);
child.arg("-c").arg(source);
child
}
_ => {
let mut child = process::Command::new("sh");
child.arg("-c").arg(cmd.source);
child.arg("-c").arg(source);
child
}
};

child = add_utility_variables(child, maskfile_path);

// Add all required args as environment variables
for arg in cmd.required_args {
child.env(arg.name, arg.val);
}
}

// Add all optional flags as environment variables if they have a value
for flag in cmd.option_flags {
if flag.val != "" {
child.env(flag.name, flag.val);
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"),
"bash" | "zsh" | "fish" => {
run_with_init_script(&init_script, &cmd, &format!("{} -c", executor))
}
_ => run_with_init_script(&init_script, &cmd, "sh -c"),
}
}

child.spawn()?.wait()
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
Expand Down Expand Up @@ -88,3 +142,19 @@ fn add_utility_variables(mut child: process::Command, maskfile_path: String) ->

child
}

fn add_flag_variables(mut child: process::Command, cmd: &Command) -> process::Command {
// Add all required args as environment variables
for arg in &cmd.required_args {
child.env(arg.name.clone(), arg.val.clone());
}

// Add all optional flags as environment variables if they have a value
for flag in &cmd.option_flags {
if flag.val != "" {
child.env(flag.name.clone(), flag.val.clone());
}
}

child
}
Loading

0 comments on commit ac965d9

Please sign in to comment.