88#
99######################################################################
1010
11+ import fnmatch
1112import logging
13+ import pathlib
1214
1315from typing import Optional , Tuple
1416
@@ -323,7 +325,8 @@ def ls(
323325 folder_to_list : str = '' ,
324326 latest_only : bool = True ,
325327 recursive : bool = False ,
326- fetch_count : Optional [int ] = 10000
328+ fetch_count : Optional [int ] = 10000 ,
329+ with_wildcard : bool = False ,
327330 ):
328331 """
329332 Pretend that folders exist and yields the information about the files in a folder.
@@ -339,21 +342,59 @@ def ls(
339342 :param folder_to_list: the name of the folder to list; must not start with "/".
340343 Empty string means top-level folder
341344 :param latest_only: when ``False`` returns info about all versions of a file,
342- when ``True``, just returns info about the most recent versions
345+ when ``True``, just returns info about the most recent versions
343346 :param recursive: if ``True``, list folders recursively
344347 :param fetch_count: how many entries to return or ``None`` to use the default. Acceptable values: 1 - 10000
348+ :param with_wildcard: Accepts "*", "?", "[]" and "[!]" in folder_to_list, similarly to what shell does.
349+ As of 1.19.0 it can only be enabled when recursive is also enabled.
350+ Also, in this mode, folder_to_list is considered to be a filename or a pattern.
345351 :rtype: generator[tuple[b2sdk.v2.FileVersion, str]]
346352 :returns: generator of (file_version, folder_name) tuples
347353
348354 .. note::
349- In case of `recursive=True`, folder_name is returned only for first file in the folder .
355+ In case of `recursive=True`, folder_name is not returned .
350356 """
357+ # Ensure that recursive is enabled when with_wildcard is enabled.
358+ if with_wildcard and not recursive :
359+ raise ValueError ('with_wildcard requires recursive to be turned on as well' )
360+
351361 # Every file returned must have a name that starts with the
352362 # folder name and a "/".
353363 prefix = folder_to_list
354- if prefix != '' and not prefix .endswith ('/' ):
364+ # In case of wildcards, we don't assume that this is folder that we're searching through.
365+ # It could be an exact file, e.g. 'a/b.txt' that we're trying to locate.
366+ if prefix != '' and not prefix .endswith ('/' ) and not with_wildcard :
355367 prefix += '/'
356368
369+ # If we're running with wildcard-matching, we could get
370+ # a different prefix from it. We search for the first
371+ # occurrence of the special characters and fetch
372+ # parent path from that place.
373+ # Examples:
374+ # 'b/c/*.txt' –> 'b/c/'
375+ # '*.txt' –> ''
376+ # 'a/*/result.[ct]sv' –> 'a/'
377+ if with_wildcard :
378+ for wildcard_character in '*?[' :
379+ try :
380+ starter_index = folder_to_list .index (wildcard_character )
381+ except ValueError :
382+ continue
383+
384+ # +1 to include the starter character. Using posix path to
385+ # ensure consistent behaviour on Windows (e.g. case sensitivity).
386+ path = pathlib .PurePosixPath (folder_to_list [:starter_index + 1 ])
387+ parent_path = str (path .parent )
388+ # Path considers dot to be the empty path.
389+ # There's no shorter path than that.
390+ if parent_path == '.' :
391+ prefix = ''
392+ break
393+ # We could receive paths in different stage, e.g. 'a/*/result.[ct]sv' has two
394+ # possible parent paths: 'a/' and 'a/*/', with the first one being the correct one
395+ if len (parent_path ) < len (prefix ):
396+ prefix = parent_path
397+
357398 # Loop until all files in the named directory have been listed.
358399 # The starting point of the first list_file_names request is the
359400 # prefix we're looking for. The prefix ends with '/', which is
@@ -378,7 +419,13 @@ def ls(
378419 if not file_version .file_name .startswith (prefix ):
379420 # We're past the files we care about
380421 return
422+ if with_wildcard and not fnmatch .fnmatchcase (
423+ file_version .file_name , folder_to_list
424+ ):
425+ # File doesn't match our wildcard rules
426+ continue
381427 after_prefix = file_version .file_name [len (prefix ):]
428+ # In case of wildcards, we don't care about folders at all, and it's recursive by default.
382429 if '/' not in after_prefix or recursive :
383430 # This is not a folder, so we'll print it out and
384431 # continue on.
0 commit comments