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