Skip to content

Conversation

jh7370
Copy link
Collaborator

@jh7370 jh7370 commented Sep 23, 2025

The option recursively replaces any directories on the command-line with all entries within the specified directories, making it easier to reformat an entire directory tree.

Fixes #62108

The option recursively replaces any directories on the command-line with
all entries within the specified directories, making it easier to
reformat an entire directory tree.

Fixes llvm#62108
@llvmbot llvmbot added clang Clang issues not falling into any other category clang-format labels Sep 23, 2025
@llvmbot
Copy link
Member

llvmbot commented Sep 23, 2025

@llvm/pr-subscribers-clang

Author: James Henderson (jh7370)

Changes

The option recursively replaces any directories on the command-line with all entries within the specified directories, making it easier to reformat an entire directory tree.

Fixes #62108


Full diff: https://github.com/llvm/llvm-project/pull/160299.diff

4 Files Affected:

  • (modified) clang/docs/ClangFormat.rst (+1)
  • (modified) clang/docs/ReleaseNotes.rst (+1)
  • (added) clang/test/Format/recursive.test (+21)
  • (modified) clang/tools/clang-format/ClangFormat.cpp (+29)
diff --git a/clang/docs/ClangFormat.rst b/clang/docs/ClangFormat.rst
index 92af06e5083d6..cd00bedefa21b 100644
--- a/clang/docs/ClangFormat.rst
+++ b/clang/docs/ClangFormat.rst
@@ -93,6 +93,7 @@ to format C/C++/Java/JavaScript/JSON/Objective-C/Protobuf/C# code.
     --output-replacements-xml      - Output replacements as XML.
     --qualifier-alignment=<string> - If set, overrides the qualifier alignment style
                                      determined by the QualifierAlignment style flag
+    -r                             - Recursively format files in any specified directories
     --sort-includes                - If set, overrides the include sorting behavior
                                      determined by the SortIncludes style flag
     --style=<string>               - Set coding style. <string> can be:
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index fb429a675476d..e64ef490d292a 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -532,6 +532,7 @@ clang-format
   literals.
 - Add ``Leave`` suboption to ``IndentPPDirectives``.
 - Add ``AllowBreakBeforeQtProperty`` option.
+- Add ``-r`` option for recursing into specified directories when formatting.
 
 libclang
 --------
diff --git a/clang/test/Format/recursive.test b/clang/test/Format/recursive.test
new file mode 100644
index 0000000000000..f0f3b9e5a3f07
--- /dev/null
+++ b/clang/test/Format/recursive.test
@@ -0,0 +1,21 @@
+RUN: rm -rf %t && mkdir %t
+RUN: split-file %s %t
+
+RUN: clang-format -i -r --verbose %t 2>&1 | FileCheck %s -DDIR=%t -DSEP=%{fs-sep}
+
+CHECK: Formatting [1/4] [[DIR]][[SEP]]1.cpp
+CHECK: Formatting [2/4] [[DIR]][[SEP]]2[[SEP]]3.cpp
+CHECK: Formatting [3/4] [[DIR]][[SEP]]2[[SEP]]4[[SEP]]5.cpp
+CHECK: Formatting [4/4] [[DIR]][[SEP]]2[[SEP]]6.cpp
+
+#--- 1.cpp
+int x;
+
+#--- 2/3.cpp
+int x;
+
+#--- 2/4/5.cpp
+int x;
+
+#--- 2/6.cpp
+int x;
diff --git a/clang/tools/clang-format/ClangFormat.cpp b/clang/tools/clang-format/ClangFormat.cpp
index 5f6502f7f18a8..8267417e311f0 100644
--- a/clang/tools/clang-format/ClangFormat.cpp
+++ b/clang/tools/clang-format/ClangFormat.cpp
@@ -131,6 +131,10 @@ static cl::opt<std::string> Files(
     cl::desc("A file containing a list of files to process, one per line."),
     cl::value_desc("filename"), cl::init(""), cl::cat(ClangFormatCategory));
 
