nutools/lib/nulib/python/nulib/args.py

611 lines
22 KiB
Python

# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
"""Gestion des arguments de la ligne de commande.
"""
__all__ = ('split_args', 'join_args', 'build_options', 'get_args',
'Options',
)
import sys, re
from getopt import gnu_getopt
from .base import isstr, isbool, seqof, odict
from .output import set_verbosity, VERBOSITY_OPTS
from .input import set_interaction, INTERACTION_OPTS
from .functions import apply_args
RE_SPACES = re.compile(r'[ \t\r\n]+')
RE_QUOTE = re.compile(r'"')
RE_QQUOTE = re.compile(r'\\"')
RE_SPACES_OR_QUOTES = re.compile(r'[ \t\r\n"]+')
RE_TOKEN = re.compile(r'[^ \t\r\n"]+')
RE_QTOKEN = re.compile(r'"((?:\\"|[^"])*)"?')
def has_spaces(cl):
return RE_SPACES.match(cl) is not None
def skip_spaces(pcl):
mo = RE_SPACES.match(pcl[0])
if mo is not None:
pcl[0] = pcl[0][mo.end(0):]
def get_token(pcl):
token = None
mo = RE_TOKEN.match(pcl[0])
if mo is not None:
token, pcl[0] = pcl[0][:mo.end(0)], pcl[0][mo.end(0):]
return token
def get_qtoken(pcl):
qtoken = None
mo = RE_QTOKEN.match(pcl[0])
if mo is not None:
qtoken, pcl[0] = mo.group(1), pcl[0][mo.end(0):]
return qtoken
def split_args(cl):
"""Lire une chaine, et la découper en plusieurs arguments, à utiliser par
exemple avec getopt() ou get_args().
Note: les arguments peuvent être entre quotes, mais pour le moment, seul "
est supporté, pas '.
XXX ajouter le support de ' comme quote.
@return: la liste des arguments, ou None si cl==None
@rtype: list
"""
if cl is None: return None
args = []
pcl = [cl]
while pcl[0]:
if has_spaces(pcl[0]):
skip_spaces(pcl)
if not pcl[0]:
break
arg = ''
while pcl[0] and not has_spaces(pcl[0]):
if pcl[0][:1] == '"':
arg = arg + RE_QQUOTE.sub('"', get_qtoken(pcl))
else:
arg = arg + get_token(pcl)
args.append(arg)
return args
def join_args(args):
"""L'opération inverse de split_args
@return: une chaine, ou None si args==None
"""
if args is None: return None
i = 0
for i in range(len(args)):
arg = args[i]
if not args or RE_SPACES_OR_QUOTES.search(arg) is not None:
args[i] = '"%s"' % RE_QUOTE.sub(r'\"', arg)
return ' '.join(args)
def build_options(argsdesc):
"""Construire une liste d'options pour utilisation avec get_args ou getopt.
A partir d'une liste de termes (option, longoptions, desc), construire et
retourner (options, longoptions), où options est un chaine et longoptions
une liste, pour utilisation avec getopt() ou get_args().
@return: (options, longoptions)
@rtype: tuple
"""
options = ''
longoptions = []
if argsdesc is not None:
for argdesc in argsdesc:
if argdesc[0:1] and argdesc[0] is not None:
options += argdesc[0]
if argdesc[1:2] and argdesc[1] is not None:
longopts = argdesc[1]
if isstr(longopts): longopts = (longopts,)
longoptions.extend(filter(None, longopts))
return options, longoptions
# options courtes à faire traiter par set_verbosity() ou set_interaction()
M_OPTIONS = {}
# options longues à faire traiter par set_verbosity() ou set_interaction()
M_LONGOPTIONS = {}
for _opt in VERBOSITY_OPTS:
if _opt.startswith('--'): M_LONGOPTIONS[_opt] = False
elif _opt.startswith('-'): M_OPTIONS[_opt] = False
for _opt in INTERACTION_OPTS:
if _opt.startswith('--'): M_LONGOPTIONS[_opt] = False
elif _opt.startswith('-'): M_OPTIONS[_opt] = False
del _opt
RE_OPTION = re.compile(r'.:?')
def get_args(args=None, options=None, longoptions=None, **optdescs):
"""frontend pour getopt qui reconnait les options de set_verbosity et
set_interaction(), et mets à jour les niveaux automatiquement.
"""
if args is None: args = sys.argv[1:]
if options is None: options = ''
longoptions = seqof(longoptions, [])
options = RE_OPTION.findall(options)
longoptions = list(longoptions)
def in_options(opt, options=options):
"""Retourner True si l'option opt est mentionnée dans options, sans
tenir compte du fait qu'elle prend ou non un argument dans options.
Si opt n'est pas mentionné dans options, l'y rajouter.
opt doit être de la forme 'o' ou 'o:'
"""
normopt = opt[:1]
for option in options:
normoption = option[:1]
if normopt == normoption: return True
options.append(opt)
return False
def in_longoptions(longopt, longoptions=longoptions):
"""Retourner True si l'option longue longopt est mentionnée dans
longoptions, sans tenir compte du fait qu'elle prend ou non un argument
dans longoptions.
Si longopt n'est pas mentionné dans longoptions, l'y rajouter.
longopt doit être de la forme 'longopt' ou 'longopt='
"""
if longopt[-1:] == '=': normlongopt = longopt[:-1]
else: normlongopt = longopt
for longoption in longoptions:
if longoption[-1:] == '=': normlongoption = longoption[:-1]
else: normlongoption = longoption
if normlongopt == normlongoption: return True
longoptions.append(longopt)
return False
# déterminer quelles options seront reconnues par set_verbosity. il s'agit
# de toutes celles qui ne sont pas traitées par l'utilisateur
m_options = M_OPTIONS.copy()
m_longoptions = M_LONGOPTIONS.copy()
for m_option in m_options.keys():
# m_option est de la forme '-o'
if not in_options(m_option[1:]):
m_options[m_option] = True
for m_longoption in m_longoptions.keys():
# m_longoption est de la forme '--longopt'
if not in_longoptions(m_longoption[2:]):
m_longoptions[m_longoption] = True
# appliquer les options reconnues par set_verbosity
options = ''.join(options)
optvalues, args = gnu_getopt(args, options, longoptions)
for i in range(len(optvalues)):
opt, _ = optvalues[i]
set_verbosity_or_interaction = False
if m_longoptions.get(opt, False): # long options
set_verbosity_or_interaction = True
elif m_options.get(opt, False): # options
set_verbosity_or_interaction = True
if set_verbosity_or_interaction:
if opt in VERBOSITY_OPTS:
set_verbosity(opt)
elif opt in INTERACTION_OPTS:
set_interaction(opt)
optvalues[i] = None
# retourner les autres options qui n'ont pas été reconnues
return filter(None, optvalues), args
################################################################################
_none = object()
RE_PREFIX = re.compile(r'^-*')
RE_SUFFIX = re.compile(r'[:=]$')
RE_STUFF = re.compile(r'[^a-zA-Z0-9]')
def opt2name(opt):
"""Obtenir un nom de variable dérivé d'un nom d'option
Les tirets de début et les caractères : et = de fin sont supprimés, et les
caractères spéciaux sont remplacés par '_'
"""
name = RE_PREFIX.sub('', opt)
name = RE_SUFFIX.sub('', name)
name = RE_STUFF.sub('_', name)
return name
class Option(object):
"""Un objet stockant la description d'une option unique
optdef définition de l'option, e.g. 'o', 'o:', 'long-option', ou
'long-option='
optname nom de l'option, e.g. 'o' ou 'long-option'
short est-ce une option courte?
takes_value
cette option prend-elle un argument?
action action associée à cette option.
name nom de la variable associée à l'option.
"""
_short, short = None, property(lambda self: self._short)
_optdef, optdef = None, property(lambda self: self._optdef)
_optname, optname = None, property(lambda self: self._optname)
_takes_value, takes_value = None, property(lambda self: self._takes_value)
def __init(self, short, optdef, optname, takes_value):
self._short = short
self._optdef = optdef
self._optname = optname
self._takes_value = takes_value
_action, action = None, property(lambda self: self._action)
_name, name = None, property(lambda self: self._name)
LONGOPTION_PATTERN = r'(([a-zA-Z0-9$*@!_][a-zA-Z0-9$*@!_-]*)=?)'
RE_LONGOPTION0 = re.compile(r'--%s$' % LONGOPTION_PATTERN)
RE_LONGOPTION1 = re.compile(r'%s$' % LONGOPTION_PATTERN)
OPTION_PATTERN = r'(([a-zA-Z0-9$*@!_]):?)'
RE_OPTION0 = re.compile(r'-%s$' % OPTION_PATTERN)
RE_OPTION1 = re.compile(r'%s$' % OPTION_PATTERN)
def __init__(self, optdef):
if not optdef: raise ValueError("optdef is required")
mo = self.RE_LONGOPTION0.match(optdef)
if mo is not None:
self.__init(False, mo.group(1), mo.group(2), mo.group(1) != mo.group(2))
else:
mo = self.RE_OPTION0.match(optdef)
if mo is not None:
self.__init(True, mo.group(1), mo.group(2), mo.group(1) != mo.group(2))
else:
mo = self.RE_OPTION1.match(optdef)
if mo is not None:
self.__init(True, mo.group(1), mo.group(2), mo.group(1) != mo.group(2))
else:
mo = self.RE_LONGOPTION1.match(optdef)
if mo is not None:
self.__init(False, mo.group(1), mo.group(2), mo.group(1) != mo.group(2))
else:
raise ValueError("Invalid option: %s" % optdef)
def __str__(self):
prefix = self._short and '-' or '--'
return '%s%s' % (prefix, self._optname)
str = __str__
opt = property(__str__)
def __repr__(self):
option = self.__str__()
if self._takes_value:
if self._short: option += ':'
else: option += '='
return '%s(%s)' % (self.__class__.__name__, repr(option))
repr = __repr__
def same_optdef(self, other):
return isinstance(other, Option) and self._optdef == other.optdef
def same_optname(self, other):
return isinstance(other, Option) and \
self._optname == other.optname and \
self._takes_value == other.takes_value
def __eq__(self, other):
if isstr(other):
return self.__str__() == other
elif isinstance(other, Option):
return self._optdef == other.optdef
else:
return False
def set_action(self, action, name=None):
self._action = action
self._name = name
class Action(object):
"""Une action associée à une option quand elle est rencontrée sur la ligne
de commande.
name nom de la variable associée à l'option, None s'il faut le calculer
initial si une valeur est associée à l'option, valeur initiale de cette
option.
Cet objet doit implémenter une méthode __call__() qui prend les arguments
(option[, value[, options]])
La méthode doit retourner False si elle veut indiquer qu'elle n'a pas pu
mettre à jour la valeur. Tout autre valeur indique le succès.
option est une instance de Option. value est la valeur associée à l'option,
ou _none si l'option ne prend pas d'argument. options est l'instance de
l'objet Options qui analyse les arguments.
"""
name = property(lambda self: None)
initial = property(lambda self: None)
def __call__(self, option=None, value=_none, options=None):
pass
class Options(object):
"""Une classe permettant de traiter des arguments en ligne de commande.
Son objectif est d'offrir une solution plus flexible que les fonctions
build_options et get_args()
Avec le constructeur et la méthode add_option(), il est possible de
construire la liste des options valides.
Ensuite, la méthode parse() permet d'analyser la ligne de commande. Par
défaut, si une méthode n'est pas définie pour une option, ou si la méthode
définie retourne False, initialiser une variable nommée d'après l'option, en
remplaçant sa valeur (si l'option prend un argument) ou lui ajoutant 1 (si
l'option ne prend pas d'argument).
"""
class SetValue(Action):
"""Mettre à jour une variable
value valeur qu'il faut forcer, ou _none s'il faut prendre la valeur par
défaut. Si l'option prend un argument, la valeur par défaut est la
valeur spécifiée sur la ligne de commande. Sinon, il s'agit d'une
valeur incrémentée représentant le nombre de fois que l'option
apparait.
name nom de la variable à initialiser, ou None s'il faut dériver le nom
de la variable à partir du nom de l'option.
initial valeur initiale de la variable
"""
_value = None
_name, name = None, property(lambda self: self._name)
_initial, initial = None, property(lambda self: self._initial)
def __init__(self, value=_none, name=None, initial=None):
self._value = value
self._name = name
self._initial = initial
def __call__(self, option=None, value=_none, options=None):
# nom: celui qui est spécifié dans le constructeur, ou un nom dérivé du
# nom de l'option
name = self._name
if name is None: name = opt2name(option.optname)
# valeur: celle qui est spécifiée dans le constructeur, ou alors laisser
# options sans charger
if self._value is not _none: value = self._value
# mettre à jour la valeur
options.update_value(option, value)
class CallMethod(Action):
_method = None
def __init__(self, method=None):
self._method = method
def __call__(self, option=None, value=None, options=None):
return apply_args(self._method, option, value, options)
# type d'analyse: '+' pour s'arrêter à la première non option, '' sinon
_parseopt = None
# liste d'options courtes, instances de Option
_soptions = None
# liste d'options longues, instances de Option
_loptions = None
# valeurs stockées dans cet objet
_values = None
# dictionnaire des options définies, avec chacune une instance de Option
# associée
_options = None
############################################################################
# Constructeur
def __init__(self, *optdescs):
"""Initialiser l'objet avec un ensemble d'argument de la forme
(options, longoptions, desc)
où options est une chaine avec des lettres de la forme 'o' ou 'o:',
longoptions une liste de chaines de la forme 'option' ou 'option=', et
desc une chaine quelconque.
Ce format est pour assurer la compatibilité avec la fonction
build_options()
"""
super(Options, self).__init__()
object.__setattr__(self, '_parseopt', '')
object.__setattr__(self, '_soptions', [])
object.__setattr__(self, '_loptions', [])
object.__setattr__(self, '_values', {})
object.__setattr__(self, '_options', {})
self.add_option(VERBOSITY_OPTS, set_verbosity)
self.add_option(INTERACTION_OPTS, set_interaction)
for optdesc in optdescs:
options = filter(None, optdesc[:2])
desc = optdesc[2:3] and optdesc[2] or None
self.add_option(options, None, desc)
def __option(self, opt):
"""Obtenir l'instance de Option correspondant à l'argument
"""
if isinstance(opt, Option): return opt
if not opt.startswith('-'):
if len(opt) == 1: opt = '-' + opt
else: opt = '--' + opt
option = self._options.get(opt, None)
if option is None: raise ValueError("Unknown option: %s" % opt)
return option
def add_option(self, options=None, action=None, desc=None):
"""Ajouter une option
options peut être une chaine de l'une des formes suivantes:
'+' arrêter l'analyse à la première non-option (configuration de gnu_getopt)
'o', '-o', 'o:', '-o:'
option courte sans et avec argument
'longo', '--longo', 'longo=', '--longo='
option longue sans et avec argument
options peut aussi être une liste de ces chaines
"""
default_name = None
for opt in filter(None, seqof(options, ())):
# traiter la configuration de l'analyse '+'
if opt.startswith('+'):
self._parseopt = '+'
opt = opt[1:]
if not opt: continue
# nom par défaut
if default_name is None:
default_name = opt2name(opt)
# option
option = Option(opt)
# action
if isinstance(action, Action):
# action déjà spécifiée
pass
elif action is None:
# pas d'action: mettre à jour la variable d'après le nom de la
# première option
action = Options.SetValue(name=default_name)
elif isstr(action):
# mettre à jour la variable nommée d'après l'action
action = Options.SetValue(name=action)
elif callable(action):
# appeler l'action
action = Options.CallMethod(action)
else:
raise ValueError("Unsupported action: %s" % repr(action))
name = action.name
if name is None: name = default_name
option.set_action(action, name)
# si une précédente option est définie, il faut la remplacer
self._soptions = filter(lambda soption: not soption.same_optname(option), self._soptions)
self._loptions = filter(lambda loption: not loption.same_optname(option), self._loptions)
# nouvelle option
if option.short: self._soptions.append(option)
else: self._loptions.append(option)
self._options[option.opt] = option
# valeur initiale
# ne spécifier la valeur initiale que si elle n'existe pas déjà
if not self.has_value(option):
self.set_value(option, action.initial)
return self
############################################################################
# Gestion des valeurs
def __getitem__(self, key):
return self._values[key]
def __setitem__(self, key, value):
self._values[key] = value
def __delitem__(self, key):
del self._values[key]
def get(self, key, default=None):
return self._values.get(key, default)
def __getattr__(self, key, default=_none):
try:
if default is _none: return self._values[key]
else: return self._values.get(key, default)
except KeyError: raise AttributeError(key)
def __setattr__(self, key, value):
if self._values.has_key(key): self._values[key] = value
else: return super(Options, self).__setattr__(key, value)
def __delattr__(self, key):
try: del self._values[key]
except KeyError: raise AttributeError(key)
def get_value(self, option, default=_none):
"""Obtenir la valeur correspondant à l'option
"""
option = self.__option(option)
return self.get(option.name, default)
def has_value(self, option):
option = self.__option(option)
return self._values.has_key(option.name)
def set_value(self, option, value):
"""Spécifier la valeur correspondant à l'option
"""
option = self.__option(option)
self._values[option.name] = value
return True
def update_value(self, option, value=_none):
option = self.__option(option)
if value is _none:
if option.takes_value:
raise ValueError("Required value")
else:
value = self.get_value(option, None)
if value is None: value = 0
self.set_value(option, value + 1)
else:
self.set_value(option, value)
############################################################################
# Exploitation
def get_args(self, args=None):
"""Analyser les arguments à la recherche des options valides. Si
args==None, prendre sys.argv[1:]
@return (optvalues, args)
optvalues est une liste de tuple (opt, value) correspondant à toutes les
options qui ont été analysées par gnu_getopt(). args est la liste des
arguments qui ne sont pas des options.
"""
if args is None: args = sys.argv[1:]
soptions = self._parseopt + ''.join([option.optdef for option in self._soptions])
loptions = [option.optdef for option in self._loptions]
optvalues, args = gnu_getopt(args, soptions, loptions)
return filter(None, optvalues), args
_parsed_names = None
def parse(self, args=None, optvalues=None):
"""Traiter les options analysées par get_args(). Si optvalues==None,
analyser les arguments de args avec get_args() d'abord.
@return (roptvalues, args)
optvalues est une liste de tuple (opt, value) correspondant à toutes les
options qui ont été analysées, mais n'ont pas pu être traitées par cet
objet.
args est la liste des arguments qui ne sont pas des options.
"""
self._parsed_names = {}
if optvalues is None: optvalues, args = self.get_args(args)
roptvalues = []
for opt, value in optvalues:
option = self.__option(opt)
self._parsed_names[option.name] = True
if not option.takes_value: value = _none
if option.action(option, value, self) == False:
roptvalues.append((opt, value))
self.update_value(option, value)
return roptvalues, args
def was_parsed(self, name):
"""Indiquer si une option correspondant à la variable name a été
mentionnée sur la ligne de commande.
"""
if self._parsed_names is None: return False
return self._parsed_names.has_key(name)