bcrypt.ctf/bcrypter/cli/opt.py

174 lines
5.9 KiB
Python
Raw Normal View History

2025-06-21 21:29:00 +10:00
from enum import Enum
from dataclass import dataclass
2025-06-21 21:29:00 +10:00
from bcrypter.lib.other import tryget
from bcrypter.exceptions import (
BadOptNameError,
InvalidOptError,
DisjointOverlapError,
)
'''
Overlap dataclass is used to check if two Opts are incompatible
ie same name, same shortform, same longform
'''
@dataclass
class Overlap:
master: Opt # the Opt we measure overlap against
name: list[Opt]
shortform: list[Opt]
longform: list[Opt]
'''
Create an empty Overlap for a master
'''
@classmethod
def empty(cls: Overlap, A: Opt) -> Overlap:
return cls(A, [], [], [])
'''
Calculate the overlap of two objects inheriting from Opt
'''
@classmethod
def of(cls: Overlap, A: Opt, B: Opt) -> Overlap:
# "equal to and not None"
eqnn = lambda x, y: x is not None and x == y
return cls(A,
[B] if eqnn(A.name, B.name) else [],
[B] if eqnn(A.shortform, B.shortform) else [],
[B] if eqnn(A.longform, B.longform) else [])
def __add__(self, O: Overlap) -> Overlap:
if self.master != O.master:
raise DisjointOverlapError()
return Overlap(master,
self.name + O.name,
self.shortform + O.shortform,
self.longform + O.longform)
def is_empty(self) -> bool:
return not (self.name or self.shortform or self.longform):
class ParamType(Enum):
AbstractParam # only used by Param
AbstractOpt # only used by Opt
Arg
2025-06-21 21:29:00 +10:00
Flag
Option
class Param:
_TYPE: ParamType = ParamType.AbstractParam
def __init__(self, name: str, description: str) -> None:
self.name = name
self.description = description
class Opt(Param):
_TYPE: ParamType = ParamType.AbstractOpt
__FAIL_MSG = 'Invalid Opt'
def __init__(self, *args, name: str = '', description: str = '') -> None:
# ensure non-empty name kwarg provided
if not name:
raise BadOptNameError()
super().__init__(name, description)
# result.value is a list of error messages (if failed)
self.result: Result[None|list[str]] = Result.succeed(None)
self.shortform: str | None = None
self.longform: str | None = None
self._parse_args(args)
'''
Parse provided *args as short/longform
NOTE: sets self.result as fail with error message list as value
'''
def _parse_args(self, args: list[str]) -> None:
# store all short/long/unknown forms although multiple arent allowed
# used to provide better errors (displaying ALL duplicates)
shortforms = []
longforms = []
unknowns = []
for arg in args:
if Opt.is_longform(arg):
longforms.append(arg)
elif Opt.is_shortform(arg):
shortforms.append(arg)
else:
unknowns.append(arg)
self.shortform = tryget(shortforms, 0) # default: None
self.longform = tryget(longforms, 0) # default: None
# check if any duplicate arg forms occurred
errs = []
if len(shortforms) > 1:
errs.append(Opt._fail_duplicate_shortsforms(shortforms))
if len(longforms) > 1:
errs.append(Opt._fail_duplicate_longforms(longforms))
if unknowns:
errs.append(Opt._fail_unknown_form(unknowns))
# ensure at least one short/longform given
if not (shortforms or longforms or unknowns):
raise InvalidOptError()
# set Opt.result according to derived errors
if errs:
if self.result.is_err():
self.result.value.extend(errs)
else:
self.result = Result.fail(Opt.__FAIL_MSG, value=errs)
'''
Opt.is_shortform and Opt.is_longform check whether an
arg is in the form ie `-v`, or `--verbose` respectively.
NOTE: Always check longform before shortform, ie anything
NOTE: with is_longform will also be is_shortform (im lazy)
'''
@staticmethod
def is_shortform(arg: str) -> bool:
return arg.startswith('-')
@staticmethod
def is_longform(arg: str) -> bool:
return arg.startswith('--')
'''
=========================================================
Methods for formatting+returning error messages as a way
of logging the outcome of longform/shortform/etc parsing.
=========================================================
'''
@staticmethod
def _fail_bad_forms(form_type: str, forms: list) -> str:
forms = ', '.join[f'\"{f}\"' for f in forms]
return f'Opt \"{self.name}\" got {form_type}: {forms}'
@staticmethod
def _fail_duplicate_shortforms(args: list[str]) -> str:
return Opt._fail_bad_forms('duplicate shortforms', args)
@staticmethod
def _fail_duplicate_longforms(args: list[str]) -> str:
return Opt._fail_bad_forms('duplicate longforms', args)
@staticmethod
def _fail_unknown_forms(args: [str]) -> str:
form_type = 'unknown arg'
if len(args) > 1:
form_type += 's' # make plural
return Opt._fail_bad_forms(form_type, args)
# @staticmethod
# def _fail_overlap(form_type: str, form: str, name1: str, name2: str) -> str:
# return f'Duplicate {form_type} \"{form}\" for Opts \"{name1}\" & \"{name2}\"'
class Arg(Param):
_TYPE: ParamType = ParamType.Arg
def __init__(self, name: str = '', description: str = '') -> None:
super().__init__(name=name, description=description)
2025-06-21 21:29:00 +10:00
class Flag(Opt):
_TYPE: ParamType = ParamType.Flag
def __init__(self, *args, name: str = '', description: str = '') -> None:
super().__init__(name=name, description=description)
2025-06-21 21:29:00 +10:00
class Option(Opt):
_TYPE: ParamType = ParamType.Option
def __init__(self, *args, name: str = '', description: str = '') -> None:
super().__init__(name=name, description=description)