diff --git a/crates/bashkit/src/builtins/vars.rs b/crates/bashkit/src/builtins/vars.rs index d5f0c57c..4bc2182e 100644 --- a/crates/bashkit/src/builtins/vars.rs +++ b/crates/bashkit/src/builtins/vars.rs @@ -48,6 +48,40 @@ fn option_name_to_var(name: &str) -> Option<&'static str> { } } +/// All known `set -o` options with their variable names, in display order. +const SET_O_OPTIONS: &[(&str, &str)] = &[ + ("errexit", "SHOPT_e"), + ("noglob", "SHOPT_f"), + ("noclobber", "SHOPT_C"), + ("noexec", "SHOPT_n"), + ("nounset", "SHOPT_u"), + ("pipefail", "SHOPT_pipefail"), + ("verbose", "SHOPT_v"), + ("xtrace", "SHOPT_x"), +]; + +/// Format option display for `set -o` (human-readable). +fn format_set_dash_o(variables: &std::collections::HashMap) -> String { + let mut output = String::new(); + for (name, var) in SET_O_OPTIONS { + let enabled = variables.get(*var).map(|v| v == "1").unwrap_or(false); + let state = if enabled { "on" } else { "off" }; + output.push_str(&format!("{:<15}\t{}\n", name, state)); + } + output +} + +/// Format option display for `set +o` (re-executable). +fn format_set_plus_o(variables: &std::collections::HashMap) -> String { + let mut output = String::new(); + for (name, var) in SET_O_OPTIONS { + let enabled = variables.get(*var).map(|v| v == "1").unwrap_or(false); + let flag = if enabled { "-o" } else { "+o" }; + output.push_str(&format!("set {} {}\n", flag, name)); + } + output +} + #[async_trait] impl Builtin for Set { async fn execute(&self, ctx: Context<'_>) -> Result { @@ -69,14 +103,23 @@ impl Builtin for Set { && arg.len() > 1 && (arg.as_bytes()[1] == b'o' && arg.len() == 2) { - // -o option_name / +o option_name + // -o / +o: either display options or set/unset a named option let enable = arg.starts_with('-'); - i += 1; - if i < ctx.args.len() { + if i + 1 < ctx.args.len() { + // -o option_name / +o option_name + i += 1; if let Some(var) = option_name_to_var(&ctx.args[i]) { ctx.variables .insert(var.to_string(), if enable { "1" } else { "0" }.to_string()); } + } else { + // Bare -o or +o: display options + let output = if enable { + format_set_dash_o(ctx.variables) + } else { + format_set_plus_o(ctx.variables) + }; + return Ok(ExecResult::ok(output)); } } else if arg.starts_with('-') || arg.starts_with('+') { let enable = arg.starts_with('-'); diff --git a/crates/bashkit/tests/spec_cases/bash/variables.test.sh b/crates/bashkit/tests/spec_cases/bash/variables.test.sh index ec30ff59..b3df3fb8 100644 --- a/crates/bashkit/tests/spec_cases/bash/variables.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/variables.test.sh @@ -672,3 +672,50 @@ shopt -q extglob && echo "ext:on" || echo "ext:off" null:on ext:on ### end + +### set_dash_o_display +# set -o shows options in human-readable format with grep +set -o | grep "^errexit" +### expect +errexit off +### end + +### set_dash_o_shows_enabled +# set -o reflects enabled options +set -e +set -o | grep "^errexit" +### expect +errexit on +### end + +### set_plus_o_display +# set +o shows options in re-executable format +set +o | grep "errexit" +### expect +set +o errexit +### end + +### set_plus_o_shows_enabled +# set +o reflects enabled options +set -o pipefail +set +o | grep "pipefail" +### expect +set -o pipefail +### end + +### set_dash_o_noclobber +# set -o noclobber and display +set -o noclobber +set -o | grep "^noclobber" +### expect +noclobber on +### end + +### set_plus_o_restore +# set +o output can be used to restore state +set -o xtrace +state=$(set +o 2>/dev/null | grep xtrace) +echo "$state" +### expect +set -o xtrace +### end diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index e32e9726..abb10d45 100644 --- a/specs/009-implementation-status.md +++ b/specs/009-implementation-status.md @@ -103,17 +103,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See ## Spec Test Coverage -**Total spec test cases:** 1318 (1313 pass, 5 skip) +**Total spec test cases:** 1324 (1319 pass, 5 skip) | Category | Cases | In CI | Pass | Skip | Notes | |----------|-------|-------|------|------|-------| -| Bash (core) | 900 | Yes | 895 | 5 | `bash_spec_tests` in CI | +| Bash (core) | 906 | Yes | 901 | 5 | `bash_spec_tests` in CI | | AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g | | Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect | | Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E | | JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env | | Python | 57 | Yes | 57 | 0 | embedded Python (Monty) | -| **Total** | **1318** | **Yes** | **1313** | **5** | | +| **Total** | **1324** | **Yes** | **1319** | **5** | | ### Bash Spec Tests Breakdown @@ -157,7 +157,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | test-operators.test.sh | 17 | file/string tests | | time.test.sh | 11 | Wall-clock only (user/sys always 0) | | timeout.test.sh | 17 | | -| variables.test.sh | 86 | includes special vars, prefix env, PIPESTATUS, trap EXIT, `${var@Q}`, `\` line continuation, PWD/HOME/USER/HOSTNAME/BASH_VERSION/SECONDS, `set -x` xtrace, `shopt` builtin, nullglob | +| variables.test.sh | 92 | includes special vars, prefix env, PIPESTATUS, trap EXIT, `${var@Q}`, `\` line continuation, PWD/HOME/USER/HOSTNAME/BASH_VERSION/SECONDS, `set -x` xtrace, `shopt` builtin, nullglob, `set -o`/`set +o` display | | wc.test.sh | 35 | word count (5 skipped) | | type.test.sh | 15 | `type`, `which`, `hash` builtins | | declare.test.sh | 23 | `declare`/`typeset`, `-i`, `-r`, `-x`, `-a`, `-p`, `-n` nameref, `-l`/`-u` case conversion |