Today's changes:

2022-07-31  Bruno Haible  <br...@clisp.org>

        gnulib-tool.py: Fix typo.
        * pygnulib/GLTestDir.py (GLMegaTestDir.execute): Invoke os.mkdir as
        intended.

        gnulib-tool.py: Improve some error messages.
        * gnulib-tool.py (main): Write "*** Stop." instead of "*** Exit.".
        (__main__): Print an error message for GLError 5, 13, 14, 15, 16, 17, 
18.
        * pygnulib/GLError.py (GLError.__repr__): Compute one error message,
        not 19.

        gnulib-tool.py: Write errors to stderr, not stdout.
        * pygnulib/constants.py: Write error messages to stderr, not stdout.

        gnulib-tool.py: Use mainstream coding style.
        * pygnulib/GLConfig.py: Use 'raise' to re-throw an exception.

        gnulib-tool.py: Implement options --symlink and --local-symlink.
        * gnulib-tool.py (main): Handle options --symlink and --local-symlink.
        * pygnulib/constants.py (link_if_changed): Ignore FileNotFoundError from
        os.remove call.

        gnulib-tool.py: Make --copy-file work.
        * gnulib-tool.py (main) [copy-file]: Fix reference to uninitialized
        variable. Fix error handling of os.makedirs. Pass the destdir to the
        GLFileAssistant.

        gnulib-tool.py: Allow module arguments to occur at any position.
        * gnulib-tool.py (main): Collect the non-option arguments in a single
        list, regardless of their position. Use parse_known_args instead of
        parse_args, and give an error message about unknown options ourselves.
        * gnulib-tool: Fix typo in error message.

        gnulib-tool.py: Make --test behaviour more similar to gnulib-tool.
        * gnulib-tool.py (main) [test]: Remove space from the testdir name.

        gnulib-tool: Clarify that --test allows zero module arguments.
        * gnulib-tool (func_usage): Mark the modules for --test as optional.
        * pygnulib/GLInfo.py (GLInfo.usage): Likewise.

        gnulib-tool.py: Make option processing more similar to gnulib-tool.
        * gnulib-tool.py (main): Allow --add-import and --remove-import with 0
        modules.

        gnulib-tool.py: Improve compliance with GNU standards.
        * gnulib-tool.py (main): Handle --help and --version before testing for
        conflicting modes.

        gnulib-tool.py: Emit error message when conflicting modes are specified.
        * gnulib-tool.py (main): Fix test of conflicting modes. (Some options
        produce a value of [], and as a condition, [] evaluates to False.)

        gnulib-tool.py: Remove most short options.
        * gnulib-tool.py (main): Reorder the list of options. Remove most short
        options, for consistency with gnulib-tool.

        gnulib-tool.py: Follow gnulib-tool changes, part 19.
        Follow gnulib-tool changes
        2015-12-09  Pavel Raiskup  <prais...@redhat.com>
        gnulib-tool: allow multiple --local-dir usage
        2019-02-14  Bruno Haible  <br...@clisp.org>
        gnulib-tool: Improve handling of multiple --local-dir options.
        * gnulib-tool (func_reconstruct_cached_dir): When the argument is
        absolute, return it unmodified.
        (func_compute_relative_local_gnulib_path): Renamed from
        func_count_relative_local_gnulib_path. Add comment.
        * gnulib-tool.py: Accept multiple --local-dir options and collect the
        values into localpath.
        * pygnulib/GLConfig.py: Take a localpath argument instead of a localdir
        argument.
        (getLocalDir, setLocalDir, resetLocalDir): Remove methods.
        (getLocalPath, setLocalPath, resetLocalPath): New methods.
        * pygnulib/GLFileSystem.py (CopyAction): New class.
        (GLFileSystem.lookup): Consider all dirs in localpath.
        (GLFileSystem.shouldLink): New method.
        (GLFileAssistant): Use shouldLink.
        * pygnulib/GLModuleSystem.py (GLModuleSystem.exists): Iterate over all
        dirs in localpath.
        (GLModuleSystem.list): Likewise.
        * pygnulib/GLEmiter.py: Update.
        * pygnulib/GLImport.py (GLImport.__init__): Put the argument of
        gl_LOCAL_DIR into localpath, not localdir.
        (GLImport.actioncmd): Consider all dirs in localpath.
        (GLImport.relative_to_destdir, GLImport.relative_to_currdir): New
        methods.
        (GLImport.gnulib_cache): Combine all dirs in localpath. Use
        self.relative_to_destdir.
        * pygnulib/GLTestDir.py (GLTestDir.execute): Use shouldLink.

        gnulib-tool.py: Improve the primitives for relative file names.
        * pygnulib/constants.py (relativize): Don't attempt to handle absolute
        file names. Fix bug with relativize('../foo/bar', '../foo/bla/zut').
        (relconcat): New function.

        gnulib-tool.py: Follow gnulib-tool changes, part 18.
        Follow gnulib-tool change
        2005-09-20  Bruno Haible  <br...@clisp.org>
        gnulib-tool: Remove trailing slashes
        * pygnulib/constants.py (remove_trailing_slashes): New function.
        * pygnulib/GLConfig.py (GLConfig): Use it in the setters.

>From 412c51e6c9faa6445cea9609e7c091777f513aa7 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:30:50 +0200
Subject: [PATCH 01/16] gnulib-tool.py: Follow gnulib-tool changes, part 18.

Follow gnulib-tool change
2005-09-20  Bruno Haible  <br...@clisp.org>
gnulib-tool: Remove trailing slashes

* pygnulib/constants.py (remove_trailing_slashes): New function.
* pygnulib/GLConfig.py (GLConfig): Use it in the setters.
---
 ChangeLog             | 11 ++++++++++-
 pygnulib/GLConfig.py  | 19 ++++++++++++-------
 pygnulib/constants.py | 12 ++++++++++++
 3 files changed, 34 insertions(+), 8 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index cb311920f7..9fec245cf5 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,13 @@
-2022-07-06  Akim Demaille  <a...@lrde.epita.fr>
+2022-07-31  Bruno Haible  <br...@clisp.org>
+
+	gnulib-tool.py: Follow gnulib-tool changes, part 18.
+	Follow gnulib-tool change
+	2005-09-20  Bruno Haible  <br...@clisp.org>
+	gnulib-tool: Remove trailing slashes
+	* pygnulib/constants.py (remove_trailing_slashes): New function.
+	* pygnulib/GLConfig.py (GLConfig): Use it in the setters.
+
+2022-07-31  Akim Demaille  <a...@lrde.epita.fr>
 
 	gnulib-tool: add support for --automake-subdir-tests
 	<https://lists.gnu.org/r/bug-gnulib/2022-01/msg00111.html>
diff --git a/pygnulib/GLConfig.py b/pygnulib/GLConfig.py
index 457dc2c1e3..d203820427 100644
--- a/pygnulib/GLConfig.py
+++ b/pygnulib/GLConfig.py
@@ -42,6 +42,7 @@ MODES = constants.MODES
 TESTS = constants.TESTS
 joinpath = constants.joinpath
 relpath = constants.relativize
+remove_trailing_slashes = constants.remove_trailing_slashes
 isfile = os.path.isfile
 normpath = os.path.normpath
 
@@ -68,6 +69,10 @@ class GLConfig(object):
         Create new GLConfig instance.'''
         self.table = dict()
         self.table['tempdir'] = tempfile.mkdtemp()
+        # Check and store the attributes.
+        # Remove trailing slashes from the directory names. This is necessary
+        # for m4base (to avoid an error in func_import) and optional for the
+        # others.
         # destdir
         self.resetDestDir()
         if destdir != None:
@@ -408,7 +413,7 @@ class GLConfig(object):
         in gnulib's directory.'''
         if type(localdir) is str:
             if localdir:
-                self.table['localdir'] = localdir
+                self.table['localdir'] = remove_trailing_slashes(localdir)
         else:  # if localdir has not str type
             raise TypeError('localdir must be a string, not %s' %
                             type(localdir).__name__)
@@ -431,7 +436,7 @@ class GLConfig(object):
         placed. Default comes from configure.ac or configure.in.'''
         if type(auxdir) is str:
             if auxdir:
-                self.table['auxdir'] = auxdir
+                self.table['auxdir'] = remove_trailing_slashes(auxdir)
         else:  # if type of auxdir is not str
             raise TypeError('auxdir must be a string, not %s' %
                             type(auxdir).__name__)
@@ -450,7 +455,7 @@ class GLConfig(object):
         '''Specify directory relative to destdir where source code is placed.'''
         if type(sourcebase) is str:
             if sourcebase:
-                self.table['sourcebase'] = sourcebase
+                self.table['sourcebase'] = remove_trailing_slashes(sourcebase)
         else:  # if type of sourcebase is not str
             raise TypeError('sourcebase must be a string, not %s' %
                             type(sourcebase).__name__)
@@ -468,7 +473,7 @@ class GLConfig(object):
         '''Specify directory relative to destdir where *.m4 macros are placed.'''
         if type(m4base) is str:
             if m4base:
-                self.table['m4base'] = m4base
+                self.table['m4base'] = remove_trailing_slashes(m4base)
         else:  # if type of m4base is not str
             raise TypeError('m4base must be a string, not %s' %
                             type(m4base).__name__)
@@ -486,7 +491,7 @@ class GLConfig(object):
         '''Specify directory relative to destdir where *.po files are placed.'''
         if type(pobase) is str:
             if pobase:
-                self.table['pobase'] = pobase
+                self.table['pobase'] = remove_trailing_slashes(pobase)
         else:  # if type of pobase is not str
             raise TypeError('pobase must be a string, not %s' %
                             type(pobase).__name__)
@@ -506,7 +511,7 @@ class GLConfig(object):
         Default value for this variable is 'doc').'''
         if type(docbase) is str:
             if docbase:
-                self.table['docbase'] = docbase
+                self.table['docbase'] = remove_trailing_slashes(docbase)
         else:  # if type of docbase is not str
             raise TypeError('docbase must be a string, not %s' %
                             type(docbase).__name__)
@@ -527,7 +532,7 @@ class GLConfig(object):
         Default value for this variable is 'tests').'''
         if type(testsbase) is str:
             if testsbase:
-                self.table['testsbase'] = testsbase
+                self.table['testsbase'] = remove_trailing_slashes(testsbase)
         else:  # if type of testsbase is not str
             raise TypeError('testsbase must be a string, not %s' %
                             type(testsbase).__name__)
diff --git a/pygnulib/constants.py b/pygnulib/constants.py
index 8fd982f7a7..e951c906d0 100644
--- a/pygnulib/constants.py
+++ b/pygnulib/constants.py
@@ -394,6 +394,18 @@ def nlremove(text):
     return text
 
 
+def remove_trailing_slashes(text):
+    '''Remove trailing slashes from a file name, except when the file name
+    consists only of slashes.'''
+    result = text
+    while result.endswith('/'):
+        result = result[:-1]
+        if result == '':
+            result = text
+            break
+    return result
+
+
 def remove_backslash_newline(text):
     '''Given a multiline string text, join lines:
     When a line ends in a backslash, remove the backslash and join the next
-- 
2.34.1

>From 72d3a4615864e9f77109e86ee4c1a6fe5fcb0946 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:35:58 +0200
Subject: [PATCH 02/16] gnulib-tool.py: Improve the primitives for relative
 file names.

* pygnulib/constants.py (relativize): Don't attempt to handle absolute
file names. Fix bug with relativize('../foo/bar', '../foo/bla/zut').
(relconcat): New function.
---
 ChangeLog             |  5 +++++
 pygnulib/constants.py | 22 ++++++++++++----------
 2 files changed, 17 insertions(+), 10 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 9fec245cf5..00ffeffa52 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Improve the primitives for relative file names.
+	* pygnulib/constants.py (relativize): Don't attempt to handle absolute
+	file names. Fix bug with relativize('../foo/bar', '../foo/bla/zut').
+	(relconcat): New function.
+
 	gnulib-tool.py: Follow gnulib-tool changes, part 18.
 	Follow gnulib-tool change
 	2005-09-20  Bruno Haible  <br...@clisp.org>
diff --git a/pygnulib/constants.py b/pygnulib/constants.py
index e951c906d0..9ef2e01089 100644
--- a/pygnulib/constants.py
+++ b/pygnulib/constants.py
@@ -272,25 +272,20 @@ def joinpath(head, *tail):
 
 
 def relativize(dir1, dir2):
-    '''Compute a relative pathname reldir such that dir1/reldir = dir2.'''
+    '''Compute a relative pathname reldir such that dir1/reldir = dir2.
+    dir1 and dir2 must be relative pathnames.'''
     dir0 = os.getcwd()
     while dir1:
         dir1 = '%s%s' % (os.path.normpath(dir1), os.path.sep)
         dir2 = '%s%s' % (os.path.normpath(dir2), os.path.sep)