+static cl::opt<bool> Recursive("r",
+                             cl::desc("Recursively format files in any specified directories"),
+                             cl::cat(ClangFormatCategory));
+
 static cl::opt<bool>
     Verbose("verbose", cl::desc("If set, shows the list of processed files"),
             cl::cat(ClangFormatCategory));
@@ -700,6 +704,31 @@ int main(int argc, const char **argv) {
     errs() << "Clang-formatting " << LineNo << " files\n";
   }
 
+  if (Recursive) {
+    SmallVector<std::string> ExpandedNames;
+    for (const std::string &Path : FileNames) {
+      if (sys::fs::is_directory(Path)) {
+        std::error_code ErrorCode;
+        for (sys::fs::recursive_directory_iterator I(Path, ErrorCode), E;
+             I != E && !ErrorCode; I.increment(ErrorCode)) {
+          bool Result = false;
+          ErrorCode = sys::fs::is_regular_file(I->path(), Result);
+          // Conservatively assume that any unopenable entries are also regular
+          // files. Later code will emit an error when trying to format them, if
+          // they aren't valid by then.
+          if (ErrorCode || Result)
+            ExpandedNames.push_back(I->path());
+        }
+      } else {
+        ExpandedNames.push_back(std::move(Path));
+      }
+    }
+
+    FileNames.clear();
+    for (const std::string &Name : ExpandedNames)
+      FileNames.push_back(Name);
+  }
+
   if (FileNames.empty()) {
     if (isIgnored(AssumeFileName))
       return 0;

@llvmbot
Copy link
Member

llvmbot commented Sep 23, 2025

@llvm/pr-subscribers-clang-format

Author: James Henderson (jh7370)

Changes

The option recursively replaces any directories on the command-line with all entries within the specified directories, making it easier to reformat an entire directory tree.

Fixes #62108


Full diff: https://github.com/llvm/llvm-project/pull/160299.diff

4 Files Affected:

  • (modified) clang/docs/ClangFormat.rst (+1)
  • (modified) clang/docs/ReleaseNotes.rst (+1)
  • (added) clang/test/Format/recursive.test (+21)
  • (modified) clang/tools/clang-format/ClangFormat.cpp (+29)
diff --git a/clang/docs/ClangFormat.rst b/clang/docs/ClangFormat.rst
index 92af06e5083d6..cd00bedefa21b 100644
--- a/clang/docs/ClangFormat.rst
+++ b/clang/docs/ClangFormat.rst
@@ -93,6 +93,7 @@ to format C/C++/Java/JavaScript/JSON/Objective-C/Protobuf/C# code.
     --output-replacements-xml      - Output replacements as XML.
     --qualifier-alignment=<string> - If set, overrides the qualifier alignment style
                                      determined by the QualifierAlignment style flag
+    -r                             - Recursively format files in any specified directories
     --sort-includes                - If set, overrides the include sorting behavior
                                      determined by the SortIncludes style flag
     --style=<string>               - Set coding style. <string> can be:
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index fb429a675476d..e64ef490d292a 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -532,6 +532,7 @@ clang-format
   literals.
 - Add ``Leave`` suboption to ``IndentPPDirectives``.
 - Add ``AllowBreakBeforeQtProperty`` option.
+- Add ``-r`` option for recursing into specified directories when formatting.
 
 libclang
 --------
diff --git a/clang/test/Format/recursive.test b/clang/test/Format/recursive.test
new file mode 100644
index 0000000000000..f0f3b9e5a3f07
--- /dev/null
+++ b/clang/test/Format/recursive.test
@@ -0,0 +1,21 @@
+RUN: rm -rf %t && mkdir %t
+RUN: split-file %s %t
+
+RUN: clang-format -i -r --verbose %t 2>&1 | FileCheck %s -DDIR=%t -DSEP=%{fs-sep}
+
+CHECK: Formatting [1/4] [[DIR]][[SEP]]1.cpp
+CHECK: Formatting [2/4] [[DIR]][[SEP]]2[[SEP]]3.cpp
+CHECK: Formatting [3/4] [[DIR]][[SEP]]2[[SEP]]4[[SEP]]5.cpp
+CHECK: Formatting [4/4] [[DIR]][[SEP]]2[[SEP]]6.cpp
+
+#--- 1.cpp
+int x;
+
+#--- 2/3.cpp
+int x;
+
+#--- 2/4/5.cpp
+int x;
+
+#--- 2/6.cpp
+int x;
diff --git a/clang/tools/clang-format/ClangFormat.cpp b/clang/tools/clang-format/ClangFormat.cpp
index 5f6502f7f18a8..8267417e311f0 100644
--- a/clang/tools/clang-format/ClangFormat.cpp
+++ b/clang/tools/clang-format/ClangFormat.cpp
@@ -131,6 +131,10 @@ static cl::opt<std::string> Files(
     cl::desc("A file containing a list of files to process, one per line."),
     cl::value_desc("filename"), cl::init(""), cl::cat(ClangFormatCategory));
 
+static cl::opt<bool> Recursive("r",
+                             cl::desc("Recursively format files in any specified directories"),
+                             cl::cat(ClangFormatCategory));
+
 static cl::opt<bool>
     Verbose("verbose", cl::desc("If set, shows the list of processed files"),
             cl::cat(ClangFormatCategory));
@@ -700,6 +704,31 @@ int main(int argc, const char **argv) {
     errs() << "Clang-formatting " << LineNo << " files\n";
   }
 
+  if (Recursive) {
+    SmallVector<std::string> ExpandedNames;
+    for (const std::string &Path : FileNames) {
+      if (sys::fs::is_directory(Path)) {
+        std::error_code ErrorCode;
+        for (sys::fs::recursive_directory_iterator I(Path, ErrorCode), E;
+             I != E && !ErrorCode; I.increment(ErrorCode)) {
+          bool Result = false;
+          ErrorCode = sys::fs::is_regular_file(I->path(), Result);
+          // Conservatively assume that any unopenable entries are also regular
+          // files. Later code will emit an error when trying to format them, if
+          // they aren't valid by then.
+          if (ErrorCode || Result)
+            ExpandedNames.push_back(I->path());
+        }
+      } else {
+        ExpandedNames.push_back(std::move(Path));
+      }
+    }
+
+    FileNames.clear();
+    for (const std::string &Name : ExpandedNames)
+      FileNames.push_back(Name);
+  }
+
   if (FileNames.empty()) {
     if (isIgnored(AssumeFileName))
       return 0;

@jh7370
Copy link
Collaborator Author

jh7370 commented Sep 23, 2025

The request for this has been open for two and a half years. I've needed it several times myself and I've not seen a convincing argument that we shouldn't support this (writing a script that recurses a directory tree to run clang-format on each level is not a one line command invocation). There is prior art in the form of things like the go formatter, plus plenty of other tools recurse through directory trees, either when explicitly asked or by default, so I think we should do it.

Copy link

github-actions bot commented Sep 23, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@jh7370 jh7370 marked this pull request as draft September 23, 2025 14:12
@jh7370 jh7370 marked this pull request as ready for review September 23, 2025 14:18
@owenca
Copy link
Contributor

owenca commented Sep 24, 2025

The option recursively replaces any directories on the command-line with all entries within the specified directories, making it easier to reformat an entire directory tree.

Fixes #62108

See #62108 (comment). I agreed with @mkurdej then and haven't changed my mind. Let's see what @mydeveloperday thinks.

@jh7370
Copy link
Collaborator Author

jh7370 commented Sep 24, 2025

The option recursively replaces any directories on the command-line with all entries within the specified directories, making it easier to reformat an entire directory tree.
Fixes #62108

See #62108 (comment). I agreed with @mkurdej then and haven't changed my mind. Let's see what @mydeveloperday thinks.

The reason I put this up for review is because there is quite considerable demand for the feature, based on both the ticket and the linked Stack Overflow post. Writing a script or crafting a find command to walk the directory tree and gather the desired files is non-zero work, which most people don't want to do. Depending on a user's setup, it may not even be that simple. Given the clear demand and relatively stragihtforward implementation, it seems silly to tell people simply "no, we refuse to do this". That's hardly listening to what the community want, which, since clang-format is probably one of the best known tools within the wider llvm project after clang itself, we should be doing.

Anyway, the clang-format-ignore file allows ignoring files from a set passed in, I assume so that things like *.cpp can be used in the input list without formatting certain specific files, or so that directory-walking scripts don't have to do the filtering themselves. That's hardly a formatting-related feature and rather violates the "do one thing well" idea that seems to be the sole argument against this. -r works well with it in fact (and I could see an argument that without it, -r shouldn't exist either).

@mydeveloperday
Copy link
Contributor

I've seen this request many times, and I feel it would be helpful sometimes, but it's not really our job. Especially when you can use find etc

If others felt it was ok I wouldn't oppose but the arguments of the original authors still remain

@mydeveloperday
Copy link
Contributor

if we were going to do this... wouldn't something like --glob '**/dir/*.cpp' be nicer to support? than doing just -r .

clang-format --glob '/src/*.cpp' --glob '/include/*.h'

@jh7370
Copy link
Collaborator Author

jh7370 commented Sep 25, 2025

I've seen this request many times, and I feel it would be helpful sometimes, but it's not really our job. Especially when you can use find etc

I'm on Windows, so I don't have access to find. I can use PowerShell's Get-Child-Item, in theory, but trying to remember the necessary invocation for the occasional times I need it is non-trivial - I'd be more likely to write a python script to do the work, especially as I could still use it if I'm using cmd for some reason. However, if I'm writing that script, so are dozens/hundreds/... of other developers (many of whom will be getting annoyed that they have to do it).

Also, if it's not our job, why does .clang-format-ignore exist? It feels like it's as much our job (or not) to ignore some set of files as it is to find them.

if we were going to do this... wouldn't something like --glob '**/dir/*.cpp' be nicer to support? than doing just -r .

clang-format --glob '/src/*.cpp' --glob '/include/*.h'

I could certainly look at that and it would fit my specific needs, though I imagine the implementation would be more complex, so I'd rather wait for buy-in before doing that work. This would avoid the issue I ran into with -r and trying to pair it with .clang-format-ignore: there's no way in the latter to have a pattern that allows *.cpp and *.h but nothing else, as far as I can see (if you have multiple negations, nothing will match, unless it happened to match BOTH negations, which obviously isn't possible for two different file endings).

@mydeveloperday
Copy link
Contributor

mydeveloperday commented Sep 26, 2025

Its not that I don't agree with you..see https://reviews.llvm.org/D29039 (from 2017-19) , but if you look at this review, Daniel was saying he didn't feel it was our place. Its hard for us to reverse those decisions, especially made my the inventor of cf. But if you could get @owenca and @HazardyKnusperkeks to agree then I'd definitely say, those who don't want to use it, don't,those that do go for it. I would investigate a more sophisticated method like globs (as LLVM has a GlobPattern class) but it should allow multiple extensions '{.cpp,directory/**/.h}' in one go (like prettier can do)... but I'd want something better than just -r in my view

@HazardyKnusperkeks
Copy link
Contributor

I actually wanted to use that option 3 days ago, but it didn't exist. ;)
I would be in favor of having it. And while I understand the raised concerns, I see it more of a convenience to just use clang-format.

@jh7370
Copy link
Collaborator Author

jh7370 commented Sep 29, 2025

Its not that I don't agree with you..see https://reviews.llvm.org/D29039 (from 2017-19) , but if you look at this review, Daniel was saying he didn't feel it was our place. Its hard for us to reverse those decisions, especially made my the inventor of cf. But if you could get @owenca and @HazardyKnusperkeks to agree then I'd definitely say, those who don't want to use it, don't,those that do go for it. I would investigate a more sophisticated method like globs (as LLVM has a GlobPattern class) but it should allow multiple extensions '{.cpp,directory/**/.h}' in one go (like prettier can do)... but I'd want something better than just -r in my view

Thanks for the comments. It may be a while, but I'll try and look for a smarter alternative (I do agree that a glob pattern would be nicer from a user's perspective).

@mydeveloperday
Copy link
Contributor

Thanks for the comments. It may be a while, but I'll try and look for a smarter alternative (I do agree that a glob pattern would be nicer from a user's perspective).

