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

Optional arguments #109

Merged
merged 9 commits into from
Jul 27, 2024
Merged
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
platform: [ubuntu-latest, windows-latest, macos-latest-large]
jpal91 marked this conversation as resolved.
Show resolved Hide resolved
steps:
- uses: actions/checkout@v4
- name: Fetch dependencies
Expand Down
19 changes: 19 additions & 0 deletions mask-parser/src/maskfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct Command {
pub script: Option<Script>,
pub subcommands: Vec<Command>,
pub required_args: Vec<RequiredArg>,
pub optional_args: Vec<OptionalArg>,
pub named_flags: Vec<NamedFlag>,
}

Expand All @@ -34,6 +35,7 @@ impl Command {
script: Some(Script::new()),
subcommands: vec![],
required_args: vec![],
optional_args: vec![],
named_flags: vec![],
}
}
Expand Down Expand Up @@ -98,6 +100,23 @@ impl RequiredArg {
}
}

#[derive(Debug, Serialize, Clone)]
pub struct OptionalArg {
pub name: String,
/// Used within mask. TODO: store in a different place within mask instead of here.
#[serde(skip)]
pub val: String,
}

impl OptionalArg {
pub fn new(name: String) -> Self {
Self {
name,
val: "".to_string(),
}
}
}

#[derive(Debug, Serialize, Clone)]
pub struct NamedFlag {
pub name: String,
Expand Down
81 changes: 63 additions & 18 deletions mask-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ pub fn parse(maskfile_contents: String) -> Maskfile {
}
End(tag) => match tag {
Tag::Header(_) => {
let (name, required_args) = parse_command_name_and_required_args(text.clone());
let (name, required_args, optional_args) =
parse_command_name_required_and_optional_args(text.clone());
current_command.name = name;
current_command.required_args = required_args;
current_command.optional_args = optional_args;
}
Tag::BlockQuote => {
current_command.description = text.clone();
Expand Down Expand Up @@ -224,23 +226,37 @@ fn treeify_commands(commands: Vec<Command>) -> Vec<Command> {
command_tree
}

fn parse_command_name_and_required_args(text: String) -> (String, Vec<RequiredArg>) {
// Find any required arguments. They look like this: (required_arg_name)
let name_and_args: Vec<&str> = text.split(|c| c == '(' || c == ')').collect();
let (name, args) = name_and_args.split_at(1);
let name = name.join(" ").trim().to_string();
let mut required_args: Vec<RequiredArg> = vec![];

if !args.is_empty() {
let args = args.join("");
let args: Vec<&str> = args.split(" ").collect();
required_args = args
.iter()
.map(|a| RequiredArg::new(a.to_string()))
.collect();
}

(name, required_args)
fn parse_command_name_required_and_optional_args(
text: String,
) -> (String, Vec<RequiredArg>, Vec<OptionalArg>) {
// Checks if any args are present and if not, return early
let split_idx = match text.find(|c| c == '(' || c == '[') {
Some(idx) => idx,
None => return (text.trim().to_string(), vec![], vec![]),
};

let (name, args) = text.split_at(split_idx);
let name = name.trim().to_string();

// Collects (required_args)
let required_args = args
.split(|c| c == '(' || c == ')')
.filter_map(|arg| match arg.trim() {
a if !a.is_empty() && !a.contains('[') => Some(RequiredArg::new(a.trim().to_string())),
_ => None,
})
.collect();

// Collects [optional_args]
let optional_args = args
.split(|c| c == '[' || c == ']')
.filter_map(|arg| match arg.trim() {
a if !a.is_empty() && !a.contains('(') => Some(OptionalArg::new(a.trim().to_string())),
_ => None,
})
.collect();

(name, required_args, optional_args)
}

#[cfg(test)]
Expand Down Expand Up @@ -279,6 +295,18 @@ echo hey
## no_script

This command has no source/script.

## multi (required) [optional]

> Example with optional args

~~~bash
if ! [ -z "$optional" ]; then
echo "This is optional - $optional"
fi

echo "This is required - $required"
~~~
"#;

#[cfg(test)]
Expand Down Expand Up @@ -320,6 +348,7 @@ mod parse {
"name": "port"
}
],
"optional_args": [],
"named_flags": [verbose_flag],
},
{
Expand All @@ -336,6 +365,7 @@ mod parse {
"name": "name"
}
],
"optional_args": [],
"named_flags": [verbose_flag],
},
{
Expand All @@ -353,12 +383,27 @@ mod parse {
"source": "echo hey\n",
},
"subcommands": [],
"optional_args": [],
"required_args": [],
"named_flags": [verbose_flag],
}
],
"required_args": [],
"optional_args": [],
"named_flags": [],
},
{
"level": 2,
"name": "multi",
"description": "Example with optional args",
"script": {
"executor": "bash",
"source": "if ! [ -z \"$optional\" ]; then\n echo \"This is optional - $optional\"\nfi\n\necho \"This is required - $required\"\n",
},
"subcommands": [],
"required_args": [{ "name": "required" }],
"optional_args": [{ "name": "optional" }],
"named_flags": [verbose_flag],
}
]
}),
Expand Down
5 changes: 5 additions & 0 deletions mask/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ fn add_flag_variables(mut child: process::Command, cmd: &Command) -> process::Co
child.env(arg.name.clone(), arg.val.clone());
}