-        if dir1.startswith(os.path.sep):
-            first = dir1[:dir1.find(os.path.sep, 1)]
-        else:  # if not dir1.startswith('/')
-            first = dir1[:dir1.find(os.path.sep)]
+        first = dir1[:dir1.find(os.path.sep)]
         if first != '.':
             if first == '..':
-                dir2 = os.path.basename(joinpath(dir0, dir2))
+                dir2 = joinpath(os.path.basename(dir0), dir2)
                 dir0 = os.path.dirname(dir0)
             else:  # if first != '..'
                 # Get first component of dir2
-                if dir2.startswith(os.path.sep):
-                    first2 = dir2[:dir2.find(os.path.sep, 1)]
-                else:  # if not dir1.startswith('/')
-                    first2 = dir2[:dir2.find(os.path.sep)]
+                first2 = dir2[:dir2.find(os.path.sep)]
                 if first == first2:
                     dir2 = dir2[dir2.find(os.path.sep) + 1:]
                 else:  # if first != first2
@@ -301,6 +296,13 @@ def relativize(dir1, dir2):
     return result
 
 
+def relconcat(dir1, dir2):
+    '''Compute a relative pathname dir1/dir2, with obvious simplifications.
+    dir1 and dir2 must be relative pathnames.
+    dir2 is considered to be relative to dir1.'''
+    return os.path.normpath(os.path.join(dir1, dir2))
+
+
 def link_relative(src, dest):
     '''Like ln -s, except that src is given relative to the current directory
     (or absolute), not given relative to the directory of dest.'''
-- 
2.34.1

>From b228f782b10a44736af152f0bdb7c94c7429ceab Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:39:19 +0200
Subject: [PATCH 03/16] gnulib-tool.py: Follow gnulib-tool changes, part 19.

Follow gnulib-tool changes
2015-12-09  Pavel Raiskup  <prais...@redhat.com>
gnulib-tool: allow multiple --local-dir usage
2019-02-14  Bruno Haible  <br...@clisp.org>
gnulib-tool: Improve handling of multiple --local-dir options.

* gnulib-tool (func_reconstruct_cached_dir): When the argument is
absolute, return it unmodified.
(func_compute_relative_local_gnulib_path): Renamed from
func_count_relative_local_gnulib_path. Add comment.
* gnulib-tool.py: Accept multiple --local-dir options and collect the
values into localpath.
* pygnulib/GLConfig.py: Take a localpath argument instead of a localdir
argument.
(getLocalDir, setLocalDir, resetLocalDir): Remove methods.
(getLocalPath, setLocalPath, resetLocalPath): New methods.
* pygnulib/GLFileSystem.py (CopyAction): New class.
(GLFileSystem.lookup): Consider all dirs in localpath.
(GLFileSystem.shouldLink): New method.
(GLFileAssistant): Use shouldLink.
* pygnulib/GLModuleSystem.py (GLModuleSystem.exists): Iterate over all
dirs in localpath.
(GLModuleSystem.list): Likewise.
* pygnulib/GLEmiter.py: Update.
* pygnulib/GLImport.py (GLImport.__init__): Put the argument of
gl_LOCAL_DIR into localpath, not localdir.
(GLImport.actioncmd): Consider all dirs in localpath.
(GLImport.relative_to_destdir, GLImport.relative_to_currdir): New
methods.
(GLImport.gnulib_cache): Combine all dirs in localpath. Use
self.relative_to_destdir.
* pygnulib/GLTestDir.py (GLTestDir.execute): Use shouldLink.
---
 ChangeLog                  |  33 +++++++++++
 gnulib-tool                |  17 +++---
 gnulib-tool.py             |  17 +++---
 gnulib-tool.py.TODO        |  90 ++++++++++------------------
 pygnulib/GLConfig.py       |  61 ++++++++++---------
 pygnulib/GLEmiter.py       |   6 +-
 pygnulib/GLFileSystem.py   | 117 ++++++++++++++++++++++++-------------
 pygnulib/GLImport.py       |  87 ++++++++++++++++-----------
 pygnulib/GLModuleSystem.py |  45 +++++++-------
 pygnulib/GLTestDir.py      |   6 +-
 10 files changed, 269 insertions(+), 210 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 00ffeffa52..98438a58a4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,38 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Follow gnulib-tool changes, part 19.
+	Follow gnulib-tool changes
+	2015-12-09  Pavel Raiskup  <prais...@redhat.com>
+	gnulib-tool: allow multiple --local-dir usage
+	2019-02-14  Bruno Haible  <br...@clisp.org>
+	gnulib-tool: Improve handling of multiple --local-dir options.
+	* gnulib-tool (func_reconstruct_cached_dir): When the argument is
+	absolute, return it unmodified.
+	(func_compute_relative_local_gnulib_path): Renamed from
+	func_count_relative_local_gnulib_path. Add comment.
+	* gnulib-tool.py: Accept multiple --local-dir options and collect the
+	values into localpath.
+	* pygnulib/GLConfig.py: Take a localpath argument instead of a localdir
+	argument.
+	(getLocalDir, setLocalDir, resetLocalDir): Remove methods.
+	(getLocalPath, setLocalPath, resetLocalPath): New methods.
+	* pygnulib/GLFileSystem.py (CopyAction): New class.
+	(GLFileSystem.lookup): Consider all dirs in localpath.
+	(GLFileSystem.shouldLink): New method.
+	(GLFileAssistant): Use shouldLink.
+	* pygnulib/GLModuleSystem.py (GLModuleSystem.exists): Iterate over all
+	dirs in localpath.
+	(GLModuleSystem.list): Likewise.
+	* pygnulib/GLEmiter.py: Update.
+	* pygnulib/GLImport.py (GLImport.__init__): Put the argument of
+	gl_LOCAL_DIR into localpath, not localdir.
+	(GLImport.actioncmd): Consider all dirs in localpath.
+	(GLImport.relative_to_destdir, GLImport.relative_to_currdir): New
+	methods.
+	(GLImport.gnulib_cache): Combine all dirs in localpath. Use
+	self.relative_to_destdir.
+	* pygnulib/GLTestDir.py (GLTestDir.execute): Use shouldLink.
+
 	gnulib-tool.py: Improve the primitives for relative file names.
 	* pygnulib/constants.py (relativize): Don't attempt to handle absolute
 	file names. Fix bug with relativize('../foo/bar', '../foo/bla/zut').
