nubia/internal/parser.py (64 lines of code) (raw):

#!/usr/bin/env python3 # Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # import pyparsing as pp from nubia.internal.exceptions import CommandParseError allowed_symbols_in_string = r"-_/#@£$€%*+~|<>?." def _no_transform(x): return x def _bool_transform(x): return x in ["True", "true"] def _str_transform(x): return x.strip("\"'") _TRANSFORMS = { "bool": _bool_transform, "str": _str_transform, "int": int, "float": float, "dict": dict, } def _parse_type(datatype): transform = _TRANSFORMS.get(datatype, _no_transform) def _parse(s, loc, toks): return list(map(transform, toks)) return _parse identifier = pp.Word(pp.alphas + "_-", pp.alphanums + "_-") int_value = pp.Regex(r"\-?\d+").setParseAction(_parse_type("int")) float_value = pp.Regex(r"\-?\d+\.\d*([eE]\d+)?").setParseAction(_parse_type("float")) bool_value = ( pp.Literal("True") ^ pp.Literal("true") ^ pp.Literal("False") ^ pp.Literal("false") ).setParseAction(_parse_type("bool")) # may have spaces quoted_string = pp.quotedString.setParseAction(_parse_type("str")) # cannot have spaces unquoted_string = pp.Word(pp.alphanums + allowed_symbols_in_string).setParseAction( _parse_type("str") ) string_value = quoted_string | unquoted_string single_value = bool_value | float_value | string_value | int_value list_value = pp.Group( pp.Suppress("[") + pp.Optional(pp.delimitedList(single_value)) + pp.Suppress("]") ).setParseAction(_parse_type("list")) # because this is a recursive construct, a dict can contain dicts in values dict_value = pp.Forward() value = list_value ^ single_value ^ dict_value dict_key_value = pp.dictOf(string_value + pp.Suppress(":"), value) dict_value << pp.Group( pp.Suppress("{") + pp.delimitedList(dict_key_value) + pp.Suppress("}") ).setParseAction(_parse_type("dict")) # Positionals must be end of line or has a space (or more) afterwards. # This is to ensure that the parser treats text like "something=" as invalid # instead of parsing this as positional "something" and leaving the "=" as # invalid on its own. positionals = pp.ZeroOrMore( value + (pp.StringEnd() ^ pp.Suppress(pp.OneOrMore(pp.White()))) ).setResultsName("positionals") key_value = pp.Dict( pp.ZeroOrMore(pp.Group(identifier + pp.Suppress("=") + value)) ).setResultsName("kv") subcommand = identifier.setResultsName("__subcommand__") # Subcommand is optional here as it maybe missing, in this case we still want to # pass the parsing and we will handle the fact that the subcommand is missing # while validating the arguments command_with_subcommand = pp.Optional(subcommand) + key_value + positionals # Positionals will be passed as the last argument command = key_value + positionals def parse(text: str, expect_subcommand: bool) -> pp.ParseResults: expected_pattern = command_with_subcommand if expect_subcommand else command try: result = expected_pattern.parseString(text, parseAll=True) return result except pp.ParseException as e: exception = CommandParseError(str(e)) remaining = e.markInputline() partial_result = expected_pattern.parseString(text, parseAll=False) exception.remaining = remaining[(remaining.find(">!<") + 3) :] exception.partial_result = partial_result exception.col = e.col raise exception