#!/usr/bin/env python3
#
# Copyright (C) 2002-2024 Free Software Foundation, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#

# This program is meant for authors or maintainers which want to import
# modules from gnulib into their packages.

# CODING STYLE for this file and its companions:
# Like PEP 8 <https://peps.python.org/pep-0008/>, except
# - Line length is not limited to 79 characters.
# - Line breaking before or after binary operators? Better before, like in GNU.
# - Place line breaks to help reading the code:
#   Avoid line breaks inside expressions, if they can be avoided.
#   Do use line breaks to separate the parts of [ ... for ... ] iterations.
# You can use this command to check the style:
#   $ pycodestyle --max-line-length=136 --ignore=E265,W503,E241,E711,E712,E201,E202,E221 gnulib-tool.py pygnulib/*.py

# You can use this command to check for mistakes:
#   $ pylint --disable=C0103,C0114,C0121,C0209,C0301,C0302,R0902,R0912,R0913,R0914,R0915,R1705,R1702,R1720 gnulib-tool.py pygnulib/*.py


#===============================================================================
# Define global imports
#===============================================================================
import os
import re
import sys
import codecs
import random
import argparse
import subprocess as sp
import shlex
from tempfile import mktemp
from pygnulib import constants
from pygnulib import classes


#===============================================================================
# Define global constants
#===============================================================================
APP = constants.APP
DIRS = constants.DIRS
ENCS = constants.ENCS
UTILS = constants.UTILS
MODES = constants.MODES
TESTS = constants.TESTS
joinpath = constants.joinpath
copyfile = constants.copyfile
isabs = os.path.isabs
isdir = os.path.isdir
isfile = os.path.isfile