Take a look here
https://llvm.org/doxygen/classllvm_1_1GlobPattern.html

*[/\]foo.{c,cpp} will match (unix or windows) paths to files named foo.c or foo.cpp.

I've no idea if its any good but it might help... I think if we can have a solution that is more than just -r then I would feel more comfortable like @HazardyKnusperkeks it might just be more acceptable especially as tools like prettier support this, it kind of gives us a precident. (see https://globster.xyz/) for examples

@owenca
Copy link
Contributor

owenca commented Oct 4, 2025

Running the new clang-format from this patch on clang/tools/clang-format/ results in the following:

$ clang-format -ir tools/clang-format
/llvm-project/clang/tools/clang-format/.clang-format:1:1: error: Unknown value for BasedOnStyle: clang - format
BasedOnStyle : clang - format
^
Error reading /llvm-project/clang/tools/clang-format/.clang-format: Invalid argument
/llvm-project/clang/tools/clang-format/.clang-format:1:1: error: Unknown value for BasedOnStyle: clang - format
BasedOnStyle : clang - format
^
Error reading /llvm-project/clang/tools/clang-format/.clang-format: Invalid argument
/llvm-project/clang/tools/clang-format/.clang-format:1:1: error: Unknown value for BasedOnStyle: clang - format
BasedOnStyle : clang - format
^
Error reading /llvm-project/clang/tools/clang-format/.clang-format: Invalid argument
/llvm-project/clang/tools/clang-format/.clang-format:1:1: error: Unknown value for BasedOnStyle: clang - format
BasedOnStyle : clang - format
^
Error reading /llvm-project/clang/tools/clang-format/.clang-format: Invalid argument
$ git status
On branch pr160299
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   tools/clang-format/.clang-format
	modified:   tools/clang-format/CMakeLists.txt
	modified:   tools/clang-format/clang-format-diff.py
	modified:   tools/clang-format/clang-format-test.el
	modified:   tools/clang-format/clang-format.el
	modified:   tools/clang-format/clang-format.py
	modified:   tools/clang-format/fuzzer/CMakeLists.txt
	modified:   tools/clang-format/git-clang-format.bat

