From 314e6805aa7e64614893b01fa604d742c897652a Mon Sep 17 00:00:00 2001 From: Bastian Kleineidam Date: Sun, 21 Feb 2010 12:56:13 +0100 Subject: [PATCH 1/4] Added the baker module locally because it is small, not widely known and I have patched it. --- doc/todo.txt | 1 - patool | 11 +- patoolib/__init__.py | 2 +- patoolib/baker.py | 559 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 565 insertions(+), 8 deletions(-) create mode 100644 patoolib/baker.py diff --git a/doc/todo.txt b/doc/todo.txt index 1e99398..e749024 100644 --- a/doc/todo.txt +++ b/doc/todo.txt @@ -1,3 +1,2 @@ -- add project to github - add create mode? - add test mode? diff --git a/patool b/patool index 26e1d50..93395bb 100755 --- a/patool +++ b/patool @@ -20,31 +20,30 @@ patool [extract|list] [sub-command-options] import sys if not hasattr(sys, "version_info") or sys.version_info < (2, 5, 0, "final", 0): raise SystemExit("Error, this script needs Python >= 2.5 installed") -import baker import patoolib -@baker.command +@patoolib.baker.command def extract (archive, verbose=False, force=False): """Extract files from an archive.""" return patoolib.handle_archive(archive, 'extract') -@baker.command +@patoolib.baker.command def list (archive, verbose=False): """List files in an archive.""" return patoolib.handle_archive(archive, 'list') -#@baker.command +#@patoolib.baker.command #def create (archive, *args): # """Create an archive from given files.""" # return patoolib.handle_archive(archive, 'create', *args) -@baker.command +@patoolib.baker.command def formats (): return patoolib.list_formats() -sys.exit(baker.run()) +sys.exit(patoolib.baker.run()) diff --git a/patoolib/__init__.py b/patoolib/__init__.py index da470c4..bdb0ec5 100644 --- a/patoolib/__init__.py +++ b/patoolib/__init__.py @@ -16,7 +16,7 @@ import os import shutil from distutils.spawn import find_executable -from . import util +from . import util, baker # Supported archive commands ArchiveCommands = ('list', 'extract') diff --git a/patoolib/baker.py b/patoolib/baker.py new file mode 100644 index 0000000..bd8a204 --- /dev/null +++ b/patoolib/baker.py @@ -0,0 +1,559 @@ +#=============================================================================== +# Copyright 2010 Matt Chaput +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#=============================================================================== + +import re, sys +from inspect import getargspec +from textwrap import wrap + + +def normalize_docstring(docstring): + """Normalizes whitespace in the given string. + """ + return re.sub(r"[\r\n\t ]+", " ", docstring).strip() + + +param_exp = re.compile(r"^([\t ]*):param (.*?): ([^\n]*\n(\1[ \t]+[^\n]*\n)*)", + re.MULTILINE) + +def find_param_docs(docstring): + """Finds ReStructuredText-style ":param:" lines in the docstring and + returns a dictionary mapping param names to doc strings. + """ + + paramdocs = {} + for match in param_exp.finditer(docstring): + name = match.group(2) + value = match.group(3) + paramdocs[name] = value + return paramdocs + +def remove_param_docs(docstring): + """Finds ReStructuredText-style ":param:" lines in the docstring and + returns a new string with the param documentation removed. + """ + return param_exp.sub("", docstring) + + +def process_docstring(docstring): + """Takes a docstring and returns a list of strings representing + the paragraphs in the docstring. + """ + + lines = docstring.split("\n") + paras = [[]] + for line in lines: + if not line.strip(): + paras.append([]) + else: + paras[-1].append(line) + paras = [normalize_docstring(" ".join(ls)) + for ls in paras if ls] + return paras + + +def format_paras(paras, width, indent=0): + """Takes a list of paragraph strings and formats them into a word-wrapped, + optionally indented string. + """ + + output = [] + for para in paras: + lines = wrap(para, width-indent) + if lines: + for line in lines: + output.append((" " * indent) + line) + output.append("") + return "\n".join(output) + + +def totype(v, default): + """Tries to convert the value 'v' into the same type as 'default'. + """ + + t = type(default) + if t is int: + return int(v) + elif t is float: + return float(v) + elif t is long: + return long(v) + elif t is bool: + lv = v.lower() + if lv in ("true", "yes", "on", "1"): + return True + elif lv in ("false", "no", "off", "0"): + return False + else: + raise TypeError + else: + return v + + +class CommandError(Exception): pass + + +class Cmd(object): + """Stores metadata about a command. + """ + + def __init__(self, name, fn, argnames, keywords, shortopts, + has_varargs, has_kwargs, docstring, paramdocs): + self.name = name + self.fn = fn + self.argnames = argnames + self.keywords = keywords + self.shortopts = shortopts + self.has_varargs = has_varargs + self.has_kwargs = has_kwargs + self.docstring = docstring + self.paramdocs = paramdocs + + +class Baker(object): + def __init__(self): + self.commands = {} + self.defaultcommand = None + + def command(self, fn=None, name=None, default=False, + params=None, shortopts=None): + """Registers a command with the bakery. This does not call the + function, it simply adds it to the list of functions this Baker + knows about. + + This method is usually used as a decorator:: + + b = Baker() + + @b.command + def test(): + pass + + :param fn: the function to register. + :param name: use this argument to register the command under a + different name than the function name. + :param default: if True, this command is used when a command is not + specified on the command line. + :param params: a dictionary mapping parameter names to docstrings. If + you don't specify this argument, parameter annotations will be used + (Python 3.x only), or the functions docstring will be searched for + Sphinx-style ':param' blocks. + :param shortopts: a dictionary mapping parameter names to short + options, e.g. {"verbose": "v"}. + """ + + # This method works as a decorator with or without arguments. + if fn is None: + # The decorator was given arguments, e.g. @command(default=True), + # so we have to return a function that will wrap the function when + # the decorator is applied. + return lambda fn: self.command(fn, default=default, + params=params, + shortopts=shortopts) + else: + name = name or fn.__name__ + + # Inspect the argument signature of the function + arglist, vargsname, kwargsname, defaults = getargspec(fn) + has_varargs = bool(vargsname) + has_kwargs = bool(kwargsname) + + # Get the function's docstring + docstring = fn.__doc__ or "" + + # If the user didn't specify parameter help in the decorator + # arguments, try to get it from parameter annotations (Python 3.x) + # or RST-style :param: lines in the docstring + if params is None: + if hasattr(fn, "func_annotations") and fn.func_annotations: + params = fn.func_annotations + else: + params = find_param_docs(docstring) + docstring = remove_param_docs(docstring) + + # If the user didn't specify + shortopts = shortopts or {} + + # Zip up the keyword argument names with their defaults + if defaults: + keywords = dict(zip(arglist[0-len(defaults):], defaults)) + else: + keywords = {} + + # If this is a method, remove 'self' from the argument list + if arglist and arglist[0] == "self": + arglist.pop(0) + + # Create a Cmd object to represent this command and store it + cmd = Cmd(name, fn, arglist, keywords, shortopts, + has_varargs, has_kwargs, + docstring, params) + self.commands[cmd.name] = cmd + + # If default is True, set this as the default command + if default: self.defaultcommand = cmd + + return fn + + def print_top_help(self, scriptname, file=sys.stdout): + """Prints the documentation for the script and exits. + + :param scriptname: the name of the script being executed (argv[0]). + :param file: the file to write the help to. The default is stdout. + """ + + # Get a sorted list of all command names + cmdnames = sorted(self.commands.keys()) + + # Calculate the indent for the doc strings by taking the longest + # command name and adding 3 (one space before the name and two after) + rindent = max(len(name) for name in cmdnames) + 3 + + # Print the basic help for running a command + file.write("\nUsage: %s COMMAND \n\n" % scriptname) + + print("Available commands:\n\n") + for cmdname in cmdnames: + # Get the Cmd object for this command + cmd = self.commands[cmdname] + + # Calculate the padding necessary to fill from the end of the + # command name to the documentation margin + tab = " " * (rindent - (len(cmdname)+1)) + file.write(" " + cmdname + tab) + + # Get the paragraphs of the command's docstring + paras = process_docstring(cmd.docstring) + if paras: + # Print the first paragraph + file.write(format_paras([paras[0]], 76, indent=rindent).lstrip()) + + file.write("\n") + file.write('Use "%s --help" for individual command help.\n' % scriptname) + sys.exit(0) + + def print_command_help(self, scriptname, cmd, file=sys.stdout): + """Prints the documentation for a specific command and exits. + + :param scriptname: the name of the script being executed (argv[0]). + :param cmd: the Cmd object representing the command. + :param file: the file to write the help to. The default is stdout. + """ + + # Print the usage for the command + file.write("\nUsage: %s %s" % (scriptname, cmd.name)) + + # Print the required and "optional" arguments (where optional + # arguments are keyword arguments with default None). + for name in cmd.argnames: + if name not in cmd.keywords: + # This is a positional argument + file.write(" <%s>" % name) + else: + # This is a keyword argument, so skip it unless the default is + # None, in which case treat it like an optional argument. + if cmd.keywords[name] is None: + file.write(" [<%s>]" % name) + + if cmd.has_varargs: + # This command accepts a variable number of positional arguments + file.write(" [...]") + file.write("\n\n") + + # Print the documentation for this command + paras = process_docstring(cmd.docstring) + if paras: + # Print the first paragraph with no indent (usually just a summary + # line) + file.write(format_paras([paras[0]], 76)) + + # Print subsequent paragraphs indented by 4 spaces + if len(paras) > 1: + file.write("\n") + file.write(format_paras(paras[1:], 76, indent=4)) + file.write("\n") + + # Print documentation for keyword options + if cmd.keywords: + file.write("Options:\n\n") + + # Get a sorted list of keyword argument names + keynames = sorted(cmd.keywords.keys()) + + # Make formatted headings, e.g. " -k --keyword ", and put them in + # a list like [(name, heading), ...] + heads = [] + for keyname in keynames: + if cmd.keywords[keyname] is None: continue + + head = " --" + keyname + if keyname in cmd.shortopts: + head = " -" + cmd.shortopts[keyname] + head + head += " " + heads.append((keyname, head)) + + if heads: + # Find the length of the longest formatted heading + rindent = max(len(head) for keyname, head in heads) + # Pad the headings so they're all as long as the longest one + heads = [(keyname, head + (" " * (rindent - len(head)))) + for keyname, head in heads] + + # Print the option docs + for keyname, head in heads: + # Print the heading + file.write(head) + + # If this parameter has documentation, print it after the + # heading + if keyname in cmd.paramdocs: + paras = process_docstring(cmd.paramdocs.get(keyname, "")) + file.write(format_paras(paras, 76, indent=rindent).lstrip()) + else: + file.write("\n") + file.write("\n") + + if any((cmd.keywords.get(a) is None) for a in cmd.argnames): + file.write("(specifying a single hyphen (-) in the argument list means all\n") + file.write("subsequent arguments are treated as bare arguments, not options)\n") + file.write("\n") + + sys.exit(0) + + def parse_args(self, scriptname, cmd, argv): + keywords = cmd.keywords + shortopts = cmd.shortopts + + # shortopts maps long option names to characters. To look up short + # options, we need to create a reverse mapping. + shortchars = dict((v, k) for k, v in shortopts.iteritems()) + + # The *vargs list and **kwargs dict to build up from the command line + # arguments + vargs = [] + kwargs = {} + + while argv: + # Take the next argument + arg = argv.pop(0) + + if arg == "-": + # All arguments following a single hyphen are treated as + # positional arguments + vargs.extend(argv) + break + + elif arg == "--": + # What to do with a bare --? Right now, it's ignored. + continue + + elif arg.startswith("--"): + # Process long option + + value = None + if "=" in arg: + # The argument was specified like --keyword=value + name, value = arg[2:].split("=", 1) + default = keywords.get(name) + try: + value = totype(value, default) + except TypeError: + pass + else: + # The argument was not specified with an equals sign... + name = arg[2:] + default = keywords.get(name) + + if type(default) is bool: + # If this option is a boolean, it doesn't need a value; + # specifying it on the command line means "do the + # opposite of the default". + value = not default + else: + # The next item in the argument list is the value, i.e. + # --keyword value + if not argv or argv[0].startswith("-"): + # Oops, there isn't a value available... just use + # True, assuming this is a flag. + value = True + else: + value = argv.pop(0) + + try: + value = totype(value, default) + except TypeError: + pass + + # Store this option + kwargs[name] = value + + elif arg.startswith("-") and cmd.shortopts: + # Process short option(s) + + # For each character after the '-'... + for i in xrange(1, len(arg)): + char = arg[i] + if char not in shortchars: + continue + + # Get the long option name corresponding to this char + name = shortchars[char] + + default = keywords[name] + if type(default) is bool: + # If this option is a boolean, it doesn't need a value; + # specifying it on the command line means "do the + # opposite of the default". + kwargs[name] = not default + else: + # This option requires a value... + if i == len(arg)-1: + # This is the last character in the list, so the + # next argument on the command line is the value. + value = argv.pop(0) + else: + # There are other characters after this one, so + # the rest of the characters must represent the + # value (i.e. old-style UNIX option like -Nname) + value = totype(arg[i+1:], default) + + try: + kwargs[name] = totype(value, default) + except ValueError: + raise CommandError("Couldn't convert %s value %r to type %s" % (name, value, type(default))) + break + else: + # This doesn't start with "-", so just add it to the list of + # positional arguments. + vargs.append(arg) + + return vargs, kwargs + + def parse(self, argv=None): + """Parses the command and parameters to call from the list of command + line arguments. Returns a tuple of (Cmd object, position arg list, + keyword arg dict). + + :param argv: the list of options passed to the command line (sys.argv). + """ + + if argv is None: argv = sys.argv + + scriptname = argv[0] + + if (len(argv) < 2) or (argv[1] == "-h" or argv[1] == "--help"): + # Print the documentation for the script + self.print_top_help(scriptname) + + if argv[1] == "help": + if len(argv) > 2 and argv[2] in self.commands: + cmd = self.commands[argv[2]] + self.print_command_help(scriptname, cmd) + self.print_top_help(scriptname) + + if len(argv) > 1 and argv[1] in self.commands: + # The first argument on the command line (after the script name + # is the command to run. + cmd = self.commands[argv[1]] + + if len(argv) > 2 and (argv[2] == "-h" or argv[2] == "--help"): + # Print the help for this command and exit + self.print_command_help(scriptname, cmd) + + options = argv[2:] + else: + # No known command was specified. If there's a default command, + # use that. + cmd = self.defaultcommand + if cmd is None: + raise CommandError("No command specified") + + options = argv[1:] + + # Parse the rest of the arguments on the command line and use them to + # call the command function. + args, kwargs = self.parse_args(scriptname, cmd, options) + return (cmd, args, kwargs) + + def apply(self, cmd, args, kwargs): + """Calls the command function. + """ + + # Create a list of positional arguments: arguments that are either + # required (not in keywords), or where the default is None (taken to be + # an optional positional argument). This is different from the Python + # calling convention, which will fill in keyword arguments with extra + # positional arguments. + posargs = [a for a in cmd.argnames if cmd.keywords.get(a) is None] + + if len(args) > len(posargs) and not cmd.has_varargs: + raise CommandError("Too many arguments to %s: %s" % (cmd.name, " ".join(args))) + + if not cmd.has_kwargs: + for k in sorted(kwargs.iterkeys()): + if k not in cmd.keywords: + raise CommandError("Unknown option --%s" % k) + + # Rearrange the arguments into the order Python expects + newargs = [] + newkwargs = {} + for name in cmd.argnames: + if args and cmd.keywords.get(name) is None: + # This argument is required or optional and we have a bare arg + # to fill it + newargs.append(args.pop(0)) + elif name not in cmd.keywords and not args: + # This argument is required but we don't have a bare arg to + # fill it + raise CommandError("Required argument '%s' not given" % name) + else: + # This is a keyword argument + newkwargs[name] = kwargs.get(name, cmd.keywords[name]) + newargs.extend(args) + + return cmd.fn(*newargs, **newkwargs) + + def run(self, argv=None): + """Takes a list of command line arguments, parses it into a command + name and options, and calls the function corresponding to the command + with the given arguments. + + :param argv: the list of options passed to the command line (sys.argv). + """ + + return self.apply(*self.parse(argv)) + + def test(self, argv=None): + """Takes a list of command line arguments, parses it into a command + name and options, and prints what the resulting function call would + look like. This may be useful for testing how command line arguments + would be passed to your functions. + + :param argv: the list of options passed to the command line (sys.argv). + """ + + cmd, args, kwargs = self.parse(argv) + result = "%s(%s" % (cmd.name, ",".join(repr(a) for a in args)) + if kwargs: + kws = ", ".join("%s=%r" % (k, v) for k, v in kwargs.iteritems()) + result += ", " + kws + result += ")" + print result + + +_baker = Baker() +command = _baker.command +run = _baker.run +test = _baker.test From ed244c1338d437669d3320cc7d1f5acaac74724a Mon Sep 17 00:00:00 2001 From: Bastian Kleineidam Date: Sun, 21 Feb 2010 13:00:21 +0100 Subject: [PATCH 2/4] Improved error message for unknown commands. --- patoolib/baker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patoolib/baker.py b/patoolib/baker.py index bd8a204..058a7f0 100644 --- a/patoolib/baker.py +++ b/patoolib/baker.py @@ -478,7 +478,7 @@ class Baker(object): # use that. cmd = self.defaultcommand if cmd is None: - raise CommandError("No command specified") + raise CommandError("unknown command `%s' specified" % argv[1]) options = argv[1:] From c1ada5cc39ab0019111feef80c4adead10b5ddb5 Mon Sep 17 00:00:00 2001 From: Bastian Kleineidam Date: Sun, 21 Feb 2010 13:01:06 +0100 Subject: [PATCH 3/4] Show error for unknown commands. --- patool | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/patool b/patool index 93395bb..c044114 100755 --- a/patool +++ b/patool @@ -45,5 +45,8 @@ def list (archive, verbose=False): def formats (): return patoolib.list_formats() - -sys.exit(patoolib.baker.run()) +try: + sys.exit(patoolib.baker.run()) +except patoolib.baker.CommandError, msg: + print >>sys.stderr, "patool error:", msg + sys.exit(1) From 2587bfda867a8da3f6d8dcc78330cde894bd015b Mon Sep 17 00:00:00 2001 From: Bastian Kleineidam Date: Sun, 21 Feb 2010 13:40:42 +0100 Subject: [PATCH 4/4] Added test archive command. --- doc/patool.1 | 16 +++++++++++++--- doc/patool.txt | 23 +++++++++++++++++------ patool | 6 ++++++ patoolib/__init__.py | 11 ++++++++++- patoolib/arj.py | 16 ++++++++++++++-- patoolib/bzip2.py | 9 +++++++++ patoolib/cabextract.py | 7 +++++++ patoolib/dpkg.py | 3 +++ patoolib/gzip.py | 12 ++++++++++++ patoolib/lzop.py | 9 +++++++++ patoolib/p7zip.py | 23 +++++++++++++++++++++++ patoolib/pbzip2.py | 2 +- patoolib/rar.py | 9 +++++++++ patoolib/rpm.py | 9 +++++++++ patoolib/star.py | 2 ++ patoolib/tar.py | 2 ++ patoolib/unrar.py | 9 +++++++++ patoolib/unzip.py | 9 +++++++++ tests/__init__.py | 16 +++++++++++----- tests/test_archives.py | 41 +++++++++++++++++++++++++++++------------ 20 files changed, 204 insertions(+), 30 deletions(-) diff --git a/doc/patool.1 b/doc/patool.1 index 657bc26..514cd58 100644 --- a/doc/patool.1 +++ b/doc/patool.1 @@ -15,9 +15,11 @@ .SH NAME patool - simple manager for file archives of various types .SH SYNOPSIS -\fBpatool\fP <\fIcommand\fP> [\fIoptions\fP] <\fIarchive-file\fP> + \fBpatool\fP (\fBextract\fP|\fBlist\fP|\fBtest\fP) [\fIoptions\fP] <\fIarchive-file\fP> + \fBpatool\fP \fBcreate\fP [\fIoptions\fP] <\fIarchive-file\fP> [\fIfiles..\fP] + \fBpatool\fP \fBformats\fP [\fIoptions\fP] .SH DESCRIPTION -Various archive types can be created, extracted and listed by +Various archive types can be created, extracted, tested and listed by \fBpatool\fP. The archive format is determined by the archive file extension and as a fallback with file(1). @@ -54,6 +56,14 @@ Verbose archive listing (if the helper application supports it). .TP \fB\-\-help\fP Show help for this command. +.SS \fBtest\fP +Test files in an archive. +.TP +\fB\-\-verbose\fP +Verbose archive testing (if the helper application supports it). +.TP +\fB\-\-help\fP +Show help for this command. .SS \fBformats\fP Show all supported archive formats. .TP @@ -81,7 +91,7 @@ Supported archive formats are listed by the \fBformats\fP command. .TP \fImode\fP\fB=/usr/bin/mycommand\fP Set the application to handle the archive format for given mode. -\fImode\fP can be one of \fBextract\fP, \fBlist\fP. +\fImode\fP can be one of \fBextract\fP, \fBlist\fP, \fBtest\fP. .SH FILES \fB/etc/patool.conf\fP, \fB~/.patool.conf\fP - configuration files diff --git a/doc/patool.txt b/doc/patool.txt index 10959b1..c55f84c 100644 --- a/doc/patool.txt +++ b/doc/patool.txt @@ -6,20 +6,23 @@ NAME patool - simple manager for file archives of various types SYNOPSIS - patool [options] + patool (extract|list|test) [options] + patool create [options] [files..] + patool formats [options] DESCRIPTION - Various archive types can be created, extracted and listed by patool. The archive format is determined by the archive file extension and as a fallback with file(1). + Various archive types can be created, extracted, tested and listed by patool. The archive format is determined by the archive file extension and as a fallback with + file(1). - patool supports 7z (.7z), ZIP (.zip, .jar), GZIP (.gz), compress (.Z), BZIP2 (.bz2), TAR (.tar), ARJ (.arj), CAB (.cab), CPIO (.cpio), RPM (.rpm), DEB (.deb), LZOP + patool supports 7z (.7z), ZIP (.zip, .jar), GZIP (.gz), compress (.Z), BZIP2 (.bz2), TAR (.tar), ARJ (.arj), CAB (.cab), CPIO (.cpio), RPM (.rpm), DEB (.deb), LZOP (.lzo)and RAR (.rar) formats. It relies on helper applications to handle those archive formats (for example bzip2 for BZIP2 archives). COMMANDS Several commands and options are available. extract - Extract files from an archive4. Often one wants to extract all files in an archive to a single subdirectory. However, some archives contain multiple files in their - root directories. The patool program overcomes this problem by first extracting files to a unique (temporary) directory, and then moving its contents back if possible. + Extract files from an archive4. Often one wants to extract all files in an archive to a single subdirectory. However, some archives contain multiple files in their + root directories. The patool program overcomes this problem by first extracting files to a unique (temporary) directory, and then moving its contents back if possible. This also prevents local files from being overwritten by mistake. --force @@ -38,6 +41,14 @@ COMMANDS --help Show help for this command. + test + Test files in an archive. + + --verbose + Verbose archive testing (if the helper application supports it). + + --help Show help for this command. + formats Show all supported archive formats. @@ -61,7 +72,7 @@ CONFIGURATION Supported archive formats are listed by the formats command. mode=/usr/bin/mycommand - Set the application to handle the archive format for given mode. mode can be one of extract, list. + Set the application to handle the archive format for given mode. mode can be one of extract, list, test. FILES /etc/patool.conf, ~/.patool.conf - configuration files diff --git a/patool b/patool index c044114..ee36375 100755 --- a/patool +++ b/patool @@ -35,6 +35,12 @@ def list (archive, verbose=False): return patoolib.handle_archive(archive, 'list') +@patoolib.baker.command +def test (archive, verbose=False): + """Test files in an archive.""" + return patoolib.handle_archive(archive, 'test', verbose=verbose) + + #@patoolib.baker.command #def create (archive, *args): # """Create an archive from given files.""" diff --git a/patoolib/__init__.py b/patoolib/__init__.py index bdb0ec5..5f8d2bf 100644 --- a/patoolib/__init__.py +++ b/patoolib/__init__.py @@ -19,7 +19,7 @@ from distutils.spawn import find_executable from . import util, baker # Supported archive commands -ArchiveCommands = ('list', 'extract') +ArchiveCommands = ('list', 'extract', 'test') # Supported archive formats ArchiveFormats = ('gzip', 'bzip2', 'tar', 'zip', 'compress', '7z', 'rar', @@ -55,6 +55,7 @@ ArchiveMimetypes = { ArchivePrograms = { 'bzip2': { 'extract': ('pbzip2', 'bzip2', '7z'), + 'test': ('pbzip2', 'bzip2', '7z'), 'list': ('7z', 'echo',), }, 'tar': { @@ -63,6 +64,7 @@ ArchivePrograms = { 'zip': { 'extract': ('unzip', '7z'), 'list': ('unzip', '7z'), + 'test': ('unzip', '7z'), }, 'gzip': { None: ('gzip', '7z'), @@ -70,6 +72,7 @@ ArchivePrograms = { 'compress': { 'extract': ('gzip', '7z', 'uncompress.real'), 'list': ('7z', 'echo',), + 'test': ('gzip', '7z'), }, '7z': { None: ('7z',), @@ -78,27 +81,33 @@ ArchivePrograms = { None: ('rar',), 'extract': ('unrar', '7z'), 'list': ('unrar', '7z'), + 'test': ('unrar', '7z'), }, 'cab': { 'extract': ('cabextract', '7z'), 'list': ('cabextract', '7z'), + 'test': ('cabextract', '7z'), }, 'arj': { 'extract': ('arj', '7z'), 'list': ('arj', '7z'), + 'test': ('arj', '7z'), }, 'cpio': { 'extract': ('cpio', '7z'), 'list': ('cpio', '7z'), + 'test': ('7z',), }, 'rpm': { # XXX rpm2cpio depends on cpio which is not checked 'extract': ('rpm2cpio', '7z'), 'list': ('rpm', '7z'), + 'test': ('rpm', '7z'), }, 'deb': { 'extract': ('dpkg-deb', '7z'), 'list': ('dpkg-deb', '7z'), + 'test': ('dpkg-deb', '7z'), }, 'lzop': { None: ('lzop',), diff --git a/patoolib/arj.py b/patoolib/arj.py index e890f35..c9dd1a4 100644 --- a/patoolib/arj.py +++ b/patoolib/arj.py @@ -21,8 +21,8 @@ def extract_arj (archive, encoding, cmd, **kwargs): cmdlist.append('x') cmdlist.append('-r') cmdlist.append('-y') - if kwargs['verbose']: - cmdlist.append('-v') + if not kwargs['verbose']: + cmdlist.append('-i-') cmdlist.extend([archive, kwargs['outdir']]) return cmdlist @@ -33,6 +33,18 @@ def list_arj (archive, encoding, cmd, **kwargs): cmdlist.append('v') else: cmdlist.append('l') + cmdlist.append('-i-') + cmdlist.append('-r') + cmdlist.append('-y') + cmdlist.extend([archive]) + return cmdlist + +def test_arj (archive, encoding, cmd, **kwargs): + """Test a ARJ archive.""" + cmdlist = [cmd] + cmdlist.append('t') + if not kwargs['verbose']: + cmdlist.append('-i-') cmdlist.append('-r') cmdlist.append('-y') cmdlist.extend([archive]) diff --git a/patoolib/bzip2.py b/patoolib/bzip2.py index 0f3aa33..8f3f3ba 100644 --- a/patoolib/bzip2.py +++ b/patoolib/bzip2.py @@ -30,3 +30,12 @@ def extract_bzip2 (archive, encoding, cmd, **kwargs): # note that for shell calls the command must be a string cmd = " ".join([util.shell_quote(x) for x in cmdlist]) return (cmd, {'shell': True}) + + +def test_bzip2 (archive, encoding, cmd, **kwargs): + cmdlist = [cmd] + if kwargs['verbose']: + cmdlist.append('-v') + cmdlist.extend(['-t', '--']) + cmdlist.extend([archive]) + return cmdlist diff --git a/patoolib/cabextract.py b/patoolib/cabextract.py index 7536001..977e774 100644 --- a/patoolib/cabextract.py +++ b/patoolib/cabextract.py @@ -32,3 +32,10 @@ def list_cab (archive, encoding, cmd, **kwargs): cmdlist.append('-v') cmdlist.extend([archive]) return cmdlist + +def test_cab (archive, encoding, cmd, **kwargs): + """Test a CAB archive.""" + cmdlist = [cmd] + cmdlist.append('-t') + cmdlist.extend([archive]) + return cmdlist diff --git a/patoolib/dpkg.py b/patoolib/dpkg.py index dbc8920..b2854ca 100644 --- a/patoolib/dpkg.py +++ b/patoolib/dpkg.py @@ -33,3 +33,6 @@ def list_deb (archive, encoding, cmd, **kwargs): cmdlist.append('--') cmdlist.extend([archive]) return cmdlist + +test_deb = list_deb + diff --git a/patoolib/gzip.py b/patoolib/gzip.py index cd6300f..56b8650 100644 --- a/patoolib/gzip.py +++ b/patoolib/gzip.py @@ -43,3 +43,15 @@ def list_gzip (archive, encoding, cmd, **kwargs): cmdlist.append(archive) return cmdlist + +def test_gzip (archive, encoding, cmd, **kwargs): + """Test a GZIP archive.""" + cmdlist = [cmd] + if kwargs['verbose']: + cmdlist.append('-v') + cmdlist.append('-t') + cmdlist.append('--') + cmdlist.append(archive) + return cmdlist + +test_compress = test_gzip diff --git a/patoolib/lzop.py b/patoolib/lzop.py index 38c9c67..8cdf7d1 100644 --- a/patoolib/lzop.py +++ b/patoolib/lzop.py @@ -32,3 +32,12 @@ def list_lzop (archive, encoding, cmd, **kwargs): cmdlist.append('--verbose') cmdlist.extend(['--', archive]) return cmdlist + +def test_lzop (archive, encoding, cmd, **kwargs): + """Test a LZOP archive.""" + cmdlist = [cmd] + cmdlist.append('--test') + if kwargs['verbose']: + cmdlist.append('--verbose') + cmdlist.extend(['--', archive]) + return cmdlist diff --git a/patoolib/p7zip.py b/patoolib/p7zip.py index df7bd88..e69ed26 100644 --- a/patoolib/p7zip.py +++ b/patoolib/p7zip.py @@ -59,3 +59,26 @@ list_bzip2 = \ list_rpm = \ list_deb = \ list_7z + + +def test_7z (archive, encoding, cmd, **kwargs): + """Test a 7z archive.""" + cmdlist = [cmd] + cmdlist.append('t') + if not kwargs['verbose']: + cmdlist.append('-bd') + cmdlist.append('--') + cmdlist.append(archive) + return cmdlist + +test_bzip2 = \ + test_gzip = \ + test_zip = \ + test_compress = \ + test_rar = \ + test_cab = \ + test_arj = \ + test_cpio = \ + test_rpm = \ + test_deb = \ + test_7z diff --git a/patoolib/pbzip2.py b/patoolib/pbzip2.py index d985023..a6b66cc 100644 --- a/patoolib/pbzip2.py +++ b/patoolib/pbzip2.py @@ -15,4 +15,4 @@ # along with this program. If not, see . """Archive commands for the pbzip2 program.""" # bzip2 and pbzip2 are compatible -from .bzip2 import extract_bzip2 +from .bzip2 import extract_bzip2, test_bzip2 diff --git a/patoolib/rar.py b/patoolib/rar.py index 8d95bf4..c69a057 100644 --- a/patoolib/rar.py +++ b/patoolib/rar.py @@ -34,3 +34,12 @@ def list_rar (archive, encoding, cmd, **kwargs): cmdlist.append('-c-') cmdlist.extend(['--', archive]) return cmdlist + +def test_rar (archive, encoding, cmd, **kwargs): + """Test a RAR archive.""" + cmdlist = [cmd] + cmdlist.append('t') + if not kwargs['verbose']: + cmdlist.append('-c-') + cmdlist.extend(['--', archive]) + return cmdlist diff --git a/patoolib/rpm.py b/patoolib/rpm.py index bb3171a..76220ea 100644 --- a/patoolib/rpm.py +++ b/patoolib/rpm.py @@ -23,3 +23,12 @@ def list_rpm (archive, encoding, cmd, **kwargs): cmdlist.append('-v') cmdlist.extend(['-p', '--', archive]) return cmdlist + +def test_rpm (archive, encoding, cmd, **kwargs): + """Test a RPM archive.""" + cmdlist = [cmd] + cmdlist.append('-V') + if kwargs['verbose']: + cmdlist.append('-v') + cmdlist.extend(['-p', '--', archive]) + return cmdlist diff --git a/patoolib/star.py b/patoolib/star.py index 6be9d55..1fd6095 100644 --- a/patoolib/star.py +++ b/patoolib/star.py @@ -44,3 +44,5 @@ def list_tar (archive, encoding, cmd, **kwargs): cmdlist.append('-v') cmdlist.append("file=%s" % archive) return cmdlist + +test_tar = list_tar diff --git a/patoolib/tar.py b/patoolib/tar.py index 832eda0..4c38667 100644 --- a/patoolib/tar.py +++ b/patoolib/tar.py @@ -34,3 +34,5 @@ def list_tar (archive, encoding, cmd, **kwargs): cmdlist.append('--verbose') cmdlist.extend(["--file", archive]) return cmdlist + +test_tar = list_tar diff --git a/patoolib/unrar.py b/patoolib/unrar.py index 8d95bf4..c69a057 100644 --- a/patoolib/unrar.py +++ b/patoolib/unrar.py @@ -34,3 +34,12 @@ def list_rar (archive, encoding, cmd, **kwargs): cmdlist.append('-c-') cmdlist.extend(['--', archive]) return cmdlist + +def test_rar (archive, encoding, cmd, **kwargs): + """Test a RAR archive.""" + cmdlist = [cmd] + cmdlist.append('t') + if not kwargs['verbose']: + cmdlist.append('-c-') + cmdlist.extend(['--', archive]) + return cmdlist diff --git a/patoolib/unzip.py b/patoolib/unzip.py index 9a5adbe..3594ada 100644 --- a/patoolib/unzip.py +++ b/patoolib/unzip.py @@ -31,3 +31,12 @@ def list_zip (archive, encoding, cmd, **kwargs): cmdlist.append('-v') cmdlist.extend(['--', archive]) return cmdlist + +def test_zip (archive, encoding, cmd, **kwargs): + """Test a ZIP archive.""" + cmdlist = [cmd] + cmdlist.append('-t') + if kwargs['verbose']: + cmdlist.append('-v') + cmdlist.extend(['--', archive]) + return cmdlist diff --git a/tests/__init__.py b/tests/__init__.py index f1915f4..986e14c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -26,8 +26,9 @@ datadir = os.path.join(basedir, 'data') class ArchiveTest (unittest.TestCase): """Helper class for achive tests.""" - def archive_test (self, filename, cmd): + def archive_commands (self, filename, cmd): self.archive_list(filename, cmd) + self.archive_test(filename, cmd) self.archive_extract(filename, cmd) def archive_extract (self, filename, cmd): @@ -36,16 +37,21 @@ class ArchiveTest (unittest.TestCase): tmpdir = patoolib.util.tmpdir(dir=basedir) os.chdir(tmpdir) try: - patoolib._handle_archive(archive, 'extract', cmd=cmd) - patoolib._handle_archive(archive, 'extract', cmd=cmd, force=True) + patoolib._handle_archive(archive, 'extract', program=cmd) + patoolib._handle_archive(archive, 'extract', program=cmd, force=True) finally: os.chdir(basedir) shutil.rmtree(tmpdir) def archive_list (self, filename, cmd): archive = os.path.join(datadir, filename) - patoolib._handle_archive(archive, 'list', cmd=cmd) - patoolib._handle_archive(archive, 'list', cmd=cmd, verbose=True) + patoolib._handle_archive(archive, 'list', program=cmd) + patoolib._handle_archive(archive, 'list', program=cmd, verbose=True) + + def archive_test (self, filename, cmd): + archive = os.path.join(datadir, filename) + patoolib._handle_archive(archive, 'test', program=cmd) + patoolib._handle_archive(archive, 'test', program=cmd, verbose=True) def needs_cmd (cmd): diff --git a/tests/test_archives.py b/tests/test_archives.py index e7caacf..b0e96cf 100644 --- a/tests/test_archives.py +++ b/tests/test_archives.py @@ -26,19 +26,21 @@ class TestArchives (ArchiveTest): self.tar_test('star') def tar_test (self, cmd): - self.archive_test('t.tar', cmd) - self.archive_test('t.tar.gz', cmd) - self.archive_test('t.tar.Z', cmd) - self.archive_test('t.tar.bz2', cmd) - self.archive_test('t.tbz2', cmd) + self.archive_commands('t.tar', cmd) + self.archive_commands('t.tar.gz', cmd) + self.archive_commands('t.tar.Z', cmd) + self.archive_commands('t.tar.bz2', cmd) + self.archive_commands('t.tbz2', cmd) @needs_cmd('bzip2') def test_bzip2 (self): self.archive_extract('t.bz2', 'bzip2') + self.archive_test('t.bz2', 'bzip2') @needs_cmd('pbzip2') def test_pbzip2 (self): self.archive_extract('t.bz2', 'pbzip2') + self.archive_test('t.bz2', 'pbzip2') @needs_cmd('echo') def test_echo (self): @@ -47,13 +49,13 @@ class TestArchives (ArchiveTest): @needs_cmd('unzip') def test_unzip (self): - self.archive_test('t.zip', 'unzip') - self.archive_test('t.jar', 'unzip') + self.archive_commands('t.zip', 'unzip') + self.archive_commands('t.jar', 'unzip') @needs_cmd('gzip') def test_gzip (self): - self.archive_test('t.gz', 'gzip') - self.archive_test('t.txt.gz', 'gzip') + self.archive_commands('t.gz', 'gzip') + self.archive_commands('t.txt.gz', 'gzip') self.archive_extract('t.Z', 'gzip') @needs_cmd('uncompress.real') @@ -62,7 +64,7 @@ class TestArchives (ArchiveTest): @needs_cmd('7z') def test_p7zip (self): - self.archive_test('t.7z', '7z') + self.archive_commands('t.7z', '7z') self.archive_list('t.gz', '7z') self.archive_list('t.bz2', '7z') self.archive_list('t.zip', '7z') @@ -85,6 +87,17 @@ class TestArchives (ArchiveTest): self.archive_extract('t.cpio', '7z') self.archive_extract('t.rpm', '7z') self.archive_extract('t.deb', '7z') + self.archive_test('t.gz', '7z') + self.archive_test('t.bz2', '7z') + self.archive_test('t.zip', '7z') + self.archive_test('t.jar', '7z') + self.archive_test('t.Z', '7z') + self.archive_test('t.rar', '7z') + self.archive_test('t.cab', '7z') + self.archive_test('t.arj', '7z') + self.archive_test('t.cpio', '7z') + self.archive_test('t.rpm', '7z') + self.archive_test('t.deb', '7z') @needs_cmd('unrar') def test_unrar (self): @@ -93,7 +106,7 @@ class TestArchives (ArchiveTest): @needs_cmd('rar') def test_rar (self): - self.archive_test('t.rar', 'rar') + self.archive_commands('t.rar', 'rar') @needs_cmd('cabextract') def test_capextract (self): @@ -103,6 +116,7 @@ class TestArchives (ArchiveTest): @needs_cmd('arj') def test_arj (self): self.archive_list('t.arj', 'arj') + self.archive_test('t.arj', 'arj') self.archive_extract('t.arj', 'arj') @needs_cmd('cpio') @@ -113,6 +127,8 @@ class TestArchives (ArchiveTest): @needs_cmd('rpm') def test_rpm (self): self.archive_list('t.rpm', 'rpm') + # the rpm test fails on non-rpm system with missing dependencies + #self.archive_test('t.rpm', 'rpm') @needs_cmd('rpm2cpio') @needs_cmd('cpio') @@ -123,8 +139,9 @@ class TestArchives (ArchiveTest): def test_dpkg (self): self.archive_list('t.deb', 'dpkg') self.archive_extract('t.deb', 'dpkg') + self.archive_test('t.deb', 'dpkg') @needs_cmd('lzop') def test_lzop (self): - self.archive_test('t.lzo', 'lzop') + self.archive_commands('t.lzo', 'lzop')