1#!/usr/local/bin/python3.8
2
3# python3 status: started
4
5# all about options (so may merge with afni_base)
6
7# do we want all of the
8
9import sys, os
10from afnipy import afni_base as BASE
11from afnipy import afni_util as UTIL
12
13# whine about execution as a main program
14if __name__ == '__main__':
15   import sys
16   print('** %s: not a main program' % sys.argv[0].split('/')[-1])
17   sys.exit(1)
18
19# ---------------------------------------------------------------------------
20# history:              see: afni_history -program option_list.py
21#
22#   07 May 2008 [rickr]:
23#     - added doc string and reformatted add_opt()
24#     - modified show()
25#     - added class functions get_string_opt, get_string_list,
26#       get_type_opt and get_type_list
27#
28#   06 June 2008 [rickr]:
29#     - get_*_opt functions now return an error code along with the result
30#
31#   06 Nov 2008 [rickr]:
32#     - added 'opt' param to get_type_opt and get_type_list
33#       (to skip find_)
34#
35#   01 Dec 2008 [rickr]:
36#     - added 'opt' param to get_string_opt and get_string_list
37#     - initialized more parameters (to get_*) to make them optional
38#
39#   03 Oct 2012 [rickr]:
40#     - add okdash parameter to option instances, to denote whether any
41#       parameters may have dashes
42#
43#   27 Feb 2013 [rickr]:
44#     - added Ziad's apsearch options: -all_opts, -h_find, -h_view
45#
46#   09 May 2014 [rickr]:
47#     - added find_opt_index, which allows for popping
48#
49#   05 Feb 2020 [rickr]:
50#     - added -optlist_show_argv_array, which takes a parameter
51#
52#   16 Mar 2020 [rickr]:
53#     - added apsearch options: -hweb, -h_web
54# ---------------------------------------------------------------------------
55
56# ---------------------------------------------------------------------------
57# This class provides functionality for processing lists of comopt elements.
58class OptionList:
59    def __init__(self, label):
60        self.label    = label
61        self.olist    = []      # list of comopt elements
62        self.trailers = 0       # for  read_options: no trailing args allowed
63                                # from read_options: say there were such args
64        self.show_count = 1     # display option count in show()
65        self.verb     = 1       # verbosity level
66
67        # terminal options
68        self.show_argv_array='' # show found arguments and exit (method name)
69
70        # parameters for terminal options
71        self.argv_array_types = ['arglist', 'dict', 'pretty', 'nested']
72
73    def add_opt(self, name, npar, deflist=[], acplist=[], req=0, setpar=0,  \
74                helpstr = "", okdash=1):
75        """add an option to the current OptionList
76
77                name    : option name, to be provided on command line
78                npar    : number of parameters
79                              > 0 --> require exactly that number
80                              < 0 --> require at least the positive number
81                deflist : default parmeter list (required, for now)
82                acplist : list of acceptable values
83                req     : flag: is this required?
84                setpar  : flag: set option parlist from deflist
85                okdash  : flag: if set, params are allowed to start with '-'
86        """
87
88        com = BASE.comopt(name, npar, deflist, acplist, helpstr)
89        com.required = req
90        com.okdash = okdash
91        if setpar: com.parlist = com.deflist
92        self.olist.append(com)
93
94    def sort(self):
95        """sort command option list by name"""
96        # cmp keywork has been removed in python3, use key instead
97        # self.olist.sort(cmp=compare_comopts)
98        self.olist.sort(key=comopts_key)
99
100    def show(self, mesg = '', verb = 0, show_count=-1):
101        if verb or mesg != '': print("%sOptionList: %s (len %d)" % \
102                                      (mesg, self.label, len(self.olist)))
103        # allow override of class
104        if show_count < 0: show_count = self.show_count
105        for index in range(len(self.olist)):
106            # possibly add short help string
107            if verb and self.olist[index].helpstr :
108                hs = ": %s" % self.olist[index].helpstr
109            elif self.olist[index].n_found > 0 :
110                hs = '  args found = %2d' % self.olist[index].n_found
111            else :
112                hs = ''
113            if show_count:
114               print("opt %02d: %-24s%s" % (index, self.olist[index].name, hs))
115            else:
116               print("    %-24s%s" % (self.olist[index].name, hs))
117
118    def show_as_array(self, mesg='', atype='pretty', verb=0):
119        """atype
120                    arglist     - forget opts, just show the option list
121                    dict        - show as a dictionary
122                    nested      - show as nested array
123                    pretty      - enumerated options with params
124        """
125        if verb or mesg != '': print("\n%sOptionList: %s (len %d)" % \
126                                     (mesg, self.label, len(self.olist)))
127        if atype == 'arglist':
128           print("%s" % [opt.name for opt in self.olist])
129        elif atype == 'dict':
130           print("{")
131           for ind, opt in enumerate(self.olist):
132               print("  %-28s: %s," % ("'%s'"%opt.name, opt.parlist))
133           print("}")
134        elif atype == 'pretty':
135           for ind, opt in enumerate(self.olist):
136               print("%5s %-24s : %s" % ('[%d]'%ind, opt.name,
137                                         ' '.join(opt.parlist)))
138        elif atype == 'nested':
139           print("[")
140           for opt in self.olist:
141               print("  [%-25s %s]," % ("'%s',"%opt.name, opt.parlist))
142           print("]")
143
144    def find_opt(self, name, nth=1):    # find nth occurance of option name
145        """return nth comopt where name=name, else None"""
146        index = 0
147        for com in self.olist:
148            if com.name == name:
149                index += 1
150                if index == nth: return com
151        return None
152
153    def find_opt_index(self, name, nth=1): # same, but return the index
154        """return nth comopt index where name=name, else -1
155           same as find_opt, but return index
156        """
157        index = 0
158        cind = 0        # avoid enumerate, since python might be old?
159        for com in self.olist:
160            if com.name == name:
161                index += 1
162                if index == nth: return cind
163            cind += 1
164        return -1
165
166    def find_all_opts(self, name):
167        """return all comopts where name=name"""
168        olist = []
169        for com in self.olist:
170            if com.name == name:
171                olist.append(com)
172        return olist
173
174    def have_yes_opt(self, name, default=0, nth=1):
175        """return whether such an option exists and param[0] looks like 'yes'
176
177           default : default value to return if the option does not exist
178           nth     : parameter for matching 'find_opt'
179        """
180        opt = self.find_opt(name, nth=nth)
181        if opt == None: return default
182        if opt_is_yes(opt): return 1
183        return 0
184
185    def have_no_opt(self, name, default=0, nth=1):
186        """return whether such an option exists and param[0] looks like 'no'
187
188           default : default value to return if the option does not exist
189           nth : parameter for matching 'find_opt'
190        """
191        opt = self.find_opt(name, nth=nth)
192        if opt == None: return default
193        if opt_is_no(opt): return 1
194        return 0
195
196    def opt_has_arg(self, opt_name=None, opt=None, arg=''):
197        """is the given argument in opt.parlist
198           (if opt is passed, we don't need to find it)"""
199
200        if opt == None: opt = self.find_opt(opt_name)
201        if not opt or not opt.parlist or len(opt.parlist) < 1: return 0
202        return arg in opt.parlist
203
204    def count_opt(self, name):
205        """return number of comopts where name=name"""
206        count = 0
207        for com in self.olist:
208            if com.name == name: count += 1
209        return count
210
211    def del_opt(self, name, nth=1):     # delete nth occurance of option label
212        """delete nth comopt where name=name, else None"""
213        count = 0
214        for index in range(len(self.olist)):
215            if self.olist[index].name == name:
216                count += 1
217                if count == nth:
218                    del self.olist[index]
219                    return 1
220
221    def get_string_opt(self, opt_name=None, opt=None, default=None):
222        """return the option parameter string and err
223           (if opt is passed, we don't need to find it)
224           err = 0 on success, 1 on failure"""
225
226        if opt == None: opt = self.find_opt(opt_name)
227        if not opt or not opt.parlist: return default, 0
228        if not opt_name: opt_name = opt.name
229        if len(opt.parlist) != 1:
230            print("** expecting 1 parmeter for option '%s', have: %s" % \
231                  (opt_name, opt.parlist))
232            return default, 1
233        return opt.parlist[0], 0
234
235    def get_joined_strings(self, opt_name=None, opt=None, prefix=''):
236        """like get_string_list(), but join any list together and only
237           return a string
238
239           only apply 'prefix' if something is found"""
240        olist, rv = self.get_string_list(opt_name=opt_name, opt=opt)
241        if rv or olist == None: return ''
242        if len(olist) < 1:      return ''
243
244        # we have something
245        return prefix + ' '.join(UTIL.quotize_list(olist))
246
247    def get_string_list(self, opt_name=None, opt=None):
248        """return the option parameter string and an error code
249           (if opt is passed, we don't need to find it)"""
250
251        if opt == None: opt = self.find_opt(opt_name)
252        if not opt or not opt.parlist or len(opt.parlist) < 1: return None,0
253        return opt.parlist, 0
254
255    def get_type_opt(self, otype, opt_name='', opt=None, default=None):
256        """return the option param value converted to the given type, and err
257           (err = 0 on success, 1 on failure)
258
259           If the opt element is passed, we don't need to find it.
260        """
261
262        # if no opt was passed, try to find it
263        if opt == None: opt = self.find_opt(opt_name)
264
265        if not opt or not opt.parlist: return default, 0
266        if not opt_name: opt_name = opt.name
267        if len(opt.parlist) != 1:
268            print("** expectin 1 parameter for option '%s', have: %s" % \
269                  (opt_name, opt.parlist))
270            return default, 1
271        try: val = otype(opt.parlist[0])
272        except:
273            print("** cannot convert '%s' to %s" % (opt.parlist[0], otype))
274            return default, 1
275
276        return val, 0
277
278    def get_type_list(self, otype, opt_name='', length=0, len_name='',
279                      opt=None, verb=1):
280        """return a list of values of the given otype, and err
281
282            err will be set (1) if there is an error
283
284            otype     : expected conversion type
285            opt_name  : option name to find in opts list
286            length    : expected length of option parameters (or 1)
287                        (if length == 0, return whatever is found)
288            len_name  : name of option that would define expected length
289            opt       : optionally provide a comopt element
290            verb      : verbose level
291
292            Find opt_name in opts list.  Verify that the parlist values are of
293            the proper otype and that there are either 1 or 'length' of them.
294            If 1, duplicate it to length."""
295
296        if opt == None: opt = self.find_opt(opt_name)
297        if not opt or not opt.parlist: return None, 0
298        if not opt_name: opt_name = opt.name
299        olen = len(opt.parlist)
300        if length > 0 and olen != 1 and olen != length:
301            if verb:
302               print('** %s takes 1 or %s (%d) values, have %d: %s' % \
303                  (opt_name, len_name, length, olen, ', '.join(opt.parlist)))
304            return None, 1
305        try:
306            tlist = list(map(otype,opt.parlist))
307        except:
308            if verb: print("** %s takes only %ss, have: %s"  \
309                           % (opt_name,otype,opt.parlist))
310            return None, 1
311        if length > 0 and olen != length:     # expand the list
312            tlist = [tlist[0] for i in range(length)]
313            if verb > 1: print('++ expanding %s to list %s' % (opt_name, tlist))
314        elif verb > 1: print('-- have %s list %s' % (opt_name, tlist))
315
316        return tlist, 0        # return the list
317
318    def replace_opt(self, opt_name, vals):
319        """replace the parlist from the first instace of opt_name with vals
320           if not found, add a new option
321        """
322
323        opt = self.find_opt(opt_name)
324        if not opt:
325           setpar = len(vals)
326           self.add_opt(opt_name, len(vals), deflist=vals, setpar=setpar)
327           return
328
329        # make a copy, to be safe
330        if len(vals) == 0: opt.parlist = []
331        else:              opt.parlist = vals[:]
332
333        return
334
335    def append_to_opt(self, opt_name, vals):
336        """append the vals to parlist from the first instace of opt_name
337           if not found, add a new option
338        """
339
340        opt = self.find_opt(opt_name)
341        if not opt:
342           setpar = len(vals)
343           self.add_opt(opt_name, len(vals), deflist=vals, setpar=setpar)
344           return
345
346        # make a copy, to be safe
347        if len(vals) == 0: opt.parlist = vals[:]
348        else:              opt.parlist.extend(vals)
349
350        return
351
352    # rcr - improve this garbage
353    def check_special_opts(self, argv):
354        """process known '-optlist_* options' and other global_opts,
355           nuking them from argv
356
357           some options are terminal
358        """
359
360        # global options (some take a parameter)
361        global_opts = [ '-optlist_verbose', '-optlist_no_show_count',
362                        '-optlist_show_global_opts',
363                        '-optlist_show_valid_opts',
364                        '-optlist_show_argv_array',
365                        '-h_find', '-h_view', '-hview' ]
366
367        alen = len(argv)
368
369        if '-optlist_verbose' in argv:
370            ind = argv.index('-optlist_verbose')
371            self.verb = 4
372            argv[ind:ind+1] = []
373            print('++ optlist: setting verb to %d' % self.verb)
374        if '-optlist_no_show_count' in argv:
375            ind = argv.index('-optlist_no_show_count')
376            self.show_count = 0
377            argv[ind:ind+1] = []
378            if self.verb>1: print('++ optlist: clearing show_count')
379
380        # terminal options (all end in exit)
381
382        # terminal opts specific to this library
383        if '-optlist_show_global_opts' in argv:
384            global_opts.sort()
385            print("-- global OptionList options (%d):" % len(global_opts))
386            print("     %s\n" % '\n     '.join(global_opts))
387            sys.exit(0)
388
389        if '-optlist_show_valid_opts' in argv:
390            oname = '-optlist_show_valid_opts'
391            ind = argv.index(oname)
392            prog = os.path.basename(argv[0])
393            self.show(verb=1)
394            sys.exit(0)
395
396        if '-optlist_show_argv_array' in argv:
397            oname = '-optlist_show_argv_array'
398            ind = argv.index(oname)
399            # this takes one parameter, which must be in list
400            atype = ''
401            if alen >= ind+2:
402               atype = argv[ind+1]
403            if atype not in self.argv_array_types:
404               print("** %s: requires a parameter in %s" \
405                     % (oname, self.argv_array_types))
406               sys.exit(1)
407            argv[ind:ind+2] = []
408            self.show_argv_array = atype
409
410        # terminal general options
411        if '-h_find' in argv:
412            oname = '-h_find'
413            ind = argv.index(oname)
414            prog = os.path.basename(argv[0])
415            if ind == alen-1:
416               print('** global opt %s needs %s option as parameter' \
417                     % (oname, prog))
418               sys.exit(1)
419            cmd = 'apsearch -phelp %s -word %s' % (prog, argv[ind+1])
420            if self.verb>1: print('++ optlist: applying %s via: %s'%(oname,cmd))
421            BASE.simple_shell_exec(cmd)
422            sys.exit(0)
423
424        if '-h_view' in argv:
425            oname = '-h_view'
426            ind = argv.index(oname)
427            prog = os.path.basename(argv[0])
428            cmd = 'apsearch -view_prog_help %s' % prog
429            if self.verb>1: print('++ optlist: applying %s via: %s'%(oname,cmd))
430            BASE.simple_shell_exec(cmd)
431            sys.exit(0)
432
433        if '-hview' in argv:
434            oname = '-hview'
435            ind = argv.index(oname)
436            prog = os.path.basename(argv[0])
437            cmd = 'apsearch -view_prog_help %s' % prog
438            if self.verb>1: print('++ optlist: applying %s via: %s'%(oname,cmd))
439            BASE.simple_shell_exec(cmd)
440            sys.exit(0)
441
442        if   '-hweb'  in argv: oname = '-hweb'
443        elif '-h_web' in argv: oname = '-h_web'
444        else:                  oname = ''
445        if oname != '':
446            ind = argv.index(oname)
447            prog = os.path.basename(argv[0])
448            cmd = 'apsearch -web_prog_help %s' % prog
449            if self.verb>1: print('++ optlist: applying %s via: %s'%(oname,cmd))
450            BASE.simple_shell_exec(cmd)
451            sys.exit(0)
452
453        if self.verb > 1:
454            print('-- argv: orig len %d, new len %d' % (alen,len(argv)))
455
456
457# ---------------------------------------------------------------------------
458# read_options:
459#   given an argument list, and OptionList of acceptable options,
460#   return an OptionList of found options, or None on failure
461def read_options(argv, oplist, verb = -1):
462    """Input an OptionList element, containing a list of options, required
463       or not, and return an OptionList of options as they are found.
464
465       If verb is not passed, apply that of oplist.
466
467       return: an OptionList element, or None on a terminal error
468       note: options may occur more than once
469    """
470
471    OL = OptionList("read_options")
472
473    if verb < 0: verb = oplist.verb
474
475    alen = len(argv)
476    if alen == 0: return OL
477
478    # prepare a dictionary counting uses of each user option
479    namelist = {}
480    for co in oplist.olist:
481        if co.name in namelist:   # complain if input list contains repeats
482            print("** RO warning: option '%s' appears more than once"%co.name)
483        namelist[co.name] = 0
484    if verb > 1 : print("-d namelist: ", namelist)
485
486    # parse the input arguments:
487    #   for each arg, verify arg is option, then process params
488    #   so ac increments by 1+num_params each time
489    ac = 1
490    while ac < alen:
491        # -optlist_* : global options to be ignored
492        if argv[ac] in [ '-optlist_verbose', '-optlist_no_show_count' ]:
493            if oplist.verb > 1: print("-- found optlist opt '%s'" % argv[ac])
494            ac += 1
495            continue
496
497        com = oplist.find_opt(argv[ac])
498        if com:
499            namelist[argv[ac]] += 1     # increment dictionary count
500            if verb > 2: print("+d found option '%s'" % com.name)
501            if verb > 3: print("-d remaining args: %s" % argv[ac:-1])
502
503            # create new return option
504            newopt = BASE.comopt(com.name, com.n_exp, com.deflist)
505            newopt.i_name = ac          # current index into argv
506            newopt.acceptlist = com.acceptlist
507            newopt.required = com.required
508            ac += 1                     # now point to next argument
509
510            # create parlist of potential parameters
511            if newopt.n_exp > 0:    # try to insert that number of args
512                if newopt.n_exp <= alen - ac:
513                    if verb > 2: print("+d adding %d params" % newopt.n_exp)
514                    parlist = argv[ac:ac+newopt.n_exp]
515                else:   # too few args
516                    print("** error: arg #%d (%s) requires %d params" % \
517                          (ac-1, newopt.name, newopt.n_exp))
518                    return None
519            elif newopt.n_exp < 0:  # grab everything, and truncate later
520                if verb > 2: print("+d start with all %d params" % (alen-ac))
521                parlist = argv[ac:]
522            else: parlist = []      # n_exp == 0
523
524            # truncate parlist if it contains an option
525            for pc in range(len(parlist)):
526                if parlist[pc] in namelist: # then we have pc 'good' params
527                    parlist = parlist[:pc]
528                    if verb > 1: print("-d truncate %s after %d of %d" % \
529                                       (newopt.name, pc, len(parlist)))
530                    break;
531
532            # now check parlist against acceptlist
533            if newopt.acceptlist:
534                for par in parlist:
535                    # check against repr(list element), since par is a string
536                    # (search slowly for older versions of python)
537                    found = 0
538                    for accpar in newopt.acceptlist:
539                        if par == str(accpar): found = 1
540                    if not found:  # panic into error!  aaas yoooou wiiiiish...
541                        print("** option %s: param '%s' is not in: %s" % \
542                              (newopt.name, par, newopt.acceptlist))
543                        return None  # what else can we do?
544
545            # so do we still have enough parameters?
546            if newopt.n_exp < 0: nreq = abs(newopt.n_exp)
547            else:                nreq = newopt.n_exp
548            if len(parlist) < nreq:
549                print("** error: arg #%d (%s) requires %d params, found %d" % \
550                      (ac-1, newopt.name, nreq, len(parlist)))
551                return None
552
553            # we have a full parlist, possibly check for dashes now
554            if not com.okdash:
555               for par in parlist:
556                  if not par: continue  # check for empty param?  too anal?
557                  if par[0] == '-':
558                     print('** option %s has illegal dashed parameter: %s' \
559                           % (newopt.name, par))
560                     print('   --> maybe parameter is a mis-typed option?')
561                     return None
562
563            # success!  insert the remaining list
564            newopt.parlist = parlist
565            newopt.n_found = len(parlist)
566
567        else:   # we seem to be done with expected arguments
568            # there should not be any options in this final list
569            for arg in argv[ac:]:
570                if arg in namelist:
571                    print("** error: option %s follows unknown arg #%d (%s)" % \
572                          (arg, ac, argv[ac]))
573                    return None
574
575            if not oplist.trailers :   # then trailers are not allowed
576                print("** error: unknown trailing arguments : %s" % argv[ac:])
577                return None
578
579            # insert remaining args as trailers
580            newopt = BASE.comopt('trailers', -1, [])
581            newopt.n_found = alen - ac
582            newopt.parlist = argv[ac:]
583            OL.trailers = 1   # flag to calling function
584            if verb > 2: print("-- found trailing args: %s" % newopt.parlist)
585
586        OL.olist.append(newopt) # insert newopt into our return list
587        ac += newopt.n_found    # and increment the argument counter
588
589    # now we have processed all of argv
590    # any unused comopt that has a deflist can be used (else error)
591
592    for co in oplist.olist:
593        if namelist[co.name] == 0:  # may still be okay
594            if co.required:
595                print("** error: missing option %s" % co.name)
596                return None
597            elif len(co.deflist) > 0:  # use it
598                newopt = BASE.comopt(co.name, len(co.deflist), co.deflist)
599                newopt.parlist = newopt.deflist
600                # leave n_found at -1, so calling function knows
601                OL.olist.append(newopt) # insert newopt into our return list
602                if verb > 2: print("++ applying default opt '%s', args: %s" % \
603                                   (co.name, newopt.deflist))
604
605    if verb > 1 : OL.show("-d all found options: ")
606    if verb > 3 : print("-d final optlist with counts: ", namelist)
607
608    # check for terminal options in oplist
609    if oplist.show_argv_array != '':
610       OL.show_as_array("-- show_argv_array: found options",
611                        atype=oplist.show_argv_array)
612       sys.exit(0)
613
614    return OL
615
616def opt_is_yes(opt):
617    """return 1 if and only if option has yes/Yes/YES for oplist[0]"""
618
619    if opt == None: return 0
620
621    rv = 0
622    try:
623        val = opt.parlist[0]
624        if val == 'yes' or val == 'Yes' or val == 'YES' \
625                        or val == 'Y'   or val == 'y': rv = 1
626    except: pass
627
628    return rv
629
630def opt_is_no(opt):
631    """return 1 if and only if option has no/No/NO for oplist[0]"""
632
633    if opt == None: return 0
634
635    rv = 0
636    try:
637        val = opt.parlist[0]
638        if val == 'no' or val == 'No' or val == 'NO' \
639                       or val == 'N'  or val == 'n': rv = 1
640    except: pass
641
642    return rv
643
644def opt_is_val(opt, optval):
645    """return 1 if and only if opt.oplist[0] == optval"""
646
647    if opt == None: return 0
648
649    rv = 0
650    try:
651        if opt.parlist[0] == optval: rv = 1
652    except: pass
653
654    return rv
655
656def comopts_key(copt):
657    """function to be called on each comopts struct for use
658       in sort(key=), since the cmp parameter is gone in python3
659
660       return name field, as that is comparable for sort()
661    """
662    return copt.name
663
664def compare_comopts(c1, c2):
665    """comparison function for use in sort()
666     return -1, 0, 1 for c1 compared with c2
667    """
668    if c1.name < c2.name: return -1
669    if c1.name > c2.name: return  1
670    return 0
671
672def test_comopts():
673
674    okopts = OptionList('for_input')
675    okopts.add_opt('-a',      1, ['4'       ]               )
676    okopts.add_opt('-dsets', -1, [          ]               )
677    okopts.add_opt('-debug',  1, ['0'       ],     list(range(4)) )
678    okopts.add_opt('-c',      2, ['21', '24']               )
679    okopts.add_opt('-d',     -1, [          ]               )
680    okopts.add_opt('-e',     -2, ['21', '24', '265']        )
681    okopts.trailers = 1 # allow trailing args
682
683    okopts.show('------ possible input options ------ ')
684
685    found_opts = read_options(sys.argv, okopts)
686
687    if found_opts: found_opts.show('------ found options ------ ')
688
689# if __name__ == '__main__':
690#     test_comopts()
691
692