1#
2# Copyright 2002-2006 Zuza Software Foundation
3#
4# This file is part of translate.
5#
6# translate is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# translate is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, see <http://www.gnu.org/licenses/>.
18
19import fnmatch
20import logging
21import optparse
22import os.path
23import re
24import sys
25import traceback
26from collections import OrderedDict
27from io import BytesIO
28
29from translate import __version__
30from translate.misc import progressbar
31
32
33class ProgressBar:
34    progress_types = OrderedDict(
35        [
36            ("dots", progressbar.DotsProgressBar),
37            ("none", progressbar.NoProgressBar),
38            ("bar", progressbar.HashProgressBar),
39            ("names", progressbar.MessageProgressBar),
40            ("verbose", progressbar.VerboseProgressBar),
41        ]
42    )
43
44    def __init__(self, progress_type, allfiles):
45        """Set up a progress bar appropriate to the progress_type and files."""
46        if progress_type in ("bar", "verbose"):
47            file_count = len(allfiles)
48            self._progressbar = self.progress_types[progress_type](0, file_count)
49            logger = logging.getLogger(os.path.basename(sys.argv[0])).getChild(
50                "progress"
51            )
52            logger.setLevel(logging.INFO)
53            logger.propagate = False
54            handler = logging.StreamHandler()
55            handler.setLevel(logging.INFO)
56            handler.setFormatter(logging.Formatter())
57            logger.addHandler(handler)
58            logger.info("processing %d files...", file_count)
59        else:
60            self._progressbar = self.progress_types[progress_type]()
61
62    def report_progress(self, filename, success):
63        """Show that we are progressing..."""
64        self._progressbar.amount += 1
65        self._progressbar.show(filename)
66
67
68class ManPageOption(optparse.Option):
69    ACTIONS = optparse.Option.ACTIONS + ("manpage",)
70
71    def take_action(self, action, dest, opt, value, values, parser):
72        """take_action that can handle manpage as well as standard actions"""
73        if action == "manpage":
74            parser.print_manpage()
75            sys.exit(0)
76        return super().take_action(action, dest, opt, value, values, parser)
77
78
79class ManHelpFormatter(optparse.HelpFormatter):
80    def __init__(
81        self, indent_increment=0, max_help_position=0, width=80, short_first=1
82    ):
83        super().__init__(indent_increment, max_help_position, width, short_first)
84
85    def format_option_strings(self, option):
86        """Return a comma-separated list of option strings & metavariables."""
87        if option.takes_value():
88            metavar = option.metavar or option.dest.upper()
89            metavar = "\\fI%s\\fP" % metavar
90            short_opts = [sopt + metavar for sopt in option._short_opts]
91            long_opts = [lopt + "\\fR=\\fP" + metavar for lopt in option._long_opts]
92        else:
93            short_opts = option._short_opts
94            long_opts = option._long_opts
95
96        if self.short_first:
97            opts = short_opts + long_opts
98        else:
99            opts = long_opts + short_opts
100
101        return "\\fB%s\\fP" % ("\\fR, \\fP".join(opts))
102
103
104class StdoutWrapper:
105    def __init__(self):
106        self.out = sys.stdout
107
108    def __getattr__(self, name):
109        return getattr(self.out, name)
110
111    def write(self, content):
112        if isinstance(content, bytes):
113            try:
114                self.out.write(content.decode("utf-8"))
115            except UnicodeDecodeError:
116                self.out.write("Unable to write binary content to the terminal")
117        else:
118            self.out.write(content)
119
120
121class RecursiveOptionParser(optparse.OptionParser):
122    """A specialized Option Parser for recursing through directories."""
123
124    def __init__(
125        self, formats, usetemplates=False, allowmissingtemplate=False, description=None
126    ):
127        """Construct the specialized Option Parser.
128
129        :type formats: Dictionary
130        :param formats: See :meth:`~.RecursiveOptionParser.setformats`
131        for an explanation of the formats parameter.
132        """
133
134        super().__init__(version="%prog " + __version__.sver, description=description)
135        self.setmanpageoption()
136        self.setprogressoptions()
137        self.seterrorleveloptions()
138        self.setformats(formats, usetemplates)
139        self.passthrough = []
140        self.allowmissingtemplate = allowmissingtemplate
141        logging.basicConfig(format="%(name)s: %(levelname)s: %(message)s")
142
143    def get_prog_name(self):
144        return os.path.basename(sys.argv[0])
145
146    def setmanpageoption(self):
147        """creates a manpage option that allows the optionparser to generate a
148        manpage
149        """
150        manpageoption = ManPageOption(
151            None,
152            "--manpage",
153            dest="manpage",
154            default=False,
155            action="manpage",
156            help="output a manpage based on the help",
157        )
158        self.define_option(manpageoption)
159
160    def format_manpage(self):
161        """returns a formatted manpage"""
162        result = []
163        prog = self.get_prog_name()
164        formatprog = lambda x: x.replace("%prog", prog)
165        formatToolkit = lambda x: x.replace("%prog", "Translate Toolkit")
166        result.append('.\\" Autogenerated manpage\n')
167        result.append(
168            '.TH %s 1 "%s" "" "%s"\n'
169            % (prog, formatToolkit(self.version), formatToolkit(self.version))
170        )
171        result.append(".SH NAME\n")
172        result.append(
173            "{} \\- {}\n".format(
174                self.get_prog_name(), self.description.split("\n\n")[0]
175            )
176        )
177        result.append(".SH SYNOPSIS\n")
178        result.append(".PP\n")
179        usage = "\\fB%prog "
180        usage += " ".join(self.getusageman(option) for option in self.option_list)
181        usage += "\\fP"
182        result.append("%s\n" % formatprog(usage))
183        description_lines = self.description.split("\n\n")[1:]
184        if description_lines:
185            result.append(".SH DESCRIPTION\n")
186            result.append(
187                "\n\n".join(
188                    re.sub(r"\.\. note::", "Note:", l) for l in description_lines
189                )
190            )
191        result.append(".SH OPTIONS\n")
192        ManHelpFormatter().store_option_strings(self)
193        result.append(".PP\n")
194        for option in self.option_list:
195            result.append(".TP\n")
196            result.append("%s\n" % str(option).replace("-", r"\-"))
197            result.append("%s\n" % option.help.replace("-", r"\-"))
198        return "".join(result)
199
200    def print_manpage(self, file=None):
201        """outputs a manpage for the program using the help information"""
202        if file is None:
203            file = sys.stdout
204        file.write(self.format_manpage())
205
206    def set_usage(self, usage=None):
207        """sets the usage string - if usage not given, uses getusagestring for
208        each option
209        """
210        if usage is None:
211            self.usage = "%prog " + " ".join(
212                self.getusagestring(option) for option in self.option_list
213            )
214        else:
215            super().set_usage(usage)
216
217    def warning(self, msg, options=None, exc_info=None):
218        """Print a warning message incorporating 'msg' to stderr."""
219        if options:
220            if options.errorlevel == "traceback":
221                errorinfo = "\n".join(
222                    traceback.format_exception(exc_info[0], exc_info[1], exc_info[2])
223                )
224            elif options.errorlevel == "exception":
225                errorinfo = "\n".join(
226                    traceback.format_exception_only(exc_info[0], exc_info[1])
227                )
228            elif options.errorlevel == "message":
229                errorinfo = str(exc_info[1])
230            else:
231                errorinfo = ""
232            if errorinfo:
233                msg += ": " + errorinfo
234        logging.getLogger(self.get_prog_name()).warning(msg)
235
236    def getusagestring(self, option):
237        """returns the usage string for the given option"""
238        optionstring = "|".join(option._short_opts + option._long_opts)
239        if getattr(option, "optionalswitch", False):
240            optionstring = "[%s]" % optionstring
241        if option.metavar:
242            optionstring += " " + option.metavar
243        if getattr(option, "required", False):
244            return optionstring
245        else:
246            return "[%s]" % optionstring
247
248    def getusageman(self, option):
249        """returns the usage string for the given option"""
250        optionstring = "\\fR|\\fP".join(option._short_opts + option._long_opts)
251        if getattr(option, "optionalswitch", False):
252            optionstring = "\\fR[\\fP%s\\fR]\\fP" % optionstring
253        if option.metavar:
254            optionstring += " \\fI%s\\fP" % option.metavar
255        if getattr(option, "required", False):
256            return optionstring
257        else:
258            return "\\fR[\\fP%s\\fR]\\fP" % optionstring
259
260    def define_option(self, option):
261        """Defines the given option, replacing an existing one of the same
262        short name if neccessary...
263        """
264        for short_opt in option._short_opts:
265            if self.has_option(short_opt):
266                self.remove_option(short_opt)
267        for long_opt in option._long_opts:
268            if self.has_option(long_opt):
269                self.remove_option(long_opt)
270        self.add_option(option)
271
272    def setformats(self, formats, usetemplates):
273        """Sets the format options using the given format dictionary.
274
275        :type formats: Dictionary or iterable
276        :param formats: The dictionary *keys* should be:
277
278                        - Single strings (or 1-tuples) containing an
279                          input format (if not *usetemplates*)
280                        - Tuples containing an input format and
281                          template format (if *usetemplates*)
282                        - Formats can be *None* to indicate what to do
283                          with standard input
284
285                        The dictionary *values* should be tuples of
286                        outputformat (string) and processor method.
287        """
288
289        self.inputformats = []
290        outputformats = []
291        templateformats = []
292        self.outputoptions = {}
293        self.usetemplates = usetemplates
294        if isinstance(formats, dict):
295            formats = formats.items()
296        for formatgroup, outputoptions in formats:
297            if isinstance(formatgroup, str) or formatgroup is None:
298                formatgroup = (formatgroup,)
299            if not isinstance(formatgroup, tuple):
300                raise ValueError("formatgroups must be tuples or None/str/unicode")
301            if len(formatgroup) < 1 or len(formatgroup) > 2:
302                raise ValueError("formatgroups must be tuples of length 1 or 2")
303            if len(formatgroup) == 1:
304                formatgroup += (None,)
305            inputformat, templateformat = formatgroup
306            if not isinstance(outputoptions, tuple) or len(outputoptions) != 2:
307                raise ValueError("output options must be tuples of length 2")
308            outputformat, processor = outputoptions
309            if inputformat not in self.inputformats:
310                self.inputformats.append(inputformat)
311            if outputformat not in outputformats:
312                outputformats.append(outputformat)
313            if templateformat not in templateformats:
314                templateformats.append(templateformat)
315            self.outputoptions[(inputformat, templateformat)] = (
316                outputformat,
317                processor,
318            )
319        inputformathelp = self.getformathelp(self.inputformats)
320        inputoption = optparse.Option(
321            "-i",
322            "--input",
323            dest="input",
324            default=None,
325            metavar="INPUT",
326            help="read from INPUT in %s" % (inputformathelp),
327        )
328        inputoption.optionalswitch = True
329        inputoption.required = True
330        self.define_option(inputoption)
331        excludeoption = optparse.Option(
332            "-x",
333            "--exclude",
334            dest="exclude",
335            action="append",
336            type="string",
337            metavar="EXCLUDE",
338            default=["CVS", ".svn", "_darcs", ".git", ".hg", ".bzr"],
339            help="exclude names matching EXCLUDE from input paths",
340        )
341        self.define_option(excludeoption)
342        outputformathelp = self.getformathelp(outputformats)
343        outputoption = optparse.Option(
344            "-o",
345            "--output",
346            dest="output",
347            default=None,
348            metavar="OUTPUT",
349            help="write to OUTPUT in %s" % (outputformathelp),
350        )
351        outputoption.optionalswitch = True
352        outputoption.required = True
353        self.define_option(outputoption)
354        if self.usetemplates:
355            self.templateformats = templateformats
356            templateformathelp = self.getformathelp(self.templateformats)
357            templateoption = optparse.Option(
358                "-t",
359                "--template",
360                dest="template",
361                default=None,
362                metavar="TEMPLATE",
363                help="read from TEMPLATE in %s" % (templateformathelp),
364            )
365            self.define_option(templateoption)
366
367    def setprogressoptions(self):
368        """Sets the progress options."""
369        progressoption = optparse.Option(
370            None,
371            "--progress",
372            dest="progress",
373            default="bar",
374            choices=list(ProgressBar.progress_types.keys()),
375            metavar="PROGRESS",
376            help="show progress as: %s" % (", ".join(ProgressBar.progress_types)),
377        )
378        self.define_option(progressoption)
379
380    def seterrorleveloptions(self):
381        """Sets the errorlevel options."""
382        self.errorleveltypes = ["none", "message", "exception", "traceback"]
383        errorleveloption = optparse.Option(
384            None,
385            "--errorlevel",
386            dest="errorlevel",
387            default="message",
388            choices=self.errorleveltypes,
389            metavar="ERRORLEVEL",
390            help="show errorlevel as: %s" % (", ".join(self.errorleveltypes)),
391        )
392        self.define_option(errorleveloption)
393
394    def getformathelp(self, formats):
395        """Make a nice help string for describing formats..."""
396        formats = sorted(f for f in formats if f is not None)
397        if len(formats) == 0:
398            return ""
399        elif len(formats) == 1:
400            return "%s format" % (", ".join(formats))
401        else:
402            return "%s formats" % (", ".join(formats))
403
404    def isrecursive(self, fileoption, filepurpose="input"):
405        """Checks if fileoption is a recursive file."""
406        if fileoption is None:
407            return False
408        elif isinstance(fileoption, list):
409            return True
410        else:
411            return os.path.isdir(fileoption)
412
413    def parse_args(self, args=None, values=None):
414        """Parses the command line options, handling implicit input/output
415        args.
416        """
417        (options, args) = super().parse_args(args, values)
418        # some intelligent as to what reasonable people might give on the
419        # command line
420        if args and not options.input:
421            if len(args) > 1:
422                options.input = args[:-1]
423                args = args[-1:]
424            else:
425                options.input = args[0]
426                args = []
427        if args and not options.output:
428            options.output = args[-1]
429            args = args[:-1]
430        if args:
431            self.error(
432                "You have used an invalid combination of --input, --output and freestanding args"
433            )
434        if isinstance(options.input, list) and len(options.input) == 1:
435            options.input = options.input[0]
436        if options.input is None:
437            self.error(
438                "You need to give an inputfile or use - for stdin ; use --help for full usage instructions"
439            )
440        elif options.input == "-":
441            options.input = None
442        return (options, args)
443
444    def getpassthroughoptions(self, options):
445        """Get the options required to pass to the filtermethod..."""
446        passthroughoptions = {}
447        for optionname in dir(options):
448            if optionname in self.passthrough:
449                passthroughoptions[optionname] = getattr(options, optionname)
450        return passthroughoptions
451
452    def getoutputoptions(self, options, inputpath, templatepath):
453        """Works out which output format and processor method to use..."""
454        if inputpath:
455            inputbase, inputext = self.splitinputext(inputpath)
456        else:
457            inputext = None
458        if templatepath:
459            templatebase, templateext = self.splittemplateext(templatepath)
460        else:
461            templateext = None
462        if (inputext, templateext) in self.outputoptions:
463            return self.outputoptions[inputext, templateext]
464        elif (inputext, "*") in self.outputoptions:
465            outputformat, fileprocessor = self.outputoptions[inputext, "*"]
466        elif ("*", templateext) in self.outputoptions:
467            outputformat, fileprocessor = self.outputoptions["*", templateext]
468        elif ("*", "*") in self.outputoptions:
469            outputformat, fileprocessor = self.outputoptions["*", "*"]
470        elif (inputext, None) in self.outputoptions:
471            return self.outputoptions[inputext, None]
472        elif (None, templateext) in self.outputoptions:
473            return self.outputoptions[None, templateext]
474        elif ("*", None) in self.outputoptions:
475            outputformat, fileprocessor = self.outputoptions["*", None]
476        elif (None, "*") in self.outputoptions:
477            outputformat, fileprocessor = self.outputoptions[None, "*"]
478        else:
479            if self.usetemplates:
480                if inputext is None:
481                    raise ValueError(
482                        "don't know what to do with input format (no file extension), no template file"
483                    )
484                elif templateext is None:
485                    raise ValueError(
486                        "don't know what to do with input format %s, no template file"
487                        % (os.extsep + inputext)
488                    )
489                else:
490                    raise ValueError(
491                        "don't know what to do with input format %s, template format %s"
492                        % (os.extsep + inputext, os.extsep + templateext)
493                    )
494            else:
495                raise ValueError(
496                    "don't know what to do with input format %s"
497                    % (os.extsep + inputext)
498                )
499        if outputformat == "*":
500            if inputext:
501                outputformat = inputext
502            elif templateext:
503                outputformat = templateext
504            elif ("*", "*") in self.outputoptions:
505                outputformat = None
506            else:
507                if self.usetemplates:
508                    raise ValueError(
509                        "don't know what to do with input format (no file extension), no template file"
510                    )
511                else:
512                    raise ValueError(
513                        "don't know what to do with input format (no file extension)"
514                    )
515        return outputformat, fileprocessor
516
517    def getfullinputpath(self, options, inputpath):
518        """Gets the full path to an input file."""
519        if options.input:
520            return os.path.join(options.input, inputpath)
521        else:
522            return inputpath
523
524    def getfulloutputpath(self, options, outputpath):
525        """Gets the full path to an output file."""
526        if options.recursiveoutput and options.output:
527            return os.path.join(options.output, outputpath)
528        else:
529            return outputpath
530
531    def getfulltemplatepath(self, options, templatepath):
532        """Gets the full path to a template file."""
533        if not options.recursivetemplate:
534            return templatepath
535        elif templatepath is not None and self.usetemplates and options.template:
536            return os.path.join(options.template, templatepath)
537        else:
538            return None
539
540    def run(self):
541        """Parses the arguments, and runs recursiveprocess with the resulting
542        options...
543        """
544        (options, args) = self.parse_args()
545        self.recursiveprocess(options)
546
547    def recursiveprocess(self, options):
548        """Recurse through directories and process files."""
549        if self.isrecursive(options.input, "input") and getattr(
550            options, "allowrecursiveinput", True
551        ):
552            self.ensurerecursiveoutputdirexists(options)
553            if isinstance(options.input, list):
554                inputfiles = self.recurseinputfilelist(options)
555            else:
556                inputfiles = self.recurseinputfiles(options)
557        else:
558            if options.input:
559                inputfiles = [os.path.basename(options.input)]
560                options.input = os.path.dirname(options.input)
561            else:
562                inputfiles = [options.input]
563        options.recursiveoutput = self.isrecursive(
564            options.output, "output"
565        ) and getattr(options, "allowrecursiveoutput", True)
566        options.recursivetemplate = (
567            self.usetemplates
568            and self.isrecursive(options.template, "template")
569            and getattr(options, "allowrecursivetemplate", True)
570        )
571        # sort the input files to preserve the order between runs as much as possible.
572        # this makes for more merge-friendly content in single-output-file mode.
573        inputfiles.sort()
574        progress_bar = ProgressBar(options.progress, inputfiles)
575        for inputpath in inputfiles:
576            try:
577                templatepath = self.gettemplatename(options, inputpath)
578                # If we have a recursive template, but the template doesn't
579                # have this input file, let's drop it.
580                if (
581                    options.recursivetemplate
582                    and templatepath is None
583                    and not self.allowmissingtemplate
584                ):
585                    self.warning(
586                        f"No template at {templatepath}. Skipping {inputpath}."
587                    )
588                    continue
589                outputformat, fileprocessor = self.getoutputoptions(
590                    options, inputpath, templatepath
591                )
592                fullinputpath = self.getfullinputpath(options, inputpath)
593                fulltemplatepath = self.getfulltemplatepath(options, templatepath)
594                outputpath = self.getoutputname(options, inputpath, outputformat)
595                fulloutputpath = self.getfulloutputpath(options, outputpath)
596                if options.recursiveoutput and outputpath:
597                    self.checkoutputsubdir(options, os.path.dirname(outputpath))
598            except Exception:
599                self.warning(
600                    "Couldn't handle input file %s" % inputpath, options, sys.exc_info()
601                )
602                continue
603            try:
604                success = self.processfile(
605                    fileprocessor,
606                    options,
607                    fullinputpath,
608                    fulloutputpath,
609                    fulltemplatepath,
610                )
611            except Exception:
612                self.warning(
613                    "Error processing: input %s, output %s, template %s"
614                    % (fullinputpath, fulloutputpath, fulltemplatepath),
615                    options,
616                    sys.exc_info(),
617                )
618                success = False
619            progress_bar.report_progress(inputpath, success)
620        del progress_bar
621
622    def ensurerecursiveoutputdirexists(self, options):
623        if not self.isrecursive(options.output, "output"):
624            if not options.output:
625                self.error(optparse.OptionValueError("No output directory given"))
626            try:
627                self.warning("Output directory does not exist. Attempting to create")
628                os.mkdir(options.output)
629            except OSError:
630                self.error(
631                    optparse.OptionValueError(
632                        "Output directory does not exist, attempt to create failed"
633                    )
634                )
635
636    def openinputfile(self, options, fullinputpath):
637        """Opens the input file."""
638        if fullinputpath is None:
639            return sys.stdin
640        return open(fullinputpath, "rb")
641
642    def openoutputfile(self, options, fulloutputpath):
643        """Opens the output file."""
644        if fulloutputpath is None:
645            return StdoutWrapper()
646        return open(fulloutputpath, "wb")
647
648    def opentempoutputfile(self, options, fulloutputpath):
649        """Opens a temporary output file."""
650        return BytesIO()
651
652    def finalizetempoutputfile(self, options, outputfile, fulloutputpath):
653        """Write the temp outputfile to its final destination."""
654        outputfile.seek(0, 0)
655        outputstring = outputfile.read()
656        outputfile = self.openoutputfile(options, fulloutputpath)
657        outputfile.write(outputstring)
658        outputfile.close()
659
660    def opentemplatefile(self, options, fulltemplatepath):
661        """Opens the template file (if required)."""
662        if fulltemplatepath is not None:
663            if os.path.isfile(fulltemplatepath):
664                return open(fulltemplatepath, "rb")
665            else:
666                self.warning("missing template file %s" % fulltemplatepath)
667        return None
668
669    def processfile(
670        self, fileprocessor, options, fullinputpath, fulloutputpath, fulltemplatepath
671    ):
672        """Process an individual file."""
673        inputfile = self.openinputfile(options, fullinputpath)
674        if fulloutputpath and fulloutputpath in (fullinputpath, fulltemplatepath):
675            outputfile = self.opentempoutputfile(options, fulloutputpath)
676            tempoutput = True
677        else:
678            outputfile = self.openoutputfile(options, fulloutputpath)
679            tempoutput = False
680        templatefile = self.opentemplatefile(options, fulltemplatepath)
681        passthroughoptions = self.getpassthroughoptions(options)
682        if fileprocessor(inputfile, outputfile, templatefile, **passthroughoptions):
683            if tempoutput:
684                self.warning("writing to temporary output...")
685                self.finalizetempoutputfile(options, outputfile, fulloutputpath)
686            return True
687        else:
688            # remove the file if it is a file (could be stdout etc)
689            if fulloutputpath and os.path.isfile(fulloutputpath):
690                outputfile.close()
691                os.unlink(fulloutputpath)
692            return False
693
694    def mkdir(self, parent, subdir):
695        """Makes a subdirectory (recursively if neccessary)."""
696        if not os.path.isdir(parent):
697            raise ValueError(
698                "cannot make child directory %r if parent %r does not exist"
699                % (subdir, parent)
700            )
701        currentpath = parent
702        subparts = subdir.split(os.sep)
703        for part in subparts:
704            currentpath = os.path.join(currentpath, part)
705            if not os.path.isdir(currentpath):
706                os.mkdir(currentpath)
707
708    def checkoutputsubdir(self, options, subdir):
709        """Checks to see if subdir under options.output needs to be created,
710        creates if neccessary.
711        """
712        fullpath = os.path.join(options.output, subdir)
713        if not os.path.isdir(fullpath):
714            self.mkdir(options.output, subdir)
715
716    def isexcluded(self, options, inputpath):
717        """Checks if this path has been excluded."""
718        basename = os.path.basename(inputpath)
719        for excludename in options.exclude:
720            if fnmatch.fnmatch(basename, excludename):
721                return True
722        return False
723
724    def recurseinputfilelist(self, options):
725        """Use a list of files, and find a common base directory for them."""
726        # find a common base directory for the files to do everything
727        # relative to
728        commondir = os.path.dirname(os.path.commonprefix(options.input))
729        inputfiles = []
730        for inputfile in options.input:
731            if self.isexcluded(options, inputfile):
732                continue
733            if inputfile.startswith(commondir + os.sep):
734                inputfiles.append(inputfile.replace(commondir + os.sep, "", 1))
735            else:
736                inputfiles.append(inputfile.replace(commondir, "", 1))
737        options.input = commondir
738        return inputfiles
739
740    def recurseinputfiles(self, options):
741        """Recurse through directories and return files to be processed."""
742        dirstack = [""]
743        join = os.path.join
744        inputfiles = []
745        while dirstack:
746            top = dirstack.pop(-1)
747            names = os.listdir(join(options.input, top))
748            dirs = []
749            for name in names:
750                inputpath = join(top, name)
751                if self.isexcluded(options, inputpath):
752                    continue
753                fullinputpath = self.getfullinputpath(options, inputpath)
754                # handle directories...
755                if os.path.isdir(fullinputpath):
756                    dirs.append(inputpath)
757                elif os.path.isfile(fullinputpath):
758                    if not self.isvalidinputname(name):
759                        # only handle names that match recognized input
760                        # file extensions
761                        continue
762                    inputfiles.append(inputpath)
763            # make sure the directories are processed next time round.
764            dirs.reverse()
765            dirstack.extend(dirs)
766        return inputfiles
767
768    def splitext(self, pathname):
769        """Splits *pathname* into name and ext, and removes the extsep.
770
771        :param pathname: A file path
772        :type pathname: string
773        :return: root, ext
774        :rtype: tuple
775        """
776        root, ext = os.path.splitext(pathname)
777        ext = ext.replace(os.extsep, "", 1)
778        return (root, ext)
779
780    def splitinputext(self, inputpath):
781        """Splits an *inputpath* into name and extension."""
782        return self.splitext(inputpath)
783
784    def splittemplateext(self, templatepath):
785        """Splits a *templatepath* into name and extension."""
786        return self.splitext(templatepath)
787
788    def templateexists(self, options, templatepath):
789        """Returns whether the given template exists..."""
790        fulltemplatepath = self.getfulltemplatepath(options, templatepath)
791        return os.path.isfile(fulltemplatepath)
792
793    def gettemplatename(self, options, inputname):
794        """Gets an output filename based on the input filename."""
795        if not self.usetemplates:
796            return None
797        if not inputname or not options.recursivetemplate:
798            return options.template
799        inputbase, inputext = self.splitinputext(inputname)
800        if options.template:
801            for inputext1, templateext1 in self.outputoptions:
802                if inputext == inputext1:
803                    if templateext1:
804                        templatepath = inputbase + os.extsep + templateext1
805                        if self.templateexists(options, templatepath):
806                            return templatepath
807            if "*" in self.inputformats:
808                for inputext1, templateext1 in self.outputoptions:
809                    if (inputext == inputext1) or (inputext1 == "*"):
810                        if templateext1 == "*":
811                            templatepath = inputname
812                            if self.templateexists(options, templatepath):
813                                return templatepath
814                        elif templateext1:
815                            templatepath = inputbase + os.extsep + templateext1
816                            if self.templateexists(options, templatepath):
817                                return templatepath
818        return None
819
820    def getoutputname(self, options, inputname, outputformat):
821        """Gets an output filename based on the input filename."""
822        if not inputname or not options.recursiveoutput:
823            return options.output
824        inputbase, inputext = self.splitinputext(inputname)
825        outputname = inputbase
826        if outputformat:
827            outputname += os.extsep + outputformat
828        return outputname
829
830    def isvalidinputname(self, inputname):
831        """Checks if this is a valid input filename."""
832        inputbase, inputext = self.splitinputext(inputname)
833        return (inputext in self.inputformats) or ("*" in self.inputformats)
834