Skip to content
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
49 changes: 46 additions & 3 deletions crates/bashkit/src/builtins/vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, String>) -> 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, String>) -> 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<ExecResult> {
Expand All @@ -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('-');
Expand Down
47 changes: 47 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/variables.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions specs/009-implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}`, `\<newline>` 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}`, `\<newline>` 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 |
Expand Down
Loading