Opts.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. #!/usr/bin/env python3
  2. #
  3. # Opts.py, an options parsing library for Python.
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. Opts.py: Generic options parsing
  20. """
  21. import sys,re
  22. from collections import namedtuple
  23. pat = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)')
  24. def make_usage_str(prog_name,caller,data):
  25. lines = [data.strip()] if type(data) == str else data
  26. indent,col1_w = {
  27. 'help': (2,len(prog_name)+1),
  28. 'user': (0,len('USAGE:')),
  29. }[caller]
  30. def gen():
  31. ulbl = 'USAGE:'
  32. for line in lines:
  33. yield f'{ulbl:{col1_w}} {prog_name} {line}'
  34. ulbl = ''
  35. return ('\n'+(' '*indent)).join(gen())
  36. def usage(opts_data):
  37. print(make_usage_str(
  38. prog_name = opts_data['prog_name'],
  39. caller = 'user',
  40. data = opts_data['text'].get('usage2') or opts_data['text']['usage'] ))
  41. sys.exit(1)
  42. def print_help(*args):
  43. print(make_help(*args))
  44. sys.exit(0)
  45. def make_help(proto,po,opts_data,opt_filter):
  46. def parse_lines(text):
  47. filtered = False
  48. for line in text.strip().splitlines():
  49. m = pat.match(line)
  50. if m:
  51. filtered = bool(opt_filter and m[1] not in opt_filter)
  52. if not filtered:
  53. yield fs.format( ('-'+m[1]+',','')[m[1]=='-'], m[2], m[4] )
  54. elif not filtered:
  55. yield line
  56. opts_type,fs = ('options','{:<3} --{} {}') if 'help' in po.user_opts else ('long_options','{} --{} {}')
  57. t = opts_data['text']
  58. c = opts_data['code']
  59. nl = '\n '
  60. pn = opts_data['prog_name']
  61. from mmgen.help import help_notes_func
  62. def help_notes(k):
  63. return help_notes_func(proto,po,k)
  64. def gen_arg_tuple(func,text):
  65. d = {'proto': proto,'help_notes':help_notes}
  66. for arg in func.__code__.co_varnames:
  67. yield d[arg] if arg in d else text
  68. def gen_text():
  69. yield ' {} {}'.format(pn.upper()+':',t['desc'].strip())
  70. yield make_usage_str(pn,'help',t.get('usage2') or t['usage'])
  71. yield opts_type.upper().replace('_',' ') + ':'
  72. # process code for options
  73. opts_text = nl.join(parse_lines(t[opts_type]))
  74. if opts_type in c:
  75. arg_tuple = tuple(gen_arg_tuple(c[opts_type],opts_text))
  76. yield c[opts_type](*arg_tuple)
  77. else:
  78. yield opts_text
  79. # process code for notes
  80. if opts_type == 'options' and 'notes' in t:
  81. notes_text = t['notes']
  82. if 'notes' in c:
  83. arg_tuple = tuple(gen_arg_tuple(c['notes'],notes_text))
  84. notes_text = c['notes'](*arg_tuple)
  85. for line in notes_text.splitlines():
  86. yield line
  87. return nl.join(gen_text()) + '\n'
  88. def process_uopts(opts_data,short_opts,long_opts):
  89. import os,getopt
  90. opts_data['prog_name'] = os.path.basename(sys.argv[0])
  91. try:
  92. cl_uopts,uargs = getopt.getopt(sys.argv[1:],''.join(short_opts),long_opts)
  93. except getopt.GetoptError as e:
  94. print(e.args[0])
  95. sys.exit(1)
  96. def get_uopts():
  97. for uopt,uparm in cl_uopts:
  98. if uopt.startswith('--'):
  99. lo = uopt[2:]
  100. if lo in long_opts:
  101. yield (lo.replace('-','_'), True)
  102. else: # lo+'=' in long_opts
  103. yield (lo.replace('-','_'), uparm)
  104. else: # uopt.startswith('-')
  105. so = uopt[1]
  106. if so in short_opts:
  107. yield (long_opts[short_opts.index(so)].replace('-','_'), True)
  108. else: # so+':' in short_opts
  109. yield (long_opts[short_opts.index(so+':')][:-1].replace('-','_'), uparm)
  110. uopts = dict(get_uopts())
  111. if 'sets' in opts_data:
  112. for a_opt,a_val,b_opt,b_val in opts_data['sets']:
  113. if a_opt in uopts:
  114. u_val = uopts[a_opt]
  115. if (u_val and a_val == bool) or u_val == a_val:
  116. if b_opt in uopts and uopts[b_opt] != b_val:
  117. sys.stderr.write(
  118. 'Option conflict:'
  119. + '\n --{}={}, with'.format(b_opt.replace('_','-'),uopts[b_opt])
  120. + '\n --{}={}\n'.format(a_opt.replace('_','-'),uopts[a_opt]) )
  121. sys.exit(1)
  122. else:
  123. uopts[b_opt] = b_val
  124. return uopts,uargs
  125. def parse_opts(opts_data,opt_filter=None,parse_only=False):
  126. short_opts,long_opts,skipped_opts = [],[],[]
  127. def parse_lines(opts_type):
  128. for line in opts_data['text'][opts_type].strip().splitlines():
  129. m = pat.match(line)
  130. if m:
  131. if bool(opt_filter and m[1] not in opt_filter):
  132. skipped_opts.append(m[2])
  133. else:
  134. if opts_type == 'options':
  135. short_opts.append(m[1] + ('',':')[m[3] == '='])
  136. long_opts.append(m[2] + ('','=')[m[3] == '='])
  137. parse_lines('options')
  138. if 'long_options' in opts_data['text']:
  139. parse_lines('long_options')
  140. uopts,uargs = process_uopts(opts_data,short_opts,long_opts)
  141. po = namedtuple('parsed_cmd_opts',['user_opts','cmd_args','opts','skipped_opts'])
  142. return po(
  143. uopts, # dict
  144. uargs, # list, callers can pop
  145. tuple(o.replace('-','_').rstrip('=') for o in long_opts),
  146. tuple(o.replace('-','_') for o in skipped_opts),
  147. )