diff --git a/llvm/docs/CommandGuide/lit.rst b/llvm/docs/CommandGuide/lit.rst --- a/llvm/docs/CommandGuide/lit.rst +++ b/llvm/docs/CommandGuide/lit.rst @@ -556,6 +556,7 @@ Otherwise, %t but with a single leading ``/`` removed. %:T On Windows, %/T but a ``:`` is removed if its the second character. Otherwise, %T but with a single leading ``/`` removed. + %{for-each-file } Invokes RUN command for each file in ````. Accepts wildcards. ======================= ============== Other substitutions are provided that are variations on this base set and diff --git a/llvm/docs/ReleaseNotes.rst b/llvm/docs/ReleaseNotes.rst --- a/llvm/docs/ReleaseNotes.rst +++ b/llvm/docs/ReleaseNotes.rst @@ -268,6 +268,8 @@ * Made significant changes to JSON output format of `llvm-readobj`/`llvm-readelf` to improve correctness and clarity. +* llvm-lit now accepts %{for-each-file} in RUN commands. + Changes to LLDB --------------------------------- diff --git a/llvm/utils/lit/lit/TestRunner.py b/llvm/utils/lit/lit/TestRunner.py --- a/llvm/utils/lit/lit/TestRunner.py +++ b/llvm/utils/lit/lit/TestRunner.py @@ -1529,9 +1529,15 @@ # We use #_MARKER_# to hide %% while we do the other substitutions. def escapePercents(ln): + if isinstance(ln, list): + return [_caching_re_compile("%%").sub("#_MARKER_#", line) + for line in ln] return _caching_re_compile("%%").sub("#_MARKER_#", ln) def unescapePercents(ln): + if isinstance(ln, list): + return [_caching_re_compile("#_MARKER_#").sub("%", line) + for line in ln] return _caching_re_compile("#_MARKER_#").sub("%", ln) def substituteIfElse(ln): @@ -1623,6 +1629,43 @@ assert len(ln) == 0 return result + def substituteForEachFile(ln): + from glob import iglob + from pathlib import Path + + if "%{for-each-file" not in ln: + return [ln] + + pattern = re.compile(r'%{for-each-file ([^}]+)}') + matches = list(pattern.finditer(ln)) + if len(matches) == 0: + raise ValueError( + "%{for-each-file} expects a path, but it is not specified." + ) + elif len(matches) > 1: + # FIXME: here we make sure there is only one %{for-each-file} + # to expand, but we can support multiple via cartesian product. + # It doesn't seem immediately useful, so we are keeping this + # function simple. + raise ValueError( + "Multiple %{for-each-file} per directive are not supported" + " yet." + ) + match = matches[0] + path = Path(match[1]).resolve() + if not path.is_absolute(): + raise ValueError( + "%{for-each-file} expects an absolute path," + " but '{}' is not absolute." + .format(str(path)) + ) + if path.is_dir() and '*' not in str(path): + path = path / '*' + lines = [] + for file in iglob(str(path), recursive=True): + lines.append(pattern.sub(file, ln)) + return lines + def processLine(ln): # Apply substitutions ln = substituteIfElse(escapePercents(ln)) @@ -1660,7 +1703,7 @@ return processed process = processLine if recursion_limit is None else processLineToFixedPoint - output = [] + output_stage1 = [] for directive in script: if isinstance(directive, SubstDirective): directive.adjust_substitutions(substitutions) @@ -1671,9 +1714,15 @@ # Can come from preamble_commands. assert isinstance(directive, str) line = directive - output.append(unescapePercents(process(line))) + output_stage1.append(process(line)) + + output_stage2 = [] + for directive_str in output_stage1: + output_stage2.extend(unescapePercents( + substituteForEachFile(directive_str) + )) - return output + return output_stage2 class ParserKind(object):