diff --git a/Help/command/file.rst b/Help/command/file.rst
index 42b087161e..d17057a6c5 100644
--- a/Help/command/file.rst
+++ b/Help/command/file.rst
@@ -1080,6 +1080,7 @@ Archiving
[DESTINATION
]
[ENCODING ]
[PATTERNS ...]
+ [PATTERNS_EXCLUDE ...]
[LIST_ONLY]
[VERBOSE]
[TOUCH])
@@ -1137,6 +1138,18 @@ Archiving
patterns. Wildcards are supported. If the ``PATTERNS`` option is
not given, the entire archive will be listed or extracted.
+ ``PATTERNS_EXCLUDE ...``
+ .. versionadded:: 4.5
+
+ Do not extract or list files and directories that match one of the given
+ patterns. Wildcards are supported. This option may be combined with
+ ``PATTERNS``; an entry that matches both an inclusion pattern and an
+ exclusion pattern is excluded.
+
+ Unlike ``PATTERNS``, exclusion patterns are not anchored to the start of
+ an entry's path: a pattern matches if it matches any portion of the path.
+ This is consistent with the ``--exclude`` option of command-line ``tar``.
+
``LIST_ONLY``
List the files in the archive rather than extract them.
diff --git a/Help/release/dev/file-ARCHIVE_EXTRACT-patterns-exclude.rst b/Help/release/dev/file-ARCHIVE_EXTRACT-patterns-exclude.rst
new file mode 100644
index 0000000000..c661ceb24c
--- /dev/null
+++ b/Help/release/dev/file-ARCHIVE_EXTRACT-patterns-exclude.rst
@@ -0,0 +1,6 @@
+file-ARCHIVE_EXTRACT-patterns-exclude
+-------------------------------------
+
+* The :command:`file(ARCHIVE_EXTRACT)` command gained a ``PATTERNS_EXCLUDE``
+ option to omit archive entries matching the given patterns from being
+ extracted or listed.
diff --git a/Source/cmFileCommand.cxx b/Source/cmFileCommand.cxx
index 366314c86a..222acd7f6f 100644
--- a/Source/cmFileCommand.cxx
+++ b/Source/cmFileCommand.cxx
@@ -3888,17 +3888,20 @@ bool HandleArchiveExtractCommand(std::vector const& args,
bool ListOnly = false;
std::string Destination;
ArgumentParser::MaybeEmpty> Patterns;
+ ArgumentParser::MaybeEmpty> PatternsExclude;
bool Touch = false;
};
- static auto const parser = cmArgumentParser{}
- .Bind("INPUT"_s, &Arguments::Input)
- .Bind("ENCODING"_s, &Arguments::Encoding)
- .Bind("VERBOSE"_s, &Arguments::Verbose)
- .Bind("LIST_ONLY"_s, &Arguments::ListOnly)
- .Bind("DESTINATION"_s, &Arguments::Destination)
- .Bind("PATTERNS"_s, &Arguments::Patterns)
- .Bind("TOUCH"_s, &Arguments::Touch);
+ static auto const parser =
+ cmArgumentParser{}
+ .Bind("INPUT"_s, &Arguments::Input)
+ .Bind("ENCODING"_s, &Arguments::Encoding)
+ .Bind("VERBOSE"_s, &Arguments::Verbose)
+ .Bind("LIST_ONLY"_s, &Arguments::ListOnly)
+ .Bind("DESTINATION"_s, &Arguments::Destination)
+ .Bind("PATTERNS"_s, &Arguments::Patterns)
+ .Bind("PATTERNS_EXCLUDE"_s, &Arguments::PatternsExclude)
+ .Bind("TOUCH"_s, &Arguments::Touch);
std::vector unrecognizedArguments;
auto parsedArgs =
@@ -3928,6 +3931,7 @@ bool HandleArchiveExtractCommand(std::vector const& args,
if (parsedArgs.ListOnly) {
if (!cmSystemTools::ListTar(inFile, parsedArgs.Patterns,
+ parsedArgs.PatternsExclude,
parsedArgs.Encoding, parsedArgs.Verbose)) {
status.SetError(cmStrCat("failed to list: ", inFile));
cmSystemTools::SetFatalErrorOccurred();
@@ -3962,7 +3966,7 @@ bool HandleArchiveExtractCommand(std::vector const& args,
}
if (!cmSystemTools::ExtractTar(
- inFile, parsedArgs.Patterns,
+ inFile, parsedArgs.Patterns, parsedArgs.PatternsExclude,
parsedArgs.Touch ? cmSystemTools::cmTarExtractTimestamps::No
: cmSystemTools::cmTarExtractTimestamps::Yes,
parsedArgs.Encoding, parsedArgs.Verbose)) {
diff --git a/Source/cmSystemTools.cxx b/Source/cmSystemTools.cxx
index d9a77deade..7711dd4cf3 100644
--- a/Source/cmSystemTools.cxx
+++ b/Source/cmSystemTools.cxx
@@ -2505,6 +2505,7 @@ bool copy_data(struct archive* ar, struct archive* aw)
bool extract_tar(std::string const& arFileName,
std::vector const& files,
+ std::vector const& excludeFiles,
std::string const& encoding, bool verbose,
cmSystemTools::cmTarExtractTimestamps extractTimestamps,
bool extract)
@@ -2551,6 +2552,14 @@ bool extract_tar(std::string const& arFileName,
}
}
+ for (auto const& filename : excludeFiles) {
+ if (archive_match_exclude_pattern(matching, filename.c_str()) !=
+ ARCHIVE_OK) {
+ cmSystemTools::Error("Failed to add to exclusion list: " + filename);
+ return false;
+ }
+ }
+
int r = cm_archive_read_open_filename(a, arFileName.c_str(), 10240);
if (r) {
ArchiveError("Problem with archive_read_open_filename(): ", a);
@@ -2644,15 +2653,17 @@ bool extract_tar(std::string const& arFileName,
bool cmSystemTools::ExtractTar(std::string const& arFileName,
std::vector const& files,
+ std::vector const& excludeFiles,
cmTarExtractTimestamps extractTimestamps,
std::string const& encoding, bool verbose)
{
#if !defined(CMAKE_BOOTSTRAP)
- return extract_tar(arFileName, files, encoding, verbose, extractTimestamps,
- true);
+ return extract_tar(arFileName, files, excludeFiles, encoding, verbose,
+ extractTimestamps, true);
#else
(void)arFileName;
(void)files;
+ (void)excludeFiles;
(void)extractTimestamps;
(void)encoding;
(void)verbose;
@@ -2662,14 +2673,16 @@ bool cmSystemTools::ExtractTar(std::string const& arFileName,
bool cmSystemTools::ListTar(std::string const& arFileName,
std::vector const& files,
+ std::vector const& excludeFiles,
std::string const& encoding, bool verbose)
{
#if !defined(CMAKE_BOOTSTRAP)
- return extract_tar(arFileName, files, encoding, verbose,
+ return extract_tar(arFileName, files, excludeFiles, encoding, verbose,
cmTarExtractTimestamps::Yes, false);
#else
(void)arFileName;
(void)files;
+ (void)excludeFiles;
(void)encoding;
(void)verbose;
return false;
diff --git a/Source/cmSystemTools.h b/Source/cmSystemTools.h
index 17a6310bda..e72e5ea187 100644
--- a/Source/cmSystemTools.h
+++ b/Source/cmSystemTools.h
@@ -523,6 +523,7 @@ public:
static bool ListTar(std::string const& arFileName,
std::vector const& files,
+ std::vector const& excludeFiles,
std::string const& encoding, bool verbose);
static bool CreateTar(std::string const& arFileName,
std::vector const& files,
@@ -534,6 +535,7 @@ public:
int compressionLevel = 0, int numThreads = 1);
static bool ExtractTar(std::string const& arFileName,
std::vector const& files,
+ std::vector const& excludeFiles,
cmTarExtractTimestamps extractTimestamps,
std::string const& encoding, bool verbose);
diff --git a/Source/cmcmd.cxx b/Source/cmcmd.cxx
index 6610da07c0..e942370ae4 100644
--- a/Source/cmcmd.cxx
+++ b/Source/cmcmd.cxx
@@ -2121,7 +2121,7 @@ int cmcmd::ExecuteCMakeCommand(std::vector const& args,
}
if (action == cmSystemTools::TarActionList) {
- if (!cmSystemTools::ListTar(outFile, files, encoding, verbose)) {
+ if (!cmSystemTools::ListTar(outFile, files, {}, encoding, verbose)) {
cmSystemTools::Error("Problem listing tar: " + outFile);
return 1;
}
@@ -2136,7 +2136,7 @@ int cmcmd::ExecuteCMakeCommand(std::vector const& args,
return 1;
}
} else if (action == cmSystemTools::TarActionExtract) {
- if (!cmSystemTools::ExtractTar(outFile, files, extractTimestamps,
+ if (!cmSystemTools::ExtractTar(outFile, files, {}, extractTimestamps,
encoding, verbose)) {
cmSystemTools::Error(
cmStrCat("Problem extracting tar:\n ", outFile));
diff --git a/Tests/Fuzzing/cmArchiveExtractFuzzer.cxx b/Tests/Fuzzing/cmArchiveExtractFuzzer.cxx
index 4fb2b73757..ab72a9d423 100644
--- a/Tests/Fuzzing/cmArchiveExtractFuzzer.cxx
+++ b/Tests/Fuzzing/cmArchiveExtractFuzzer.cxx
@@ -86,9 +86,10 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
std::vector files;
// Extract without verbose, with timestamps
+ std::vector excludeFiles;
bool result1 = cmSystemTools::ExtractTar(
- archiveFile, files, cmSystemTools::cmTarExtractTimestamps::Yes, "UTF-8",
- false);
+ archiveFile, files, excludeFiles,
+ cmSystemTools::cmTarExtractTimestamps::Yes, "UTF-8", false);
(void)result1;
// Restore directory BEFORE removing (can't remove cwd)
@@ -103,8 +104,8 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
// Extract with verbose, without timestamps
files.clear();
bool result2 = cmSystemTools::ExtractTar(
- archiveFile, files, cmSystemTools::cmTarExtractTimestamps::No, "UTF-8",
- true);
+ archiveFile, files, excludeFiles,
+ cmSystemTools::cmTarExtractTimestamps::No, "UTF-8", true);
(void)result2;
// Restore directory
diff --git a/Tests/RunCMake/File_Archive/RunCMakeTest.cmake b/Tests/RunCMake/File_Archive/RunCMakeTest.cmake
index 63071cb7e4..6b4da44b08 100644
--- a/Tests/RunCMake/File_Archive/RunCMakeTest.cmake
+++ b/Tests/RunCMake/File_Archive/RunCMakeTest.cmake
@@ -68,6 +68,10 @@ endif()
# Extracting only selected files or directories
run_cmake(zip-filtered)
+# Excluding selected files or directories from extraction
+run_cmake(zip-filtered-exclude)
+run_cmake(zip-filtered-exclude-precedence)
+
run_cmake(create-missing-args)
run_cmake(extract-missing-args)
diff --git a/Tests/RunCMake/File_Archive/zip-filtered-exclude-precedence.cmake b/Tests/RunCMake/File_Archive/zip-filtered-exclude-precedence.cmake
new file mode 100644
index 0000000000..5930e77d5e
--- /dev/null
+++ b/Tests/RunCMake/File_Archive/zip-filtered-exclude-precedence.cmake
@@ -0,0 +1,28 @@
+set(OUTPUT_NAME "test.zip")
+
+set(ARCHIVE_FORMAT zip)
+
+# Combine inclusion and exclusion patterns. When an entry matches both,
+# the exclusion takes precedence.
+set(DECOMPRESSION_OPTIONS
+ PATTERNS
+ "compress_dir/d1/*" # include contents of d1
+ "compress_dir/d_4/*" # include contents of d_4 ...
+ PATTERNS_EXCLUDE
+ "d_4" # ... but exclude d_4: exclusion wins
+)
+
+# Only the included-and-not-excluded entry survives.
+set(CUSTOM_CHECK_FILES
+ "d1/f1.txt"
+)
+
+set(NOT_EXISTING_FILES_CHECK
+ "f1.txt" # never included by PATTERNS
+ "d 2/f1.txt" # never included by PATTERNS
+ "d_4/f1.txt" # included by PATTERNS but excluded: exclusion wins
+)
+
+include(${CMAKE_CURRENT_LIST_DIR}/roundtrip.cmake)
+
+check_magic("504b0304" LIMIT 4 HEX)
diff --git a/Tests/RunCMake/File_Archive/zip-filtered-exclude.cmake b/Tests/RunCMake/File_Archive/zip-filtered-exclude.cmake
new file mode 100644
index 0000000000..866236a677
--- /dev/null
+++ b/Tests/RunCMake/File_Archive/zip-filtered-exclude.cmake
@@ -0,0 +1,30 @@
+set(OUTPUT_NAME "test.zip")
+
+set(ARCHIVE_FORMAT zip)
+
+# Extract the whole archive except entries matching PATTERNS_EXCLUDE.
+set(DECOMPRESSION_OPTIONS
+ PATTERNS_EXCLUDE
+ "compress_dir/d 2/*" # exclude everything under a directory
+ "d-4" # exclude by unanchored name (dir + contents)
+ "no_such_entry" # matches nothing: must not be an error
+)
+
+# Everything that was not excluded must still be extracted.
+set(CUSTOM_CHECK_FILES
+ "f1.txt"
+ "d1/f1.txt"
+ "d + 3/f1.txt"
+ "d_4/f1.txt" # underscore, not the excluded "d-4"
+ "My Special Directory/f1.txt"
+)
+
+# The excluded entries must not be extracted.
+set(NOT_EXISTING_FILES_CHECK
+ "d 2/f1.txt"
+ "d-4/f1.txt"
+)
+
+include(${CMAKE_CURRENT_LIST_DIR}/roundtrip.cmake)
+
+check_magic("504b0304" LIMIT 4 HEX)