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

cmds: make "zsh" split words just like other shells #25

Merged
merged 3 commits into from
Feb 12, 2024
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
8 changes: 8 additions & 0 deletions doc/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
when a variable with a cyclical expression is evaluated.
([#24](https://github.com/davvid/garden/pull/24))

- When `zsh` is used as the `garden.shell`, which happens automatically when `zsh`
is installed, `garden` will now use `zsh -o shwordsplit` in order to enable
word-splitting of `$variable` expressions by default. This makes `zsh` behave
just like other shells by default, which improves the portability of commands.
Configure `garden.shell-wordsplit` to `false` or use the
`garden <cmd> -z | --no-wordsplit` option to opt-out of this behavior.
([#25](https://github.com/davvid/garden/pull/25))


## v1.2.1

Expand Down
10 changes: 9 additions & 1 deletion doc/src/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,17 @@ The `--no-errexit` option causes commands with multiple statements to run to com
even when a non-zero exit code is encountered. This is akin to a regular shell script.

Configure `garden.shell-errexit` to `false` in `garden.yaml` to opt-out of this behavior.
You can also opt-out of the `errexit` behavior on a per-command basis by adding
You can also opt-out of the `errexit` behavior on a per-command basis by using
`set +e` as the first line of a multi-line command.

When `zsh` is used it is executed with the `-o shwordsplit` option so that zsh behaves
similarly to traditional shells and splits words in unquoted `$variable` expressions
rather than treating `$variable` like a single argument.

Configure `garden.shell-wordsplit` to `false` to opt-out of this behavior.
You can also opt-out of the `shwordsplit` behavior on a per-command basis by using
`set +o shwordsplit` as the first line of a multi-line command.

Additional command-line `<arguments>` specified after a double-dash (`--`)
end-of-options marker are forwarded to each command.

Expand Down
63 changes: 44 additions & 19 deletions src/cmds/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ pub struct CmdOptions {
/// Prevent the "errexit" shell option from being set. By default, the "-e" option
/// is passed to the configured shell so that multi-line and multi-statement
/// commands halt execution when the first statement with a non-zero exit code is
/// encountered. "--no-errexit" has the effect of making multi-line and
/// encountered. This option has the effect of making multi-line and
/// multi-statement commands run all statements even when an earlier statement
/// returns a non-zero exit code.
#[arg(long, short)]
no_errexit: bool,
#[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
exit_on_error: bool,
/// Do not pass "-o shwordsplit" to zsh.
/// Prevent the "shwordsplit" shell option from being set when using zsh.
/// The "-o shwordsplit" option is passed to zsh by default so that unquoted
/// $variable expressions are subject to word splitting, just like other shells.
/// This option disables this behavior.
#[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
word_split: bool,
/// Tree query for the gardens, groups or trees to execute commands within
query: String,
/// Custom commands to run over the resolved trees
Expand All @@ -52,11 +59,18 @@ pub struct CustomOptions {
/// Prevent the "errexit" shell option from being set. By default, the "-e" option
/// is passed to the configured shell so that multi-line and multi-statement
/// commands halt execution when the first statement with a non-zero exit code is
/// encountered. "--no-errexit" has the effect of making multi-line and
/// encountered. This option has the effect of making multi-line and
/// multi-statement commands run all statements even when an earlier statement
/// returns a non-zero exit code.
#[arg(long, short)]
no_errexit: bool,
#[arg(long = "no-errexit", short = 'n', default_value_t = true, action = clap::ArgAction::SetFalse)]
exit_on_error: bool,
/// Do not pass "-o shwordsplit" to zsh.
/// Prevent the "shwordsplit" shell option from being set when using zsh.
/// The "-o shwordsplit" option is passed to zsh by default so that unquoted
/// $variable expressions are subject to word splitting, just like other shells.
/// This option disables this behavior.
#[arg(long = "no-wordsplit", short = 'z', default_value_t = true, action = clap::ArgAction::SetFalse)]
word_split: bool,
/// Tree queries for the Gardens/Groups/Trees to execute commands within
// NOTE: value_terminator may not be needed in future versions of clap_complete.
// https://github.com/clap-rs/clap/pull/4612
Expand All @@ -76,7 +90,10 @@ pub fn main_cmd(app_context: &model::ApplicationContext, options: &mut CmdOption
debug!("trees: {:?}", options.trees);
}
if !app_context.get_root_config().shell_exit_on_error {
options.no_errexit = true;
options.exit_on_error = false;
}
if !app_context.get_root_config().shell_word_split {
options.word_split = false;
}
let params: CmdParams = options.clone().into();
let exit_status = cmd(app_context, &options.query, &params)?;
Expand All @@ -98,6 +115,8 @@ pub struct CmdParams {
keep_going: bool,
#[derivative(Default(value = "true"))]
exit_on_error: bool,
#[derivative(Default(value = "true"))]
word_split: bool,
}

/// Build CmdParams from a CmdOptions struct.
Expand All @@ -107,9 +126,10 @@ impl From<CmdOptions> for CmdParams {
commands: options.commands.clone(),
arguments: options.arguments.clone(),
breadth_first: options.breadth_first,
exit_on_error: !options.no_errexit,
exit_on_error: options.exit_on_error,
keep_going: options.keep_going,
tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
word_split: options.word_split,
..Default::default()
}
}
Expand All @@ -128,8 +148,9 @@ impl From<CustomOptions> for CmdParams {
// --breadth-first was added to "garden cmd" and made opt-in.
breadth_first: true,
keep_going: options.keep_going,
exit_on_error: !options.no_errexit,
exit_on_error: options.exit_on_error,
tree_pattern: glob::Pattern::new(&options.trees).unwrap_or_default(),
word_split: options.word_split,
..Default::default()
};

Expand Down Expand Up @@ -159,7 +180,10 @@ pub fn main_custom(app_context: &model::ApplicationContext, arguments: &Vec<Stri
let mut options = <CustomOptions as FromArgMatches>::from_arg_matches(&matches)
.map_err(format_error::<CustomOptions>)?;
if !app_context.get_root_config().shell_exit_on_error {
options.no_errexit = true;
options.exit_on_error = false;
}
if !app_context.get_root_config().shell_word_split {
options.word_split = false;
}

if app_context.options.debug_level(constants::DEBUG_LEVEL_CMD) > 0 {
Expand Down Expand Up @@ -253,8 +277,7 @@ fn run_cmd_breadth_first(
&shell,
&env,
&cmd_seq_vec,
&params.arguments,
params.exit_on_error,
params,
) {
exit_status = cmd_status;
if !params.keep_going {
Expand Down Expand Up @@ -324,8 +347,7 @@ fn run_cmd_depth_first(
&shell,
&env,
&cmd_seq_vec,
&params.arguments,
params.exit_on_error,
params,
) {
exit_status = cmd_status;
if !params.keep_going {
Expand Down Expand Up @@ -353,8 +375,7 @@ fn run_cmd_vec(
shell: &str,
env: &Vec<(String, String)>,
cmd_seq_vec: &[Vec<String>],
arguments: &[String],
exit_on_error: bool,
params: &CmdParams,
) -> Result<(), i32> {
// Get the current executable name
let current_exe = cmd::current_exe();
Expand All @@ -375,6 +396,7 @@ fn run_cmd_vec(
| constants::SHELL_SH
| constants::SHELL_ZSH
);
let is_zsh = matches!(basename, constants::SHELL_ZSH);
// Does the shell use "-e <string>" or "-c <string>" to evaluate commands?
let use_dash_e = matches!(
basename,
Expand All @@ -394,7 +416,10 @@ fn run_cmd_vec(
);
}
let mut exec = subprocess::Exec::cmd(shell).cwd(path);
if exit_on_error && is_shell {
if params.word_split && is_zsh {
exec = exec.arg("-o").arg("shwordsplit");
}
if params.exit_on_error && is_shell {
exec = exec.arg("-e");
}
if use_dash_e {
Expand All @@ -406,7 +431,7 @@ fn run_cmd_vec(
if is_shell {
exec = exec.arg(current_exe.as_str());
}
exec = exec.args(arguments);
exec = exec.args(&params.arguments);
// Update the command environment
for (k, v) in env {
exec = exec.env(k, v);
Expand All @@ -416,7 +441,7 @@ fn run_cmd_vec(
let status = cmd::status(exec);
if status != errors::EX_OK {
exit_status = status;
if exit_on_error {
if params.exit_on_error {
return Err(status);
}
} else {
Expand Down
8 changes: 7 additions & 1 deletion src/cmds/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,17 @@ pub fn main(options: &cli::MainOptions, completion_options: &CompletionOptions)
.long("keep-going"),
)
.arg(
Arg::new("no_errexit")
Arg::new("no-errexit")
.help("Do not pass -e to the shell")
.short('n')
.long("no-errexit"),
)
.arg(
Arg::new("no-wordsplit")
.help("Do not pass -o shwordsplit to zsh")
.short('z')
.long("no-wordsplit"),
)
.arg(
Arg::new("queries")
// NOTE: value_terminator may not be needed in future versions of clap_complete.
Expand Down
55 changes: 36 additions & 19 deletions src/config/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ fn parse_recursive(
config.root_is_dynamic = true;
}
if config_verbose > 0 {
debug!("yaml: garden.root = {}", config.root.get_expr());
debug!("config: garden.root = {}", config.root.get_expr());
}
}

// garden.shell
if get_str(&doc[constants::GARDEN][constants::SHELL], &mut config.shell) && config_verbose > 0 {
debug!("yaml: garden.shell = {}", config.shell);
debug!("config: {} = {}", constants::GARDEN_SHELL, config.shell);
}
// garden.shell-errexit
if get_bool(
Expand All @@ -71,24 +71,41 @@ fn parse_recursive(
) && config_verbose > 0
{
debug!(
"yaml: garden.shell-errexit = {}",
"config: {} = {}",
constants::GARDEN_SHELL_ERREXIT,
config.shell_exit_on_error
);
}
// garden.shell-wordsplit
if get_bool(
&doc[constants::GARDEN][constants::SHELL_WORDSPLIT],
&mut config.shell_word_split,
) && config_verbose > 0
{
debug!(
"config: {} = {}",
constants::GARDEN_SHELL_WORDSPLIT,
config.shell_word_split
);
}
// garden.tree-branches
if get_bool(
&doc[constants::GARDEN][constants::TREE_BRANCHES],
&mut config.tree_branches,
) && config_verbose > 0
{
debug!("yaml: garden.tree-branches = {}", config.tree_branches);
debug!(
"config: {} = {}",
constants::GARDEN_TREE_BRANCHES,
config.tree_branches
);
}

// GARDEN_ROOT and GARDEN_CONFIG_DIR are relative to the root configuration.
// Referencing these variables from garden files included using garden.includes
// resolves to the root config's location, not the included location.
if config_verbose > 1 {
debug!("yaml: built-in variables");
debug!("config: built-in variables");
}
// Provide GARDEN_ROOT.
config.variables.insert(
Expand All @@ -111,7 +128,7 @@ fn parse_recursive(
if !get_variables_hashmap(&doc[constants::VARIABLES], &mut config.variables)
&& config_verbose > 1
{
debug!("yaml: no variables");
debug!("config: no variables");
}

// Process "includes" after initializing the GARDEN_ROOT and GARDEN_CONFIG_DIR.
Expand Down Expand Up @@ -160,65 +177,65 @@ fn parse_recursive(
if !get_variables_hashmap(&doc[constants::VARIABLES], &mut config.variables)
&& config_verbose > 1
{
debug!("yaml: no reloaded variables");
debug!("config: no reloaded variables");
}
}

// grafts
if config_verbose > 1 {
debug!("yaml: grafts");
debug!("config: grafts");
}
if !get_grafts(&doc[constants::GRAFTS], &mut config.grafts) && config_verbose > 1 {
debug!("yaml: no grafts");
debug!("config: no grafts");
}

get_multivariables(&doc[constants::ENVIRONMENT], &mut config.environment);

// commands
if config_verbose > 1 {
debug!("yaml: commands");
debug!("config: commands");
}
if !get_multivariables_hashmap(&doc[constants::COMMANDS], &mut config.commands)
&& config_verbose > 1
{
debug!("yaml: no commands");
debug!("config: no commands");
}

// templates
if config_verbose > 1 {
debug!("yaml: templates");
debug!("config: templates");
}
if !get_templates(
&doc["templates"],
&config.templates.clone(),
&mut config.templates,
) && config_verbose > 1
{
debug!("yaml: no templates");
debug!("config: no templates");
}

// trees
if config_verbose > 1 {
debug!("yaml: trees");
debug!("config: trees");
}
if !get_trees(app_context, config, &doc[constants::TREES]) && config_verbose > 1 {
debug!("yaml: no trees");
debug!("config: no trees");
}

// groups
if config_verbose > 1 {
debug!("yaml: groups");
debug!("config: groups");
}
if !get_groups(&doc[constants::GROUPS], &mut config.groups) && config_verbose > 1 {
debug!("yaml: no groups");
debug!("config: no groups");
}

// gardens
if config_verbose > 1 {
debug!("yaml: gardens");
debug!("config: gardens");
}
if !get_gardens(&doc[constants::GARDENS], &mut config.gardens) && config_verbose > 1 {
debug!("yaml: no gardens");
debug!("config: no gardens");
}

Ok(())
Expand Down
9 changes: 9 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ pub const GARDEN_CONFIG_DIR_EXPR: &str = "${GARDEN_CONFIG_DIR}";
/// Builtin variable for the "garden.root" location where trees are grown.
pub const GARDEN_ROOT: &str = "GARDEN_ROOT";

/// Command-line defines for overriding configurable behavior.
pub(crate) const GARDEN_SHELL: &str = "garden.shell";
pub(crate) const GARDEN_SHELL_ERREXIT: &str = "garden.shell-errexit";
pub(crate) const GARDEN_SHELL_WORDSPLIT: &str = "garden.shell-wordsplit";
pub(crate) const GARDEN_TREE_BRANCHES: &str = "garden.tree-branches";

/// The "gitconfig" section in a tree block defines local ".git/config"
/// settings that are applied when a tree is grown.
pub const GITCONFIG: &str = "gitconfig";
Expand Down Expand Up @@ -129,6 +135,9 @@ pub(crate) const SHELL_DASH: &str = "dash";
/// shell option.
pub const SHELL_ERREXIT: &str = "shell-errexit";

/// The "shell-wordsplit" key in the garden block disables the `zsh -o shwordsplit` option.
pub const SHELL_WORDSPLIT: &str = "shell-wordsplit";

/// KornShell is a standard/restricted command and programming language.
pub(crate) const SHELL_KSH: &str = "ksh";

Expand Down
Loading
Loading