#===============================================================================
# Define main part
#===============================================================================
def main():
    info = classes.GLInfo()
    parser = argparse.ArgumentParser(
        prog=constants.APP['name'],
        usage='gnulib-tool.py --help',
        add_help=False)

    # Here we list the options in the order they are listed in the --help output.

    # list
    parser.add_argument('--list',
                        dest='mode_list',
                        default=None,
                        action='store_true')
    # find
    parser.add_argument('--find',
                        dest='mode_find',
                        default=None,
                        action='store_true')
    # import
    parser.add_argument('--import',
                        dest='mode_import',
                        default=None,
                        action='store_true')
    # add-import
    parser.add_argument('--add-import',
                        dest='mode_add_import',
                        default=None,
                        action='store_true')
    # remove-import
    parser.add_argument('--remove-import',
                        dest='mode_remove_import',
                        default=None,
                        action='store_true')
    # update
    parser.add_argument('--update',
                        dest='mode_update',
                        default=None,
                        action='store_true')
    # create-testdir
    parser.add_argument('--create-testdir',
                        dest='mode_create_testdir',
                        default=None,
                        action='store_true')
    # create-megatestdir
    parser.add_argument('--create-megatestdir',
                        dest='mode_create_megatestdir',
                        default=None,
                        action='store_true')
    # test
    parser.add_argument('--test',
                        dest='mode_test',
                        default=None,
                        action='store_true')
    # megatest
    parser.add_argument('--megatest',
                        dest='mode_megatest',
                        default=None,
                        action='store_true')
    # extract-*
    parser.add_argument('--extract-description',
                        dest='mode_xdescription',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-comment',
                        dest='mode_xcomment',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-status',
                        dest='mode_xstatus',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-notice',
                        dest='mode_xnotice',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-applicability',
                        dest='mode_xapplicability',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-filelist',
                        dest='mode_xfilelist',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-dependencies',
                        dest='mode_xdependencies',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-autoconf-snippet',
                        dest='mode_xautoconf',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-automake-snippet',
                        dest='mode_xautomake',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-include-directive',
                        dest='mode_xinclude',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-link-directive',
                        dest='mode_xlink',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-license',
                        dest='mode_xlicense',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-maintainer',
                        dest='mode_xmaintainer',
                        default=None,
                        action='store_true')
    parser.add_argument('--extract-tests-module',
                        dest='mode_xtests',
                        default=None,
                        action='store_true')
    # copy-file
    parser.add_argument('--copy-file',
                        dest='mode_copy_file',
                        default=None,
                        action='store_true')
    # help
    parser.add_argument('--help', '--hel', '--he', '--h',
                        dest='help',
                        default=None,
                        action='store_true')
    # version
    parser.add_argument('--version', '--versio', '--versi', '--vers',
                        dest='version',
                        default=None,
                        action='store_true')
    # no-changelog: a no-op for backward compatibility
    parser.add_argument('--no-changelog',
                        dest='changelog',
                        default=None,
                        action='store_false')
    # destdir
    parser.add_argument('--dir',
                        dest='destdir',
                        default=None,
                        nargs=1)
    # localpath
    parser.add_argument('--local-dir',
                        action='append',
                        dest='localpath',
                        default=None,
                        nargs=1)
    # cache-modules: a no-op for backward compatibility
    parser.add_argument('--cache-modules',
                        dest='cache_modules',
                        default=None,
                        action='store_true')
    parser.add_argument('--no-cache-modules',
                        dest='cache_modules',
                        default=None,
                        action='store_false')
    # verbose
    parser.add_argument('--verbose',
                        default=0,
                        action='count')
    # quiet
    parser.add_argument('--quiet',
                        default=0,
                        action='count')
    # dryrun
    parser.add_argument('--dry-run',
                        dest='dryrun',
                        default=None,
                        action='store_true')
    # inctests
    parser.add_argument('--with-tests',
                        dest='inctests',
                        default=None,
                        action='store_true')
    parser.add_argument('--without-tests',
                        dest='inctests',
                        default=None,
                        action='store_false')
    # obsolete
    parser.add_argument('--with-obsolete',
                        dest='obsolete',
                        default=None,
                        action='store_true')
    # c++-tests
    parser.add_argument('--with-c++-tests',
                        dest='inc_cxx_tests',
                        default=None,
                        action='store_true')
    parser.add_argument('--without-c++-tests',
                        dest='excl_cxx_tests',
                        default=None,
                        action='store_true')
    # longrunning-tests
    parser.add_argument('--with-longrunning-tests',
                        dest='inc_longrunning_tests',
                        default=None,
                        action='store_true')
    parser.add_argument('--without-longrunning-tests',
                        dest='excl_longrunning_tests',
                        default=None,
                        action='store_true')
    # privileged-tests
    parser.add_argument('--with-privileged-tests',
                        dest='inc_privileged_tests',
                        default=None,
                        action='store_true')
    parser.add_argument('--without-privileged-tests',
                        dest='excl_privileged_tests',
                        default=None,
                        action='store_true')
    # unportable-tests
    parser.add_argument('--with-unportable-tests',
                        dest='inc_unportable_tests',
                        default=None,
                        action='store_true')
    parser.add_argument('--without-unportable-tests',
                        dest='excl_unportable_tests',
                        default=None,
                        action='store_true')
    # all-tests
    parser.add_argument('--with-all-tests',
                        dest='alltests',
                        default=None,
                        action='store_true')
    # avoids
    parser.add_argument('--avoid',
                        dest='avoids',
                        default=None,
                        action='append',
                        nargs=1)
    # conditional-dependencies
    parser.add_argument('--conditional-dependencies',
                        dest='cond_dependencies',
                        default=None,
                        action="store_true")
    parser.add_argument('--no-conditional-dependencies',
                        dest='cond_dependencies',
                        default=None,
                        action="store_false")
    # libtool
    parser.add_argument('--libtool',
                        dest='libtool',
                        default=None,
                        action="store_true")
    parser.add_argument('--no-libtool',
                        dest='libtool',
                        default=None,
                        action="store_false")
    # libname
    parser.add_argument('--lib',
                        dest='libname',
                        default=None,
                        nargs=1)
    # sourcebase
    parser.add_argument('--source-base',
                        dest='sourcebase',
                        default=None,
                        nargs=1)
    # m4base
    parser.add_argument('--m4-base',
                        dest='m4base',
                        default=None,
                        nargs=1)
    # pobase
    parser.add_argument('--po-base',
                        dest='pobase',
                        default=None,
                        nargs=1)
    # docbase
    parser.add_argument('--doc-base',
                        dest='docbase',
                        default=None,
                        nargs=1)
    # testsbase
    parser.add_argument('--tests-base',
                        dest='testsbase',
                        default=None,
                        nargs=1)
    # auxdir
    parser.add_argument('--aux-dir',
                        dest='auxdir',
                        default=None,
                        nargs=1)
    # lgpl
    parser.add_argument('--lgpl',
                        dest='lgpl',
                        default=None,
                        action='append',
                        choices=['2', '3orGPLv2', '3'],
                        nargs='?')
    # makefile-name
    parser.add_argument('--makefile-name',
                        dest='makefile_name',
                        default=None,
                        nargs=1)
    # macro-prefix
    parser.add_argument('--macro-prefix',
                        dest='macro_prefix',
                        default=None,
                        nargs=1)
    # po-domain
    parser.add_argument('--po-domain',
                        dest='podomain',
                        default=None,
                        nargs=1)
    # witness-c-macro
    parser.add_argument('--witness-c-macro',
                        dest='witness_c_macro',
                        default=None,
                        nargs=1)
    # vc-files
    parser.add_argument('--vc-files',
                        dest='vc_files',
                        default=None,
                        action='store_true')
    parser.add_argument('--no-vc-files',
                        dest='vc_files',
                        default=None,
                        action='store_false')
    # single-configure
    parser.add_argument('--single-configure',
                        dest='single_configure',
                        default=None,
                        action='store_true')
    # symlink
    parser.add_argument('-s', '--symbolic', '--symlink',
                        dest='symlink',
                        default=None,
                        action='store_true')
    # local-symlink
    parser.add_argument('--local-symlink',
                        dest='lsymlink',
                        default=None,
                        action='store_true')
    # All other arguments are collected.
    parser.add_argument("non_option_arguments",
                        nargs='*')

    # Parse the given arguments. Don't signal an error if non-option arguments
    # occur between or after options.
    cmdargs, unhandled = parser.parse_known_args()

    # Handle --help and --version, ignoring all other options.
    if cmdargs.help != None:
        print(info.usage())
        sys.exit(0)
    if cmdargs.version != None:
        version = info.version()
        if version != '':
            version = ' ' + version
        message = '''gnulib-tool (%s %s)%s\n%s\n%s\n\nWritten by %s.''' \
            % (info.package(), info.date(), version, info.copyright(),
               info.license(), info.authors())
        print(message)
        sys.exit(0)

    # Report unhandled arguments.
    for arg in unhandled:
        if arg.startswith('-'):
            message = '%s: Unrecognized option \'%s\'.\n' % (constants.APP['name'], arg)
            message += 'Try \'gnulib-tool --help\' for more information.\n'
            sys.stderr.write(message)
            sys.exit(1)
    # By now, all unhandled arguments were non-options.
    cmdargs.non_option_arguments += unhandled

    # Determine when user tries to combine modes.
    args = [
        cmdargs.mode_list,
        cmdargs.mode_find,
        cmdargs.mode_import,
        cmdargs.mode_add_import,
        cmdargs.mode_remove_import,
        cmdargs.mode_update,
        cmdargs.mode_create_testdir,
        cmdargs.mode_create_megatestdir,
        cmdargs.mode_test,
        cmdargs.mode_megatest,
        cmdargs.mode_xdescription,
        cmdargs.mode_xcomment,
        cmdargs.mode_xstatus,
        cmdargs.mode_xnotice,
        cmdargs.mode_xapplicability,
        cmdargs.mode_xfilelist,
        cmdargs.mode_xdependencies,
        cmdargs.mode_xautoconf,
        cmdargs.mode_xautomake,
        cmdargs.mode_xinclude,
        cmdargs.mode_xlink,
        cmdargs.mode_xlicense,
        cmdargs.mode_xmaintainer,
        cmdargs.mode_xtests,
        cmdargs.mode_copy_file,
    ]
    overflow = [ arg
                 for arg in args
                 if arg != None ]
    if len(overflow) > 1:
        message = '%s: Unable to combine different modes of work.\n' % constants.APP['name']
        message += 'Try \'gnulib-tool --help\' for more information.\n'
        sys.stderr.write(message)
        sys.exit(1)

    # Determine selected mode.
    mode = None
    modules = None
    files = None
    if cmdargs.mode_list != None:
        mode = 'list'
    if cmdargs.mode_find != None:
        mode = 'find'
        files = list(cmdargs.non_option_arguments)
    if cmdargs.mode_import != None:
        mode = 'import'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_add_import != None:
        mode = 'add-import'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_remove_import != None:
        mode = 'remove-import'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_update != None:
        mode = 'update'
        if len(cmdargs.non_option_arguments) > 0:
            message = '%s: too many arguments in \'update\' mode\n'
            message += 'Try \'gnulib-tool --help\' for more information.\n'
            message += 'If you really want to modify the gnulib configuration of your project,\n'
            message += 'you need to use \'gnulib-tool --import\' - at your own risk!\n'
            sys.stderr.write(message)
            sys.exit(1)
    if cmdargs.mode_create_testdir != None:
        mode = 'create-testdir'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_create_megatestdir != None:
        mode = 'create-megatestdir'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_test != None:
        mode = 'test'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_megatest != None:
        mode = 'megatest'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xdescription != None:
        mode = 'extract-description'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xcomment != None:
        mode = 'extract-comment'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xstatus != None:
        mode = 'extract-status'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xnotice != None:
        mode = 'extract-notice'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xapplicability != None:
        mode = 'extract-applicability'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xfilelist != None:
        mode = 'extract-filelist'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xautoconf != None:
        mode = 'extract-autoconf-snippet'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xautomake != None:
        mode = 'extract-automake-snippet'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xdependencies != None:
        mode = 'extract-dependencies'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xinclude != None:
        mode = 'extract-include-directive'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xlink != None:
        mode = 'extract-link-directive'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xlicense != None:
        mode = 'extract-license'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xmaintainer != None:
        mode = 'extract-maintainer'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_xtests != None:
        mode = 'extract-tests-module'
        modules = list(cmdargs.non_option_arguments)
    if cmdargs.mode_copy_file != None:
        mode = 'copy-file'
        if len(cmdargs.non_option_arguments) < 1 or len(cmdargs.non_option_arguments) > 2:
            message = '%s: *** ' % constants.APP['name']
            message += 'invalid number of arguments for --%s\n' % mode
            message += 'Try \'gnulib-tool --help\' for more information.\n'
            message += '%s: *** Stop.\n' % constants.APP['name']
            sys.stderr.write(message)
            sys.exit(1)
        files = list(cmdargs.non_option_arguments)

    if ((mode in ['import', 'add-import', 'remove-import']
         and (cmdargs.excl_cxx_tests or cmdargs.excl_longrunning_tests
              or cmdargs.excl_privileged_tests or cmdargs.excl_unportable_tests
              or cmdargs.single_configure))
        or (mode == 'update'
            and (cmdargs.localpath != None or cmdargs.libname != None
                 or cmdargs.sourcebase != None or cmdargs.m4base != None
                 or cmdargs.pobase != None or cmdargs.docbase != None
                 or cmdargs.testsbase != None or cmdargs.auxdir != None
                 or cmdargs.inctests != None or cmdargs.obsolete != None
                 or cmdargs.inc_cxx_tests != None
                 or cmdargs.inc_longrunning_tests != None
                 or cmdargs.inc_privileged_tests != None
                 or cmdargs.inc_unportable_tests != None
                 or cmdargs.alltests != None
                 or cmdargs.excl_cxx_tests != None
                 or cmdargs.excl_longrunning_tests != None
                 or cmdargs.excl_privileged_tests != None
                 or cmdargs.excl_unportable_tests != None
                 or cmdargs.avoids != None or cmdargs.lgpl != None
                 or cmdargs.makefile_name != None
                 or cmdargs.macro_prefix != None or cmdargs.podomain != None
                 or cmdargs.witness_c_macro != None or cmdargs.vc_files != None))):
        message = '%s: *** ' % constants.APP['name']
        message += 'invalid options for --%s mode\n' % mode
        message += 'Try \'gnulib-tool --help\' for more information.\n'
        message += '%s: *** Stop.\n' % constants.APP['name']
        sys.stderr.write(message)
        sys.exit(1)
    if cmdargs.pobase != None and cmdargs.podomain == None:
        message = '%s: *** ' % constants.APP['name']
        message += 'together with --po-base, you need to specify --po-domain\n'
        message += 'Try \'gnulib-tool --help\' for more information.\n'
        message += '%s: *** Stop.\n' % constants.APP['name']
        sys.stderr.write(message)
        sys.exit(1)
    if cmdargs.pobase == None and cmdargs.podomain != None:
        message = '%s: warning: --po-domain has no effect without a --po-base option\n' % constants.APP['name']
        sys.stderr.write(message)

    # Determine specific settings.
    destdir = cmdargs.destdir
    if destdir != None:
        destdir = cmdargs.destdir[0]
    localpath = cmdargs.localpath
    if localpath != None:
        localpath = [ dir
                      for list1 in localpath
                      for dir in list1 ]
    libname = cmdargs.libname
    if libname != None:
        libname = cmdargs.libname[0]
    auxdir = cmdargs.auxdir
    if auxdir != None:
        auxdir = cmdargs.auxdir[0]
    sourcebase = cmdargs.sourcebase
    if sourcebase != None:
        sourcebase = cmdargs.sourcebase[0]
    m4base = cmdargs.m4base
    if m4base != None:
        m4base = cmdargs.m4base[0]
    pobase = cmdargs.pobase
    if pobase != None:
        pobase = cmdargs.pobase[0]
    testsbase = cmdargs.testsbase
    if testsbase != None:
        testsbase = cmdargs.testsbase[0]
    dryrun = cmdargs.dryrun
    verbose = -cmdargs.quiet + cmdargs.verbose
    inctests = cmdargs.inctests
    # Canonicalize the inctests variable.
    if inctests == None:
        if mode in ['import', 'add-import', 'remove-import', 'update']:
            inctests = False
        elif mode in ['create-testdir', 'create-megatestdir', 'test', 'megatest']:
            inctests = True
    incl_test_categories = []
    if inctests:
        incl_test_categories += [constants.TESTS['tests']]
    if cmdargs.obsolete:
        incl_test_categories += [constants.TESTS['obsolete']]
    if cmdargs.inc_cxx_tests:
        incl_test_categories += [constants.TESTS['cxx-tests']]
    if cmdargs.inc_longrunning_tests:
        incl_test_categories += [constants.TESTS['longrunning-tests']]
    if cmdargs.inc_privileged_tests:
        incl_test_categories += [constants.TESTS['privileged-tests']]
    if cmdargs.inc_unportable_tests:
        incl_test_categories += [constants.TESTS['unportable-tests']]
    if cmdargs.alltests:
        incl_test_categories += [constants.TESTS['all-tests']]
    excl_test_categories = []
    if cmdargs.excl_cxx_tests:
        excl_test_categories += [constants.TESTS['cxx-tests']]
    if cmdargs.excl_longrunning_tests:
        excl_test_categories += [constants.TESTS['longrunning-tests']]
    if cmdargs.excl_privileged_tests:
        excl_test_categories += [constants.TESTS['privileged-tests']]
    if cmdargs.excl_unportable_tests:
        excl_test_categories += [constants.TESTS['unportable-tests']]
    lgpl = cmdargs.lgpl
    if lgpl != None:
        lgpl = lgpl[-1]
        if lgpl == None:
            lgpl = True
    cond_dependencies = cmdargs.cond_dependencies
    libtool = cmdargs.libtool
    makefile_name = cmdargs.makefile_name
    if makefile_name != None:
        makefile_name = makefile_name[0]
    macro_prefix = cmdargs.macro_prefix
    if macro_prefix != None:
        macro_prefix = macro_prefix[0]
    podomain = cmdargs.podomain
    if podomain != None:
        podomain = podomain[0]
    witness_c_macro = cmdargs.witness_c_macro
    if witness_c_macro != None:
        witness_c_macro = witness_c_macro[0]
    vc_files = cmdargs.vc_files
    avoids = cmdargs.avoids
    if avoids != None:
        avoids = [ module
                   for list1 in avoids
                   for module in list1 ]
    symlink = cmdargs.symlink == True
    lsymlink = cmdargs.lsymlink == True
    single_configure = cmdargs.single_configure
    docbase = None

    # Create pygnulib configuration.
    config = classes.GLConfig(
        destdir=destdir,
        localpath=localpath,
        m4base=m4base,
        auxdir=auxdir,
        modules=modules,
        avoids=avoids,
        sourcebase=sourcebase,
        pobase=pobase,
        docbase=docbase,
        testsbase=testsbase,
        incl_test_categories=incl_test_categories,
        excl_test_categories=excl_test_categories,
        libname=libname,
        lgpl=lgpl,
        makefile_name=makefile_name,
        libtool=libtool,
        conddeps=cond_dependencies,
        macro_prefix=macro_prefix,
        podomain=podomain,
        witness_c_macro=witness_c_macro,
        vc_files=vc_files,
        symbolic=symlink,
        lsymbolic=lsymlink,
        single_configure=single_configure,
        verbose=verbose,
        dryrun=dryrun,
    )

    # Work in the given mode.
    if mode == 'list':
        modulesystem = classes.GLModuleSystem(config)
        listing = modulesystem.list()
        result = '\n'.join(listing)
        os.rmdir(config['tempdir'])
        print(result)

    elif mode == 'find':
        modulesystem = classes.GLModuleSystem(config)
        for filename in files:
            if (isfile(joinpath(DIRS['root'], filename))
                    or (localpath != None
                        and any([ isfile(joinpath(localdir, filename))
                                  for localdir in localpath ]))):
                # Convert the file name to a POSIX basic regex.
                # Needs to handle . [ \ * ^ $.
                filename_regex = filename.replace('\\', '\\\\').replace('[', '\\[').replace('^', '\\^')
                filename_regex = re.compile('([.*$])').sub('[\\1]', filename_regex)
                filename_line_regex = '^' + filename_regex + '$'
                # Read module candidates from gnulib root directory.
                command = "find modules -type f -print | xargs -n 100 grep -l %s /dev/null | sed -e 's,^modules/,,'" % shlex.quote(filename_line_regex)
                os.chdir(constants.DIRS['root'])
                with sp.Popen(command, shell=True, stdout=sp.PIPE) as proc:
                    result = proc.stdout.read().decode("UTF-8")
                os.chdir(DIRS['cwd'])
                # Read module candidates from local directories.
                if localpath != None and len(localpath) > 0:
                    command = "find modules -type f -print | xargs -n 100 grep -l %s /dev/null | sed -e 's,^modules/,,' -e 's,\\.diff$,,'" % shlex.quote(filename_line_regex)
                    for localdir in localpath:
                        os.chdir(localdir)
                        with sp.Popen(command, shell=True, stdout=sp.PIPE) as proc:
                            result += proc.stdout.read().decode("UTF-8")
                        os.chdir(DIRS['cwd'])
                listing = [ line
                            for line in result.split('\n')
                            if line.strip() ]
                # Remove modules/ prefix from each file name.
                pattern = re.compile('^modules/')
                listing = [ pattern.sub('', line)
                            for line in listing ]
                # Filter out undesired file names.
                listing = [ line
                            for line in listing
                            if modulesystem.file_is_module(line) ]
                candidates = sorted(set(listing))
                for name in candidates:
                    module = modulesystem.find(name)
                    if module:  # Ignore module candidates that don't actually exist.
                        if module.getFiles():
                            print(name)
            else:
                message = '%s: warning: file %s does not exist\n' % (constants.APP['name'], filename)
                sys.stderr.write(message)

    elif mode in ['import', 'add-import', 'remove-import', 'update']:
        mode = MODES[mode]
        if not destdir:
            destdir = '.'
        config.setDestDir(destdir)

        if mode == MODES['import']:
            # Set variables to default values.
            if not sourcebase:
                sourcebase = 'lib'
            if not m4base:
                m4base = 'm4'
            if not docbase:
                docbase = 'doc'
            if not testsbase:
                testsbase = 'tests'
            if not macro_prefix:
                macro_prefix = 'gl'
            config.setSourceBase(sourcebase)
            config.setM4Base(m4base)
            config.setDocBase(docbase)
            config.setTestsBase(testsbase)
            config.setMacroPrefix(macro_prefix)

            # Perform GLImport actions.
            importer = classes.GLImport(config, mode)
            filetable, transformers = importer.prepare()
            importer.execute(filetable, transformers)

        else:  # if mode != MODE['--import']
            if m4base:
                if not isfile(joinpath(destdir, m4base, 'gnulib-cache.m4')):
                    if not sourcebase:
                        sourcebase = 'lib'
                    if not docbase:
                        docbase = 'doc'
                    if not testsbase:
                        testsbase = 'tests'
                    if not macro_prefix:
                        macro_prefix = 'gl'
                    config.setSourceBase(sourcebase)
                    config.setM4Base(m4base)
                    config.setDocBase(docbase)
                    config.setTestsBase(testsbase)
                    config.setMacroPrefix(macro_prefix)
                # Perform GLImport actions.
                importer = classes.GLImport(config, mode)
                filetable, transformers = importer.prepare()
                importer.execute(filetable, transformers)
            else:  # if not m4base
                m4dirs = list()
                dirisnext = bool()
                filepath = joinpath(destdir, 'Makefile.am')
                if isfile(filepath):
                    with codecs.open(filepath, 'rb', 'UTF-8') as file:
                        data = file.read()
                        data = data.split('ACLOCAL_AMFLAGS')[1]
                        data = data[data.find('=') + 1:data.find('\n')]
                    aclocal_amflags = data.split()
                    for aclocal_amflag in aclocal_amflags:
                        if dirisnext:
                            if not isabs(aclocal_amflag):
                                m4dirs += [aclocal_amflag]
                        else:  # if not dirisnext
                            if aclocal_amflag == '-I':
                                dirisnext = True
                            else:  # if aclocal_amflag != '-I'
                                dirisnext = False
                else:  # if not isfile(filepath)
                    filepath = joinpath(destdir, 'aclocal.m4')
                    if isfile(filepath):
                        pattern = re.compile(r'm4_include\(\[(.*?)\]\)')
                        with codecs.open(filepath, 'rb', 'UTF-8') as file:
                            m4dirs = pattern.findall(file.read())
                        m4dirs = [ os.path.dirname(m4dir)
                                   for m4dir in m4dirs ]
                        m4dirs = [ m4dir
                                   for m4dir in m4dirs
                                   if isfile(joinpath(destdir, m4dir, 'gnulib-cache.m4')) ]
                        m4dirs = sorted(set(m4dirs))
                if len(m4dirs) == 0:
                    # First use of gnulib in a package.
                    # Any number of additional modules can be given.
                    if not sourcebase:
                        sourcebase = 'lib'
                    m4base = 'm4'
                    if not docbase:
                        docbase = 'doc'
                    if not testsbase:
                        testsbase = 'tests'
                    if not macro_prefix:
                        macro_prefix = 'gl'
                    config.setSourceBase(sourcebase)
                    config.setM4Base(m4base)
                    config.setDocBase(docbase)
                    config.setTestsBase(testsbase)
                    config.setMacroPrefix(macro_prefix)
                    # Perform GLImport actions.
                    importer = classes.GLImport(config, mode)
                    filetable, transformers = importer.prepare()
                    importer.execute(filetable, transformers)
                elif len(m4dirs) == 1:
                    m4base = m4dirs[-1]
                    config.setM4Base(m4base)
                    # Perform GLImport actions.
                    importer = classes.GLImport(config, mode)
                    filetable, transformers = importer.prepare()
                    importer.execute(filetable, transformers)
                else:  # if len(m4dirs) > 1
                    for m4base in m4dirs:
                        config.setM4Base(m4base)
                        # Perform GLImport actions.
                        importer = classes.GLImport(config, mode)
                        filetable, transformers = importer.prepare()
                        importer.execute(filetable, transformers)

    elif mode == 'create-testdir':
        if not destdir:
            message = '%s: *** ' % constants.APP['name']
            message += 'please specify --dir option\n'
            message += '%s: *** Stop.\n' % constants.APP['name']
            sys.stderr.write(message)
            sys.exit(1)
        if not auxdir:
            auxdir = 'build-aux'
        config.setAuxDir(auxdir)
        testdir = classes.GLTestDir(config, destdir)
        testdir.execute()

    elif mode == 'create-megatestdir':
        if not destdir:
            message = '%s: *** ' % constants.APP['name']
            message += 'please specify --dir option\n'
            message += '%s: *** Stop.\n' % constants.APP['name']
            sys.stderr.write(message)
            sys.exit(1)
        if not auxdir:
            auxdir = 'build-aux'
        config.setAuxDir(auxdir)
        testdir = classes.GLMegaTestDir(config, destdir)
        testdir.execute()

    elif mode == 'test':
        if not destdir:
            destdir = 'testdir%04d' % random.randrange(0, 9999)
        if not auxdir:
            auxdir = 'build-aux'
        config.setAuxDir(auxdir)
        testdir = classes.GLTestDir(config, destdir)
        testdir.execute()
        os.chdir(destdir)
        os.mkdir('build')
        os.chdir('build')
        try:  # Try to execute commands
            sp.call(['../configure'])
            sp.call([UTILS['make']])
            sp.call([UTILS['make'], 'check'])
            sp.call([UTILS['make'], 'distclean'])
        except Exception as error:
            sys.exit(1)
        args = ['find', '.', '-type', 'f', '-print']
        remaining = sp.check_output(args).decode(ENCS['shell'])
        lines = [ line.strip()
                  for line in remaining.split('\n')
                  if line.strip() ]
        remaining = ' '.join(lines)
        if remaining:
            message = 'Remaining files: %s\n' % remaining
            message += 'gnulib-tool: *** Stop.\n'
            sys.stderr.write(message)
            sys.exit(1)
        os.chdir('../..')
        sp.call(['rm', '-rf', destdir], shell=False)

    elif mode == 'megatest':
        if not destdir:
            destdir = 'testdir %04d' % random.randrange(0, 9999)
        if not auxdir:
            auxdir = 'build-aux'
        config.setAuxDir(auxdir)
        testdir = classes.GLMegaTestDir(config, destdir)
        testdir.execute()
        os.chdir(destdir)
        os.mkdir('build')
        os.chdir('build')
        sp.call(['../configure'])
        sp.call([UTILS['make']])
        sp.call([UTILS['make'], 'check'])
        sp.call([UTILS['make'], 'distclean'])
        args = ['find', '.', '-type', 'f', '-print']
        remaining = sp.check_output(args).decode(ENCS['shell'])
        lines = [ line.strip()
                  for line in remaining.split('\n')
                  if line.strip() ]
        remaining = ' '.join(lines)
        if remaining:
            message = 'Remaining files: %s\n' % remaining
            message += 'gnulib-tool: *** Stop.\n'
            sys.stderr.write(message)
            sys.exit(1)
        os.chdir('../..')
        sp.call(['rm', '-rf', destdir], shell=False)

    elif mode == 'extract-description':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getDescription())

    elif mode == 'extract-comment':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getComment())

    elif mode == 'extract-status':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getStatus())

    elif mode == 'extract-notice':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getNotice())

    elif mode == 'extract-applicability':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                print(module.getApplicability())

    elif mode == 'extract-filelist':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                files = module.getFiles()
                print('\n'.join(files))

    elif mode == 'extract-dependencies':
        if avoids:
            message = '%s: *** ' % constants.APP['name']
            message += 'cannot combine --avoid and --extract-dependencies\n'
            message += '%s: *** Stop.\n' % constants.APP['name']
            sys.stderr.write(message)
            sys.exit(1)
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getDependencies())

    elif mode == 'extract-autoconf-snippet':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getAutoconfSnippet())

    elif mode == 'extract-automake-snippet':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getAutomakeSnippet())

    elif mode == 'extract-include-directive':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getInclude())

    elif mode == 'extract-link-directive':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getLink())

    elif mode == 'extract-license':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                print(module.getLicense())

    elif mode == 'extract-maintainer':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                sys.stdout.write(module.getMaintainer())

    elif mode == 'extract-tests-module':
        modulesystem = classes.GLModuleSystem(config)
        for name in modules:
            module = modulesystem.find(name)
            if module:
                if module.getTestsModule():
                    print(module.getTestsName())

    elif mode == 'copy-file':
        srcpath = files[0]
        # The second argument is the destination; either a directory ot a file.
        # It defaults to the current directory.
        if len(files) == 2:
            dest = files[1]
        else:  # if len(files) < 2
            dest = '.'
        if not auxdir:
            auxdir = 'build-aux'
        if not sourcebase:
            sourcebase = 'lib'
        if not m4base:
            m4base = 'm4'
        if not docbase:
            docbase = 'doc'
        if not testsbase:
            testsbase = 'tests'
        config.setAuxDir(auxdir)
        config.setSourceBase(sourcebase)
        config.setM4Base(m4base)
        config.setDocBase(docbase)
        config.setTestsBase(testsbase)
        filesystem = classes.GLFileSystem(config)
        lookedup, flag = filesystem.lookup(srcpath)
        if isdir(dest):
            destdir = dest
            if srcpath.startswith('build-aux/'):
                destpath = constants.substart('build-aux/', '%s/' % auxdir, srcpath)
            elif srcpath.startswith('doc/'):
                destpath = constants.substart('doc/', '%s/' % docbase, srcpath)
            elif srcpath.startswith('lib/'):
                destpath = constants.substart('lib/', '%s/' % sourcebase, srcpath)
            elif srcpath.startswith('m4/'):
                destpath = constants.substart('m4/', '%s/' % m4base, srcpath)
            elif srcpath.startswith('tests/'):
                destpath = constants.substart('tests/', '%s/' % testsbase, srcpath)
            elif srcpath.startswith('top/'):
                destpath = constants.substart('top/', '', srcpath)
            else:  # either case
                destpath = srcpath
        else:  # if not isdir(dest)
            destdir = os.path.dirname(dest)
            destpath = os.path.basename(dest)
        # Create the directory for destfile.
        dirname = os.path.dirname(joinpath(destdir, destpath))
        if not config['dryrun']:
            if dirname and not isdir(dirname):
                try:  # Try to create directories
                    os.makedirs(dirname)
                except FileExistsError:
                    pass
        # Copy the file.
        assistant = classes.GLFileAssistant(config)
        tmpfile = assistant.tmpfilename(destpath)
        copyfile(lookedup, tmpfile)
        assistant.setOriginal(srcpath)
        assistant.config.setDestDir(destdir)
        assistant.setRewritten(destpath)
        if isfile(joinpath(destdir, destpath)):
            # The file already exists.
            assistant.update(lookedup, flag, tmpfile, True)
        else:  # if not isfile(joinpath(destdir, destpath))
            # Install the file.
            # Don't protest if the file should be there but isn't: it happens
            # frequently that developers don't put autogenerated files under
            # version control.
            assistant.add(lookedup, flag, tmpfile)
        if isfile(tmpfile):
            os.remove(tmpfile)

    else:
        message = '%s: *** ' % constants.APP['name']
        message += 'no mode specified\n'
        message += '%s: *** Stop.\n' % constants.APP['name']
        sys.stderr.write(message)
        sys.exit(1)


