Skip to content

Commit

Permalink
feat: support @meta require-tools (#316)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigoden authored Apr 12, 2024
1 parent 69202df commit 647c487
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 24 deletions.
26 changes: 14 additions & 12 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,26 +169,28 @@ Add a metadata.
> **<sup>Syntax</sup>**\
> `@meta` [_name_] [_value_]<sup>?</sup>
| syntax | scope | description |
| :--------------------------- | ------ | :------------------------------------------------------------------- |
| `@meta version <value>` | any | Set the version for the command. |
| `@meta author <value>` | any | Set the author for the command. |
| `@meta dotenv [<path>]` | root | Load a dotenv file from a custom path, if persent. |
| `@meta symbol <param>` | any | Define a symbolic parameter, e.g. `+toolchain`, `@argument-file`. |
| `@meta man-section <1-8>` | root | Override the section for the man page, defaulting to 1. |
| `@meta default-subcommand` | subcmd | Set the current subcommand as the default. |
| `@meta inherit-flag-options` | root | Subcommands will inherit the flags/options from their parent. |
| `@meta no-inherit-env` | subcmd | Subcommands will not inherit the @env from their parent. |
| `@meta combine-shorts` | root | Short flags/options can be combined, e.g. `prog -xf => prog -x -f `. |
| syntax | scope | description |
| :------------------------------- | ------ | :------------------------------------------------------------------- |
| `@meta version <value>` | any | Set the version for the command. |
| `@meta author <value>` | any | Set the author for the command. |
| `@meta dotenv [<path>]` | root | Load a dotenv file from a custom path, if persent. |
| `@meta default-subcommand` | subcmd | Set the current subcommand as the default. |
| `@meta require-tools <tool>,...` | any | Require certain tools to be available on the system. |
| `@meta man-section <1-8>` | root | Override the section for the man page, defaulting to 1. |
| `@meta inherit-flag-options` | root | Subcommands will inherit the flags/options from their parent. |
| `@meta no-inherit-env` | subcmd | Subcommands will not inherit the @env from their parent. |
| `@meta combine-shorts` | root | Short flags/options can be combined, e.g. `prog -xf => prog -x -f `. |
| `@meta symbol <param>` | any | Define a symbolic parameter, e.g. `+toolchain`, `@argument-file`. |


```sh
# @meta version 1.0.0
# @meta author nobody <[email protected]>
# @meta dotenv
# @meta dotenv .env.local
# @meta symbol +toolchain[`_choice_fn`]
# @meta require-tools git,yq
# @meta man-section 8
# @meta symbol +toolchain[`_choice_fn`]
```

## Deprecated tags
Expand Down
17 changes: 17 additions & 0 deletions examples/require-tools.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# @describe Demonstrate how to use require-tools meta

# @meta require-tools curl,sed

# @cmd
# @meta require-tools git
cmd1() {
:;
}

# @cmd
# @meta require-tools not-found
cmd2() {
:;
}

eval "$(argc --argc-eval "$0" "$@")"
18 changes: 17 additions & 1 deletion src/argc_value.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use indexmap::IndexMap;

use crate::utils::{
argc_var_name, escape_shell_words, expand_dotenv, AFTER_HOOK, BEFORE_HOOK, VARIABLE_PREFIX,
argc_var_name, escape_shell_words, expand_dotenv, AFTER_HOOK, ARGC_REQUIRE_TOOLS, BEFORE_HOOK,
VARIABLE_PREFIX,
};

#[derive(Debug, PartialEq, Eq)]
Expand All @@ -18,6 +19,7 @@ pub enum ArgcValue {
EnvFn(String, String),
Hook((bool, bool)),
Dotenv(String),
RequireTools(Vec<String>),
CommandFn(String),
ParamFn(String),
Error((String, i32)),
Expand All @@ -29,6 +31,7 @@ impl ArgcValue {
let mut last = String::new();
let mut exit = false;
let mut positional_args = vec![];
let mut require_tools = vec![];
let (mut before_hook, mut after_hook) = (false, false);
for value in values {
match value {
Expand Down Expand Up @@ -106,6 +109,9 @@ impl ArgcValue {
ArgcValue::Dotenv(value) => {
list.push(expand_dotenv(value));
}
ArgcValue::RequireTools(tools) => {
require_tools = tools.to_vec();
}
ArgcValue::CommandFn(name) => {
if positional_args.is_empty() {
last = name.to_string();
Expand Down Expand Up @@ -133,6 +139,16 @@ impl ArgcValue {
VARIABLE_PREFIX,
positional_args.join(" ")
));
if !require_tools.is_empty() {
let tools = require_tools
.iter()
.map(|v| escape_shell_words(v))
.collect::<Vec<_>>()
.join(" ");
list.push(format!(
"\n{ARGC_REQUIRE_TOOLS}\n_argc_require_tools {tools}\n"
));
}
if before_hook {
list.push(BEFORE_HOOK.to_string())
}
Expand Down
31 changes: 28 additions & 3 deletions src/build.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
command::Command,
param::{FlagOptionParam, Param, PositionalParam},
utils::{escape_shell_words, expand_dotenv},
utils::{escape_shell_words, expand_dotenv, ARGC_REQUIRE_TOOLS},
ChoiceValue, DefaultValue,
};
use anyhow::Result;
Expand Down Expand Up @@ -230,6 +230,13 @@ fn build_root(cmd: &Command) -> String {
util_fns.push_str(util_fn);
}
}
let require_tools = if command.contains("_argc_tools") {
util_fns.push_str(&format!("\n{ARGC_REQUIRE_TOOLS}\n"));
r#"
_argc_require_tools "${_argc_tools[@]}""#
} else {
""
};

format!(
r#"# ARGC-BUILD {{
Expand All @@ -244,7 +251,8 @@ _argc_run() {{
argc__positionals=()
_argc_index=1
_argc_len="${{#argc__args[@]}}"{dotenv}
_argc_parse{before_hook}
_argc_tools=()
_argc_parse{require_tools}{before_hook}
if [ -n "$argc__fn" ]; then
$argc__fn "${{argc__positionals[@]}}"{after_hook}
fi
Expand Down Expand Up @@ -466,6 +474,7 @@ fn build_parse(cmd: &Command, suffix: &str) -> String {
let flag_option_bind_envs = build_flag_option_bind_envs(cmd);
let required_flag_options = build_required_flag_options(cmd);

let require_tools = build_require_tools(cmd);
let handle = build_handle(cmd, suffix);

if cmd.delegated() {
Expand Down Expand Up @@ -496,7 +505,7 @@ _argc_parse{suffix}() {{
_argc_key="${{_argc_item%%=*}}"
case "$_argc_key" in{combined_case}
esac
done{flag_option_bind_envs}{required_flag_options}
done{flag_option_bind_envs}{required_flag_options}{require_tools}
if [[ -n "$_argc_action" ]]; then
$_argc_action
else{handle}
Expand Down Expand Up @@ -639,6 +648,22 @@ fn build_handle(cmd: &Command, suffix: &str) -> String {
}
}

fn build_require_tools(cmd: &Command) -> String {
if cmd.require_tools.is_empty() {
return String::new();
}
let tools = cmd
.require_tools
.iter()
.map(|v| escape_shell_words(v))
.collect::<Vec<_>>()
.join(" ");
format!(
r#"
_argc_tools=({tools})"#
)
}

fn build_positionals(cmd: &Command) -> String {
if cmd.positional_params.is_empty() {
return String::new();
Expand Down
33 changes: 27 additions & 6 deletions src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::parser::{parse, parse_symbol, Event, EventData, EventScope, Position}
use crate::utils::{
AFTER_HOOK, BEFORE_HOOK, INTERNAL_SYMBOL, MAIN_NAME, META_AUTHOR, META_COMBINE_SHORTS,
META_DEFAULT_SUBCOMMAND, META_DOTENV, META_INHERIT_FLAG_OPTIONS, META_NO_INHERIT_ENV,
META_SYMBOL, META_VERSION, ROOT_NAME,
META_REQUIRE_TOOLS, META_SYMBOL, META_VERSION, ROOT_NAME,
};
use crate::Result;

Expand Down Expand Up @@ -44,16 +44,18 @@ pub(crate) struct Command {
pub(crate) version: Option<String>,
pub(crate) names_checker: NamesChecker,
pub(crate) share: Arc<RefCell<ShareData>>,
// (key, value, position)
pub(crate) metadata: Vec<(String, String, Position)>,
pub(crate) symbols: IndexMap<char, SymbolParam>,
pub(crate) require_tools: IndexSet<String>,
}

impl Command {
pub(crate) fn new(source: &str, root_name: &str) -> Result<Self> {
let events = parse(source)?;
let mut root = Command::new_from_events(&events)?;
root.share.borrow_mut().name = Some(root_name.to_string());
root.update_recursively(vec![]);
root.update_recursively(vec![], IndexSet::new());
if root.has_metadata(META_INHERIT_FLAG_OPTIONS) {
root.inherit_flag_options();
}
Expand Down Expand Up @@ -96,6 +98,10 @@ impl Command {

pub(crate) fn export(&self) -> CommandValue {
let mut extra: IndexMap<String, serde_json::Value> = IndexMap::new();
let require_tools = self.meta_require_tools();
if !require_tools.is_empty() {
extra.insert("require_tools".into(), require_tools.into());
}
if self.is_root() {
if self.get_metadata(META_COMBINE_SHORTS).is_some() {
extra.insert("combine_shorts".into(), true.into());
Expand All @@ -113,6 +119,7 @@ impl Command {
} else if let Some((idx, _)) = &self.default_subcommand {
extra.insert("default_subcommand".into(), (*idx).into());
}
extra.insert("command_fn".into(), self.command_fn.clone().into());
let flag_options = self.all_flag_options().iter().map(|v| v.export()).collect();
CommandValue {
name: self.cmd_name(),
Expand All @@ -124,7 +131,6 @@ impl Command {
positionals: self.positional_params.iter().map(|v| v.export()).collect(),
envs: self.env_params.iter().map(|v| v.export()).collect(),
subcommands: self.subcommands.iter().map(|v| v.export()).collect(),
command_fn: self.command_fn.clone(),
extra,
}
}
Expand Down Expand Up @@ -331,6 +337,18 @@ impl Command {
.map(|(_, v, _)| v.as_str())
}

pub(crate) fn meta_require_tools(&self) -> Vec<String> {
let raw_require_tools = self.get_metadata(META_REQUIRE_TOOLS).unwrap_or_default();
if raw_require_tools.is_empty() {
vec![]
} else {
raw_require_tools
.split(',')
.map(|v| v.to_string())
.collect()
}
}

pub(crate) fn flag_option_signs(&self) -> String {
let mut signs: IndexSet<char> = ['-'].into();
for param in &self.flag_option_params {
Expand Down Expand Up @@ -632,7 +650,7 @@ impl Command {
Some(dotenv)
}

fn update_recursively(&mut self, paths: Vec<String>) {
fn update_recursively(&mut self, paths: Vec<String>, mut require_tools: IndexSet<String>) {
self.paths = paths.clone();

// update command_fn
Expand Down Expand Up @@ -672,11 +690,15 @@ impl Command {
.flatten(),
);

// update require_tools
require_tools.extend(self.meta_require_tools());
self.require_tools = require_tools;

// update recursively
for subcmd in self.subcommands.iter_mut() {
let mut parents = paths.clone();
parents.push(subcmd.name.clone().unwrap_or_default());
subcmd.update_recursively(parents);
subcmd.update_recursively(parents, self.require_tools.clone());
}
}

Expand Down Expand Up @@ -804,7 +826,6 @@ pub struct CommandValue {
pub positionals: Vec<PositionalValue>,
pub envs: Vec<EnvValue>,
pub subcommands: Vec<CommandValue>,
pub command_fn: Option<String>,
#[serde(flatten)]
pub extra: IndexMap<String, serde_json::Value>,
}
Expand Down
5 changes: 5 additions & 0 deletions src/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ impl<'a, 'b> Matcher<'a, 'b> {
self.positional_args.iter().map(|v| v.to_string()).collect(),
));
}
if !last_cmd.require_tools.is_empty() {
output.push(ArgcValue::RequireTools(
last_cmd.require_tools.iter().cloned().collect(),
));
}
if let Some(command_fn) = &last_cmd.command_fn {
output.push(ArgcValue::CommandFn(command_fn.clone()));
}
Expand Down
14 changes: 14 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,23 @@ pub(crate) const META_NO_INHERIT_ENV: &str = "no-inherit-env";
pub(crate) const META_SYMBOL: &str = "symbol";
pub(crate) const META_COMBINE_SHORTS: &str = "combine-shorts";
pub(crate) const META_MAN_SECTION: &str = "man-section";
pub(crate) const META_REQUIRE_TOOLS: &str = "require-tools";

pub(crate) const MAX_ARGS: usize = 32767;

pub(crate) const ARGC_REQUIRE_TOOLS: &str = r#"_argc_require_tools() {
local tool missing_tools=()
for tool in "$@"; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing_tools+=("$tool")
fi
done
if [[ "${#missing_tools[@]}" -gt 0 ]]; then
echo "error: missing tools: ${missing_tools[*]}" >&2
exit 1
fi
}"#;

pub fn to_cobol_case(value: &str) -> String {
Converter::new()
.set_pattern(Pattern::Uppercase)
Expand Down
4 changes: 2 additions & 2 deletions tests/snapshots/integration__cli__export.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2958,6 +2958,6 @@ expression: stdout
"command_fn": "test3"
}
],
"command_fn": null,
"combine_shorts": true
"combine_shorts": true,
"command_fn": null
}
Loading

0 comments on commit 647c487

Please sign in to comment.