Skip to content

Commit 40eabf3

Browse files
committed
feat(interpreter): implement declare -l/-u case conversion attributes
Add support for `declare -l` (lowercase) and `declare -u` (uppercase) variable attributes. When set, all subsequent assignments to the variable are automatically converted to the specified case. Implementation: - Parse `-l` and `-u` flags in `execute_declare_builtin` - Store attributes as `_LOWER_<name>` / `_UPPER_<name>` marker variables - Apply case conversion in `set_variable()` for subsequent assignments - Setting one attribute clears the opposite (e.g., -u clears _LOWER_) Features: - `declare -l x=HELLO` → x=hello - `declare -u x=hello` → x=HELLO - Attribute persists: `declare -l x; x=WORLD` → x=world - Works in functions: `declare -u result="$1"` Tests: 7 new spec tests (declare.test.sh: 16 → 23)
1 parent 4d514d2 commit 40eabf3

File tree

4 files changed

+152
-8
lines changed

4 files changed

+152
-8
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3860,6 +3860,8 @@ impl Interpreter {
38603860
let mut is_assoc = false;
38613861
let mut is_integer = false;
38623862
let mut is_nameref = false;
3863+
let mut is_lowercase = false;
3864+
let mut is_uppercase = false;
38633865
let mut names: Vec<&str> = Vec::new();
38643866

38653867
for arg in args {
@@ -3873,7 +3875,9 @@ impl Interpreter {
38733875
'i' => is_integer = true,
38743876
'A' => is_assoc = true,
38753877
'n' => is_nameref = true,
3876-
'g' | 'l' | 'u' | 't' | 'f' | 'F' => {} // ignored
3878+
'l' => is_lowercase = true,
3879+
'u' => is_uppercase = true,
3880+
'g' | 't' | 'f' | 'F' => {} // ignored
38773881
_ => {}
38783882
}
38793883
}
@@ -4027,10 +4031,28 @@ impl Interpreter {
40274031
self.variables
40284032
.insert(var_name.to_string(), int_val.to_string());
40294033
} else {
4030-
self.variables
4031-
.insert(var_name.to_string(), value.to_string());
4034+
// Apply case conversion attributes
4035+
let final_value = if is_lowercase {
4036+
value.to_lowercase()
4037+
} else if is_uppercase {
4038+
value.to_uppercase()
4039+
} else {
4040+
value.to_string()
4041+
};
4042+
self.variables.insert(var_name.to_string(), final_value);
40324043
}
40334044

4045+
// Set case conversion attribute markers
4046+
if is_lowercase {
4047+
self.variables
4048+
.insert(format!("_LOWER_{}", var_name), "1".to_string());
4049+
self.variables.remove(&format!("_UPPER_{}", var_name));
4050+
}
4051+
if is_uppercase {
4052+
self.variables
4053+
.insert(format!("_UPPER_{}", var_name), "1".to_string());
4054+
self.variables.remove(&format!("_LOWER_{}", var_name));
4055+
}
40344056
if is_readonly {
40354057
self.variables
40364058
.insert(format!("_READONLY_{}", var_name), "1".to_string());
@@ -4055,6 +4077,17 @@ impl Interpreter {
40554077
} else if !self.variables.contains_key(name.as_str()) {
40564078
self.variables.insert(name.to_string(), String::new());
40574079
}
4080+
// Set case conversion attribute markers
4081+
if is_lowercase {
4082+
self.variables
4083+
.insert(format!("_LOWER_{}", name), "1".to_string());
4084+
self.variables.remove(&format!("_UPPER_{}", name));
4085+
}
4086+
if is_uppercase {
4087+
self.variables
4088+
.insert(format!("_UPPER_{}", name), "1".to_string());
4089+
self.variables.remove(&format!("_LOWER_{}", name));
4090+
}
40584091
if is_readonly {
40594092
self.variables
40604093
.insert(format!("_READONLY_{}", name), "1".to_string());
@@ -5627,6 +5660,24 @@ impl Interpreter {
56275660
fn set_variable(&mut self, name: String, value: String) {
56285661
// Resolve nameref: if `name` is a nameref, assign to the target instead
56295662
let resolved = self.resolve_nameref(&name).to_string();
5663+
// Apply case conversion attributes (declare -l / declare -u)
5664+
let value = if self
5665+
.variables
5666+
.get(&format!("_LOWER_{}", resolved))
5667+
.map(|v| v == "1")
5668+
.unwrap_or(false)
5669+
{
5670+
value.to_lowercase()
5671+
} else if self
5672+
.variables
5673+
.get(&format!("_UPPER_{}", resolved))
5674+
.map(|v| v == "1")
5675+
.unwrap_or(false)
5676+
{
5677+
value.to_uppercase()
5678+
} else {
5679+
value
5680+
};
56305681
for frame in self.call_stack.iter_mut().rev() {
56315682
if let std::collections::hash_map::Entry::Occupied(mut e) =
56325683
frame.locals.entry(resolved.clone())

crates/bashkit/tests/spec_cases/bash/declare.test.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,68 @@ echo "$ref"
143143
first
144144
second
145145
### end
146+
147+
### declare_lowercase
148+
# declare -l converts value to lowercase
149+
declare -l x=HELLO
150+
echo "$x"
151+
### expect
152+
hello
153+
### end
154+
155+
### declare_uppercase
156+
# declare -u converts value to uppercase
157+
declare -u x=hello
158+
echo "$x"
159+
### expect
160+
HELLO
161+
### end
162+
163+
### declare_lowercase_subsequent
164+
# declare -l applies to subsequent assignments
165+
declare -l x
166+
x=WORLD
167+
echo "$x"
168+
### expect
169+
world
170+
### end
171+
172+
### declare_uppercase_subsequent
173+
# declare -u applies to subsequent assignments
174+
declare -u x
175+
x=world
176+
echo "$x"
177+
### expect
178+
WORLD
179+
### end
180+
181+
### declare_lowercase_mixed
182+
# declare -l handles mixed case
183+
declare -l x=HeLLo_WoRLd
184+
echo "$x"
185+
### expect
186+
hello_world
187+
### end
188+
189+
### declare_uppercase_overrides_lowercase
190+
# declare -u after -l overrides to uppercase
191+
declare -l x=Hello
192+
echo "$x"
193+
declare -u x
194+
x=Hello
195+
echo "$x"
196+
### expect
197+
hello
198+
HELLO
199+
### end
200+
201+
### declare_case_in_function
202+
# declare -l works in functions
203+
toupper() {
204+
declare -u result="$1"
205+
echo "$result"
206+
}
207+
toupper "hello world"
208+
### expect
209+
HELLO WORLD
210+
### end

specs/009-implementation-status.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
103103

104104
## Spec Test Coverage
105105

106-
**Total spec test cases:** 1305 (1300 pass, 5 skip)
106+
**Total spec test cases:** 1318 (1313 pass, 5 skip)
107107

108108
| Category | Cases | In CI | Pass | Skip | Notes |
109109
|----------|-------|-------|------|------|-------|
110-
| Bash (core) | 893 | Yes | 888 | 5 | `bash_spec_tests` in CI |
110+
| Bash (core) | 900 | Yes | 895 | 5 | `bash_spec_tests` in CI |
111111
| AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g |
112112
| Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect |
113113
| Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E |
114114
| JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env |
115115
| Python | 57 | Yes | 57 | 0 | embedded Python (Monty) |
116-
| **Total** | **1311** | **Yes** | **1306** | **5** | |
116+
| **Total** | **1318** | **Yes** | **1313** | **5** | |
117117

118118
### Bash Spec Tests Breakdown
119119

@@ -160,7 +160,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
160160
| 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 |
161161
| wc.test.sh | 35 | word count (5 skipped) |
162162
| type.test.sh | 15 | `type`, `which`, `hash` builtins |
163-
| declare.test.sh | 16 | `declare`/`typeset`, `-i`, `-r`, `-x`, `-a`, `-p`, `-n` nameref |
163+
| declare.test.sh | 23 | `declare`/`typeset`, `-i`, `-r`, `-x`, `-a`, `-p`, `-n` nameref, `-l`/`-u` case conversion |
164164
| ln.test.sh | 5 | `ln -s`, `-f`, symlink creation |
165165
| eval-bugs.test.sh | 4 | regression tests for eval/script bugs |
166166
| script-exec.test.sh | 10 | script execution by path, $PATH search, exit codes |
@@ -183,7 +183,7 @@ Features that may be added in the future (not intentionally excluded):
183183
| ~~`getopts`~~ | ~~Medium~~ | Implemented: POSIX option parsing |
184184
| ~~`command` builtin~~ | ~~Medium~~ | Implemented: `-v`, `-V`, bypass functions |
185185
| ~~`type`/`which` builtins~~ | ~~Medium~~ | Implemented: `-t`, `-a`, `-p` flags |
186-
| ~~`declare` builtin~~ | ~~Medium~~ | Implemented: `-i`, `-r`, `-x`, `-a`, `-p` |
186+
| ~~`declare` builtin~~ | ~~Medium~~ | Implemented: `-i`, `-r`, `-x`, `-a`, `-p`, `-n`, `-l`, `-u` |
187187
| ~~`ln` builtin~~ | ~~Medium~~ | Implemented: symbolic links (`-s`, `-f`) |
188188
| `alias` | Low | Interactive feature |
189189
| History expansion | Out of scope | Interactive only |

supply-chain/config.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,10 @@ criteria = "safe-to-deploy"
610610
version = "0.3.89"
611611
criteria = "safe-to-deploy"
612612

613+
[[exemptions.js-sys]]
614+
version = "0.3.90"
615+
criteria = "safe-to-deploy"
616+
613617
[[exemptions.leb128fmt]]
614618
version = "0.1.0"
615619
criteria = "safe-to-deploy"
@@ -1386,22 +1390,42 @@ criteria = "safe-to-run"
13861390
version = "0.2.112"
13871391
criteria = "safe-to-deploy"
13881392

1393+
[[exemptions.wasm-bindgen]]
1394+
version = "0.2.113"
1395+
criteria = "safe-to-deploy"
1396+
13891397
[[exemptions.wasm-bindgen-futures]]
13901398
version = "0.4.62"
13911399
criteria = "safe-to-deploy"
13921400

1401+
[[exemptions.wasm-bindgen-futures]]
1402+
version = "0.4.63"
1403+
criteria = "safe-to-deploy"
1404+
13931405
[[exemptions.wasm-bindgen-macro]]
13941406
version = "0.2.112"
13951407
criteria = "safe-to-deploy"
13961408

1409+
[[exemptions.wasm-bindgen-macro]]
1410+
version = "0.2.113"
1411+
criteria = "safe-to-deploy"
1412+
13971413
[[exemptions.wasm-bindgen-macro-support]]
13981414
version = "0.2.112"
13991415
criteria = "safe-to-deploy"
14001416

1417+
[[exemptions.wasm-bindgen-macro-support]]
1418+
version = "0.2.113"
1419+
criteria = "safe-to-deploy"
1420+
14011421
[[exemptions.wasm-bindgen-shared]]
14021422
version = "0.2.112"
14031423
criteria = "safe-to-deploy"
14041424

1425+
[[exemptions.wasm-bindgen-shared]]
1426+
version = "0.2.113"
1427+
criteria = "safe-to-deploy"
1428+
14051429
[[exemptions.wasm-encoder]]
14061430
version = "0.244.0"
14071431
criteria = "safe-to-deploy"
@@ -1422,6 +1446,10 @@ criteria = "safe-to-deploy"
14221446
version = "0.3.89"
14231447
criteria = "safe-to-deploy"
14241448

1449+
[[exemptions.web-sys]]
1450+
version = "0.3.90"
1451+
criteria = "safe-to-deploy"
1452+
14251453
[[exemptions.web-time]]
14261454
version = "1.1.0"
14271455
criteria = "safe-to-deploy"

0 commit comments

Comments
 (0)