if __name__ == '__main__':
    try:  # Try to execute
        main()
    except classes.GLError as error:
        errmode = 0  # gnulib-style errors
        errno = error.errno
        errinfo = error.errinfo
        if errmode == 0:
            message = '%s: *** ' % constants.APP['name']
            if errinfo == None:
                errinfo = ''
            if errno == 1:
                message += 'file %s not found' % errinfo
            elif errno == 2:
                message += 'patch file %s didn\'t apply cleanly' % errinfo
            elif errno == 3:
                message += 'cannot find %s - make sure you run gnulib-tool from within your package\'s directory' % errinfo
            elif errno == 4:
                message += 'minimum supported autoconf version is 2.59. Try adding'
                message += 'AC_PREREQ([%s])' % constants.DEFAULT_AUTOCONF_MINVERSION
                message += ' to your configure.ac.'
            elif errno == 5:
                message += '%s is expected to contain gl_M4_BASE([%s])' % (repr(os.path.join(errinfo, 'gnulib-comp.m4')), repr(errinfo))
            elif errno == 6:
                message += 'missing --source-base option'
            elif errno == 7:
                message += 'missing --doc-base option. --doc-base has been introduced '
                message += 'on 2006-07-11; if your last invocation of \'gnulib-tool '
                message += '--import\' is before that date, you need to run'
                message += '\'gnulib-tool --import\' once, with a --doc-base option.'
            elif errno == 8:
                message += 'missing --tests-base option'
            elif errno == 9:
                message += 'missing --lib option'
            elif errno == 10:
                message = 'gnulib-tool: option --conditional-dependencies is not supported with --with-tests'
            elif errno == 11:
                incompatibilities = ''
                message += 'incompatible license on modules:%s' % constants.NL
                for pair in errinfo:
                    incompatibilities += pair[0]
                    incompatibilities += ' %s' % pair[1]
                    incompatibilities += constants.NL
                tempname = mktemp()
                with codecs.open(tempname, 'wb', 'UTF-8') as file:
                    file.write(incompatibilities)
                sed_table = 's,^\\([^ ]*\\) ,\\1' + ' ' * 51 + ',\n'
                sed_table += 's,^\\(' + '.' * 49 + '[^ ]*\\) *,' + ' ' * 17 + '\\1 ,'
                args = ['sed', '-e', sed_table, tempname]
                incompatibilities = sp.check_output(args).decode(ENCS['default'])
                message += incompatibilities
                os.remove(tempname)
            elif errno == 12:
                message += 'refusing to do nothing'
            elif errno == 13:
                message += 'could not create directory %s' % errinfo
            elif errno == 14:
                message += 'could not delete file %s' % errinfo
            elif errno == 15:
                message += 'could not create file %s' % errinfo
            elif errno == 16:
                message += 'could not transform file %s' % errinfo
            elif errno == 17:
                message += 'could not update file %s' % errinfo
            elif errno == 18:
                message += 'module %s lacks a license' % errinfo
            elif errno == 19:
                message += 'could not create destination directory: %s' % errinfo
            message += '\n%s: *** Stop.\n' % constants.APP['name']
            sys.stderr.write(message)
            sys.exit(1)
