• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #ifndef ART_CMDLINE_DETAIL_CMDLINE_PARSE_ARGUMENT_DETAIL_H_
18 #define ART_CMDLINE_DETAIL_CMDLINE_PARSE_ARGUMENT_DETAIL_H_
19 
20 #include <assert.h>
21 #include <algorithm>
22 #include <functional>
23 #include <memory>
24 #include <numeric>
25 #include <string_view>
26 #include <type_traits>
27 #include <vector>
28 
29 #include "android-base/strings.h"
30 
31 #include "base/indenter.h"
32 #include "cmdline_parse_result.h"
33 #include "cmdline_types.h"
34 #include "token_range.h"
35 #include "unit.h"
36 
37 namespace art {
38 // Implementation details for the parser. Do not look inside if you hate templates.
39 namespace detail {
40 
41 // A non-templated base class for argument parsers. Used by the general parser
42 // to parse arguments, without needing to know the argument type at compile time.
43 //
44 // This is an application of the type erasure idiom.
45 struct CmdlineParseArgumentAny {
~CmdlineParseArgumentAnyCmdlineParseArgumentAny46   virtual ~CmdlineParseArgumentAny() {}
47 
48   // Attempt to parse this argument starting at arguments[position].
49   // If the parsing succeeds, the parsed value will be saved as a side-effect.
50   //
51   // In most situations, the parsing will not match by returning kUnknown. In this case,
52   // no tokens were consumed and the position variable will not be updated.
53   //
54   // At other times, parsing may fail due to validation but the initial token was still matched
55   // (for example an out of range value, or passing in a string where an int was expected).
56   // In this case the tokens are still consumed, and the position variable will get incremented
57   // by all the consumed tokens.
58   //
59   // The # of tokens consumed by the parse attempt will be set as an out-parameter into
60   // consumed_tokens. The parser should skip this many tokens before parsing the next
61   // argument.
62   virtual CmdlineResult ParseArgument(const TokenRange& arguments, size_t* consumed_tokens) = 0;
63   // How many tokens should be taken off argv for parsing this argument.
64   // For example "--help" is just 1, "-compiler-option _" would be 2 (since there's a space).
65   //
66   // A [min,max] range is returned to represent argument definitions with multiple
67   // value tokens. (e.g. {"-h", "-h " } would return [1,2]).
68   virtual std::pair<size_t, size_t> GetNumTokens() const = 0;
69   // Get the run-time typename of the argument type.
70   virtual const char* GetTypeName() const = 0;
71   // Try to do a close match, returning how many tokens were matched against this argument
72   // definition. More tokens is better.
73   //
74   // Do a quick match token-by-token, and see if they match.
75   // Any tokens with a wildcard in them are only matched up until the wildcard.
76   // If this is true, then the wildcard matching later on can still fail, so this is not
77   // a guarantee that the argument is correct, it's more of a strong hint that the
78   // user-provided input *probably* was trying to match this argument.
79   //
80   // Returns how many tokens were either matched (or ignored because there was a
81   // wildcard present). 0 means no match. If the Size() tokens are returned.
82   virtual size_t MaybeMatches(const TokenRange& tokens) = 0;
83 
84   virtual void DumpHelp(VariableIndentationOutputStream& os) = 0;
85 
86   virtual const std::optional<const char*>& GetCategory() = 0;
87 };
88 
89 template <typename T>
90 using EnableIfNumeric = std::enable_if<std::is_arithmetic<T>::value>;
91 
92 template <typename T>
93 using DisableIfNumeric = std::enable_if<!std::is_arithmetic<T>::value>;
94 
95 // Argument definition information, created by an ArgumentBuilder and an UntypedArgumentBuilder.
96 template <typename TArg>
97 struct CmdlineParserArgumentInfo {
98   // This version will only be used if TArg is arithmetic and thus has the <= operators.
99   template <typename T = TArg>  // Necessary to get SFINAE to kick in.
100   bool CheckRange(const TArg& value, typename EnableIfNumeric<T>::type* = nullptr) {
101     if (has_range_) {
102       return min_ <= value && value <= max_;
103     }
104     return true;
105   }
106 
107   // This version will be used at other times when TArg is not arithmetic.
108   template <typename T = TArg>
109   bool CheckRange(const TArg&, typename DisableIfNumeric<T>::type* = nullptr) {
110     assert(!has_range_);
111     return true;
112   }
113 
114   // Do a quick match token-by-token, and see if they match.
115   // Any tokens with a wildcard in them only match the prefix up until the wildcard.
116   //
117   // If this is true, then the wildcard matching later on can still fail, so this is not
118   // a guarantee that the argument is correct, it's more of a strong hint that the
119   // user-provided input *probably* was trying to match this argument.
MaybeMatchesCmdlineParserArgumentInfo120   size_t MaybeMatches(const TokenRange& token_list) const {
121     auto best_match = FindClosestMatch(token_list);
122 
123     return best_match.second;
124   }
125 
126   // Attempt to find the closest match (see MaybeMatches).
127   //
128   // Returns the token range that was the closest match and the # of tokens that
129   // this range was matched up until.
FindClosestMatchCmdlineParserArgumentInfo130   std::pair<const TokenRange*, size_t> FindClosestMatch(const TokenRange& token_list) const {
131     const TokenRange* best_match_ptr = nullptr;
132 
133     size_t best_match = 0;
134     for (auto&& token_range : tokenized_names_) {
135       size_t this_match = token_range.MaybeMatches(token_list, std::string("_"));
136 
137       if (this_match > best_match) {
138         best_match_ptr = &token_range;
139         best_match = this_match;
140       }
141     }
142 
143     return std::make_pair(best_match_ptr, best_match);
144   }
145 
146   template <typename T = TArg>  // Necessary to get SFINAE to kick in.
DumpHelpCmdlineParserArgumentInfo147   void DumpHelp(VariableIndentationOutputStream& vios) {
148     // Separate arguments
149     vios.Stream() << std::endl;
150     for (auto cname : names_) {
151       std::string_view name = cname;
152       if (using_blanks_) {
153         name = name.substr(0, name.find('_'));
154       }
155       auto& os = vios.Stream();
156       auto print_once = [&]() {
157         os << name;
158         if (using_blanks_) {
159           if (has_value_map_) {
160             bool first = true;
161             for (auto [val, unused] : value_map_) {
162               os << (first ? "{" : "|") << val;
163               first = false;
164             }
165             os << "}";
166           } else if (metavar_.has_value()) {
167             os << *metavar_;
168           } else {
169             os << "{" << CmdlineType<T>::DescribeType() << "}";
170           }
171         }
172       };
173       print_once();
174       if (appending_values_) {
175         os << " [";
176         print_once();
177         os << "...]";
178       }
179       os << std::endl;
180     }
181     if (help_.has_value()) {
182       ScopedIndentation si(&vios);
183       vios.Stream() << *help_ << std::endl;
184     }
185   }
186 
187 
188   // Mark the argument definition as completed, do not mutate the object anymore after this
189   // call is done.
190   //
191   // Performs several checks of the validity and token calculations.
CompleteArgumentCmdlineParserArgumentInfo192   void CompleteArgument() {
193     assert(names_.size() >= 1);
194     assert(!is_completed_);
195 
196     is_completed_ = true;
197 
198     size_t blank_count = 0;
199     size_t token_count = 0;
200 
201     size_t global_blank_count = 0;
202     size_t global_token_count = 0;
203     for (auto&& name : names_) {
204       std::string s(name);
205 
206       size_t local_blank_count = std::count(s.begin(), s.end(), '_');
207       size_t local_token_count = std::count(s.begin(), s.end(), ' ');
208 
209       if (global_blank_count != 0) {
210         assert(local_blank_count == global_blank_count
211                && "Every argument descriptor string must have same amount of blanks (_)");
212       }
213 
214       if (local_blank_count != 0) {
215         global_blank_count = local_blank_count;
216         blank_count++;
217 
218         assert(local_blank_count == 1 && "More than one blank is not supported");
219         assert(s.back() == '_' && "The blank character must only be at the end of the string");
220       }
221 
222       if (global_token_count != 0) {
223         assert(local_token_count == global_token_count
224                && "Every argument descriptor string must have same amount of tokens (spaces)");
225       }
226 
227       if (local_token_count != 0) {
228         global_token_count = local_token_count;
229         token_count++;
230       }
231 
232       // Tokenize every name, turning it from a string to a token list.
233       tokenized_names_.clear();
234       for (auto&& name1 : names_) {
235         // Split along ' ' only, removing any duplicated spaces.
236         tokenized_names_.push_back(
237             TokenRange::Split(name1, {' '}).RemoveToken(" "));
238       }
239 
240       // remove the _ character from each of the token ranges
241       // we will often end up with an empty token (i.e. ["-XX", "_"] -> ["-XX", ""]
242       // and this is OK because we still need an empty token to simplify
243       // range comparisons
244       simple_names_.clear();
245 
246       for (auto&& tokenized_name : tokenized_names_) {
247         simple_names_.push_back(tokenized_name.RemoveCharacter('_'));
248       }
249     }
250 
251     if (token_count != 0) {
252       assert(("Every argument descriptor string must have equal amount of tokens (spaces)" &&
253           token_count == names_.size()));
254     }
255 
256     if (blank_count != 0) {
257       assert(("Every argument descriptor string must have an equal amount of blanks (_)" &&
258           blank_count == names_.size()));
259     }
260 
261     using_blanks_ = blank_count > 0;
262     {
263       size_t smallest_name_token_range_size =
264           std::accumulate(tokenized_names_.begin(), tokenized_names_.end(), ~(0u),
265                           [](size_t min, const TokenRange& cur) {
266                             return std::min(min, cur.Size());
267                           });
268       size_t largest_name_token_range_size =
269           std::accumulate(tokenized_names_.begin(), tokenized_names_.end(), 0u,
270                           [](size_t max, const TokenRange& cur) {
271                             return std::max(max, cur.Size());
272                           });
273 
274       token_range_size_ = std::make_pair(smallest_name_token_range_size,
275                                          largest_name_token_range_size);
276     }
277 
278     if (has_value_list_) {
279       assert(names_.size() == value_list_.size()
280              && "Number of arg descriptors must match number of values");
281       assert(!has_value_map_);
282     }
283     if (has_value_map_) {
284       if (!using_blanks_) {
285         assert(names_.size() == value_map_.size() &&
286                "Since no blanks were specified, each arg is mapped directly into a mapped "
287                "value without parsing; sizes must match");
288       }
289 
290       assert(!has_value_list_);
291     }
292 
293     if (!using_blanks_ && !CmdlineType<TArg>::kCanParseBlankless) {
294       assert((has_value_map_ || has_value_list_) &&
295              "Arguments without a blank (_) must provide either a value map or a value list");
296     }
297 
298     TypedCheck();
299   }
300 
301   // List of aliases for a single argument definition, e.g. {"-Xdex2oat", "-Xnodex2oat"}.
302   std::vector<const char*> names_;
303   // Is there at least 1 wildcard '_' in the argument definition?
304   bool using_blanks_ = false;
305   // [min, max] token counts in each arg def
306   std::pair<size_t, size_t> token_range_size_;
307 
308   // contains all the names in a tokenized form, i.e. as a space-delimited list
309   std::vector<TokenRange> tokenized_names_;
310 
311   // contains the tokenized names, but with the _ character stripped
312   std::vector<TokenRange> simple_names_;
313 
314   // For argument definitions created with '.AppendValues()'
315   // Meaning that parsing should mutate the existing value in-place if possible.
316   bool appending_values_ = false;
317 
318   // For argument definitions created with '.WithRange(min, max)'
319   bool has_range_ = false;
320   TArg min_;
321   TArg max_;
322 
323   // For argument definitions created with '.WithValueMap'
324   bool has_value_map_ = false;
325   std::vector<std::pair<const char*, TArg>> value_map_;
326 
327   // For argument definitions created with '.WithValues'
328   bool has_value_list_ = false;
329   std::vector<TArg> value_list_;
330 
331   std::optional<const char*> help_;
332   std::optional<const char*> category_;
333   std::optional<const char*> metavar_;
334 
335   // Make sure there's a default constructor.
336   CmdlineParserArgumentInfo() = default;
337 
338   // Ensure there's a default move constructor.
339   CmdlineParserArgumentInfo(CmdlineParserArgumentInfo&&) noexcept = default;
340 
341  private:
342   // Perform type-specific checks at runtime.
343   template <typename T = TArg>
344   void TypedCheck(typename std::enable_if<std::is_same<Unit, T>::value>::type* = 0) {
345     assert(!using_blanks_ &&
346            "Blanks are not supported in Unit arguments; since a Unit has no parse-able value");
347   }
348 
TypedCheckCmdlineParserArgumentInfo349   void TypedCheck() {}
350 
351   bool is_completed_ = false;
352 };
353 
354 // A virtual-implementation of the necessary argument information in order to
355 // be able to parse arguments.
356 template <typename TArg>
357 struct CmdlineParseArgument : CmdlineParseArgumentAny {
CmdlineParseArgumentCmdlineParseArgument358   CmdlineParseArgument(CmdlineParserArgumentInfo<TArg>&& argument_info,
359                        std::function<void(TArg&)>&& save_argument,
360                        std::function<TArg&(void)>&& load_argument)
361       : argument_info_(std::forward<decltype(argument_info)>(argument_info)),
362         save_argument_(std::forward<decltype(save_argument)>(save_argument)),
363         load_argument_(std::forward<decltype(load_argument)>(load_argument)) {
364   }
365 
366   using UserTypeInfo = CmdlineType<TArg>;
367 
ParseArgumentCmdlineParseArgument368   virtual CmdlineResult ParseArgument(const TokenRange& arguments, size_t* consumed_tokens) {
369     assert(arguments.Size() > 0);
370     assert(consumed_tokens != nullptr);
371 
372     auto closest_match_res = argument_info_.FindClosestMatch(arguments);
373     size_t best_match_size = closest_match_res.second;
374     const TokenRange* best_match_arg_def = closest_match_res.first;
375 
376     if (best_match_size > arguments.Size()) {
377       // The best match has more tokens than were provided.
378       // Shouldn't happen in practice since the outer parser does this check.
379       return CmdlineResult(CmdlineResult::kUnknown, "Size mismatch");
380     }
381 
382     assert(best_match_arg_def != nullptr);
383     *consumed_tokens = best_match_arg_def->Size();
384 
385     if (!argument_info_.using_blanks_) {
386       return ParseArgumentSingle(arguments.Join(' '));
387     }
388 
389     // Extract out the blank value from arguments
390     // e.g. for a def of "foo:_" and input "foo:bar", blank_value == "bar"
391     std::string blank_value = "";
392     size_t idx = 0;
393     for (auto&& def_token : *best_match_arg_def) {
394       auto&& arg_token = arguments[idx];
395 
396       // Does this definition-token have a wildcard in it?
397       if (def_token.find('_') == std::string::npos) {
398         // No, regular token. Match 1:1 against the argument token.
399         bool token_match = def_token == arg_token;
400 
401         if (!token_match) {
402           return CmdlineResult(CmdlineResult::kFailure,
403                                std::string("Failed to parse ") + best_match_arg_def->GetToken(0)
404                                + " at token " + std::to_string(idx));
405         }
406       } else {
407         // This is a wild-carded token.
408         TokenRange def_split_wildcards = TokenRange::Split(def_token, {'_'});
409 
410         // Extract the wildcard contents out of the user-provided arg_token.
411         std::unique_ptr<TokenRange> arg_matches =
412             def_split_wildcards.MatchSubstrings(arg_token, "_");
413         if (arg_matches == nullptr) {
414           return CmdlineResult(CmdlineResult::kFailure,
415                                std::string("Failed to parse ") + best_match_arg_def->GetToken(0)
416                                + ", with a wildcard pattern " + def_token
417                                + " at token " + std::to_string(idx));
418         }
419 
420         // Get the corresponding wildcard tokens from arg_matches,
421         // and concatenate it to blank_value.
422         for (size_t sub_idx = 0;
423             sub_idx < def_split_wildcards.Size() && sub_idx < arg_matches->Size(); ++sub_idx) {
424           if (def_split_wildcards[sub_idx] == "_") {
425             blank_value += arg_matches->GetToken(sub_idx);
426           }
427         }
428       }
429 
430       ++idx;
431     }
432 
433     return ParseArgumentSingle(blank_value);
434   }
435 
DumpHelpCmdlineParseArgument436   virtual void DumpHelp(VariableIndentationOutputStream& os) {
437     argument_info_.DumpHelp(os);
438   }
439 
GetCategoryCmdlineParseArgument440   virtual const std::optional<const char*>& GetCategory() {
441     return argument_info_.category_;
442   }
443 
444  private:
ParseArgumentSingleCmdlineParseArgument445   virtual CmdlineResult ParseArgumentSingle(const std::string& argument) {
446     // TODO: refactor to use LookupValue for the value lists/maps
447 
448     // Handle the 'WithValueMap(...)' argument definition
449     if (argument_info_.has_value_map_) {
450       for (auto&& value_pair : argument_info_.value_map_) {
451         const char* name = value_pair.first;
452 
453         if (argument == name) {
454           return SaveArgument(value_pair.second);
455         }
456       }
457 
458       // Error case: Fail, telling the user what the allowed values were.
459       std::vector<std::string> allowed_values;
460       for (auto&& value_pair : argument_info_.value_map_) {
461         const char* name = value_pair.first;
462         allowed_values.push_back(name);
463       }
464 
465       std::string allowed_values_flat = android::base::Join(allowed_values, ',');
466       return CmdlineResult(CmdlineResult::kFailure,
467                            "Argument value '" + argument + "' does not match any of known valid "
468                             "values: {" + allowed_values_flat + "}");
469     }
470 
471     // Handle the 'WithValues(...)' argument definition
472     if (argument_info_.has_value_list_) {
473       size_t arg_def_idx = 0;
474       for (auto&& value : argument_info_.value_list_) {
475         auto&& arg_def_token = argument_info_.names_[arg_def_idx];
476 
477         if (arg_def_token == argument) {
478           return SaveArgument(value);
479         }
480         ++arg_def_idx;
481       }
482 
483       assert(arg_def_idx + 1 == argument_info_.value_list_.size() &&
484              "Number of named argument definitions must match number of values defined");
485 
486       // Error case: Fail, telling the user what the allowed values were.
487       std::vector<std::string> allowed_values;
488       allowed_values.reserve(argument_info_.names_.size());
489       for (auto&& arg_name : argument_info_.names_) {
490         allowed_values.push_back(arg_name);
491       }
492 
493       std::string allowed_values_flat = android::base::Join(allowed_values, ',');
494       return CmdlineResult(CmdlineResult::kFailure,
495                            "Argument value '" + argument + "' does not match any of known valid"
496                             "values: {" + allowed_values_flat + "}");
497     }
498 
499     // Handle the regular case where we parsed an unknown value from a blank.
500     UserTypeInfo type_parser;
501 
502     if (argument_info_.appending_values_) {
503       TArg& existing = load_argument_();
504       CmdlineParseResult<TArg> result = type_parser.ParseAndAppend(argument, existing);
505 
506       assert(!argument_info_.has_range_);
507 
508       return std::move(result);
509     }
510 
511     CmdlineParseResult<TArg> result = type_parser.Parse(argument);
512 
513     if (result.IsSuccess()) {
514       TArg& value = result.GetValue();
515 
516       // Do a range check for 'WithRange(min,max)' argument definition.
517       if (!argument_info_.CheckRange(value)) {
518         return CmdlineParseResult<TArg>::OutOfRange(
519             value, argument_info_.min_, argument_info_.max_);
520       }
521 
522       return SaveArgument(value);
523     }
524 
525     // Some kind of type-specific parse error. Pass the result as-is.
526     CmdlineResult raw_result = std::move(result);
527     return raw_result;
528   }
529 
530  public:
GetTypeNameCmdlineParseArgument531   virtual const char* GetTypeName() const {
532     // TODO: Obviate the need for each type specialization to hardcode the type name
533     return UserTypeInfo::Name();
534   }
535 
536   // How many tokens should be taken off argv for parsing this argument.
537   // For example "--help" is just 1, "-compiler-option _" would be 2 (since there's a space).
538   //
539   // A [min,max] range is returned to represent argument definitions with multiple
540   // value tokens. (e.g. {"-h", "-h " } would return [1,2]).
GetNumTokensCmdlineParseArgument541   virtual std::pair<size_t, size_t> GetNumTokens() const {
542     return argument_info_.token_range_size_;
543   }
544 
545   // See if this token range might begin the same as the argument definition.
MaybeMatchesCmdlineParseArgument546   virtual size_t MaybeMatches(const TokenRange& tokens) {
547     return argument_info_.MaybeMatches(tokens);
548   }
549 
550  private:
SaveArgumentCmdlineParseArgument551   CmdlineResult SaveArgument(const TArg& value) {
552     assert(!argument_info_.appending_values_
553            && "If the values are being appended, then the updated parse value is "
554                "updated by-ref as a side effect and shouldn't be stored directly");
555     TArg val = value;
556     save_argument_(val);
557     return CmdlineResult(CmdlineResult::kSuccess);
558   }
559 
560   CmdlineParserArgumentInfo<TArg> argument_info_;
561   std::function<void(TArg&)> save_argument_;
562   std::function<TArg&(void)> load_argument_;
563 };
564 }  // namespace detail  // NOLINT [readability/namespace] [5]
565 }  // namespace art
566 
567 #endif  // ART_CMDLINE_DETAIL_CMDLINE_PARSE_ARGUMENT_DETAIL_H_
568