Stefan Schwarzer committed 34fde55

If the server's `LIST` command accepts the `-a` option, use it.
The option will be used for all subsequent directory requests.

With some servers, this makes the server send directory lines
where the file or directory entry starts with a dot. However,
there's no _guarantee_ that such entries will be displayed even
if the `-a` option is used.

Note that the fact that the FTP server doesn't complain about the
`-a` option means the option has an effect. I did an experiment
and tried arbitrary "options" with `LIST`, but neither gave an
error message, but just a directory listing as without the `-a`

  • Participants
  • Parent commits d2dbf33

Comments (0)

Files changed (5)

File ftputil/

         # Set default time shift (used in `upload_if_newer` and
         #  `download_if_newer`).
+        # Check if the server accepts the `-a` option for the `LIST`
+        #  command. If yes, always use it to tell the server to show
+        #  directory and file names with a leading dot.
+        self._accepts_list_a_option = False
+        self._check_list_a_option()
     def keep_alive(self):
             # Use straightforward command.
             ftp_error._try_with_oserror(self._session.rename, source, target)
+    def _check_list_a_option(self):
+        """Check for support of the `-a` option for the `LIST` command.
+        If the option is available, use it for all further directory
+        listing requests.
+        """
+        def callback(line):
+            """Directory listing callback."""
+            pass
+        # It seems that most servers just ignore unknown `LIST`
+        #  options instead of reacting with an error status.
+        #  In such a case, ftputil will subsequently use the `-a`
+        #  option even if it doesn't have any apparent effect.
+        try:
+            ftp_error._try_with_oserror(self._session.dir, u"-a", self.curdir,
+                                        callback)
+        except ftp_error.PermanentError:
+            pass
+        else:
+            self._accepts_list_a_option = True
     #XXX One could argue to put this method into the `_Stat` class, but
     #  I refrained from that because then `_Stat` would have to know
     #  about `FTPHost`'s `_session` attribute and in turn about
             def callback(line):
                 """Callback function."""
-            ftp_error._try_with_oserror(self._session.dir, path, callback)
+            if self._accepts_list_a_option:
+                args = (self._session.dir, u"-a", path, callback)
+            else:
+                args = (self._session.dir, path, callback)
+            ftp_error._try_with_oserror(*args)
             return lines
         lines = self._robust_ftp_command(_FTPHost_dir_command, path,

File test/

     def cwd(self, path):
         self.current_dir = self._transform_path(path)
-    def dir(self, path, callback=None):
+    def dir(self, *args):
         """Provide a callback function for processing each line of
         a directory listing. Return nothing.
+        if callable(args[-1]):
+            callback = args[-1]
+            args = args[:-1]
+        else:
+            callback = None
+        # Everything before the path argument are options.
+        path = args[-1]
         if DEBUG:
             print 'dir: %s' % path
         path = self._transform_path(path)

File test/

     def pwd(self):
         return u"/"
+    def dir(self, *args):
+        # Called by `_check_list_a_option`, otherwise not used.
+        pass
 class DummyFTPPath(object):

File test/

         raise ftplib.error_perm
 class FailOnKeepAliveSession(mock_ftplib.MockSession):
+    def dir(self, *args):
+        # Implicitly called by `_check_list_a_option`, otherwise unused.
+        pass
     def pwd(self):
         # Raise exception on second call to let the constructor work.
         if not hasattr(self, "pwd_called"):

File test/

         # Make the cache very small initially and see if it gets resized.
         cache.size = 2
         entries = host.listdir("walk_test")
-        # Actually, the cache is going to be 10 because `listdir`
-        #  implicitly calls `path.isdir` on the directory argument
-        #  which in turn reads the parent directory of `walk_test`
-        #  which happens to have 9 entries.
-        self.assertEqual(cache.size, 10)
+        # The adjusted cache size should be larger or equal to than the
+        # number of items in `walk_test` and its parent directory. The
+        # latter is read implicitly upon `listdir`'s `isdir` call.
+        expected_min_cache_size = max(len(host.listdir(host.curdir)),
+                                      len(entries))
+        self.assertTrue(cache.size >= expected_min_cache_size)
 class TestUploadAndDownload(RealFTPTest):
         self.assertRaises(ftp_error.TimeShiftError, host.synchronize_times)
+    def test_probing_of_list_a_option(self):
+        # Test probing of `LIST -a` option (ticket #63, comment 12).
+        host =
+        self.assertTrue(host._has_list_a_option)
+        directory_entries = host.listdir(host.curdir)
+        self.assertTrue(".hidden" in directory_entries)
     def _make_objects_to_be_garbage_collected(self):
         for i in xrange(10):
             with ftputil.FTPHost(server, user, password) as host: