#! /usr/bin/python -t # _*_ coding: iso-8859-1 _*_ # Last edited on 2025-07-30 21:03:09 by stolfi MODULE_NAME = "argparser" MODULE_DESC = "Tools for command line parsing" MODULE_VERS = "1.0" # !!! TO DO : write the module info. # !!! TO DO : change the {pp.next} cursor to be {pp.last} (last parsed). # !!! TO DO : add the remaining methods from {argparser.h} MODULE_COPYRIGHT = "Copyright © 2008 State University of Campinas" MODULE_INFO = "!!! MODULE_INFO to be written" import sys import re import string import copy import decimal from decimal import Decimal # Functions for parsing UNIX command line arguments. class ArgParser : "A parser for command line options.\n" \ "\n" \ " An {ArgParser} instance is a tool that parses a list of" \ " command line arguments, given when the instance is created. It supports" \ " both keyword arguments in any order, and positional arguments" \ " given in a fixed order, either relative to the keywords or absolute, as in\n" \ " \n" \ " printdoc \\\n" \ " -justify \\\n" \ " -pageRange 1 10 \\\n" \ " -page 17 -page 22 \\\n" \ " -format portrait pagenumbers '%03d' \\\n" \ " -title 'Foo' \\\n" \ " file1.txt file2.txt file3.txt\n" \ " \n" def __init__(pp, argv, errf, help, info) : "Initalizes the {ArgParser} instance and handles '-help'/'-info' options.\n" \ "\n" \ " The arguments are:\n" \ " {argv} = the command line argument list.\n" \ " {errf} = a file where error messages should be printed.\n" \ " {help} = help text to be printed by '-help' or after errors.\n" \ " {info} = documentation text to be printed by '-info'.\n" \ " Also parses the '-help' and '-info' options, if present." # Instance setup: # {pp.argv} is a local copy of the {argv} list, and {pp.argc} is its length: pp.argv = copy.copy(argv) pp.argc = len(pp.argv) # {pp.errf} is the file for error messages: pp.errf = errf; # {pp.parsed[i]} tells whether {pp.argv[i]} has been consumed: pp.parsed = [ False ]*len(pp.argv) # {pp.next} is the index of the next argument for sequential parsing: pp.parsed[0] = True; # Mark the command name as having been parsed already. pp.next = 1; # Position the cursor just after the command name. # {pp.help} is the help text to be printed in case of errors: pp.help = help # Parse the "-help" option: if pp.help != None and (pp.keyword_present("-help") or pp.keyword_present("--help")) : pp.errf.write("usage: %s\n" % pp.help) sys.exit(0) # Parse the "-info" option: if info != None and (pp.keyword_present("-info") or pp.keyword_present("--info")) : print_info(pp.errf, info + "\n") sys.exit(0) # ---------------------------------------------------------------------- def keyword_present(pp, key) : "Parses an optional keyword argument {key}.\n" \ "\n" \ " If the string {key} occurs among the arguments, still unparsed, marks it parsed," \ " positions the argument cursor just after it, and returns {True}. Otherwise" \ " does not change anything and returns {False}." \ k = 0; while (k < pp.argc) : if not pp.parsed[k] and (pp.argv[k] == key) : pp.parsed[k] = True; pp.next = k+1; return True else : k = k + 1; # Not found: return False # ---------------------------------------------------------------------- def keyword_present_next(pp, key) : "Parses an optional keyword argument {key} at the cursor.\n" \ "\n" \ " If the argument at cursor exists, is still unparsed, and is equal to {key}, marks it parsed," \ " positions the argument cursor just after it, and returns {True}. Otherwise" \ " does not change anything and returns {False}." \ if pp.next >= pp.argc or pp.parsed[pp.next] : return False elif pp.argv[pp.next] == key : pp.parsed[pp.next] = True pp.next += 1; return True else: return False # ---------------------------------------------------------------------- def get_keyword(pp, key) : "Parses a mandatory keyword argument {key}.\n" \ "\n" \ " If the string {key} occurs among the arguments, marks it parsed," \ " and positions the argument cursor just after it. Otherwise" \ " raises an error." if keyword_present(pp, key) : return else: # Not found: pp.error("keyword %s missing or already parsed" % (key)) # ---------------------------------------------------------------------- def get_next(pp, mayBeKeyword = False, mayBeNone = False) : "Returns and consumes the next argument after the cursor.\n" \ "\n" \ " If the next argument after the cursor exists and is still unparsed," \ " marks it as parsed, advances the cursor past it, and returns it. Otherwise" \ " raises {ValueError}.\n" "\n" \ " If {mayBeKeyword} is False, the next argument must not be a keyword." \ "\n" \ " If {mayBeNone} is true, the strings 'NONE', 'None', 'none' and '' are" \ " mapped to the {None} value. Otherwise the result is never {None}." if pp.next >= pp.argc : pp.error("argument %d missing" % pp.next) elif pp.parsed[pp.next] : xv = " = «" + pp.argv[pp.next] + "»" pp.error("argument %d%s already parsed" % (pp.next, xv)) elif not mayBeKeyword and re.match(r"[-].", pp.argv[pp.next]) : xv = " = «" + pp.argv[pp.next] + "»" pp.error("argument %d%s should not be a keyword" % (pp.next, xv)) else : v = pp.argv[pp.next] pp.parsed[pp.next] = True pp.next += 1; if mayBeNone and (v == 'None' or v == 'none' or v == 'NONE' or v == '') : v = None return v # ---------------------------------------------------------------------- def get_next_char(pp, mayBeNone = False) : "Returns and consumes the next 1-character argument after the cursor.\n" \ "\n" \ " Like {get_next}, but requires the argument to be a single-character" \ " string. Otherwise raises {ValueError}.\n" \ "\n" \ " If {mayBeNone} is true, also accepts the strings 'NONE', 'None', 'none' and ''," \ " which get converted to {None}." mn = ["", " or 'None'"][mayBeNone] v = pp.get_next(mayBeNone) if (mayBeNone and v == None) or (v != None and len(v) == 1) : return v else : pp.error("argument %d = «%s» should be one char%s" % (pp.next, v, mn)) # ---------------------------------------------------------------------- def get_next_int(pp, vmin, vmax, mayBeNone = False) : "Returns and consumes the next 1-character argument after the cursor.\n" \ "\n" \ " Like {get_next}, but requires the argument to be a decimal integer" \ " between {vmin} and {vmax}. Otherwise raises {ValueError}.\n" \ "\n" \ " If {mayBeNone} is true, also accepts the strings 'NONE', 'None', 'none' and ''," \ " which get converted to {None}." mn = ["", " or 'None'"][mayBeNone] v = pp.get_next(mayBeNone) if mayBeNone and v == None : return None elif v == None : # Just in case: pp.error("argument %d is missing, expected an integer" % pp.next) elif not re.fullmatch(r"[-+]?[0-9]+", v) : pp.error("argument %d is not an integer" % pp.next) else : vn = int(v) # !!! Should catch exception !!! if vn < vmin or vn > vmax : pp.error("argument %d = «%s» should be in [%d .. %d]%s" % (pp.next, v, vmin, vmax, mn)) else : return vn # ---------------------------------------------------------------------- def error(pp, msg) : "Prints the error message {msg}, the help text, and stops.\n" pp.errf.write("%s\n" % msg) pp.errf.write("usage: %s\n" % pp.help) sys.exit(1) # ---------------------------------------------------------------------- def check_all_parsed(pp, num) : "Check whether all arguments from 0 to {num-1} have been parsed.\n" max_bogus_show = 5; # Max leftover args to print. bogus = 0; for i in range(num) : if not pp.parsed[i] : bogus = bogus + 1; if (bogus <= max_bogus_show) : pp.errf.write(" argument %d = «%s» extraneous or misplaced.\n" % (i, pp.argv[i])); if (bogus > max_bogus_show) : pp.errf.write("(and %d more).\n" % (bogus - argparser_show_bogus_max)); if (bogus > 0) : pp.errf.write("usage: %s\n" % pp.help) sys.exit(1) # ---------------------------------------------------------------------- def skip_parsed(pp) : "Skips all parsed arguments, checking for holes, and posiitons before the first unparsed one.\n" pp.next = pp.argc; while (pp.next > 0) and not pp.parsed[pp.next-1] : pp.next = pp.next-1; # Check for unparsed arguments: pp.check_all_parsed(pp.next); # ---------------------------------------------------------------------- def finish(pp) : pp.check_all_parsed(pp.argc); # ---------------------------------------------------------------------- def print_info(wr, info) : "Prints a program's manpage {info} to file {wr}.\n" \ "\n" \ " Writes to file {wr} the string {info}, assumed to" \ " contain a UNIX-like program's manpage. It is usually" \ " called to procss the standard '-info'/'--info' command" \ " line argument.\n" \ "\n" \ " At present, the procedure merely writes {info} to {wr}" \ " unchanged. Eventually it should break long lines" \ " preserving their initial indentation." wr.write(info) help_info_HELP = \ " [ -help | --help ] [ -info | --info ]" help_info_INFO = \ " -help\n" \ " --help\n" \ " Prints an options summary and exits.\n" \ "\n" \ " -info\n" \ " --info\n" \ " Prints this manpage and exits." help_info_NO_WARRANTY = \ "This software is provided 'as is', WITHOUT ANY EXPLICIT OR" \ " IMPLICIT WARRANTY, not even the implied warranties of merchantibility" \ " and fitness for a particular purpose." help_info_STANDARD_RIGHTS = \ "Permission to use, copy, modify, and redistribute this software and" \ " its documentation for any purpose is hereby granted, royalty-free," \ " provided that: (1) the copyright notice at the top of this" \ " file, and the AUTHOR, WARRANTY and RIGHTS sections of this" \ " text are retained in all derived source files; (2) no executable" \ " code derived from this file is published or distributed without the" \ " corresponding source code; and (3) these same rights are granted to" \ " any recipient of such code, under the same conditions."