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
//! What:
//! Match one `--name[=value]` token against a Git option table as Git 2.56.0 does.
//! Why:
//! Git accepts any unambiguous prefix (`--am` for `--amend`),
//! `--no-` negation and
//! `--no-no-` forms (`parse_long_opt`,
//! parse-options.c:519-594).
//! A list of exact
//! spellings misses forms Git accepts.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // scanLongOption({ args, index, table, mode, parsed }): number // tokens consumed
//! ```
/// What:
/// Bring the tokenizer types and the shared value decision into this file.
/// Why:
/// A long option appends to the same result the short-cluster scan appends to.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { Occurrence, OptionError, OptionSpec, ParsedOptions, ParseMode } from './command_options.ts';
/// import { takeValue } from './command_options_value.ts';
/// ```
use super::command_options::{
Occurrence, OptionError, OptionErrorKind, OptionSpec, OptionValue, ParseMode, ParsedOptions,
};
use super::command_options_value::{Spelling, take_value};
/// `OsString` is owned operating-system text of raw bytes (sibling `String` must be UTF-8).
use std::ffi::OsString;
/// What:
/// The row an abbreviation resolved to,
/// with whether it is used in negated form.
/// Why:
/// Git remembers the latest abbreviation candidate until the whole table is checked.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type Candidate = { spec: OptionSpec; unset: boolean };
/// ```
#[derive(Clone, Copy)]
struct Candidate {
/// The candidate row.
spec: OptionSpec,
/// True when the token turns the option off.
unset: bool,
}
/// What:
/// Record a matched option and say whether it took the next token as its value.
/// `&mut ParsedOptions` lends the result for writing;
/// `bool` is true or false.
/// Why:
/// Exact and abbreviated matches end the same way (`get_value`).
/// The caller visits
/// every token once and only needs to know whether to pass over the next one,
/// so
/// no count is returned that a mistake could turn into "stay on this token".
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function accept({ args, index, spec, unset, attached, parsed }): boolean; // throws OptionError
/// ```
fn accept(
arguments: &[OsString],
index: usize,
chosen: Candidate,
attached: Option<usize>,
parsed: &mut ParsedOptions,
) -> Result<bool, OptionError> {
let spelling: Spelling = if chosen.unset {
Spelling::NegatedLong
} else {
Spelling::Long
};
// `?` returns a refusal to our caller, or unwraps the value position.
let value: Option<OptionValue> =
take_value(arguments, index, chosen.spec.arity, attached, spelling)?;
parsed.occurrences.push(Occurrence {
id: chosen.spec.id,
negated: chosen.unset,
value,
token: index,
});
// `matches!(x, pattern)` is true when `x` has that shape: here, a next-token value.
return Ok(matches!(value, Some(OptionValue::Detached { .. })));
}
/// What:
/// Byte index of the first `=` in `bytes`,
/// or the length when there is none.
/// Why:
/// Git compares abbreviations up to the `=` only (`strchrnul`,
/// parse-options.c:523).
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const end = bytes.indexOf('=') === -1 ? bytes.length : bytes.indexOf('=');
/// ```
fn name_end(bytes: &[u8]) -> usize {
let mut index: usize = 0;
while index < bytes.len() {
if bytes[index] == b'=' {
return index;
}
index += 1;
}
return bytes.len();
}
/// What:
/// Read the long option at `index` and return how many tokens it used (1 or 2).
/// Why:
/// One table walk finds an exact match first and otherwise the unique abbreviation.
/// Gotcha:
/// `OPT_ALIAS` pairs are not modeled.
/// No table in this crate holds an alias and its
/// source that share a prefix,
/// so the alias exception to ambiguity never applies.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function scanLongOption({ args, index, table, mode, parsed }): boolean; // throws OptionError
/// ```
pub(crate) fn scan_long_option(
arguments: &[OsString],
index: usize,
table: &[OptionSpec],
mode: ParseMode,
parsed: &mut ParsedOptions,
) -> Result<bool, OptionError> {
// `.as_encoded_bytes()` lends the raw bytes of the token without decoding them.
let token: &[u8] = arguments[index].as_encoded_bytes();
// Git's `arg`: everything after the two dashes, `=value` included.
let argument: &[u8] = &token[2..];
let argument_name_end: usize = name_end(argument);
// Git's `arg_start`: `argument` without one or two leading `no-` (530-535).
let mut stem: &[u8] = argument;
let mut negated: bool = false;
let mut double_negated: bool = false;
// `.strip_prefix(...)` returns the remainder when the bytes start with the prefix.
if let Some(once) = stem.strip_prefix(b"no-") {
stem = once;
if let Some(twice) = stem.strip_prefix(b"no-") {
stem = twice;
double_negated = true;
} else {
negated = true;
}
}
// The part of `stem` before any `=`: what an abbreviation is compared with.
let stem_name: &[u8] = &stem[..argument_name_end - (argument.len() - stem.len())];
let mut candidate: Option<Candidate> = None;
let mut ambiguous: bool = false;
for spec in table {
// Rows without a long spelling cannot match a long token.
let long: &str = if let Some(spelling) = spec.long {
spelling
} else {
continue;
};
let mut long_name: &[u8] = long.as_bytes();
let mut spec_negative: bool = false;
if let Some(positive) = long_name.strip_prefix(b"no-") {
// A row such as `no-verify` is reached by `--verify` in unset form (547-548).
long_name = positive;
spec_negative = true;
} else if double_negated {
continue;
}
// Git's `flags ^ opt_flags`: the token and the row disagree about `no-`.
let unset: bool = negated != spec_negative;
if unset && !spec.negatable {
continue;
}
if let Some(rest) = stem.strip_prefix(long_name) {
if rest.is_empty() {
let exact: Candidate = Candidate { spec: *spec, unset };
return accept(arguments, index, exact, None, parsed);
}
if rest[0] == b'=' {
let exact: Candidate = Candidate { spec: *spec, unset };
// The value starts one byte after the `=`.
let offset: usize = token.len() - rest.len() + 1;
return accept(arguments, index, exact, Some(offset), parsed);
}
// A longer name sharing this prefix: Git moves to the next row (558-559).
continue;
}
if mode.keep_unknown {
// `register_abbrev` does nothing under `PARSE_OPT_KEEP_UNKNOWN_OPT` (502-503).
continue;
}
if long_name.starts_with(stem_name) {
ambiguous = ambiguous || candidate.is_some();
candidate = Some(Candidate { spec: *spec, unset });
}
// "Negated and abbreviated very much": `--n`, `--no` and `--no-` (568-571).
if spec.negatable && b"no-".starts_with(argument) {
ambiguous = ambiguous || candidate.is_some();
candidate = Some(Candidate {
spec: *spec,
unset: !spec_negative,
});
}
}
if ambiguous {
return Err(OptionError {
kind: OptionErrorKind::AmbiguousOption,
token: index,
});
}
if let Some(chosen) = candidate {
let attached: Option<usize> = if argument_name_end < argument.len() {
// Two dashes, the name, then `=`.
Some(2 + argument_name_end + 1)
} else {
None
};
return accept(arguments, index, chosen, attached, parsed);
}
if mode.keep_unknown {
// The unknown token stands alone: the next token is not its value.
parsed.unknown.push(index);
return Ok(false);
}
return Err(OptionError {
kind: OptionErrorKind::UnknownOption,
token: index,
});
}
/// Abbreviation,
/// ambiguity and negation cases.
#[cfg(test)]
#[path = "command_options_long_tests.rs"]
mod tests;