// Add all optional args
for opt_arg in &cmd.optional_args {
child.env(opt_arg.name.clone(), opt_arg.val.clone());
}

// Add all named flags as environment variables if they have a value
for flag in &cmd.named_flags {
if flag.val != "" {
Expand Down
14 changes: 14 additions & 0 deletions mask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ fn build_subcommands<'a, 'b>(
subcmd = subcmd.arg(arg);
}

// Add all optional arguments
for o in &c.optional_args {
let arg = Arg::with_name(&o.name);
subcmd = subcmd.arg(arg);
}

// Add all named flags
for f in &c.named_flags {
let arg = Arg::with_name(&f.name)
Expand Down Expand Up @@ -174,6 +180,14 @@ fn get_command_options(mut cmd: Command, matches: &ArgMatches) -> Command {
arg.val = matches.value_of(arg.name.clone()).unwrap().to_string();
}

// Check optional args
for opt_arg in &mut cmd.optional_args {
opt_arg.val = matches
.value_of(opt_arg.name.clone())
.unwrap_or("")
.to_string();
}

// Check all named flags
for flag in &mut cmd.named_flags {
flag.val = if flag.takes_value {
Expand Down
65 changes: 65 additions & 0 deletions mask/tests/arguments_and_flags_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,68 @@ Write-Output "This shouldn't render"
.failure();
}
}

mod optional_args {
use predicates::boolean::PredicateBooleanExt;

use super::*;

#[test]
fn runs_with_optional_args() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## with_opt (required) [optional]

~~~bash
echo "$required" "$optional"
~~~

~~~powershell
param(
$req = $env:required,
$opt = $env:optional
)

Write-Output "$req $opt"
~~~
"#,
);

common::run_mask(&maskfile_path)
.cli("with_opt")
.arg("I am required")
.arg("I am optional")
.assert()
.stdout(contains("I am required I am optional"))
.success();
}

#[test]
fn does_not_fail_when_optional_arg_is_not_present() {
let (_temp, maskfile_path) = common::maskfile(
r#"
## with_opt (required) [optional]

~~~bash
echo "$required" "$optional"
~~~

~~~powershell
param(
$req = $env:required,
$opt = $env:optional
)

Write-Output "$req $opt"
~~~
"#,
);

common::run_mask(&maskfile_path)
.cli("with_opt")
.arg("I am required")
.assert()
.stdout(contains("I am optional").not())
.success();
}
}
1 change: 1 addition & 0 deletions mask/tests/introspect_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ echo something
},
"subcommands": [],
"required_args": [],
"optional_args": [],
"named_flags": [verbose_flag],
}
]
Expand Down
Loading