1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
use std::{collections::HashMap, io::Write};
use clap::Parser;
use brush_core::{ExecutionResult, builtins, env, variables};
/// Parse command options.
#[derive(Parser)]
pub(crate) struct GetOptsCommand {
/// Specification for options
options_string: String,
/// Name of variable to receive next option
variable_name: String,
/// Arguments to parse
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}
// We track cross-call state in special variables. They are hidden from enumeration
// (e.g. `set`, `declare`) so they don't leak into scripts' environments.
const VAR_GETOPTS_NEXT_CHAR_INDEX: &str = "__GETOPTS_NEXT_CHAR";
const VAR_GETOPTS_LAST_OPTIND: &str = "__GETOPTS_LAST_OPTIND";
const DEFAULT_NEXT_CHAR_INDEX: usize = 1;
/// The result of processing one option from the argument list.
struct GetOptsResult {
/// The value to assign to the target variable (the option char, `?`, or `:`).
variable_value: String,
/// The value for OPTARG, if any (option's argument or, on error, the offending char).
optarg: Option<String>,
/// The new value for OPTIND after this call.
optind: usize,
/// Exit code: success (0) if an option was found, general error (1) when done.
exit_code: ExecutionResult,
}
/// Parsed representation of the optstring (e.g., `":a:bc"`).
struct OptionSpec {
/// Maps each option character to whether it requires an argument.
defs: HashMap<char, bool>,
/// True when the optstring has a leading `:`, suppressing error messages
/// and reporting errors via `?`/`:` in the variable and OPTARG instead.
silent_errors: bool,
}
/// Parses the optstring into an [`OptionSpec`]. A leading `:` enables silent error
/// mode. Each letter defines an option; a `:` immediately after a letter means it
/// takes an argument. Duplicate letters are ignored (first definition wins).
fn parse_option_spec(spec: &str) -> OptionSpec {
let mut defs = HashMap::<char, bool>::new();
let mut silent_errors = false;
let mut last_char = None;
for c in spec.chars() {
if c == ':' {
if let Some(last_char) = last_char {
defs.insert(last_char, true);
} else if defs.is_empty() {
// First character is ':' — request silent error reporting.
silent_errors = true;
}
continue;
}
if let std::collections::hash_map::Entry::Vacant(e) = defs.entry(c) {
e.insert(false);
last_char = Some(c);
} else {
// Duplicate option char; first definition wins.
// Clear last_char so a trailing colon doesn't modify the first definition.
last_char = None;
}
}
OptionSpec {
defs,
silent_errors,
}
}
impl builtins::Command for GetOptsCommand {
type Error = brush_core::Error;
/// Override the default [`builtins::Command::new`] function to handle clap's limitation related
/// to `--`. See [`builtins::parse_known`] for more information
/// TODO(command): we can safely remove this after the issue is resolved
fn new<I>(args: I) -> Result<Self, clap::Error>
where
I: IntoIterator<Item = String>,
{
let (mut this, rest_args) = brush_core::builtins::try_parse_known::<Self>(args)?;
if let Some(args) = rest_args {
this.args.extend(args);
}
Ok(this)
}
async fn execute<SE: brush_core::ShellExtensions>(
&self,
mut context: brush_core::ExecutionContext<'_, SE>,
) -> Result<brush_core::ExecutionResult, Self::Error> {
// Validate the target variable name.
if !env::valid_variable_name(&self.variable_name) {
writeln!(
context.stderr(),
"{}: `{}': not a valid identifier",
context.command_name,
self.variable_name
)?;
return Ok(ExecutionResult::new(1));
}
let spec = parse_option_spec(&self.options_string);
// If unset or non-numeric, assume OPTIND is 1.
let next_index_signed = context
.shell
.env_str("OPTIND")
.and_then(|s| brush_core::int_utils::parse::<i32>(s.as_ref(), 10).ok())
.unwrap_or(1);
// Detect external OPTIND modifications (e.g., `OPTIND=1` to restart
// parsing). If the current OPTIND differs from what we last set, clear
// the internal char index so we don't resume mid-arg.
let last_optind = context
.shell
.env_str(VAR_GETOPTS_LAST_OPTIND)
.and_then(|s| s.parse::<i32>().ok());
if last_optind != Some(next_index_signed) {
context.shell.env_mut().unset(VAR_GETOPTS_NEXT_CHAR_INDEX)?;
}
#[allow(clippy::cast_sign_loss)] // .max(1) guarantees positive
let next_index = next_index_signed.max(1) as usize;
// Select the arguments to parse. If none were explicitly provided, we
// default to using the shell's current positional parameters.
// Clone positional params to avoid borrowing context immutably while we
// also need it mutably in parse_next_option.
let owned_args;
let args_to_parse = if !self.args.is_empty() {
&self.args
} else {
owned_args = context.shell.current_shell_args().to_vec();
&owned_args
};
let result = parse_next_option(&mut context, &spec, args_to_parse, next_index)?;
update_variables(&mut context, &self.variable_name, result)
}
}
/// Extracts the next option from `args_to_parse` starting at 1-based position
/// `next_index`. Handles combined flags (e.g., `-abc`), option arguments (both
/// `-pVALUE` and `-p VALUE` forms), and error reporting for unknown options or
/// missing arguments. Tracks position within combined flags via the
/// `__GETOPTS_NEXT_CHAR` shell variable.
fn parse_next_option<SE: brush_core::ShellExtensions>(
context: &mut brush_core::ExecutionContext<'_, SE>,
spec: &OptionSpec,
args_to_parse: &[String],
mut next_index: usize,
) -> Result<GetOptsResult, brush_core::Error> {
// See if there are any args left to parse.
if next_index > args_to_parse.len() {
return Ok(GetOptsResult {
variable_value: String::from("?"),
optarg: None,
// Normalize OPTIND to one past the last argument.
optind: args_to_parse.len() + 1,
exit_code: ExecutionResult::general_error(),
});
}
let arg = args_to_parse[next_index - 1].as_str();
// See if this is an option.
if !arg.starts_with('-') || arg == "--" || arg == "-" {
// Not an option. If it was "--", skip past it.
return Ok(GetOptsResult {
variable_value: String::from("?"),
optarg: None,
optind: if arg == "--" {
next_index + 1
} else {
next_index
},
exit_code: ExecutionResult::general_error(),
});
}
// Figure out how far into this option we are.
let mut next_char_index = context
.shell
.env_str(VAR_GETOPTS_NEXT_CHAR_INDEX)
.map_or(DEFAULT_NEXT_CHAR_INDEX, |s| {
s.parse().unwrap_or(DEFAULT_NEXT_CHAR_INDEX)
});
// Find the char. If the index is stale (exceeds this arg's length),
// reset to the default index, mirroring bash behavior.
let mut c = arg.chars().nth(next_char_index);
if c.is_none() {
next_char_index = DEFAULT_NEXT_CHAR_INDEX;
c = arg.chars().nth(next_char_index);
}
let Some(c) = c else {
// Arg is too short even at default index.
return Ok(GetOptsResult {
variable_value: String::from("?"),
optarg: None,
optind: next_index,
exit_code: ExecutionResult::general_error(),
});
};
let arg_char_count = arg.chars().count();
let mut is_last_char_in_option = next_char_index == arg_char_count - 1;
let mut variable_value;
let optarg;
// Look up the char in the option spec.
if let Some(takes_arg) = spec.defs.get(&c) {
variable_value = String::from(c);
if *takes_arg {
(variable_value, optarg, is_last_char_in_option, next_index) = resolve_option_argument(
context,
spec,
c,
arg,
args_to_parse,
next_char_index,
is_last_char_in_option,
next_index,
)?;
} else {
optarg = None;
}
} else {
(variable_value, optarg) = report_unknown_option(context, spec, c)?;
}
let optind = if is_last_char_in_option {
// We're done with this argument, so unset the internal char index variable
// and request an update to OPTIND.
context.shell.env_mut().unset(VAR_GETOPTS_NEXT_CHAR_INDEX)?;
next_index + 1
} else {
// We have more to go in this argument, so update the internal char index
// and request that OPTIND not be updated.
context.shell.env_mut().update_or_add(
VAR_GETOPTS_NEXT_CHAR_INDEX,
variables::ShellValueLiteral::Scalar((next_char_index + 1).to_string()),
|v| {
v.hide_from_enumeration();
Ok(())
},
brush_core::env::EnvironmentLookup::Anywhere,
brush_core::env::EnvironmentScope::Global,
)?;
next_index
};
Ok(GetOptsResult {
variable_value,
optarg,
optind,
exit_code: ExecutionResult::success(),
})
}
/// Resolves the argument for an option that takes a value. Returns the updated
/// `(variable_value, optarg, is_last_char, next_index)` tuple.
#[allow(clippy::too_many_arguments)]
fn resolve_option_argument<SE: brush_core::ShellExtensions>(
context: &brush_core::ExecutionContext<'_, SE>,
spec: &OptionSpec,
c: char,
arg: &str,
args_to_parse: &[String],
next_char_index: usize,
is_last_char_in_option: bool,
mut next_index: usize,
) -> Result<(String, Option<String>, bool, usize), brush_core::Error> {
// If this is the last character in the token, the argument value comes from
// the next token. Otherwise, the remainder of the current token is the value.
if is_last_char_in_option {
if next_index >= args_to_parse.len() {
// Missing required argument.
let (variable_value, optarg) = if spec.silent_errors {
(String::from(":"), Some(String::from(c)))
} else {
if is_opterr_enabled(context) {
writeln!(
context.stderr(),
"getopts: option requires an argument -- {c}"
)?;
}
(String::from("?"), None)
};
Ok((variable_value, optarg, true, next_index))
} else {
next_index += 1;
Ok((
String::from(c),
Some(args_to_parse[next_index - 1].clone()),
true,
next_index,
))
}
} else {
let optarg: String = arg.chars().skip(next_char_index + 1).collect();
Ok((String::from(c), Some(optarg), true, next_index))
}
}
/// Handles an unknown option character, reporting an error if appropriate.
fn report_unknown_option<SE: brush_core::ShellExtensions>(
context: &brush_core::ExecutionContext<'_, SE>,
spec: &OptionSpec,
c: char,
) -> Result<(String, Option<String>), brush_core::Error> {
if !spec.silent_errors && is_opterr_enabled(context) {
writeln!(context.stderr(), "getopts: illegal option -- {c}")?;
}
let optarg = if spec.silent_errors {
Some(String::from(c))
} else {
None
};
Ok((String::from("?"), optarg))
}
/// Writes the parsing result back into shell variables: the target variable,
/// OPTARG, OPTIND, and the internal `__GETOPTS_LAST_OPTIND` tracker.
fn update_variables<SE: brush_core::ShellExtensions>(
context: &mut brush_core::ExecutionContext<'_, SE>,
variable_name: &str,
result: GetOptsResult,
) -> Result<ExecutionResult, brush_core::Error> {
// Update variable value.
context.shell.env_mut().update_or_add(
variable_name,
variables::ShellValueLiteral::Scalar(result.variable_value),
|_| Ok(()),
brush_core::env::EnvironmentLookup::Anywhere,
brush_core::env::EnvironmentScope::Global,
)?;
// Update OPTARG
if let Some(optarg) = result.optarg {
context.shell.env_mut().update_or_add(
"OPTARG",
variables::ShellValueLiteral::Scalar(optarg),
|_| Ok(()),
brush_core::env::EnvironmentLookup::Anywhere,
brush_core::env::EnvironmentScope::Global,
)?;
} else {
context.shell.env_mut().unset("OPTARG")?;
}
// Update OPTIND and record it so we can detect external modifications.
let optind_str = result.optind.to_string();
context.shell.env_mut().update_or_add(
"OPTIND",
variables::ShellValueLiteral::Scalar(optind_str.clone()),
|_| Ok(()),
brush_core::env::EnvironmentLookup::Anywhere,
brush_core::env::EnvironmentScope::Global,
)?;
context.shell.env_mut().update_or_add(
VAR_GETOPTS_LAST_OPTIND,
variables::ShellValueLiteral::Scalar(optind_str),
|v| {
v.hide_from_enumeration();
Ok(())
},
brush_core::env::EnvironmentLookup::Anywhere,
brush_core::env::EnvironmentScope::Global,
)?;
Ok(result.exit_code)
}
/// Returns whether OPTERR is enabled (i.e., getopts should print error messages).
/// OPTERR defaults to 1; any nonzero value means errors are enabled.
fn is_opterr_enabled<SE: brush_core::ShellExtensions>(
context: &brush_core::ExecutionContext<'_, SE>,
) -> bool {
context
.shell
.env_str("OPTERR")
.is_none_or(|s| s.parse::<i64>().unwrap_or(1) != 0)
}