Serenity Operating System
at master 283 lines 9.5 kB view raw
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 9namespace AK { 10 11void OptionParser::reset_state() 12{ 13 m_arg_index = 0; 14 m_consumed_args = 0; 15 m_index_into_multioption_argument = 0; 16 m_stop_on_first_non_option = false; 17} 18 19OptionParser::GetOptResult OptionParser::getopt(Span<StringView> args, StringView short_options, Span<Option const> long_options, Optional<int&> out_long_option_index) 20{ 21 m_args = args; 22 m_short_options = short_options; 23 m_long_options = long_options; 24 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 m_stop_on_first_non_option = short_options.starts_with('+'); 34 35 bool should_reorder_argv = !m_stop_on_first_non_option; 36 int res = -1; 37 38 bool found_an_option = find_next_option(); 39 auto arg = current_arg(); 40 41 if (!found_an_option) { 42 res = -1; 43 if (arg == "--") 44 m_consumed_args = 1; 45 else 46 m_consumed_args = 0; 47 } else { 48 // Alright, so we have an option on our hands! 49 bool is_long_option = arg.starts_with("--"sv); 50 if (is_long_option) 51 res = handle_long_option(); 52 else 53 res = handle_short_option(); 54 55 // If we encountered an error, return immediately. 56 if (res == '?') { 57 return { 58 .result = '?', 59 .optopt_value = m_optopt_value, 60 .optarg_value = m_optarg_value, 61 .consumed_args = 0, 62 }; 63 } 64 } 65 66 if (should_reorder_argv) 67 shift_argv(); 68 69 m_arg_index += m_consumed_args; 70 71 return { 72 .result = res, 73 .optopt_value = m_optopt_value, 74 .optarg_value = m_optarg_value, 75 .consumed_args = m_consumed_args, 76 }; 77} 78 79Optional<OptionParser::ArgumentRequirement> OptionParser::lookup_short_option_requirement(char option) const 80{ 81 Vector<StringView> parts = m_short_options.split_view(option, SplitBehavior::KeepEmpty); 82 83 VERIFY(parts.size() <= 2); 84 if (parts.size() < 2) { 85 // Haven't found the option in the spec. 86 return {}; 87 } 88 89 if (parts[1].starts_with("::"sv)) { 90 // If an option is followed by two colons, it optionally accepts an 91 // argument. 92 return ArgumentRequirement::HasOptionalArgument; 93 } 94 if (parts[1].starts_with(':')) { 95 // If it's followed by one colon, it requires an argument. 96 return ArgumentRequirement::HasRequiredArgument; 97 } 98 // Otherwise, it doesn't accept arguments. 99 return ArgumentRequirement::NoArgument; 100} 101 102int OptionParser::handle_short_option() 103{ 104 StringView arg = current_arg(); 105 VERIFY(arg.starts_with('-')); 106 107 if (m_index_into_multioption_argument == 0) { 108 // Just starting to parse this argument, skip the "-". 109 m_index_into_multioption_argument = 1; 110 } 111 char option = arg[m_index_into_multioption_argument]; 112 m_index_into_multioption_argument++; 113 114 auto maybe_requirement = lookup_short_option_requirement(option); 115 if (!maybe_requirement.has_value()) { 116 m_optopt_value = option; 117 reportln("Unrecognized option \x1b[1m-{:c}\x1b[22m", option); 118 return '?'; 119 } 120 121 auto argument_requirement = *maybe_requirement; 122 123 // Let's see if we're at the end of this argument already. 124 if (m_index_into_multioption_argument < arg.length()) { 125 // This not yet the end. 126 if (argument_requirement == ArgumentRequirement::NoArgument) { 127 m_optarg_value = {}; 128 m_consumed_args = 0; 129 } else { 130 // Treat the rest of the argument as the value, the "-ovalue" 131 // syntax. 132 m_optarg_value = m_args[m_arg_index].substring_view(m_index_into_multioption_argument); 133 // Next time, process the next argument. 134 m_index_into_multioption_argument = 0; 135 m_consumed_args = 1; 136 } 137 } else { 138 m_index_into_multioption_argument = 0; 139 if (argument_requirement != ArgumentRequirement::HasRequiredArgument) { 140 m_optarg_value = StringView(); 141 m_consumed_args = 1; 142 } else if (m_arg_index + 1 < m_args.size()) { 143 // Treat the next argument as a value, the "-o value" syntax. 144 m_optarg_value = m_args[m_arg_index + 1]; 145 m_consumed_args = 2; 146 } else { 147 reportln("Missing value for option \x1b[1m-{:c}\x1b[22m", option); 148 return '?'; 149 } 150 } 151 152 return option; 153} 154 155Optional<OptionParser::Option const&> OptionParser::lookup_long_option(StringView arg) const 156{ 157 for (size_t index = 0; index < m_long_options.size(); index++) { 158 auto& option = m_long_options[index]; 159 160 if (!arg.starts_with(option.name)) 161 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 if (m_out_long_option_index.has_value()) 166 *m_out_long_option_index = index; 167 168 // Can either be "--option" or "--option=value". 169 if (arg.length() == option.name.length()) { 170 m_optarg_value = {}; 171 return option; 172 } 173 174 if (arg[option.name.length()] == '=') { 175 m_optarg_value = arg.substring_view(option.name.length() + 1); 176 return option; 177 } 178 } 179 180 return {}; 181} 182 183int OptionParser::handle_long_option() 184{ 185 VERIFY(current_arg().starts_with("--"sv)); 186 187 // We cannot set optopt to anything sensible for long options, so set it to 0. 188 m_optopt_value = 0; 189 190 auto option = lookup_long_option(m_args[m_arg_index].substring_view(2)); 191 if (!option.has_value()) { 192 reportln("Unrecognized option \x1b[1m{}\x1b[22m", m_args[m_arg_index]); 193 return '?'; 194 } 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 switch (option->requirement) { 202 case ArgumentRequirement::NoArgument: 203 if (m_optarg_value.has_value()) { 204 reportln("Option \x1b[1m--{}\x1b[22m doesn't accept an argument", option->name); 205 return '?'; 206 } 207 m_consumed_args = 1; 208 break; 209 case ArgumentRequirement::HasOptionalArgument: 210 m_consumed_args = 1; 211 break; 212 case ArgumentRequirement::HasRequiredArgument: 213 if (m_optarg_value.has_value()) { 214 // Value specified using "--option=value" syntax. 215 m_consumed_args = 1; 216 } else if (m_arg_index + 1 < m_args.size()) { 217 // Treat the next argument as a value in "--option value" syntax. 218 m_optarg_value = m_args[m_arg_index + 1]; 219 m_consumed_args = 2; 220 } else { 221 reportln("Missing value for option \x1b[1m--{}\x1b[22m", option->name); 222 return '?'; 223 } 224 break; 225 default: 226 VERIFY_NOT_REACHED(); 227 } 228 229 // Now that we've figured the value out, see about reporting this option to 230 // our caller. 231 if (option->flag != nullptr) { 232 *option->flag = option->val; 233 return 0; 234 } 235 return option->val; 236} 237 238void OptionParser::shift_argv() 239{ 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 if (m_consumed_args == 0 || m_skipped_arguments == 0) { 243 // Nothing to do! 244 return; 245 } 246 // x -a b c d 247 // ---- consumed 248 // -> 249 // -a b x c d 250 251 StringView buffer[2]; // We consume at most 2 arguments in one call. 252 Span<StringView> buffer_bytes { buffer, array_size(buffer) }; 253 m_args.slice(m_arg_index, m_consumed_args).copy_to(buffer_bytes); 254 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 buffer_bytes.slice(0, m_consumed_args).copy_to(m_args.slice(m_arg_index - m_skipped_arguments, m_consumed_args)); 256} 257 258bool OptionParser::find_next_option() 259{ 260 for (m_skipped_arguments = 0; m_arg_index < m_args.size(); m_skipped_arguments++, m_arg_index++) { 261 StringView arg = current_arg(); 262 // Anything that doesn't start with a "-" is not an option. 263 // As a special case, a single "-" is not an option either. 264 // (It's typically used by programs to refer to stdin). 265 if (!arg.starts_with('-') || arg == "-") { 266 if (m_stop_on_first_non_option) 267 return false; 268 continue; 269 } 270 271 // As another special case, a "--" is not an option either, and we stop 272 // looking for further options if we encounter it. 273 if (arg == "--") 274 return false; 275 // Otherwise, we have found an option! 276 return true; 277 } 278 279 // Reached the end and still found no options. 280 return false; 281} 282 283}