62

我的 python 脚本需要从命令行传递的目录中读取文件。我已经定义了一个 readable_dir 类型,如下所示与 argparse 一起使用,以验证在命令行上传递的目录是否存在且可读。此外,还为目录参数指定了默认值(以下示例中的 /tmp/non_existent_dir)。这里的问题是,即使在命令行上显式传入目录参数的情况下,argparse 也会在默认值上调用 readable_dir()。这会导致脚本崩溃,因为默认路径 /tmp/non_existent_dir 在命令行上显式传入目录的上下文中不存在。我可以通过不指定默认值并强制此参数来解决此问题,

#!/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

3 回答 3

43

几个月前,我向Python 标准库邮件列表提交了一个“路径参数”补丁。

使用此类PathType,您可以简单地指定以下参数类型以匹配现有目录 - 其他任何内容都会给出错误消息:

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

这是代码,可以很容易地修改它以要求特定的文件/目录权限:

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
于 2015-10-16T23:40:21.150 回答
39

You can create a custom action instead of a type:

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)

But this seems a little fishy to me -- if no directory is given, it passes a non-readable directory which seems to defeat the purpose of checking if the directory is accessible in the first place.

Note that as pointed out in the comments, it might be nicer to
raise argparse.ArgumentError(self, ...) rather than argparse.ArgumentTypeError.

EDIT

As far as I'm aware, there is no way to validate the default argument. I suppose the argparse developers just assumed that if you're providing a default, then it should be valid. The quickest and easiest thing to do here is to simply validate the arguments immediately after you parse them. It looks like, you're just trying to get a temporary directory to do some work. If that's the case, you can use the tempfile module to get a new directory to work in. I updated my answer above to reflect this. I create a temporary directory, use that as the default argument (tempfile already guarantees the directory it creates will be writeable) and then I register it to be deleted when your program exits.

于 2012-07-10T14:41:19.163 回答
12

如果您的脚本在没有有效的情况下无法工作launch_directory,则应将其设为强制性参数:

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

顺便说一句,你应该使用argparse.ArgumentTypeError而不是Exceptionin readable_dir()

于 2012-07-10T14:54:21.090 回答