This is not very helpful and can be dangerous. IMO clang-format should keep treating directories as invalid filenames:

$ clang-format -i tools/clang-format
tools/clang-format: Is a directory
$ echo $?
1

BTW, you can recursively list all files under a directory with dir /S /B in CMD on Windows. So I would recommend using the @<file> or --files=<filename> options for the purpose of formatting an entire directory.

Copy link
Contributor

@owenca owenca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jh7370
Copy link
Collaborator Author

jh7370 commented Oct 7, 2025

Running the new clang-format from this patch on clang/tools/clang-format/ results in the following:

This is not very helpful and can be dangerous.

Yes, indeed, that's why I think the option would typically best be paired with .clang-format-ignore files. Fun fact: when developing this, because we didn't have a .clang-format-ignore file locally at the time, I accidentally reformatted the entire contents of my .git directory, rather trashing my ability to do anything with the repo! This is also why I'm leaning more towards the glob pattern approach that was suggested as an alternative.

BTW, you can recursively list all files under a directory with dir /S /B in CMD on Windows. So I would recommend using the @<file> or --files=<filename> options for the purpose of formatting an entire directory.

Thanks, useful to know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang Clang issues not falling into any other category clang-format

Projects

None yet

Development

Successfully merging this pull request may close these issues.

request: clang-format to recurse over directories

5 participants