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

Add support for an ON::INIT script which initializes subshell environments #36

Merged
merged 8 commits into from
Oct 6, 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
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