2012-07-10 16 views
37

Mi script en Python necesita leer archivos de un directorio pasado en la línea de comandos. He definido un tipo de archivo legible como se muestra a continuación para usarlo con argparse para validar que el directorio pasado en la línea de comando es existente y legible. Además, también se ha especificado un valor predeterminado (/ tmp/non_existent_dir en el ejemplo a continuación) para el argumento del directorio. El problema aquí es que argparse invoca a readable_dir() en el valor predeterminado incluso en una situación en la que un argumento de directorio se pasa explícitamente en la línea de comando. Esto provoca que la secuencia de comandos falle ya que la ruta predeterminada/tmp/non_existent_dir no existe en un contexto donde un directorio se pasa explícitamente en la línea de comando. Podría evitar esto al no especificar un valor predeterminado y hacer que este argumento sea obligatorio, o posponiendo la validación hasta más adelante en el guión, pero ¿es una solución más elegante de la que todos tengan conocimiento?tipos de ruta de directorio con argparse

#!/usr/bin/python 
import argparse 
import os 

def readable_dir(prospective_dir): 
    if not os.path.isdir(prospective_dir): 
    raise Exception("readable_dir:{0} is not a valid path".format(prospective_dir)) 
    if os.access(prospective_dir, os.R_OK): 
    return prospective_dir 
    else: 
    raise Exception("readable_dir:{0} is not a readable dir".format(prospective_dir)) 

parser = argparse.ArgumentParser(description='test', fromfile_prefix_chars="@") 
parser.add_argument('-l', '--launch_directory', type=readable_dir, default='/tmp/non_existent_dir') 
args = parser.parse_args() 
+4

Muestra del código de utilidad. Raise debería 'raise argparse.ArgumentTypeError', pero de lo contrario, estoy extrayendo el tipo read_dir. – mlissner

Respuesta

28

Puede crear una acción personalizada en lugar de un tipo:

import argparse 
import os 
import tempfile 
import shutil 
import atexit 

class readable_dir(argparse.Action): 
    def __call__(self, parser, namespace, values, option_string=None): 
     prospective_dir=values 
     if not os.path.isdir(prospective_dir): 
      raise argparse.ArgumentTypeError("readable_dir:{0} is not a valid path".format(prospective_dir)) 
     if os.access(prospective_dir, os.R_OK): 
      setattr(namespace,self.dest,prospective_dir) 
     else: 
      raise argparse.ArgumentTypeError("readable_dir:{0} is not a readable dir".format(prospective_dir)) 

ldir = tempfile.mkdtemp() 
atexit.register(lambda dir=ldir: shutil.rmtree(ldir)) 

parser = argparse.ArgumentParser(description='test', fromfile_prefix_chars="@") 
parser.add_argument('-l', '--launch_directory', action=readable_dir, default=ldir) 
args = parser.parse_args() 
print (args) 

Pero esto parece un poco sospechoso para mí - si no se da directorio, se pasa a un directorio no se puede leer lo que parece para derrotar el propósito de verificar si el directorio es accesible en primer lugar.

Tenga en cuenta que, como se señala en los comentarios, podría ser mejor que
raise argparse.ArgumentError(self, ...) en lugar de argparse.ArgumentTypeError.

EDITAR

Por lo que yo sé, no hay manera de validar el argumento predeterminado. Supongo que los desarrolladores de argparse simplemente asumieron que si está proporcionando un valor predeterminado, entonces debería ser válido. Lo más rápido y más fácil de hacer aquí es simplemente validar los argumentos inmediatamente después de analizarlos. Parece que solo intentas obtener un directorio temporal para hacer algo de trabajo. Si ese es el caso, puede usar el módulo tempfile para obtener un nuevo directorio para trabajar. Actualicé mi respuesta anterior para reflejar esto. Creo un directorio temporal, utilicelo como el argumento predeterminado (tempfile ya garantiza que el directorio que crea será grabable) y luego lo registro para que se elimine cuando se cierre el programa.

+0

mgilson, tuve que cambiar prospective_dir = values ​​[0] a prospective_dir = values.Sin esto, solo el primer personaje del argumento estaba siendo recogido. Su solución funciona cuando se pasa un argumento explícito (en el que el valor predeterminado no se valida bajo estas circunstancias). Sin embargo, cuando no se pasa ningún argumento, NO se valida el valor predeterminado, que es un problema. – iruvar

