/src/serenity/AK/OptionParser.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/OptionParser.h> |
8 | | |
9 | | namespace AK { |
10 | | |
11 | | void OptionParser::reset_state() |
12 | 0 | { |
13 | 0 | m_arg_index = 0; |
14 | 0 | m_consumed_args = 0; |
15 | 0 | m_index_into_multioption_argument = 0; |
16 | 0 | m_stop_on_first_non_option = false; |
17 | 0 | } |
18 | | |
19 | | OptionParser::GetOptResult OptionParser::getopt(Span<StringView> args, StringView short_options, Span<Option const> long_options, Optional<int&> out_long_option_index) |
20 | 0 | { |
21 | 0 | m_args = args; |
22 | 0 | m_short_options = short_options; |
23 | 0 | m_long_options = long_options; |
24 | 0 | m_out_long_option_index = out_long_option_index; |
25 | | |
26 | | // In the following case: |
27 | | // $ foo bar -o baz |
28 | | // we want to parse the option (-o baz) first, and leave the argument (bar) |
29 | | // in argv after we return -1 when invoked the second time. So we reorder |
30 | | // argv to put options first and positional arguments next. To turn this |
31 | | // behavior off, start the short options spec with a "+". This is a GNU |
32 | | // extension that we support. |
33 | 0 | m_stop_on_first_non_option = short_options.starts_with('+'); |
34 | |
|
35 | 0 | bool should_reorder_argv = !m_stop_on_first_non_option; |
36 | 0 | int res = -1; |
37 | |
|
38 | 0 | bool found_an_option = find_next_option(); |
39 | 0 | auto arg = current_arg(); |
40 | |
|
41 | 0 | if (!found_an_option) { |
42 | 0 | res = -1; |
43 | 0 | if (arg == "--") |
44 | 0 | m_consumed_args = 1; |
45 | 0 | else |
46 | 0 | m_consumed_args = 0; |
47 | 0 | } else { |
48 | | // Alright, so we have an option on our hands! |
49 | 0 | bool is_long_option = arg.starts_with("--"sv); |
50 | 0 | if (is_long_option) |
51 | 0 | res = handle_long_option(); |
52 | 0 | else |
53 | 0 | res = handle_short_option(); |
54 | | |
55 | | // If we encountered an error, return immediately. |
56 | 0 | if (res == '?') { |
57 | 0 | return { |
58 | 0 | .result = '?', |
59 | 0 | .optopt_value = m_optopt_value, |
60 | 0 | .optarg_value = m_optarg_value, |
61 | 0 | .consumed_args = 0, |
62 | 0 | }; |
63 | 0 | } |
64 | 0 | } |
65 | | |
66 | 0 | if (should_reorder_argv) |
67 | 0 | shift_argv(); |
68 | |
|
69 | 0 | m_arg_index += m_consumed_args; |
70 | |
|
71 | 0 | return { |
72 | 0 | .result = res, |
73 | 0 | .optopt_value = m_optopt_value, |
74 | 0 | .optarg_value = m_optarg_value, |
75 | 0 | .consumed_args = m_consumed_args, |
76 | 0 | }; |
77 | 0 | } |
78 | | |
79 | | Optional<OptionParser::ArgumentRequirement> OptionParser::lookup_short_option_requirement(char option) const |
80 | 0 | { |
81 | 0 | Vector<StringView> parts = m_short_options.split_view(option, SplitBehavior::KeepEmpty); |
82 | |
|
83 | 0 | VERIFY(parts.size() <= 2); |
84 | 0 | if (parts.size() < 2) { |
85 | | // Haven't found the option in the spec. |
86 | 0 | return {}; |
87 | 0 | } |
88 | | |
89 | 0 | if (parts[1].starts_with("::"sv)) { |
90 | | // If an option is followed by two colons, it optionally accepts an |
91 | | // argument. |
92 | 0 | return ArgumentRequirement::HasOptionalArgument; |
93 | 0 | } |
94 | 0 | if (parts[1].starts_with(':')) { |
95 | | // If it's followed by one colon, it requires an argument. |
96 | 0 | return ArgumentRequirement::HasRequiredArgument; |
97 | 0 | } |
98 | | // Otherwise, it doesn't accept arguments. |
99 | 0 | return ArgumentRequirement::NoArgument; |
100 | 0 | } |
101 | | |
102 | | int OptionParser::handle_short_option() |
103 | 0 | { |
104 | 0 | StringView arg = current_arg(); |
105 | 0 | VERIFY(arg.starts_with('-')); |
106 | | |
107 | 0 | if (m_index_into_multioption_argument == 0) { |
108 | | // Just starting to parse this argument, skip the "-". |
109 | 0 | m_index_into_multioption_argument = 1; |
110 | 0 | } |
111 | 0 | char option = arg[m_index_into_multioption_argument]; |
112 | 0 | m_index_into_multioption_argument++; |
113 | |
|
114 | 0 | auto maybe_requirement = lookup_short_option_requirement(option); |
115 | 0 | if (!maybe_requirement.has_value()) { |
116 | 0 | m_optopt_value = option; |
117 | 0 | reportln("Unrecognized option \x1b[1m-{:c}\x1b[22m", option); |
118 | 0 | return '?'; |
119 | 0 | } |
120 | | |
121 | 0 | auto argument_requirement = *maybe_requirement; |
122 | | |
123 | | // Let's see if we're at the end of this argument already. |
124 | 0 | if (m_index_into_multioption_argument < arg.length()) { |
125 | | // This not yet the end. |
126 | 0 | if (argument_requirement == ArgumentRequirement::NoArgument) { |
127 | 0 | m_optarg_value = {}; |
128 | 0 | m_consumed_args = 0; |
129 | 0 | } else { |
130 | | // Treat the rest of the argument as the value, the "-ovalue" |
131 | | // syntax. |
132 | 0 | m_optarg_value = m_args[m_arg_index].substring_view(m_index_into_multioption_argument); |
133 | | // Next time, process the next argument. |
134 | 0 | m_index_into_multioption_argument = 0; |
135 | 0 | m_consumed_args = 1; |
136 | 0 | } |
137 | 0 | } else { |
138 | 0 | m_index_into_multioption_argument = 0; |
139 | 0 | if (argument_requirement != ArgumentRequirement::HasRequiredArgument) { |
140 | 0 | m_optarg_value = StringView(); |
141 | 0 | m_consumed_args = 1; |
142 | 0 | } else if (m_arg_index + 1 < m_args.size()) { |
143 | | // Treat the next argument as a value, the "-o value" syntax. |
144 | 0 | m_optarg_value = m_args[m_arg_index + 1]; |
145 | 0 | m_consumed_args = 2; |
146 | 0 | } else { |
147 | 0 | reportln("Missing value for option \x1b[1m-{:c}\x1b[22m", option); |
148 | 0 | return '?'; |
149 | 0 | } |
150 | 0 | } |
151 | | |
152 | 0 | return option; |
153 | 0 | } |
154 | | |
155 | | Optional<OptionParser::Option const&> OptionParser::lookup_long_option(StringView arg) const |
156 | 0 | { |
157 | 0 | for (size_t index = 0; index < m_long_options.size(); index++) { |
158 | 0 | auto& option = m_long_options[index]; |
159 | |
|
160 | 0 | if (!arg.starts_with(option.name)) |
161 | 0 | continue; |
162 | | |
163 | | // It would be better to not write out the index at all unless we're |
164 | | // sure we've found the right option, but whatever. |
165 | 0 | if (m_out_long_option_index.has_value()) |
166 | 0 | *m_out_long_option_index = index; |
167 | | |
168 | | // Can either be "--option" or "--option=value". |
169 | 0 | if (arg.length() == option.name.length()) { |
170 | 0 | m_optarg_value = {}; |
171 | 0 | return option; |
172 | 0 | } |
173 | | |
174 | 0 | if (arg[option.name.length()] == '=') { |
175 | 0 | m_optarg_value = arg.substring_view(option.name.length() + 1); |
176 | 0 | return option; |
177 | 0 | } |
178 | 0 | } |
179 | | |
180 | 0 | return {}; |
181 | 0 | } |
182 | | |
183 | | int OptionParser::handle_long_option() |
184 | 0 | { |
185 | 0 | VERIFY(current_arg().starts_with("--"sv)); |
186 | | |
187 | | // We cannot set optopt to anything sensible for long options, so set it to 0. |
188 | 0 | m_optopt_value = 0; |
189 | |
|
190 | 0 | auto option = lookup_long_option(m_args[m_arg_index].substring_view(2)); |
191 | 0 | if (!option.has_value()) { |
192 | 0 | reportln("Unrecognized option \x1b[1m{}\x1b[22m", m_args[m_arg_index]); |
193 | 0 | return '?'; |
194 | 0 | } |
195 | | // lookup_long_option() will also set an override for optarg if the value of the option is |
196 | | // specified using "--option=value" syntax. |
197 | | |
198 | | // Figure out whether this option needs and/or has a value (also called "an |
199 | | // argument", but let's not call it that to distinguish it from argv |
200 | | // elements). |
201 | 0 | switch (option->requirement) { |
202 | 0 | case ArgumentRequirement::NoArgument: |
203 | 0 | if (m_optarg_value.has_value()) { |
204 | 0 | reportln("Option \x1b[1m--{}\x1b[22m doesn't accept an argument", option->name); |
205 | 0 | return '?'; |
206 | 0 | } |
207 | 0 | m_consumed_args = 1; |
208 | 0 | break; |
209 | 0 | case ArgumentRequirement::HasOptionalArgument: |
210 | 0 | m_consumed_args = 1; |
211 | 0 | break; |
212 | 0 | case ArgumentRequirement::HasRequiredArgument: |
213 | 0 | if (m_optarg_value.has_value()) { |
214 | | // Value specified using "--option=value" syntax. |
215 | 0 | m_consumed_args = 1; |
216 | 0 | } else if (m_arg_index + 1 < m_args.size()) { |
217 | | // Treat the next argument as a value in "--option value" syntax. |
218 | 0 | m_optarg_value = m_args[m_arg_index + 1]; |
219 | 0 | m_consumed_args = 2; |
220 | 0 | } else { |
221 | 0 | reportln("Missing value for option \x1b[1m--{}\x1b[22m", option->name); |
222 | 0 | return '?'; |
223 | 0 | } |
224 | 0 | break; |
225 | 0 | default: |
226 | 0 | VERIFY_NOT_REACHED(); |
227 | 0 | } |
228 | | |
229 | | // Now that we've figured the value out, see about reporting this option to |
230 | | // our caller. |
231 | 0 | if (option->flag != nullptr) { |
232 | 0 | *option->flag = option->val; |
233 | 0 | return 0; |
234 | 0 | } |
235 | 0 | return option->val; |
236 | 0 | } |
237 | | |
238 | | void OptionParser::shift_argv() |
239 | 0 | { |
240 | | // We've just parsed an option (which perhaps has a value). |
241 | | // Put the option (along with its value, if any) in front of other arguments. |
242 | 0 | if (m_consumed_args == 0 && m_skipped_arguments == 0) { |
243 | | // Nothing to do! |
244 | 0 | return; |
245 | 0 | } |
246 | | // x -a b c d |
247 | | // ---- consumed |
248 | | // -> |
249 | | // -a b x c d |
250 | | |
251 | 0 | StringView buffer[2]; // We consume at most 2 arguments in one call. |
252 | 0 | Span<StringView> buffer_bytes { buffer, array_size(buffer) }; |
253 | 0 | m_args.slice(m_arg_index, m_consumed_args).copy_to(buffer_bytes); |
254 | 0 | m_args.slice(m_arg_index - m_skipped_arguments, m_skipped_arguments).copy_to(m_args.slice(m_arg_index + m_consumed_args - m_skipped_arguments)); |
255 | 0 | buffer_bytes.slice(0, m_consumed_args).copy_to(m_args.slice(m_arg_index - m_skipped_arguments, m_consumed_args)); |
256 | | |
257 | | // m_arg_index took into account m_skipped_arguments (both are inc in find_next_option) |
258 | | // so now we have to make m_arg_index point to the beginning of skipped arguments |
259 | 0 | m_arg_index -= m_skipped_arguments; |
260 | | // and let's forget about skipped arguments |
261 | 0 | m_skipped_arguments = 0; |
262 | 0 | } |
263 | | |
264 | | bool OptionParser::find_next_option() |
265 | 0 | { |
266 | 0 | for (m_skipped_arguments = 0; m_arg_index < m_args.size(); m_skipped_arguments++, m_arg_index++) { |
267 | 0 | StringView arg = current_arg(); |
268 | | // Anything that doesn't start with a "-" is not an option. |
269 | | // As a special case, a single "-" is not an option either. |
270 | | // (It's typically used by programs to refer to stdin). |
271 | 0 | if (!arg.starts_with('-') || arg == "-") { |
272 | 0 | if (m_stop_on_first_non_option) |
273 | 0 | return false; |
274 | 0 | continue; |
275 | 0 | } |
276 | | |
277 | | // As another special case, a "--" is not an option either, and we stop |
278 | | // looking for further options if we encounter it. |
279 | 0 | if (arg == "--") |
280 | 0 | return false; |
281 | | // Otherwise, we have found an option! |
282 | 0 | return true; |
283 | 0 | } |
284 | | |
285 | | // Reached the end and still found no options. |
286 | 0 | return false; |
287 | 0 | } |
288 | | |
289 | | } |