diff --git a/gnulib-tool b/gnulib-tool
index bee85856f4..f1665f4002 100755
--- a/gnulib-tool
+++ b/gnulib-tool
@@ -4846,12 +4846,13 @@ func_reconstruct_cached_dir ()
 {
   cached_dir=$1
   if test -n "$cached_dir"; then
-    case "$destdir" in
+    case "$cached_dir" in
       /*)
-        func_path_append local_gnulib_path "$destdir/$cached_dir" ;;
+        func_path_append local_gnulib_path "$cached_dir" ;;
       *)
-        case "$cached_dir" in
+        case "$destdir" in
           /*)
+            # XXX This doesn't look right.
             func_path_append local_gnulib_path "$destdir/$cached_dir" ;;
           *)
             func_relconcat "$destdir" "$cached_dir"
@@ -5835,14 +5836,14 @@ s,//*$,/,'
     fi
   fi
 
-  # func_count_relative_local_gnulib_path
+  # func_compute_relative_local_gnulib_path
   # gl_LOCAL_DIR requires local_gnulib_path to be set relatively to destdir
   # Input:
   # - local_gnulib_path  from --local-dir
   # - destdir           from --dir
   # Output:
   # - relative_local_dir  path to be stored into gl_LOCAL_DIR
-  func_count_relative_local_gnulib_path ()
+  func_compute_relative_local_gnulib_path ()
   {
     relative_local_gnulib_path=
     save_IFS="$IFS"
@@ -5856,7 +5857,9 @@ s,//*$,/,'
           relative_local_dir="$local_dir" ;;
         * )
           case "$destdir" in
-            /*) relative_local_dir="$local_dir" ;;
+            /*)
+              # XXX This doesn't look right.
+              relative_local_dir="$local_dir" ;;
             *)
               # destdir, local_dir are both relative.
               func_relativize "$destdir" "$local_dir"
@@ -5883,7 +5886,7 @@ s,//*$,/,'
     printf '%s\n' "$actioncmd"
     echo
     echo "# Specification in the form of a few gnulib-tool.m4 macro invocations:"
-    func_count_relative_local_gnulib_path
+    func_compute_relative_local_gnulib_path
     echo "gl_LOCAL_DIR([$relative_local_gnulib_path])"
     echo "gl_MODULES(["
     echo "$specified_modules" | sed -e 's/^/  /g'
diff --git a/gnulib-tool.py b/gnulib-tool.py
index 523de7769e..0b2f866920 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -24,7 +24,7 @@
 # - Line length is not limited to 79 characters.
 # - Line breaking before or after binary operators? Better before, like in GNU.
 # You can use this command to check the style:
-#   $ pycodestyle --max-line-length=128 --ignore=E265,W503,E241,E711,E712 gnulib-tool.py pygnulib/*.py
+#   $ pycodestyle --max-line-length=128 --ignore=E265,W503,E241,E711,E712,E201,E202 gnulib-tool.py pygnulib/*.py
 
 
 #===============================================================================
@@ -66,7 +66,7 @@ def main():
     # Reset arguments
     mode = None
     destdir = None
-    localdir = None
+    localpath = None
     modcache = None
     verbose = None
     auxdir = None
@@ -224,11 +224,12 @@ def main():
                         dest='destdir',
                         default=None,
                         nargs=1)
-    # localdir
+    # localpath
     parser.add_argument('-ld', '--local-dir',
-                        dest='localdir',
+                        action='append',
+                        dest='localpath',
                         default=None,
-                        nargs=1)
+                        nargs='?')
     # verbose
     parser.add_argument('-v', '--verbose',
                         default=0,
@@ -458,9 +459,7 @@ def main():
     destdir = cmdargs.destdir
     if destdir != None:
         destdir = cmdargs.destdir[0]
-    localdir = cmdargs.localdir
-    if localdir != None:
-        localdir = cmdargs.localdir[0]
+    localpath = cmdargs.localpath
     libname = cmdargs.libname
     if libname != None:
         libname = cmdargs.libname[0]
@@ -514,7 +513,7 @@ def main():
     # Create pygnulib configuration.
     config = classes.GLConfig(
         destdir=destdir,
-        localdir=localdir,
+        localpath=localpath,
         m4base=m4base,
         auxdir=auxdir,
         modules=modules,
diff --git a/gnulib-tool.py.TODO b/gnulib-tool.py.TODO
index c0a2c975f3..26c59c5353 100644
--- a/gnulib-tool.py.TODO
+++ b/gnulib-tool.py.TODO
@@ -15,6 +15,37 @@ The following commits to gnulib-tool have not yet been reflected in
 
 --------------------------------------------------------------------------------
 
+Implement the options:
+  --find
+  --without-tests
+  --without-c++-tests
+  --without-longrunning-tests
+  --without-privileged-tests
+  --without-unportable-tests
+  --single-configure
+  --conditional-dependencies
+  --no-conditional-dependencies
+  --gnu-make
+  --tests-makefile-name
+  --automake-subdir
+  --no-libtool
+  --macro-prefix
+  --po-domain
+  --witness-c-macro
+  --vc-files
+  --no-vc-files
+  --no-changelog
+  -s | --symbolic
+  --local-symlink
+  -h | --hardlink
+  --local-hardlink
+  -S | --more-symlinks
+  -H | --more-hardlinks
+  --help (same output)
+  --version
+
+--------------------------------------------------------------------------------
+
 commit 76c7703cb2e9e0e803d1296618d8ab9e86e13d6c
 Author: Akim Demaille <akim.demai...@gmail.com>
 Date:   Mon Jul 4 07:18:07 2022 +0200
@@ -1079,65 +1110,6 @@ Date:   Wed May 1 13:39:22 2013 +0900
 
 --------------------------------------------------------------------------------
 
-commit 49137e3bc6a2b3fd83c502a514e4a3b89fc1571c
-Author: Bruno Haible <br...@clisp.org>
-Date:   Thu Feb 14 20:50:57 2019 +0100
-
-    gnulib-tool: Improve handling of multiple --local-dir options.
-
-    * doc/gnulib.texi (Extending Gnulib): Explain how multiple --local-dir
-    options work.
-    * gnulib-tool (func_path_prepend): Remove function.
-    (func_path_foreach): Make IFS handling more robust.
-    (local_gnulib_path): Collect --local-dir values using func_path_append,
-    not func_path_prepend.
-    (func_determine_path_separator): Make IFS handling more robust.
-    (func_lookup_file_cb): New function.
-    (func_lookup_file): Rewritten to use func_lookup_file_cb instead of
-    func_lookup_local_file. Apply the patches in the reverse order of their
-    origin in $local_gnulib_path.
-    (func_count_relative_local_gnulib_path): Make IFS handling more robust.
-    * NEWS: Mention that the first --local-dir option is the one with
-    highest priority.
-
-commit 55c6f22b8043843aa7cf6843326eb4abed4de75c
-Author: Pavel Raiskup <prais...@redhat.com>
-Date:   Sat Nov 21 14:09:15 2015 +0100
-
-    gnulib-tool: allow multiple --local-dir usage
-
-    * gnulib-tool: Use --local-dir to construct compound
-    $local_gnulib_path path instead of $local_gnulib_dir.  Determine
-    PATH_SEPARATOR early.
-    (local_gnulib_dir): Rename into $local_gnulib_path everywhere.
-    (func_gnulib_dir): Cut out PATH_SEPARATOR detection code into
-    func_determine_path_separator because that needs to be detected
-    earlier now.
-    (func_determine_path_separator): New function.
-    (func_path_foreach, func_path_foreach_inner): New functions.
-    (func_path_prepend, func_path_append): Likewise.
-    (func_lookup_local_file, func_lookup_local_file_cb): Likewise.
-    (func_lookup_file, func_all_modules): Use new functions to work
-    with local_gnulib_path.
-    (func_modules_in_dir, func_exists_module): New callbacks for
-    func_path_foreach.
-    (func_exists_module, func_get_tests_module): Likewise.
-    (func_is_local_file, func_should_symlink): New helper methods.
-    (func_add_file, func_update_file): Use new func_should_symlink
-    instead, DRY.
-    (func_reconstruct_cached_local_gnulib_path): New helper.
-    (func_reconstruct_cached_dir): New callback.
-    (func_import): The cached_local_gnulib_dir renamed to
-    cached_local_gnulib_path similarly to local_gnulib_dir.
-    Use new func_reconstruct_cached_local_gnulib_path.
-    (func_count_relative_local_gnulib_path): New sub-method.
-    (func_create_testdir): Use func_should_symlink, DRY.
-    (func_create_megatestdir): Use new functions to work with
-    local_gnulib_path correctly.
-    (func_append_local_dir): New helper.
-
---------------------------------------------------------------------------------
-
 commit 9bdf6c8a0cdeb13c12e4b65dee9538c5468dbe1d
 Author: Bruno Haible <br...@clisp.org>
 Date:   Sun Aug 19 14:06:50 2012 +0200
diff --git a/pygnulib/GLConfig.py b/pygnulib/GLConfig.py
index d203820427..55b6cd2986 100644
--- a/pygnulib/GLConfig.py
+++ b/pygnulib/GLConfig.py
@@ -56,7 +56,7 @@ class GLConfig(object):
     By default all attributes are set to empty string, empty list or zero.
     The most common value, however, is a None value.'''
 
-    def __init__(self, destdir=None, localdir=None, auxdir=None,
+    def __init__(self, destdir=None, localpath=None, auxdir=None,
                  sourcebase=None, m4base=None, pobase=None, docbase=None, testsbase=None,
                  modules=None, avoids=None, files=None, testflags=None, libname=None,
                  lgpl=None, makefile=None, libtool=None, conddeps=None, macro_prefix=None,
@@ -77,10 +77,10 @@ class GLConfig(object):
         self.resetDestDir()
         if destdir != None:
             self.setDestDir(destdir)
-        # localdir
-        self.resetLocalDir()
-        if localdir != None:
-            self.setLocalDir(localdir)
+        # localpath
+        self.resetLocalPath()
+        if localpath != None:
+            self.setLocalPath(localpath)
         # auxdir
         self.resetAuxDir()
         if auxdir != None:
@@ -402,26 +402,29 @@ class GLConfig(object):
         configure.ac can be found. Defaults to current directory.'''
         self.table['destdir'] = ''
 
-    # Define localdir methods.
-    def getLocalDir(self):
-        '''Return a local override directory where to look up files before looking
-        in gnulib's directory.'''
-        return self.table['localdir']
-
-    def setLocalDir(self, localdir):
-        '''Specify a local override directory where to look up files before looking
-        in gnulib's directory.'''
-        if type(localdir) is str:
-            if localdir:
-                self.table['localdir'] = remove_trailing_slashes(localdir)
-        else:  # if localdir has not str type
-            raise TypeError('localdir must be a string, not %s' %
-                            type(localdir).__name__)
-
-    def resetLocalDir(self):
-        '''Reset a local override directory where to look up files before looking
-        in gnulib's directory.'''
-        self.table['localdir'] = ''
+    # Define localpath methods.
+    def getLocalPath(self):
+        '''Return a list of local override directories where to look up files
+        before looking in gnulib's directory. The first one has the highest
+        priority.'''
+        return self.table['localpath']
+
+    def setLocalPath(self, localpath):
+        '''Specify a list of local override directories where to look up files
+        before looking in gnulib's directory. The first one has the highest
+        priority.'''
+        if type(localpath) is list:
+            for dir in localpath:
+                if type(dir) is not str:
+                    raise TypeError('localpath element must be a string, not %s' % type(dir).__name__)
+        else:
+            raise TypeError('localpath must be a list, not %s' % type(localpath).__name__)
+        self.table['localpath'] = [ remove_trailing_slashes(dir) for dir in localpath ]
+
+    def resetLocalPath(self):
+        '''Reset a list of local override directories where to look up files
+        before looking in gnulib's directory.'''
+        self.table['localpath'] = []
 
     # Define auxdir methods.
     def getAuxDir(self):
@@ -998,22 +1001,22 @@ class GLConfig(object):
     # Define lsymbolic methods.
     def checkLSymbolic(self):
         '''Check if pygnulib will make symbolic links instead of copying files, only
-        for files from the local override directory.'''
+        for files from the local override directories.'''
         return self.table['lsymbolic']
 
     def enableLSymbolic(self):
         '''Enable creation of symbolic links instead of copying files, only for
-        files from the local override directory.'''
+        files from the local override directories.'''
         self.table['lsymbolic'] = True
 
     def disableLSymbolic(self):
         '''Disable creation of symbolic links instead of copying files, only for
-        files from the local override directory.'''
+        files from the local override directories.'''
         self.table['lsymbolic'] = False
 
     def resetLSymbolic(self):
         '''Reset creation of symbolic links instead of copying files, only for
-        files from the local override directory.'''
+        files from the local override directories.'''
         self.table['lsymbolic'] = False
 
     # Define verbosity methods.
diff --git a/pygnulib/GLEmiter.py b/pygnulib/GLEmiter.py
index c41d4c5518..a5a13e63fb 100644
--- a/pygnulib/GLEmiter.py
+++ b/pygnulib/GLEmiter.py
@@ -605,7 +605,7 @@ AC_DEFUN([%V1%_LIBSOURCES], [
 
         Emit the contents of the library Makefile. Returns str and a bool
         variable which shows if subdirectories are used.
-        GLConfig: localdir, sourcebase, libname, pobase, auxdir, makefile, libtool,
+        GLConfig: localpath, sourcebase, libname, pobase, auxdir, makefile, libtool,
         macro_prefix, podomain, conddeps, witness_c_macro.
 
         destfile is a filename relative to destdir of Makefile being generated.
@@ -635,7 +635,6 @@ AC_DEFUN([%V1%_LIBSOURCES], [
             raise TypeError('for_test must be a bool, not %s' %
                             type(for_test).__name__)
         emit = ''
-        localdir = self.config['localdir']
         sourcebase = self.config['sourcebase']
         modcache = self.config['modcache']
         libname = self.config['libname']
@@ -885,7 +884,7 @@ AC_DEFUN([%V1%_LIBSOURCES], [
 
         Emit the contents of the tests Makefile. Returns str and a bool variable
         which shows if subdirectories are used.
-        GLConfig: localdir, modules, libname, auxdir, makefile, libtool,
+        GLConfig: localpath, modules, libname, auxdir, makefile, libtool,
         sourcebase, m4base, testsbase, macro_prefix, witness_c_macro,
         single_configure, libtests.
 
@@ -914,7 +913,6 @@ AC_DEFUN([%V1%_LIBSOURCES], [
             raise TypeError('for_test must be a bool, not %s' %
                             type(for_test).__name__)
         emit = ''
-        localdir = self.config['localdir']
         auxdir = self.config['auxdir']
         sourcebase = self.config['sourcebase']
         modcache = self.config['modcache']
diff --git a/pygnulib/GLFileSystem.py b/pygnulib/GLFileSystem.py
index f17466187d..dfb1bb903f 100644
--- a/pygnulib/GLFileSystem.py
+++ b/pygnulib/GLFileSystem.py
@@ -23,6 +23,7 @@ import codecs
 import shutil
 import filecmp
 import subprocess as sp
+from enum import Enum
 from . import constants
 from .GLError import GLError
 from .GLConfig import GLConfig
@@ -45,18 +46,26 @@ isdir = os.path.isdir
 isfile = os.path.isfile
 
 
+#===============================================================================
+# Define CopyAction class
+#===============================================================================
+class CopyAction(Enum):
+    Copy = 0
+    Symlink = 1
+
+
 #===============================================================================
 # Define GLFileSystem class
 #===============================================================================
 class GLFileSystem(object):
-    '''GLFileSystem class is used to create virtual filesystem, which is based on
-    the gnulib directory and directory specified by localdir argument. Its main
-    method lookup(file) is used to find file in these directories or combine it
-    using Linux 'patch' utility.'''
+    '''GLFileSystem class is used to create virtual filesystem, which is based
+    on the gnulib directory and directories specified by localpath argument.
+    Its main method lookup(file) is used to find file in these directories or
+    combine it using Linux 'patch' utility.'''
 
     def __init__(self, config):
-        '''Create new GLFileSystem instance. The only argument is localdir,
-        which can be an empty string too.'''
+        '''Create new GLFileSystem instance. The only argument is localpath,
+        which can be an empty list.'''
         if type(config) is not GLConfig:
             raise TypeError('config must be a GLConfig, not %s' %
                             type(config).__name__)
@@ -70,43 +79,75 @@ class GLFileSystem(object):
     def lookup(self, name):
         '''GLFileSystem.lookup(name) -> tuple
 
-        Lookup a file in gnulib and localdir directories or combine it using Linux
+        Lookup a file in gnulib and localpath directories or combine it using
         'patch' utility. If file was found, method returns string, else it raises
         GLError telling that file was not found. Function also returns flag which
         indicates whether file is a temporary file.
-        GLConfig: localdir.'''
+        GLConfig: localpath.'''
         if type(name) is not str:
             raise TypeError(
                 'name must be a string, not %s' % type(module).__name__)
-        # If name exists in localdir, then we use it
-        path_gnulib = joinpath(DIRS['root'], name)
-        path_local = joinpath(self.config['localdir'], name)
-        path_diff = joinpath(self.config['localdir'], '%s.diff' % name)
-        path_temp = joinpath(self.config['tempdir'], name)
-        try:  # Try to create directories
-            os.makedirs(os.path.dirname(path_temp))
-        except OSError as error:
-            pass  # Skip errors if directory exists
-        if isfile(path_temp):
-            os.remove(path_temp)
-        if self.config['localdir'] and isfile(path_local):
-            result = (path_local, False)
-        else:  # if path_local does not exist
-            if isfile(path_gnulib):
-                if self.config['localdir'] and isfile(path_diff):
-                    shutil.copy(path_gnulib, path_temp)
-                    command = 'patch -s "%s" < "%s" >&2' % (path_temp, path_diff)
+        localpath = self.config['localpath']
+        # Each element in localpath is a directory whose contents overrides
+        # or amends the result of the lookup in the rest of localpath and
+        # the gnulib dir. So, the first element of localpath is the highest
+        # priority one.
+        lookedupFile = None
+        lookedupPatches = []
+        for localdir in localpath:
+            file_in_localdir = joinpath(localdir, name)
+            if isfile(file_in_localdir):
+                lookedupFile = file_in_localdir
+                break
+            diff_in_localdir = joinpath(localdir, '%s.diff' % name)
+            if isfile(diff_in_localdir):
+                lookedupPatches.append(diff_in_localdir)
+        # Treat the gnulib dir like a lowest-priority --local-dir, except that
+        # here we don't look for .diff files.
+        if lookedupFile == None:
+            file_in_localdir = joinpath(DIRS['root'], name)
+            if isfile(file_in_localdir):
+                lookedupFile = file_in_localdir
+        if lookedupFile != None:
+            if len(lookedupPatches) > 0:
+                # Apply the patches, from lowest-priority to highest-priority.
+                tempFile = joinpath(self.config['tempdir'], name)
+                try:  # Try to create directories
+                    os.makedirs(os.path.dirname(tempFile))
+                except OSError as error:
+                    pass  # Skip errors if directory exists
+                if isfile(tempFile):
+                    os.remove(tempFile)
+                shutil.copy(lookedupFile, tempFile)
+                for diff_in_localdir in reversed(lookedupPatches):
+                    command = 'patch -s "%s" < "%s" >&2' % (tempFile, diff_in_localdir)
                     try:  # Try to apply patch
                         sp.check_call(command, shell=True)
                     except sp.CalledProcessError as error:
                         raise GLError(2, name)
-                    result = (path_temp, True)
-                else:  # if path_diff does not exist
-                    result = (path_gnulib, False)
-            else:  # if path_gnulib does not exist
-                raise GLError(1, name)
+                result = (tempFile, True)
+            else:
+                result = (lookedupFile, False)
+        else:
+            raise GLError(1, name)
         return result
 
+    def shouldLink(self, original, lookedup):
+        '''GLFileSystem.shouldLink(original, lookedup)
+
+        Determines whether the original file should be copied or symlinked.
+        Returns a CopyAction.'''
+        symbolic = self.config['symbolic']
+        lsymbolic = self.config['lsymbolic']
+        localpath = self.config['localpath']
+        if symbolic:
+            return CopyAction.Symlink
+        if lsymbolic:
+            for localdir in localpath:
+                if lookedup == joinpath(localdir, original):
+                    return CopyAction.Symlink
+        return CopyAction.Copy
+
 
 #===============================================================================
 # Define GLFileAssistant class
@@ -212,16 +253,13 @@ class GLFileAssistant(object):
         original = self.original
         rewritten = self.rewritten
         destdir = self.config['destdir']
-        symbolic = self.config['symbolic']
-        lsymbolic = self.config['lsymbolic']
         if original == None:
             raise TypeError('original must be set before applying the method')
-        elif rewritten == None:
+        if rewritten == None:
             raise TypeError('rewritten must be set before applying the method')
         if not self.config['dryrun']:
             print('Copying file %s' % rewritten)
-            loriginal = joinpath(self.config['localdir'], original)
-            if (symbolic or (lsymbolic and lookedup == loriginal)) \
+            if self.filesystem.shouldLink(original, lookedup) == CopyAction.Symlink \
                     and not tmpflag and filecmp.cmp(lookedup, tmpfile):
                 constants.link_if_changed(
                     lookedup, joinpath(destdir, rewritten))
@@ -242,11 +280,9 @@ class GLFileAssistant(object):
         original = self.original
         rewritten = self.rewritten
         destdir = self.config['destdir']
-        symbolic = self.config['symbolic']
-        lsymbolic = self.config['lsymbolic']
         if original == None:
             raise TypeError('original must be set before applying the method')
-        elif rewritten == None:
+        if rewritten == None:
             raise TypeError('rewritten must be set before applying the method')
         if type(lookedup) is not str:
             raise TypeError('lookedup must be a string, not %s' %
@@ -274,8 +310,7 @@ class GLFileAssistant(object):
                     shutil.move(basepath, backuppath)
                 except Exception as error:
                     raise GLError(17, original)
-                loriginal = joinpath(self.config['localdir'], original)
-                if (symbolic or (lsymbolic and lookedup == loriginal)) \
+                if self.filesystem.shouldLink(original, lookedup) == CopyAction.Symlink \
                         and not tmpflag and filecmp.cmp(lookedup, tmpfile):
                     constants.link_if_changed(lookedup, basepath)
                 else:  # if any of these conditions is not met
diff --git a/pygnulib/GLImport.py b/pygnulib/GLImport.py
index b2691a976d..2b16ce0ea4 100644
--- a/pygnulib/GLImport.py
+++ b/pygnulib/GLImport.py
@@ -52,7 +52,6 @@ TESTS = constants.TESTS
 compiler = constants.compiler
 joinpath = constants.joinpath
 cleaner = constants.cleaner
-relpath = constants.relativize
 isabs = os.path.isabs
 isdir = os.path.isdir
 isfile = os.path.isfile
@@ -181,7 +180,7 @@ class GLImport(object):
             if tempdict['gl_LIB']:
                 self.cache.setLibName(cleaner(tempdict['gl_LIB']))
             if tempdict['gl_LOCAL_DIR']:
-                self.cache.setLocalDir(cleaner(tempdict['gl_LOCAL_DIR']))
+                self.cache.setLocalPath(cleaner(tempdict['gl_LOCAL_DIR']).split(':'))
             if tempdict['gl_MODULES']:
                 self.cache.setModules(cleaner(tempdict['gl_MODULES'].split()))
             if tempdict['gl_AVOID']:
@@ -217,23 +216,14 @@ class GLImport(object):
                 pattern = compiler(regex, re.S | re.M)
                 self.cache.setFiles(pattern.findall(data)[-1].strip().split())
 
-        # The self.config['localdir'] defaults to the cached one. Recall that the
-        # cached one is relative to $destdir, whereas the one we use is relative
-        # to . or absolute.
-        if not self.config['localdir']:
-            if self.cache['localdir']:
-                if isabs(self.config['destdir']):
-                    localdir = joinpath(
-                        self.config['destdir'], self.cache['localdir'])
-                else:  # if not isabs(self.config['destdir'])
-                    if isabs(self.cache['localdir']):
-                        localdir = joinpath(
-                            self.config['destdir'], self.cache['localdir'])
-                    else:  # if not isabs(self.cache['localdir'])
-                        # NOTE: I NEED TO IMPLEMENT RELATIVE_CONCAT
-                        localdir = os.path.relpath(joinpath(self.config['destdir'],
-                                                            self.cache['localdir']))
-                self.config.setLocalDir(localdir)
+        # The self.config['localpath'] defaults to the cached one. Recall that
+        # the cached one is relative to self.config['destdir'], whereas the one
+        # we use is relative to . or absolute.
+        if len(self.config['localpath']) == 0:
+            if len(self.cache['localpath']) > 0:
+                localpath = [ self.relative_to_currdir(localdir)
+                              for localdir in self.cache['localpath'] ]
+                self.config.setLocalPath(localpath)
 
         if self.mode != MODES['import']:
             if self.cache['m4base'] and \
@@ -367,7 +357,7 @@ class GLImport(object):
         modules = self.config.getModules()
         avoids = self.config.getAvoids()
         destdir = self.config.getDestDir()
-        localdir = self.config.getLocalDir()
+        localpath = self.config.getLocalPath()
         auxdir = self.config.getAuxDir()
         sourcebase = self.config.getSourceBase()
         m4base = self.config.getM4Base()
@@ -389,7 +379,7 @@ class GLImport(object):
         # Create command-line invocation comment.
         actioncmd = 'gnulib-tool --import'
         actioncmd += ' --dir=%s' % destdir
-        if localdir:
+        for localdir in localpath:
             actioncmd += ' --local-dir=%s' % localdir
         actioncmd += ' --lib=%s' % libname
         actioncmd += ' --source-base=%s' % sourcebase
@@ -443,17 +433,49 @@ class GLImport(object):
         actioncmd += ' '.join(modules)
         return actioncmd
 
+    def relative_to_destdir(self, dir):
+        '''GLImport.relative_to_destdir(dir) -> str
+
+        Convert a filename that represents dir, relative to the current directory,
+        to a filename relative to destdir.
+        GLConfig: destdir.'''
+        destdir = self.config['destdir']
+        if dir.startswith('/'):
+            return dir
+        else:
+            if destdir.startswith('/'):
+                # XXX This doesn't look right.
+                return dir
+            else:
+                return constants.relativize(destdir, dir)
+
+    def relative_to_currdir(self, dir):
+        '''GLImport.relative_to_currdir(dir) -> str
+
+        The opposite of GLImport.relative_to_destdir:
+        Convert a filename that represents dir, relative to destdir,
+        to a filename relative to the current directory.
+        GLConfig: destdir.'''
+        destdir = self.config['destdir']
+        if dir.startswith('/'):
+            return dir
+        else:
+            if destdir.startswith('/'):
+                # XXX This doesn't look right.
+                return joinpath(destdir, dir)
+            else:
+                return constants.relconcat(destdir, dir)
+
     def gnulib_cache(self):
         '''GLImport.gnulib_cache() -> str
 
         Emit the contents of generated $m4base/gnulib-cache.m4 file.
-        GLConfig: destdir, localdir, tests, sourcebase, m4base, pobase, docbase,
+        GLConfig: destdir, localpath, tests, sourcebase, m4base, pobase, docbase,
         testsbase, conddeps, libtool, macro_prefix, podomain, vc_files.'''
         emit = ''
         moduletable = self.moduletable
         actioncmd = self.actioncmd()
-        destdir = self.config['destdir']
-        localdir = self.config['localdir']
+        localpath = self.config['localpath']
         testflags = list(self.config['testflags'])
         sourcebase = self.config['sourcebase']
         m4base = self.config['m4base']
@@ -482,13 +504,11 @@ class GLImport(object):
 # Specification in the form of a command-line invocation:
 #   %s
 
-# Specification in the form of a few \
-gnulib-tool.m4 macro invocations:\n''' % actioncmd
-        if not localdir or localdir.startswith('/'):
-            relative_localdir = localdir
-        else:  # if localdir or not localdir.startswith('/')
-            relative_localdir = constants.relativize(destdir, localdir)
-        emit += 'gl_LOCAL_DIR([%s])\n' % relative_localdir
+# Specification in the form of a few gnulib-tool.m4 macro invocations:\n''' % actioncmd
+        # Store the localpath relative to destdir.
+        relative_localpath = [ self.relative_to_destdir(localdir)
+                               for localdir in localpath ]
+        emit += 'gl_LOCAL_DIR([%s])\n' % ':'.join(relative_localpath)
         emit += 'gl_MODULES([\n'
         emit += '  %s\n' % '\n  '.join(modules)
         emit += '])\n'
@@ -532,13 +552,12 @@ gnulib-tool.m4 macro invocations:\n''' % actioncmd
         '''GLImport.gnulib_comp(files) -> str
 
         Emit the contents of generated $m4base/gnulib-comp.m4 file.
-        GLConfig: destdir, localdir, tests, sourcebase, m4base, pobase, docbase,
+        GLConfig: destdir, localpath, tests, sourcebase, m4base, pobase, docbase,
         testsbase, conddeps, libtool, macro_prefix, podomain, vc_files.'''
         emit = ''
         assistant = self.assistant
         moduletable = self.moduletable
         destdir = self.config['destdir']
-        localdir = self.config['localdir']
         auxdir = self.config['auxdir']
         testflags = list(self.config['testflags'])
         sourcebase = self.config['sourcebase']
@@ -740,7 +759,6 @@ AC_DEFUN([%s_FILE_LIST], [\n''' % macro_prefix
         '''Make all preparations before the execution of the code.
         Returns filetable and sed transformers, which change the license.'''
         destdir = self.config['destdir']
-        localdir = self.config['localdir']
         auxdir = self.config['auxdir']
         modules = list(self.config['modules'])
         avoids = list(self.config['avoids'])
@@ -963,7 +981,6 @@ AC_DEFUN([%s_FILE_LIST], [\n''' % macro_prefix
             if key not in filetable:
                 raise KeyError('filetable must contain key %s' % repr(key))
         destdir = self.config['destdir']
-        localdir = self.config['localdir']
         auxdir = self.config['auxdir']
         modules = list(self.config['modules'])
         avoids = list(self.config['avoids'])
diff --git a/pygnulib/GLModuleSystem.py b/pygnulib/GLModuleSystem.py
index 48515ce36d..00b2c5f035 100644
--- a/pygnulib/GLModuleSystem.py
+++ b/pygnulib/GLModuleSystem.py
@@ -60,7 +60,7 @@ class GLModuleSystem(object):
         '''GLModuleSystem.__init__(config) -> GLModuleSystem
 
         Create new GLModuleSystem instance. Some functions use GLFileSystem class
-        to look up a file in localdir or gnulib directories, or combine it through
+        to look up a file in localpath or gnulib directories, or combine it through
         'patch' utility.'''
         self.args = dict()
         if type(config) is not GLConfig:
@@ -78,22 +78,22 @@ class GLModuleSystem(object):
         '''GLModuleSystem.exists(module) -> bool
 
         Check whether the given module exists.
-        GLConfig: localdir.'''
+        GLConfig: localpath.'''
         if type(module) is not str:
             raise TypeError(
                 'module must be a string, not %s' % type(module).__name__)
-        result = bool()
+        localpath = self.config['localpath']
+        result = False
         badnames = ['ChangeLog', 'COPYING', 'README', 'TEMPLATE',
                     'TEMPLATE-EXTENDED', 'TEMPLATE-TESTS']
-        if isfile(joinpath(DIRS['modules'], module)) or \
-                all([  # Begin all(iterable) function
-                    self.config['localdir'],
-                    isdir(joinpath(self.config['localdir'], 'modules')),
-                    isfile(
-                        joinpath(self.config['localdir'], 'modules', module))
-                ]):  # Close all(iterable) function
-            if module not in badnames:
-                result = True
+        if module not in badnames:
+            result = isfile(joinpath(DIRS['modules'], module))
+            if not result:
+                for localdir in localpath:
+                    if isdir(joinpath(localdir, 'modules')) \
+                       and isfile(joinpath(localdir, 'modules', module)):
+                        result = True
+                        break
         return result
 
     def find(self, module):
@@ -122,7 +122,7 @@ class GLModuleSystem(object):
         complete, so this version uses subprocess to run shell commands.'''
         result = ''
         listing = list()
-        localdir = self.config['localdir']
+        localpath = self.config['localpath']
         find_args = ['find', 'modules', '-type', 'f', '-print']
         sed_args = \
             [
@@ -146,16 +146,18 @@ class GLModuleSystem(object):
         os.chdir(constants.DIRS['root'])
         find = sp.Popen(find_args, stdout=sp.PIPE)
         result += find.stdout.read().decode("UTF-8")
+        os.chdir(DIRS['cwd'])
 
-        # Read modules from local directory.
-        if localdir and isdir(joinpath(localdir, 'modules')):
-            os.chdir(localdir)
-            find = sp.Popen(find_args, stdout=sp.PIPE)
-            result += find.stdout.read().decode("UTF-8")
+        # Read modules from local directories.
+        if len(localpath) > 0:
+            for localdir in localpath:
+                os.chdir(localdir)
+                find = sp.Popen(find_args, stdout=sp.PIPE)
+                result += find.stdout.read().decode("UTF-8")
+                os.chdir(DIRS['cwd'])
             sed_args += ['-e', r's,\.diff$,,']
 
         # Save the list of the modules to file.
-        os.chdir(DIRS['cwd'])
         path = joinpath(self.config['tempdir'], 'list')
         with codecs.open(path, 'wb', 'UTF-8') as file:
             file.write(result)
@@ -538,8 +540,7 @@ Include:|Link:|License:|Maintainer:)'
         '''GLModule.getDependencies() -> list
 
         Return list of dependencies.
-        GLConfig: localdir.'''
-        localdir = self.config['localdir']
+        GLConfig: localpath.'''
         result = list()
         section = 'Depends-on:'
         if 'dependencies' not in self.cache:
@@ -871,7 +872,7 @@ class GLModuleTable(object):
         included in the final modules list. If testflags iterable is enabled, then
         don't add module which status is in the testflags. If conddeps are enabled,
         then store condition for each dependency if it has a condition.
-        The only necessary argument is localdir, which is needed just to create
+        The only necessary argument is localpath, which is needed just to create
         modulesystem instance to look for dependencies.'''
         self.avoids = list()  # Avoids
         self.dependers = dict()  # Dependencies
diff --git a/pygnulib/GLTestDir.py b/pygnulib/GLTestDir.py
index 79f67a8fdc..7a7f5c52fb 100644
--- a/pygnulib/GLTestDir.py
+++ b/pygnulib/GLTestDir.py
@@ -30,6 +30,7 @@ from .GLConfig import GLConfig
 from .GLModuleSystem import GLModule
 from .GLModuleSystem import GLModuleTable
 from .GLModuleSystem import GLModuleSystem
+from .GLFileSystem import CopyAction
 from .GLFileSystem import GLFileSystem
 from .GLFileSystem import GLFileAssistant
 from .GLMakefileTable import GLMakefileTable
@@ -143,7 +144,6 @@ class GLTestDir(object):
         '''GLTestDir.execute()
 
         Create a scratch package with the given modules.'''
-        localdir = self.config['localdir']
         auxdir = self.config['auxdir']
         testflags = list(self.config['testflags'])
         sourcebase = self.config['sourcebase']
@@ -154,8 +154,6 @@ class GLTestDir(object):
         libname = self.config['libname']
         libtool = self.config['libtool']
         witness_c_macro = self.config['witness_c_macro']
-        symbolic = self.config['symbolic']
-        lsymbolic = self.config['lsymbolic']
         single_configure = self.config['single_configure']
         include_guard_prefix = self.config['include_guard_prefix']
         macro_prefix = self.config['macro_prefix']
@@ -337,7 +335,7 @@ class GLTestDir(object):
             if flag:
                 shutil.copy(lookedup, destpath)
             else:  # if not flag
-                if symbolic or (lsymbolic and lookedup == joinpath(localdir, src)):
+                if self.filesystem.shouldLink(src, lookedup) == CopyAction.Symlink:
                     constants.link_relative(lookedup, destpath)
                 else:
                     shutil.copy(lookedup, destpath)
-- 
2.34.1

>From 6f4b37880b220a85735e22106516d721062205c4 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:42:31 +0200
Subject: [PATCH 04/16] gnulib-tool.py: Remove most short options.

* gnulib-tool.py (main): Reorder the list of options. Remove most short
options, for consistency with gnulib-tool.
---
 ChangeLog           |   4 ++
 gnulib-tool.py      | 134 ++++++++++++++++++++++----------------------
 gnulib-tool.py.TODO |   4 +-
 3 files changed, 75 insertions(+), 67 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 98438a58a4..14db3cc7e8 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Remove most short options.
+	* gnulib-tool.py (main): Reorder the list of options. Remove most short
+	options, for consistency with gnulib-tool.
+
 	gnulib-tool.py: Follow gnulib-tool changes, part 19.
 	Follow gnulib-tool changes
 	2015-12-09  Pavel Raiskup  <prais...@redhat.com>
diff --git a/gnulib-tool.py b/gnulib-tool.py
index 0b2f866920..c7ab100d78 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -95,147 +95,149 @@ def main():
         prog=constants.APP['name'],
         usage='gnulib-tool.py --help',
         add_help=False)
-    # help
-    parser.add_argument('-h', '--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')
+
+    # Here we list the options in the order they are listed in the --help output.
+
     # list
-    parser.add_argument('-l', '--list', '--lis',
+    parser.add_argument('--list',
                         dest='mode_list',
                         default=None,
                         action='store_true')
     # find
-    parser.add_argument('-f', '--find', '--fin', '--fi', '--f',
+    parser.add_argument('--find',
                         dest='mode_find',
                         default=None,
                         nargs='*')
     # import
-    parser.add_argument('-i', '--import',
+    parser.add_argument('--import',
                         dest='mode_import',
                         default=None,
                         nargs='*')
     # add-import
-    parser.add_argument('-a', '--add-import',
+    parser.add_argument('--add-import',
                         dest='mode_add_import',
                         default=None,
                         nargs='+')
     # remove-import
-    parser.add_argument('-r', '--remove-import',
+    parser.add_argument('--remove-import',
                         dest='mode_remove_import',
                         default=None,
                         nargs='+')
     # update
-    parser.add_argument('-u', '--update',
+    parser.add_argument('--update',
                         dest='mode_update',
                         default=None,
                         action='store_true')
     # create-testdir
-    parser.add_argument('-td', '--create-testdir',
+    parser.add_argument('--create-testdir',
                         dest='mode_create_testdir',
                         default=None,
                         nargs='*')
     # create-megatestdir
-    parser.add_argument('-mtd', '--create-megatestdir',
+    parser.add_argument('--create-megatestdir',
                         dest='mode_create_megatestdir',
                         default=None,
                         nargs='*')
     # test
-    parser.add_argument('-t', '--test',
+    parser.add_argument('--test',
                         dest='mode_test',
                         default=None,
                         nargs='*')
     # megatest
-    parser.add_argument('-mt', '--megatest', '--megates', '--megate', '--megat',
-                        '--mega', '--meg', '--me', '--m',
+    parser.add_argument('--megatest',
                         dest='mode_megatest',
                         default=None,
                         nargs='*')
-    # copy-file
-    parser.add_argument('-c', '--copy-file',
-                        dest='mode_copy_file',
-                        default=None,
-                        nargs='+')
     # extract-*
-    parser.add_argument('-xD', '--extract-description',
+    parser.add_argument('--extract-description',
                         dest='mode_xdescription',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xc', '--extract-comment',
+    parser.add_argument('--extract-comment',
                         dest='mode_xcomment',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xs', '--extract-status',
+    parser.add_argument('--extract-status',
                         dest='mode_xstatus',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xn', '--extract-notice',
+    parser.add_argument('--extract-notice',
                         dest='mode_xnotice',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xa', '--extract-applicability',
+    parser.add_argument('--extract-applicability',
                         dest='mode_xapplicability',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xf', '--extract-filelist',
+    parser.add_argument('--extract-filelist',
                         dest='mode_xfilelist',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xd', '--extract-dependencies',
+    parser.add_argument('--extract-dependencies',
                         dest='mode_xdependencies',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xac', '--extract-autoconf-snippet',
+    parser.add_argument('--extract-autoconf-snippet',
                         dest='mode_xautoconf',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xam', '--extract-automake-snippet',
+    parser.add_argument('--extract-automake-snippet',
                         dest='mode_xautomake',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xi', '--extract-include-directive',
+    parser.add_argument('--extract-include-directive',
                         dest='mode_xinclude',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xl', '--extract-link-directive',
+    parser.add_argument('--extract-link-directive',
                         dest='mode_xlink',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xL', '--extract-license',
+    parser.add_argument('--extract-license',
                         dest='mode_xlicense',
                         default=None,
                         nargs='*')
-    parser.add_argument('-xm', '--extract-maintainer',
+    parser.add_argument('--extract-maintainer',
                         dest='mode_xmaintainer',
                         default=None,
                         nargs='*')
+    # copy-file
+    parser.add_argument('--copy-file',
+                        dest='mode_copy_file',
+                        default=None,
+                        nargs='+')
+    # 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('-d', '--dir',
+    parser.add_argument('--dir',
                         dest='destdir',
                         default=None,
                         nargs=1)
     # localpath
-    parser.add_argument('-ld', '--local-dir',
+    parser.add_argument('--local-dir',
                         action='append',
                         dest='localpath',
                         default=None,
                         nargs='?')
     # verbose
-    parser.add_argument('-v', '--verbose',
+    parser.add_argument('--verbose',
                         default=0,
                         action='count')
     # quiet
-    parser.add_argument('-q', '--quiet',
+    parser.add_argument('--quiet',
                         default=0,
                         action='count')
     # dryrun
@@ -248,11 +250,6 @@ def main():
                         dest='inctests',
                         default=None,
                         action='store_true')
-    # makefile
-    parser.add_argument("--makefile-name",
-                        dest="makefile",
-                        default=None,
-                        type=str)
     # obsolete
     parser.add_argument('--with-obsolete',
                         dest='obsolete',
@@ -283,57 +280,62 @@ def main():
                         dest='alltests',
                         default=None,
                         action='store_true')
-    # auxdir
-    parser.add_argument('--aux-dir',
-                        dest='auxdir',
-                        default=None,
-                        nargs=1)
-    # libname
-    parser.add_argument('--lib',
-                        dest='libname',
+    # avoids
+    parser.add_argument('--avoid',
+                        dest='avoids',
                         default=None,
-                        nargs=1)
+                        nargs='+')
     # libtool
     parser.add_argument("--libtool",
                         dest=libtool,
                         default=False,
                         action="store_true")
+    # libname
+    parser.add_argument('--lib',
+                        dest='libname',
+                        default=None,
+                        nargs=1)
     # sourcebase
-    parser.add_argument('-sb', '--source-base',
+    parser.add_argument('--source-base',
                         dest='sourcebase',
                         default=None,
                         nargs=1)
     # m4base
-    parser.add_argument('-mb', '--m4-base',
+    parser.add_argument('--m4-base',
                         dest='m4base',
                         default=None,
                         nargs=1)
     # pobase
-    parser.add_argument('-pb', '--po-base',
+    parser.add_argument('--po-base',
                         dest='pobase',
                         default=None,
                         nargs=1)
     # docbase
-    parser.add_argument('-db', '--doc-base',
+    parser.add_argument('--doc-base',
                         dest='docbase',
                         default=None,
                         nargs=1)
     # testsbase
-    parser.add_argument('-tb', '--tests-base',
+    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=False,
                         type=int,
                         nargs='?')
-    # avoids
-    parser.add_argument('--avoid',
-                        dest='avoids',
+    # makefile
+    parser.add_argument("--makefile-name",
+                        dest="makefile",
                         default=None,
-                        nargs='+')
+                        type=str)
 
     # Parse the given arguments.
     cmdargs = parser.parse_args()
diff --git a/gnulib-tool.py.TODO b/gnulib-tool.py.TODO
index 26c59c5353..eeecd5ed8a 100644
--- a/gnulib-tool.py.TODO
+++ b/gnulib-tool.py.TODO
@@ -17,6 +17,9 @@ The following commits to gnulib-tool have not yet been reflected in
 
 Implement the options:
   --find
+  --extract-recursive-dependencies
+  --extract-recursive-link-directive
+  --extract-tests-module
   --without-tests
   --without-c++-tests
   --without-longrunning-tests
@@ -34,7 +37,6 @@ Implement the options:
   --witness-c-macro
   --vc-files
   --no-vc-files
-  --no-changelog
   -s | --symbolic
   --local-symlink
   -h | --hardlink
-- 
2.34.1

>From 622cef78a7dcbd27c426f09848d0abfbb53df843 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:44:56 +0200
Subject: [PATCH 05/16] gnulib-tool.py: Emit error message when conflicting
 modes are specified.

* gnulib-tool.py (main): Fix test of conflicting modes. (Some options
produce a value of [], and as a condition, [] evaluates to False.)
---
 ChangeLog      | 4 ++++
 gnulib-tool.py | 4 ++--
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 14db3cc7e8..3c271bd4de 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Emit error message when conflicting modes are specified.
+	* gnulib-tool.py (main): Fix test of conflicting modes. (Some options
+	produce a value of [], and as a condition, [] evaluates to False.)
+
 	gnulib-tool.py: Remove most short options.
 	* gnulib-tool.py (main): Reorder the list of options. Remove most short
 	options, for consistency with gnulib-tool.
diff --git a/gnulib-tool.py b/gnulib-tool.py
index c7ab100d78..b16becb1d2 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -351,7 +351,6 @@ def main():
         cmdargs.mode_create_megatestdir,
         cmdargs.mode_test,
         cmdargs.mode_megatest,
-        cmdargs.mode_copy_file,
         cmdargs.mode_xdescription,
         cmdargs.mode_xcomment,
         cmdargs.mode_xstatus,
@@ -365,8 +364,9 @@ def main():
         cmdargs.mode_xlink,
         cmdargs.mode_xlicense,
         cmdargs.mode_xmaintainer,
+        cmdargs.mode_copy_file,
     ]
-    overflow = [arg for arg in args if arg]
+    overflow = [arg for arg in args if arg != None]
     if len(overflow) > 1:
         message = 'gnulib-tool: Unable to combine different modes of work.\n'
         message += 'Try \'gnulib-tool --help\' for more information.\n'
-- 
2.34.1

>From 56a625b004c134b41a92fc658871df2f954fcafe Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:48:25 +0200
Subject: [PATCH 06/16] gnulib-tool.py: Improve compliance with GNU standards.

* gnulib-tool.py (main): Handle --help and --version before testing for
conflicting modes.
---
 ChangeLog      |  4 ++++
 gnulib-tool.py | 20 +++++++++++---------
 2 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 3c271bd4de..f5b9c4e968 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Improve compliance with GNU standards.
+	* gnulib-tool.py (main): Handle --help and --version before testing for
+	conflicting modes.
+
 	gnulib-tool.py: Emit error message when conflicting modes are specified.
 	* gnulib-tool.py (main): Fix test of conflicting modes. (Some options
 	produce a value of [], and as a condition, [] evaluates to False.)
diff --git a/gnulib-tool.py b/gnulib-tool.py
index b16becb1d2..a6d4f6fa75 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -340,6 +340,17 @@ def main():
     # Parse the given arguments.
     cmdargs = parser.parse_args()
 
+    # Handle --help and --version, ignoring all other options.
+    if cmdargs.help != None:
+        print(info.usage())
+        sys.exit(0)
+    if cmdargs.version != None:
+        message = '''gnulib-tool (%s %s)%s\n%s\n%s\n\nWritten by %s.''' % \
+            (info.package(), info.date(), info.version(), info.copyright(),
+             info.license(), info.authors())
+        print(message)
+        sys.exit(0)
+
     # Determine when user tries to combine modes.
     args = [
         cmdargs.mode_list,
@@ -374,15 +385,6 @@ def main():
         sys.exit(1)
 
     # Determine selected mode.
-    if cmdargs.help != None:
-        print(info.usage())
-        sys.exit(0)
-    if cmdargs.version != None:
-        message = '''gnulib-tool (%s %s)%s\n%s\n%s\n\nWritten by %s.''' % \
-            (info.package(), info.date(), info.version(), info.copyright(),
-             info.license(), info.authors())
-        print(message)
-        sys.exit(0)
     if cmdargs.mode_list != None:
         mode = 'list'
     if cmdargs.mode_import != None:
-- 
2.34.1

>From 1d41c75835c08eee63692dd0c25062d05a25d538 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:51:17 +0200
Subject: [PATCH 07/16] gnulib-tool.py: Make option processing more similar to
 gnulib-tool.

* gnulib-tool.py (main): Allow --add-import and --remove-import with 0
modules.
---
 ChangeLog      | 4 ++++
 gnulib-tool.py | 4 ++--
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index f5b9c4e968..178b3986b1 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Make option processing more similar to gnulib-tool.
+	* gnulib-tool.py (main): Allow --add-import and --remove-import with 0
+	modules.
+
 	gnulib-tool.py: Improve compliance with GNU standards.
 	* gnulib-tool.py (main): Handle --help and --version before testing for
 	conflicting modes.
diff --git a/gnulib-tool.py b/gnulib-tool.py
index a6d4f6fa75..166a4ae56d 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -117,12 +117,12 @@ def main():
     parser.add_argument('--add-import',
                         dest='mode_add_import',
                         default=None,
-                        nargs='+')
+                        nargs='*')
     # remove-import
     parser.add_argument('--remove-import',
                         dest='mode_remove_import',
                         default=None,
-                        nargs='+')
+                        nargs='*')
     # update
     parser.add_argument('--update',
                         dest='mode_update',
-- 
2.34.1

>From 591c99e6b9b5ccd4eeb2a067c1102b7164cd2304 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:53:24 +0200
Subject: [PATCH 08/16] gnulib-tool: Clarify that --test allows zero module
 arguments.

* gnulib-tool (func_usage): Mark the modules for --test as optional.
* pygnulib/GLInfo.py (GLInfo.usage): Likewise.
---
 ChangeLog          | 4 ++++
 gnulib-tool        | 2 +-
 pygnulib/GLInfo.py | 2 +-
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 178b3986b1..caf9747ac3 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool: Clarify that --test allows zero module arguments.
+	* gnulib-tool (func_usage): Mark the modules for --test as optional.
+	* pygnulib/GLInfo.py (GLInfo.usage): Likewise.
+
 	gnulib-tool.py: Make option processing more similar to gnulib-tool.
 	* gnulib-tool.py (main): Allow --add-import and --remove-import with 0
 	modules.
diff --git a/gnulib-tool b/gnulib-tool
index f1665f4002..6c50b44fbc 100755
--- a/gnulib-tool
+++ b/gnulib-tool
@@ -155,7 +155,7 @@ Usage: gnulib-tool --list
        gnulib-tool --update
        gnulib-tool --create-testdir --dir=directory [module1 ... moduleN]
        gnulib-tool --create-megatestdir --dir=directory [module1 ... moduleN]
-       gnulib-tool --test --dir=directory module1 ... moduleN
+       gnulib-tool --test --dir=directory [module1 ... moduleN]
        gnulib-tool --megatest --dir=directory [module1 ... moduleN]
        gnulib-tool --extract-description module
        gnulib-tool --extract-comment module
diff --git a/pygnulib/GLInfo.py b/pygnulib/GLInfo.py
index f66e469670..79c1ba0cf1 100644
--- a/pygnulib/GLInfo.py
+++ b/pygnulib/GLInfo.py
@@ -119,7 +119,7 @@ Usage: gnulib-tool --list
        gnulib-tool --update
        gnulib-tool --create-testdir --dir=directory [module1 ... moduleN]
        gnulib-tool --create-megatestdir --dir=directory [module1 ... moduleN]
-       gnulib-tool --test --dir=directory module1 ... moduleN
+       gnulib-tool --test --dir=directory [module1 ... moduleN]
        gnulib-tool --megatest --dir=directory [module1 ... moduleN]
        gnulib-tool --extract-description module
        gnulib-tool --extract-comment module
-- 
2.34.1

>From cfb5e7537c7725da01f4854eba605006bdff0468 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:54:22 +0200
Subject: [PATCH 09/16] gnulib-tool.py: Make --test behaviour more similar to
 gnulib-tool.

* gnulib-tool.py (main) [test]: Remove space from the testdir name.
---
 ChangeLog      | 3 +++
 gnulib-tool.py | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/ChangeLog b/ChangeLog
index caf9747ac3..bff168a7a4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,8 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Make --test behaviour more similar to gnulib-tool.
+	* gnulib-tool.py (main) [test]: Remove space from the testdir name.
+
 	gnulib-tool: Clarify that --test allows zero module arguments.
 	* gnulib-tool (func_usage): Mark the modules for --test as optional.
 	* pygnulib/GLInfo.py (GLInfo.usage): Likewise.
diff --git a/gnulib-tool.py b/gnulib-tool.py
index 166a4ae56d..a646ae2d38 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -702,7 +702,7 @@ def main():
 
     elif mode == 'test':
         if not destdir:
-            destdir = 'testdir %04d' % random.randrange(0, 9999)
+            destdir = 'testdir%04d' % random.randrange(0, 9999)
         if not auxdir:
             auxdir = 'build-aux'
         config.setAuxDir(auxdir)
-- 
2.34.1

>From f1b0f11bc8a0e18f34b33fbb7955293715b695ee Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 18:56:05 +0200
Subject: [PATCH 10/16] gnulib-tool.py: Allow module arguments to occur at any
 position.

* gnulib-tool.py (main): Collect the non-option arguments in a single
list, regardless of their position. Use parse_known_args instead of
parse_args, and give an error message about unknown options ourselves.
* gnulib-tool: Fix typo in error message.
---
 ChangeLog      |   6 +++
 gnulib-tool    |   2 +-
 gnulib-tool.py | 128 ++++++++++++++++++++++++++++++-------------------
 3 files changed, 86 insertions(+), 50 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index bff168a7a4..a48f89db6d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,11 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Allow module arguments to occur at any position.
+	* gnulib-tool.py (main): Collect the non-option arguments in a single
+	list, regardless of their position. Use parse_known_args instead of
+	parse_args, and give an error message about unknown options ourselves.
+	* gnulib-tool: Fix typo in error message.
+
 	gnulib-tool.py: Make --test behaviour more similar to gnulib-tool.
 	* gnulib-tool.py (main) [test]: Remove space from the testdir name.
 
diff --git a/gnulib-tool b/gnulib-tool
index 6c50b44fbc..ac53480dbf 100755
--- a/gnulib-tool
+++ b/gnulib-tool
@@ -1518,7 +1518,7 @@ func_determine_path_separator
       echo "gnulib-tool: too many arguments in 'update' mode" 1>&2
       echo "Try 'gnulib-tool --help' for more information." 1>&2
       echo "If you really want to modify the gnulib configuration of your project," 1>&2
-      echo "you need to use 'gnulib --import' - at your own risk!" 1>&2
+      echo "you need to use 'gnulib-tool --import' - at your own risk!" 1>&2
       func_exit 1
     fi
     if test -n "$local_gnulib_path" || test -n "$supplied_libname" \
diff --git a/gnulib-tool.py b/gnulib-tool.py
index a646ae2d38..f1cf389c36 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -107,22 +107,22 @@ def main():
     parser.add_argument('--find',
                         dest='mode_find',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # import
     parser.add_argument('--import',
                         dest='mode_import',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # add-import
     parser.add_argument('--add-import',
                         dest='mode_add_import',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # remove-import
     parser.add_argument('--remove-import',
                         dest='mode_remove_import',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # update
     parser.add_argument('--update',
                         dest='mode_update',
@@ -132,80 +132,80 @@ def main():
     parser.add_argument('--create-testdir',
                         dest='mode_create_testdir',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # create-megatestdir
     parser.add_argument('--create-megatestdir',
                         dest='mode_create_megatestdir',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # test
     parser.add_argument('--test',
                         dest='mode_test',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # megatest
     parser.add_argument('--megatest',
                         dest='mode_megatest',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # extract-*
     parser.add_argument('--extract-description',
                         dest='mode_xdescription',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-comment',
                         dest='mode_xcomment',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-status',
                         dest='mode_xstatus',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-notice',
                         dest='mode_xnotice',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-applicability',
                         dest='mode_xapplicability',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-filelist',
                         dest='mode_xfilelist',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-dependencies',
                         dest='mode_xdependencies',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-autoconf-snippet',
                         dest='mode_xautoconf',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-automake-snippet',
                         dest='mode_xautomake',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-include-directive',
                         dest='mode_xinclude',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-link-directive',
                         dest='mode_xlink',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-license',
                         dest='mode_xlicense',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     parser.add_argument('--extract-maintainer',
                         dest='mode_xmaintainer',
                         default=None,
-                        nargs='*')
+                        action='store_true')
     # copy-file
     parser.add_argument('--copy-file',
                         dest='mode_copy_file',
                         default=None,
-                        nargs='+')
+                        action='store_true')
     # help
     parser.add_argument('--help', '--hel', '--he', '--h',
                         dest='help',
@@ -231,7 +231,7 @@ def main():
                         action='append',
                         dest='localpath',
                         default=None,
-                        nargs='?')
+                        nargs=1)
     # verbose
     parser.add_argument('--verbose',
                         default=0,
@@ -282,9 +282,10 @@ def main():
                         action='store_true')
     # avoids
     parser.add_argument('--avoid',
+                        action='append',
                         dest='avoids',
                         default=None,
-                        nargs='+')
+                        nargs=1)
     # libtool
     parser.add_argument("--libtool",
                         dest=libtool,
@@ -336,9 +337,13 @@ def main():
                         dest="makefile",
                         default=None,
                         type=str)
+    # All other arguments are collected.
+    parser.add_argument("non_option_arguments",
+                        nargs='*')
 
-    # Parse the given arguments.
-    cmdargs = parser.parse_args()
+    # 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:
@@ -351,6 +356,16 @@ def main():
         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,
@@ -379,7 +394,7 @@ def main():
     ]
     overflow = [arg for arg in args if arg != None]
     if len(overflow) > 1:
-        message = 'gnulib-tool: Unable to combine different modes of work.\n'
+        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)
@@ -389,81 +404,92 @@ def main():
         mode = 'list'
     if cmdargs.mode_import != None:
         mode = 'import'
-        modules = list(cmdargs.mode_import)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_add_import != None:
         mode = 'add-import'
-        modules = list(cmdargs.mode_add_import)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_remove_import != None:
         mode = 'remove-import'
-        modules = list(cmdargs.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.mode_create_testdir)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_create_megatestdir != None:
         mode = 'create-megatestdir'
-        modules = list(cmdargs.mode_create_megatestdir)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_test != None:
         mode = 'test'
-        modules = list(cmdargs.mode_test)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_megatest != None:
         mode = 'megatest'
-        modules = list(cmdargs.mode_megatest)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xdescription != None:
         mode = 'extract-description'
-        modules = list(cmdargs.mode_xdescription)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xcomment != None:
         mode = 'extract-comment'
-        modules = list(cmdargs.mode_xcomment)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xstatus != None:
         mode = 'extract-status'
-        modules = list(cmdargs.mode_xstatus)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xnotice != None:
         mode = 'extract-notice'
-        modules = list(cmdargs.mode_xnotice)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xapplicability != None:
         mode = 'extract-applicability'
-        modules = list(cmdargs.mode_xapplicability)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xfilelist != None:
         mode = 'extract-filelist'
-        modules = list(cmdargs.mode_xfilelist)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xautoconf != None:
         mode = 'extract-autoconf-snippet'
-        modules = list(cmdargs.mode_xautoconf)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xautomake != None:
         mode = 'extract-automake-snippet'
-        modules = list(cmdargs.mode_xautomake)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xdependencies != None:
         mode = 'extract-dependencies'
-        modules = list(cmdargs.mode_xdependencies)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xinclude != None:
         mode = 'extract-include-directive'
-        modules = list(cmdargs.mode_xinclude)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xlink != None:
         mode = 'extract-link-directive'
-        modules = list(cmdargs.mode_xlink)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xlicense != None:
         mode = 'extract-license'
-        modules = list(cmdargs.mode_xlicense)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_xmaintainer != None:
         mode = 'extract-maintainer'
-        modules = list(cmdargs.mode_xmaintainer)
+        modules = list(cmdargs.non_option_arguments)
     if cmdargs.mode_copy_file != None:
         mode = 'copy-file'
-        if len(cmdargs.mode_copy_file) > 2:
+        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' % mode
             message += '\n%s: *** Exit.\n' % constants.APP['name']
             sys.stderr.write(message)
             sys.exit(1)
-        files = list(cmdargs.mode_copy_file)
+        files = list(cmdargs.non_option_arguments)
 
     # 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]
@@ -513,6 +539,10 @@ def main():
     if lgpl == None:
         lgpl = True
     avoids = cmdargs.avoids
+    if avoids != None:
+        avoids = [ module
+                   for list1 in avoids
+                   for module in list1 ]
 
     # Create pygnulib configuration.
     config = classes.GLConfig(
-- 
2.34.1

>From 96f7bad09085273649f4b26a25e9d8fd51279c32 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 20:02:40 +0200
Subject: [PATCH 11/16] gnulib-tool.py: Make --copy-file work.

* gnulib-tool.py (main) [copy-file]: Fix reference to uninitialized
variable. Fix error handling of os.makedirs. Pass the destdir to the
GLFileAssistant.
---
 ChangeLog      |  5 +++++
 gnulib-tool.py | 34 +++++++++++++++-------------------
 2 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index a48f89db6d..59bfec205e 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Make --copy-file work.
+	* gnulib-tool.py (main) [copy-file]: Fix reference to uninitialized
+	variable. Fix error handling of os.makedirs. Pass the destdir to the
+	GLFileAssistant.
+
 	gnulib-tool.py: Allow module arguments to occur at any position.
 	* gnulib-tool.py (main): Collect the non-option arguments in a single
 	list, regardless of their position. Use parse_known_args instead of
diff --git a/gnulib-tool.py b/gnulib-tool.py
index f1cf389c36..f20d1157a3 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -892,9 +892,11 @@ def main():
 
     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
+        else:  # if len(files) < 2
             dest = '.'
         if not auxdir:
             auxdir = 'build-aux'
@@ -914,53 +916,47 @@ def main():
         filesystem = classes.GLFileSystem(config)
         lookedup, flag = filesystem.lookup(srcpath)
         if isdir(dest):
-            destdir = str(dest)
+            destdir = dest
             if srcpath.startswith('build-aux/'):
-                destpath = constants.substart(
-                    'build-aux/', '%s/' % auxdir, srcpath)
+                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)
+                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('tests=lib/'):
-                destpath = constants.substart(
-                    'tests=lib/', '%s/' % testsbase, srcpath)
+                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(destpath)
-            destdir = os.path.dirname(destpath)
-            destpath = os.path.basename(destpath)
+        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 Exception as error:
+                except FileExistsError:
                     pass
         # Copy the file.
         assistant = classes.GLFileAssistant(config)
         tmpfile = assistant.tmpfilename(destpath)
         shutil.copy(lookedup, tmpfile)
-        already_present = True
         assistant.setOriginal(srcpath)
+        assistant.config.setDestDir(destdir)
         assistant.setRewritten(destpath)
         if isfile(joinpath(destdir, destpath)):
             # The file already exists.
-            assistant.update(lookedup, flag, tmpfile, already_present)
+            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.
+            # frequently that developers don't put autogenerated files under
+            # version control.
             assistant.add(lookedup, flag, tmpfile)
         if isfile(tmpfile):
             os.remove(tmpfile)
-- 
2.34.1

>From ba5620880ac965d1ecd302b77dd90bfd8c7bf682 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 21:08:55 +0200
Subject: [PATCH 12/16] gnulib-tool.py: Implement options --symlink and
 --local-symlink.

* gnulib-tool.py (main): Handle options --symlink and --local-symlink.
* pygnulib/constants.py (link_if_changed): Ignore FileNotFoundError from
os.remove call.
---
 ChangeLog             |  5 +++++
 gnulib-tool.py        | 14 ++++++++++++++
 pygnulib/constants.py |  5 ++++-
 3 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/ChangeLog b/ChangeLog
index 59bfec205e..0ba5352039 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Implement options --symlink and --local-symlink.
+	* gnulib-tool.py (main): Handle options --symlink and --local-symlink.
+	* pygnulib/constants.py (link_if_changed): Ignore FileNotFoundError from
+	os.remove call.
+
 	gnulib-tool.py: Make --copy-file work.
 	* gnulib-tool.py (main) [copy-file]: Fix reference to uninitialized
 	variable. Fix error handling of os.makedirs. Pass the destdir to the
diff --git a/gnulib-tool.py b/gnulib-tool.py
index f20d1157a3..f936a92031 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -337,6 +337,16 @@ def main():
                         dest="makefile",
                         default=None,
                         type=str)
+    # 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='*')
@@ -543,6 +553,8 @@ def main():
         avoids = [ module
                    for list1 in avoids
                    for module in list1 ]
+    symlink = cmdargs.symlink == True
+    lsymlink = cmdargs.lsymlink == True
 
     # Create pygnulib configuration.
     config = classes.GLConfig(
@@ -566,6 +578,8 @@ def main():
         podomain=podomain,
         witness_c_macro=witness_c_macro,
         vc_files=vc_files,
+        symbolic=symlink,
+        lsymbolic=lsymlink,
         modcache=modcache,
         verbose=verbose,
         dryrun=dryrun,
diff --git a/pygnulib/constants.py b/pygnulib/constants.py
index 9ef2e01089..10bd363f5b 100644
--- a/pygnulib/constants.py
+++ b/pygnulib/constants.py
@@ -330,7 +330,10 @@ def link_if_changed(src, dest):
     '''Create a symlink, but avoids munging timestamps if the link is correct.'''
     ln_target = os.path.realpath(src)
     if not (os.path.islink(dest) and src == ln_target):
-        os.remove(dest)
+        try:
+            os.remove(dest)
+        except FileNotFoundError:
+            pass
         link_relative(src, dest)
 
 
-- 
2.34.1

>From c35a6f89c420c68bd25520360f111067b3d27cd5 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 22:29:15 +0200
Subject: [PATCH 13/16] gnulib-tool.py: Use mainstream coding style.

* pygnulib/GLConfig.py: Use 'raise' to re-throw an exception.
---
 ChangeLog            |  3 +++
 pygnulib/GLConfig.py | 12 ++++++------
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 0ba5352039..bc4a09088c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,8 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Use mainstream coding style.
+	* pygnulib/GLConfig.py: Use 'raise' to re-throw an exception.
+
 	gnulib-tool.py: Implement options --symlink and --local-symlink.
 	* gnulib-tool.py (main): Handle options --symlink and --local-symlink.
 	* pygnulib/constants.py (link_if_changed): Ignore FileNotFoundError from
diff --git a/pygnulib/GLConfig.py b/pygnulib/GLConfig.py
index 55b6cd2986..dc0cce3490 100644
--- a/pygnulib/GLConfig.py
+++ b/pygnulib/GLConfig.py
@@ -579,9 +579,9 @@ class GLConfig(object):
                 except TypeError as error:
                     self.table['modules'] = old_modules
                     raise TypeError('each module must be a string')
-                except GLError as error:
+                except GLError:
                     self.table['modules'] = old_modules
-                    raise GLError(error.errno, error.errinfo)
+                    raise
         else:  # if type of modules is not list or tuple
             raise TypeError('modules must be a list or a tuple, not %s' %
                             type(modules).__name__)
@@ -625,9 +625,9 @@ class GLConfig(object):
                 except TypeError as error:
                     self.table['avoids'] = old_avoids
                     raise TypeError('each module must be a string')
-                except GLError as error:
+                except GLError:
                     self.table['avoids'] = old_avoids
-                    raise GLError(error.errno, error.errinfo)
+                    raise
         else:  # if type of modules is not list or tuple
             raise TypeError('modules must be a list or a tuple, not %s' %
                             type(modules).__name__)
@@ -670,9 +670,9 @@ class GLConfig(object):
                 except TypeError as error:
                     self.table['files'] = old_files
                     raise TypeError('each file must be a string')
-                except GLError as error:
+                except GLError:
                     self.table['files'] = old_files
-                    raise GLError(error.errno, error.errinfo)
+                    raise
         else:  # if type of files is not list or tuple
             raise TypeError('files must be a list or a tuple, not %s' %
                             type(files).__name__)
-- 
2.34.1

>From 07de10784c320267fabea1edfd8b3ce1e6b88f62 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 22:35:30 +0200
Subject: [PATCH 14/16] gnulib-tool.py: Write errors to stderr, not stdout.

* pygnulib/constants.py: Write error messages to stderr, not stdout.
---
 ChangeLog             | 3 +++
 pygnulib/constants.py | 4 ++--
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index bc4a09088c..b7d13bb414 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,8 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Write errors to stderr, not stdout.
+	* pygnulib/constants.py: Write error messages to stderr, not stdout.
+
 	gnulib-tool.py: Use mainstream coding style.
 	* pygnulib/GLConfig.py: Use 'raise' to re-throw an exception.
 
diff --git a/pygnulib/constants.py b/pygnulib/constants.py
index 10bd363f5b..91a2958eb3 100644
--- a/pygnulib/constants.py
+++ b/pygnulib/constants.py
@@ -213,7 +213,7 @@ def execute(args, verbose):
         try:  # Try to run
             retcode = sp.call(args)
         except Exception as error:
-            print(error)
+            sys.stderr.write(str(error) + '\n')
             sys.exit(1)
     else:
         # Commands like automake produce output to stderr even when they succeed.
@@ -223,7 +223,7 @@ def execute(args, verbose):
         try:  # Try to run
             retcode = sp.call(xargs, shell=True)
         except Exception as error:
-            print(error)
+            sys.stderr.write(str(error) + '\n')
             sys.exit(1)
         if retcode == 0:
             os.remove(temp)
-- 
2.34.1

>From eb6a04f6ef114c801ebd0c212fbb8822c4368f50 Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 23:02:58 +0200
Subject: [PATCH 15/16] gnulib-tool.py: Improve some error messages.

* gnulib-tool.py (main): Write "*** Stop." instead of "*** Exit.".
(__main__): Print an error message for GLError 5, 13, 14, 15, 16, 17, 18.
* pygnulib/GLError.py (GLError.__repr__): Compute one error message,
not 19.
---
 ChangeLog                  |  6 ++++
 gnulib-tool.py             | 39 +++++++++++---------
 pygnulib/GLError.py        | 74 ++++++++++++++++++++++----------------
 pygnulib/GLModuleSystem.py |  3 +-
 4 files changed, 72 insertions(+), 50 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index b7d13bb414..30805d4d73 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,11 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Improve some error messages.
+	* gnulib-tool.py (main): Write "*** Stop." instead of "*** Exit.".
+	(__main__): Print an error message for GLError 5, 13, 14, 15, 16, 17, 18.
+	* pygnulib/GLError.py (GLError.__repr__): Compute one error message,
+	not 19.
+
 	gnulib-tool.py: Write errors to stderr, not stdout.
 	* pygnulib/constants.py: Write error messages to stderr, not stdout.
 
diff --git a/gnulib-tool.py b/gnulib-tool.py
index f936a92031..302aaf3405 100755
--- a/gnulib-tool.py
+++ b/gnulib-tool.py
@@ -486,7 +486,7 @@ def main():
         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' % mode
-            message += '\n%s: *** Exit.\n' % constants.APP['name']
+            message += '\n%s: *** Stop.\n' % constants.APP['name']
             sys.stderr.write(message)
             sys.exit(1)
         files = list(cmdargs.non_option_arguments)
@@ -722,7 +722,7 @@ def main():
         if not destdir:
             message = '%s: *** ' % constants.APP['name']
             message += 'please specify --dir option'
-            message += '\n%s: *** Exit.\n' % constants.APP['name']
+            message += '\n%s: *** Stop.\n' % constants.APP['name']
             sys.stderr.write(message)
             sys.exit(1)
         if not auxdir:
@@ -735,7 +735,7 @@ def main():
         if not destdir:
             message = '%s: *** ' % constants.APP['name']
             message += 'please specify --dir option'
-            message += '\n%s: *** Exit.\n' % constants.APP['name']
+            message += '\n%s: *** Stop.\n' % constants.APP['name']
             sys.stderr.write(message)
             sys.exit(1)
         if not auxdir:
@@ -846,7 +846,7 @@ def main():
         if avoids:
             message = '%s: *** ' % constants.APP['name']
             message += 'cannot combine --avoid and --extract-dependencies\n'
-            message += '%s: *** Exit.\n' % constants.APP['name']
+            message += '%s: *** Stop.\n' % constants.APP['name']
             sys.stderr.write(message)
             sys.exit(1)
         modulesystem = classes.GLModuleSystem(config)
@@ -978,7 +978,7 @@ def main():
     else:
         message = '%s: *** ' % constants.APP['name']
         message += 'no mode specified'
-        message += '\n%s: *** Exit.\n' % constants.APP['name']
+        message += '\n%s: *** Stop.\n' % constants.APP['name']
         sys.stderr.write(message)
         sys.exit(1)
 
@@ -999,15 +999,13 @@ if __name__ == '__main__':
             elif errno == 2:
                 message += 'patch file %s didn\'t apply cleanly' % errinfo
             elif errno == 3:
-                message += 'cannot find %s - make sure ' % errinfo
-                message += 'you run gnulib-tool from within your package\'s directory'
+                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:
-                "%s is expected to contain gl_M4_BASE([%s])" % \
-                    (repr(os.path.join(errinfo, 'gnulib-comp.m4')), repr(errinfo))
+                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:
@@ -1020,8 +1018,7 @@ if __name__ == '__main__':
             elif errno == 9:
                 message += 'missing --lib option'
             elif errno == 10:
-                message = 'gnulib-tool: option --conditional-dependencies is not '
-                message += 'supported with --with-tests\n'
+                message = 'gnulib-tool: option --conditional-dependencies is not supported with --with-tests'
             elif errno == 11:
                 incompatibilities = ''
                 message += 'incompatible license on modules:%s' % constants.NL
@@ -1035,17 +1032,25 @@ if __name__ == '__main__':
                 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'])
+                incompatibilities = sp.check_output(args).decode(ENCS['default'])
                 message += incompatibilities
                 os.remove(tempname)
             elif errno == 12:
                 message += 'refusing to do nothing'
-            elif errno in [13, 14, 15, 16, 17]:
-                message += 'failed'
+            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
-            if errno != 10:
-                message += '\n%s: *** Exit.\n' % constants.APP['name']
+            message += '\n%s: *** Stop.\n' % constants.APP['name']
             sys.stderr.write(message)
             sys.exit(1)
diff --git a/pygnulib/GLError.py b/pygnulib/GLError.py
index a7cb81afe2..a366da5b1a 100644
--- a/pygnulib/GLError.py
+++ b/pygnulib/GLError.py
@@ -65,41 +65,53 @@ class GLError(Exception):
          17: cannot update the given file: <file>
          18: module lacks a license: <module>
          19: could not create destination directory: <directory>
-        errinfo: additional information;
-        style: 0 or 1, whether old-style'''
+        errinfo: additional information'''
         self.errno = errno
         self.errinfo = errinfo
         self.args = (self.errno, self.errinfo)
 
     def __repr__(self):
+        errno = self.errno
         errinfo = self.errinfo
-        errors = \
-            [  # Begin list of errors
-                "file does not exist in GLFileSystem: %s" % repr(errinfo),
-                "cannot patch file inside GLFileSystem: %s" % repr(errinfo),
-                "configure file does not exist: %s" % repr(errinfo),
-                "minimum supported autoconf version is 2.59, not %s" % repr(
-                    errinfo),
-                "%s is expected to contain gl_M4_BASE([%s])" % \
-                (repr(os.path.join(errinfo, 'gnulib-comp.m4')), repr(errinfo)),
-                "missing sourcebase argument; cache file doesn't contain it,"
-                + " so you might have to set this argument",
-                "missing docbase argument; you might have to create GLImport" \
-                + " instance with mode 0 and docbase argument",
-                "missing testsbase argument; cache file doesn't contain it,"
-                + " so you might have to set this argument"
-                "missing libname argument; cache file doesn't contain it,"
-                + " so you might have to set this argument",
-                "conddeps are not supported with inctests",
-                "incompatible licenses on modules: %s" % repr(errinfo),
-                "cannot process empty filelist",
-                "cannot create the given directory: %s" % repr(errinfo),
-                "cannot remove the given file: %s" % repr(errinfo),
-                "cannot create the given file: %s" % repr(errinfo),
-                "cannot transform the given file: %s" % repr(errinfo),
-                "cannot update/replace the given file: %s" % repr(errinfo),
-                "module lacks a license: %s" % repr(errinfo),
-                "error when running subprocess: %s" % repr(errinfo),
-            ]  # Complete list of errors
-        self.message = '[Errno %d] %s' % (self.errno, errors[self.errno - 1])
+        if self.message == None:
+            message = None
+            if errno == 1:
+                message = "file does not exist in GLFileSystem: %s" % repr(errinfo)
+            elif errno == 2:
+                message = "cannot patch file inside GLFileSystem: %s" % repr(errinfo)
+            elif errno == 3:
+                message = "configure file does not exist: %s" % repr(errinfo)
+            elif errno == 4:
+                message = "minimum supported autoconf version is 2.59, not %s" % repr(errinfo)
+            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 sourcebase argument; cache file doesn't contain it, so you might have to set this argument"
+            elif errno == 7:
+                message = "missing docbase argument; you might have to create GLImport instance with mode 0 and docbase argument"
+            elif errno == 8:
+                message = "missing testsbase argument; cache file doesn't contain it, so you might have to set this argument"
+            elif errno == 9:
+                message = "missing libname argument; cache file doesn't contain it, so you might have to set this argument"
+            elif errno == 10:
+                message = "conddeps are not supported with inctests"
+            elif errno == 11:
+                message = "incompatible licenses on modules: %s" % repr(errinfo)
+            elif errno == 12:
+                message = "cannot process empty filelist"
+            elif errno == 13:
+                message = "cannot create the given directory: %s" % repr(errinfo)
+            elif errno == 14:
+                message = "cannot remove the given file: %s" % repr(errinfo)
+            elif errno == 15:
+                message = "cannot create the given file: %s" % repr(errinfo)
+            elif errno == 16:
+                message = "cannot transform the given file: %s" % repr(errinfo)
+            elif errno == 17:
+                message = "cannot update/replace the given file: %s" % repr(errinfo)
+            elif errno == 18:
+                message = "module lacks a license: %s" % repr(errinfo)
+            elif errno == 19:
+                message = "error when running subprocess: %s" % repr(errinfo)
+            self.message = '[Errno %d] %s' % (errno, message)
         return self.message
diff --git a/pygnulib/GLModuleSystem.py b/pygnulib/GLModuleSystem.py
index 00b2c5f035..319175dee8 100644
--- a/pygnulib/GLModuleSystem.py
+++ b/pygnulib/GLModuleSystem.py
@@ -802,8 +802,7 @@ Include:|Link:|License:|Maintainer:)'
                 if self.config['errors']:
                     raise GLError(18, str(self))
                 else:  # if not self.config['errors']
-                    sys.stderr.write('gnulib-tool: warning: ')
-                    sys.stderr.write('module %s lacks a license\n' % str(self))
+                    sys.stderr.write('gnulib-tool: warning: module %s lacks a license\n' % str(self))
         if not license:
             license = 'GPL'
         return license
-- 
2.34.1

>From b299bfa3fb3cf4ab31b88e843cbe16583e98b21e Mon Sep 17 00:00:00 2001
From: Bruno Haible <br...@clisp.org>
Date: Sun, 31 Jul 2022 23:28:18 +0200
Subject: [PATCH 16/16] gnulib-tool.py: Fix typo.

* pygnulib/GLTestDir.py (GLMegaTestDir.execute): Invoke os.mkdir as
intended.
---
 ChangeLog             | 4 ++++
 pygnulib/GLTestDir.py | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/ChangeLog b/ChangeLog
index 30805d4d73..e60722ff35 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
 2022-07-31  Bruno Haible  <br...@clisp.org>
 
+	gnulib-tool.py: Fix typo.
+	* pygnulib/GLTestDir.py (GLMegaTestDir.execute): Invoke os.mkdir as
+	intended.
+
 	gnulib-tool.py: Improve some error messages.
 	* gnulib-tool.py (main): Write "*** Stop." instead of "*** Exit.".
 	(__main__): Print an error message for GLError 5, 13, 14, 15, 16, 17, 18.
diff --git a/pygnulib/GLTestDir.py b/pygnulib/GLTestDir.py
index 7a7f5c52fb..36aa5d2117 100644
--- a/pygnulib/GLTestDir.py
+++ b/pygnulib/GLTestDir.py
@@ -961,7 +961,7 @@ class GLMegaTestDir(object):
         constants.execute(args, verbose)
         try:  # Try to make a directory
             if not isdir('build-aux'):
-                os, mkdir('build-aux')
+                os.mkdir('build-aux')
         except Exception as error:
             pass
         args = [UTILS['autoconf']]
-- 
2.34.1

Reply via email to