+0

@cravoori - Alguna razón pensé que 'values' sería una lista. Supongo que eso solo ocurre cuando se especifica 'nargs = ...'. De todos modos, no creo que haya ninguna manera de persuadir a argparse para que haga la validación después de analizar los argumentos (que es lo que realmente está pidiendo). Tendrás que hacer eso tú mismo. He actualizado mi código para que siempre haya un directorio válido para que trabajes y se borrará cuando salga tu programa. (los directorios especificados en la línea de comando no serán eliminados). – mgilson

+0

tenga en cuenta que el directorio temporal se usó solo como un ejemplo – iruvar

11

Si la secuencia de comandos no puede funcionar sin una válida launch_directory entonces deberían utilizarse de un argumento obligatorio:

parser.add_argument('launch_directory', type=readable_dir) 

por cierto, se debe utilizar en lugar de argparse.ArgumentTypeErrorException en readable_dir().

+2

argparse.ArgumentError (self, "cadena de error") es lo mejor de todo, si desea que el usuario vea un buen mensaje de error en lugar de un rastro de pila. Para obtener más información, consulte: http://stackoverflow.com/questions/9881933/catching-argumenttypeerror-exception-from-custom-action – Skotch

+1

@Skotch: 'readdable_dir' define un tipo por lo que' ArgumentTypeError' es apropiado aquí. He corregido el error tipográfico: acción -> escriba – jfs

+0

J.F. Sebastian: estoy bastante seguro de que es una acción personalizada de la que estamos hablando (consulte la definición de readable_dir dada por mgilson anterior, se deriva de argparse.Action). Pasar una acción argparse personalizada como un tipo no funcionará (al menos no cuando lo intenté). – Apteryx

13

presenté a patch for "path arguments" to the Python standard library mailing list hace unos meses.

Con esta clase PathType, sólo tiene que especificar el tipo siguiente argumento para que coincida con única un directorio existente - cualquier otra cosa le dará un mensaje de error:

type = PathType(exists=True, type='dir') 

está el código Aquí, lo que podría ser fácilmente modificado para requerir permisos específicos de archivo/directorio también:

from argparse import ArgumentTypeError as err 
import os 

class PathType(object): 
    def __init__(self, exists=True, type='file', dash_ok=True): 
     '''exists: 
       True: a path that does exist 
       False: a path that does not exist, in a valid parent directory 
       None: don't care 
      type: file, dir, symlink, None, or a function returning True for valid paths 
       None: don't care 
      dash_ok: whether to allow "-" as stdin/stdout''' 

     assert exists in (True, False, None) 
     assert type in ('file','dir','symlink',None) or hasattr(type,'__call__') 

     self._exists = exists 
     self._type = type 
     self._dash_ok = dash_ok 

    def __call__(self, string): 
     if string=='-': 
      # the special argument "-" means sys.std{in,out} 
      if self._type == 'dir': 
       raise err('standard input/output (-) not allowed as directory path') 
      elif self._type == 'symlink': 
       raise err('standard input/output (-) not allowed as symlink path') 
      elif not self._dash_ok: 
       raise err('standard input/output (-) not allowed') 
     else: 
      e = os.path.exists(string) 
      if self._exists==True: 
       if not e: 
        raise err("path does not exist: '%s'" % string) 

       if self._type is None: 
        pass 
       elif self._type=='file': 
        if not os.path.isfile(string): 
         raise err("path is not a file: '%s'" % string) 
       elif self._type=='symlink': 
        if not os.path.symlink(string): 
         raise err("path is not a symlink: '%s'" % string) 
       elif self._type=='dir': 
        if not os.path.isdir(string): 
         raise err("path is not a directory: '%s'" % string) 
       elif not self._type(string): 
        raise err("path not valid: '%s'" % string) 
      else: 
       if self._exists==False and e: 
        raise err("path exists: '%s'" % string) 

       p = os.path.dirname(os.path.normpath(string)) or '.' 
       if not os.path.isdir(p): 
        raise err("parent path is not a directory: '%s'" % p) 
       elif not os.path.exists(p): 
        raise err("parent directory does not exist: '%s'" % p) 

     return string 
Cuestiones relacionadas