diff --git a/gallery_dl/option.py b/gallery_dl/option.py index ecc2ee3d..2194e9e4 100644 --- a/gallery_dl/option.py +++ b/gallery_dl/option.py @@ -27,6 +27,15 @@ class ConfigConstAction(argparse.Action): namespace.options.append(((self.dest,), self.const)) +class AppendCommandAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + items = getattr(namespace, self.dest, None) or [] + val = self.const.copy() + val["command"] = values + items.append(val) + setattr(namespace, self.dest, items) + + class DeprecatedConfigConstAction(argparse.Action): """Set argparse const values as config values + deprecation warning""" def __call__(self, parser, namespace, values, option_string=None): @@ -303,6 +312,12 @@ def build_parser(): action="append_const", const={"name": "zip"}, help="Store downloaded files in a ZIP archive", ) + postprocessor.add_argument( + "--exec", + dest="postprocessors", metavar="CMD", + action=AppendCommandAction, const={"name": "exec"}, + help="Execute CMD for each downloaded file", + ) postprocessor.add_argument( "--ugoira-conv", dest="postprocessors", diff --git a/gallery_dl/postprocessor/exec.py b/gallery_dl/postprocessor/exec.py index c86b4805..f4aa971c 100644 --- a/gallery_dl/postprocessor/exec.py +++ b/gallery_dl/postprocessor/exec.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright 2018 Mike Fährmann +# Copyright 2018-2019 Mike Fährmann # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as @@ -9,7 +9,9 @@ """Execute processes""" from .common import PostProcessor +from .. import util import subprocess +import shlex class ExecPP(PostProcessor): @@ -17,27 +19,60 @@ class ExecPP(PostProcessor): def __init__(self, pathfmt, options): PostProcessor.__init__(self) - try: - self.args = options["command"] - self.args[0] # test if 'args' is subscriptable - except (KeyError, IndexError, TypeError): - raise TypeError("option 'command' must be a non-empty list") + args = options["command"] + + if isinstance(args, str): + self.args = util.Formatter(args.replace("{}", "{_temppath}")) + self.shell = True + self._format = self._format_args_string + else: + for i, arg in enumerate(args): + if "{}" in arg: + args[i] = arg.replace("{}", "{_temppath}") + self.args = [util.Formatter(arg) for arg in args] + self.shell = False + self._format = self._format_args_list if options.get("async", False): - self._exec = subprocess.Popen + self._exec = self._exec_async def run(self, pathfmt): - self._exec([ - arg.format_map(pathfmt.keywords) - for arg in self.args - ]) + kwdict = pathfmt.kwdict + kwdict["_directory"] = pathfmt.realdirectory + kwdict["_filename"] = pathfmt.filename + kwdict["_temppath"] = pathfmt.temppath + kwdict["_path"] = pathfmt.realpath + self._exec(self._format(kwdict)) + + def _format_args_list(self, kwdict): + return [arg.format_map(kwdict) for arg in self.args] + + def _format_args_string(self, kwdict): + return self.args.format_map(_quote(kwdict)) def _exec(self, args): - retcode = subprocess.Popen(args).wait() + retcode = subprocess.Popen(args, shell=self.shell).wait() if retcode: self.log.warning( - "executing '%s' returned non-zero exit status %d", - " ".join(args), retcode) + "executing '%s' returned with non-zero exit status (%d)", + " ".join(args) if isinstance(args, list) else args, retcode) + + def _exec_async(self, args): + subprocess.Popen(args, shell=self.shell) + + +def _quote(kwdict): + """Create a copy of 'kwdict' and apply shlex.quote to its string values""" + kwdict = kwdict.copy() + for key, value in kwdict.items(): + cls = value.__class__ + if cls is str: + kwdict[key] = shlex.quote(value) + elif cls is list: + kwdict[key] = shlex.quote(", ".join(value)) + elif cls is dict: + kwdict[key] = _quote(value) + return kwdict __postprocessor__ = ExecPP