diff --git a/CHANGELOG b/CHANGELOG index e9152163..982bbe9e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,9 @@ jc changelog -20230122 v1.23.0 +20230125 v1.23.0 +- Add input slicing as a `jc` command-line option - Add `ssh` configuration file parser -- Add input slicing +- Add `ver` Version string parser - Fix `acpi` command parser for "will never fully discharge" battery state 20230111 v1.22.5 diff --git a/jc/lib.py b/jc/lib.py index 1ca94c88..6139acea 100644 --- a/jc/lib.py +++ b/jc/lib.py @@ -190,6 +190,7 @@ parsers: List[str] = [ 'upower', 'uptime', 'url', + 'ver', 'vmstat', 'vmstat-s', 'w', diff --git a/jc/parsers/ver.py b/jc/parsers/ver.py new file mode 100644 index 00000000..a303590a --- /dev/null +++ b/jc/parsers/ver.py @@ -0,0 +1,151 @@ +"""jc - JSON Convert Version string output parser + +Best effort attempt to parse various styles of version numbers. This parser +is based off of the version parser included in the CPython distutils +libary. + +If the version string conforms to some de facto-standard versioning rules +followed by many developers a `strict` key will be present in the output +with a value of `true` along with the named parsed components. + +All other version strings will have a `strict` value of `false` and a +`components` key will contain a list of detected parts of the version +string. + +Usage (cli): + + $ echo '1.2b' | jc --ver + + +Usage (module): + + import jc + result = jc.parse('ver', version_string_output) + +Schema: + + [ + { + "version": string, + "bar": boolean, + "baz": integer + } + ] + +Examples: + + $ echo '1.2b' | jc --ver -p + [] +""" +import re +from typing import List, Dict +from jc.jc_types import JSONDictType +import jc.utils + + +class info(): + """Provides parser metadata (version, author, etc.)""" + version = '1.0' + description = 'Version string parser' + author = 'Kelly Brazil' + author_email = 'kellyjonbrazil@gmail.com' + details = 'Based on distutils/version.py from CPython 3.9.5' + compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd'] + tags = ['generic', 'string'] + + +__version__ = info.version + + +def _process(proc_data: JSONDictType) -> JSONDictType: + """ + Final processing to conform to the schema. + + Parameters: + + proc_data: (List of Dictionaries) raw structured data to process + + Returns: + + List of Dictionaries. Structured to conform to the schema. + """ + return proc_data + + +def strict_parse(vstring): + version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', re.VERBOSE) + match = version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + (major, minor, patch, prerelease, prerelease_num) = \ + match.group(1, 2, 4, 5, 6) + + if patch: + version = tuple(map(int, [major, minor, patch])) + else: + version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + prerelease = (prerelease[0], int(prerelease_num)) + else: + prerelease = None + + return { + 'major': major, + 'minor': minor, + 'patch': patch, + 'prerelease': prerelease, + 'prerelease_num': prerelease_num + } + + +def loose_parse(vstring): + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + components = [x for x in component_re.split(vstring) if x and x != '.'] + + return components + + +def parse( + data: str, + raw: bool = False, + quiet: bool = False +) -> JSONDictType: + """ + Main text parsing function + + Parameters: + + data: (string) text data to parse + raw: (boolean) unprocessed output if True + quiet: (boolean) suppress warning messages if True + + Returns: + + List of Dictionaries. Raw or processed structured data. + """ + jc.utils.compatibility(__name__, info.compatible, quiet) + jc.utils.input_type_check(data) + + raw_output: Dict = {} + strict = True + + if jc.utils.has_data(data): + + # based on distutils/version.py from CPython 3.9.5 + # PSF License (see https://opensource.org/licenses/Python-2.0) + + data = data.strip() + + try: + raw_output = strict_parse(data) + + except ValueError: + raw_output['components'] = loose_parse(data) + strict = False + + if raw_output: + raw_output['strict'] = strict + + return raw_output if raw else _process(raw_output)