From 39275d340e777d9c4667baf3c882a8c96eb8f3bb Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 6 Mar 2026 11:03:00 +0100 Subject: [PATCH 1/3] [main] Create C++ version of rootmkdir --- main/CMakeLists.txt | 3 +- main/src/rootmkdir.cxx | 246 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 main/src/rootmkdir.cxx diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 2eed9acb0972c..07bf7cf2eb276 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -137,7 +137,8 @@ if (TARGET Gui) ROOT_EXECUTABLE(rootbrowse src/rootbrowse.cxx LIBRARIES RIO Core Rint Gui) endif() ROOT_EXECUTABLE(rootcp src/rootcp.cxx LIBRARIES RIO Tree Core Rint) -ROOT_EXECUTABLE(rootrm src/rootrm.cxx LIBRARIES RIO Core Rint) ROOT_EXECUTABLE(rootls src/rootls.cxx LIBRARIES RIO Tree Core Rint ROOTNTuple) +ROOT_EXECUTABLE(rootmkdir_cxx src/rootmkdir.cxx LIBRARIES RIO Core Rint) +ROOT_EXECUTABLE(rootrm src/rootrm.cxx LIBRARIES RIO Core Rint) ROOT_ADD_TEST_SUBDIRECTORY(test) diff --git a/main/src/rootmkdir.cxx b/main/src/rootmkdir.cxx new file mode 100644 index 0000000000000..b8cafc14501a9 --- /dev/null +++ b/main/src/rootmkdir.cxx @@ -0,0 +1,246 @@ +/// \file rootmkdir.cxx +/// +/// Command line tool to create directories in ROOT files +/// +/// \author Giacomo Parolini +/// \date 2026-03-06 +#include + +#include "logging.hxx" +#include "optparse.hxx" +#include "RootObjTree.hxx" +#include "RootObjTree.cxx" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace ROOT::CmdLine; + +static const char *const kShortHelp = "usage: rootmkdir [-p|--parents] [-v|--verbose] FILE:path/to/dir [...]\n"; +static const char *const kLongHelp = R"( +Add directories in ROOT files + +positional arguments: + FILE:path File(s) and path(s) of directories to create + +options: + -h, --help get this help + -p, --parents create parent directories if needed, no error if directory already exists + -v, --verbose be verbose (can be repeated for increased verbosity) + +Note that calling rootmkdir without a directory path can be used to create files (similar to the `touch` command). +You can also simultaneously create files and directories in it if you pass the -p flag. + +examples: +- rootmkdir example.root:dir + Add the directory 'dir' to the ROOT file 'example.root' (file must exists) + +- rootmkdir example.root:dir1/dir2 + Add the directory 'dir2' in 'dir1' which exists in the ROOT file 'example.root' + +- rootmkdir -p example.root:dir1/dir2/dir3 + Make parent directories of 'dir3' as needed, no error if target directory already exists + +- rootmkdir non_existent.root + Create an empty ROOT file named 'non_existent.root' + +- rootmkdir -p non_existent.root:dir1 + Create a ROOT file named 'non_existent.root' and directory `dir1` in it +)"; + +struct RootMkdirArgs { + enum class EPrintUsage { + kNo, + kShort, + kLong + }; + EPrintUsage fPrintHelp = EPrintUsage::kNo; + bool fCreateParents = false; + std::vector fDstFileAndPaths; +}; + +static RootMkdirArgs ParseArgs(const char **args, int nArgs) +{ + using ROOT::RCmdLineOpts; + + RootMkdirArgs outArgs; + + RCmdLineOpts opts; + opts.AddFlag({"-h", "--help"}); + opts.AddFlag({"-p", "--parents"}); + opts.AddFlag({"-v", "--verbose"}, RCmdLineOpts::EFlagType::kSwitch, "", RCmdLineOpts::kFlagAllowMultiple); + + opts.Parse(args, nArgs); + + for (const auto &err : opts.GetErrors()) { + std::cerr << err << "\n"; + } + if (!opts.GetErrors().empty()) { + outArgs.fPrintHelp = RootMkdirArgs::EPrintUsage::kShort; + return outArgs; + } + + if (opts.GetSwitch("help")) { + outArgs.fPrintHelp = RootMkdirArgs::EPrintUsage::kLong; + return outArgs; + } + + SetLogVerbosity(opts.GetSwitch("v")); + + outArgs.fCreateParents = opts.GetSwitch("parents"); + + outArgs.fDstFileAndPaths = opts.GetArgs(); + if (outArgs.fDstFileAndPaths.size() < 1) + outArgs.fPrintHelp = RootMkdirArgs::EPrintUsage::kShort; + + return outArgs; +} + +static bool IsDirectory(const TKey &key) +{ + static const TClassRef dirClassRef("TDirectory"); + const auto *dirClass = TClass::GetClass(key.GetClassName()); + return dirClass && dirClass->InheritsFrom(dirClassRef); +} + +static bool MakeDirectory(TFile &file, std::string_view dirPath, bool createParents) +{ + // Partially copy-pasted from RFile::PutUntyped + + const auto tokens = ROOT::Split(dirPath, "/"); + const auto FullPathUntil = [&tokens](auto idx) { + return ROOT::Join("/", std::span{tokens.data(), idx + 1}); + }; + TDirectory *dir = &file; + for (auto tokIdx = 0u; tokIdx < tokens.size() - 1; ++tokIdx) { + TKey *existing = dir->GetKey(tokens[tokIdx].c_str()); + // 4 cases here: + // 1. subdirectory exists? -> no problem. + // 2. non-directory object exists with the name of the subdirectory? -> error. + // 3. subdirectory does not exist and we passed -p? -> create it + // 4. subdirectory does not exist and we didn't pass -p? -> error. + if (existing) { + if (!IsDirectory(*existing)) { + Err() << "error adding '" + std::string(dirPath) + "': path '" + FullPathUntil(tokIdx) + + "' is already taken by an object of type '" + existing->GetClassName() + "'\n"; + return false; + } + dir = existing->ReadObject(); + } else if (createParents) { + dir = dir->mkdir(tokens[tokIdx].c_str(), "", false); + if (dir) + Info(1) << "created directory '" << file.GetName() << ':' << FullPathUntil(tokIdx) << "'\n"; + } else { + Err() << "cannot create directory '" + std::string(dirPath) + "': parent directory '" + FullPathUntil(tokIdx) + + "' does not exist. If you want to create the entire hierarchy, use the -p flag.\n"; + return false; + } + + if (!dir) + return false; + } + + const TKey *existing = dir->GetKey(tokens[tokens.size() - 1].c_str()); + if (existing) { + if (!IsDirectory(*existing)) { + Err() << "error adding '" + std::string(dirPath) + "': path is already taken by an object of type '" + + existing->GetClassName() + "'\n"; + return false; + } else if (!createParents) { + Err() << "error adding '" + std::string(dirPath) + "': a directory already exists at that path.\n"; + return false; + } + + // directory already exists and we passed -p. + return true; + } + + auto newDir = dir->mkdir(tokens[tokens.size() - 1].c_str(), "", false); + if (newDir) + Info(1) << "created directory '" << file.GetName() << ':' << std::string(dirPath) << "'\n"; + + return newDir != nullptr; +} + +static bool ValidateDirPath(std::string_view path) +{ + if (path.rfind(';') != std::string_view::npos) { + Err() << "cannot specify cycle for the directory to create.\n"; + return false; + } + return true; +} + +int main(int argc, char **argv) +{ + InitLog("rootmkdir"); + + // Parse arguments + auto args = ParseArgs(const_cast(argv) + 1, argc - 1); + if (args.fPrintHelp != RootMkdirArgs::EPrintUsage::kNo) { + std::cerr << kShortHelp; + if (args.fPrintHelp == RootMkdirArgs::EPrintUsage::kLong) { + std::cerr << kLongHelp; + return 0; + } + return 1; + } + + // Validate and split all arguments into filename + dirpath + std::vector> dstFilesAndPaths; + dstFilesAndPaths.reserve(args.fDstFileAndPaths.size()); + for (const auto &src : args.fDstFileAndPaths) { + auto res = SplitIntoFileNameAndPattern(src); + if (!res) { + Err() << res.GetError()->GetReport() << "\n"; + return 1; + } + auto fnameAndPath = res.Unwrap(); + if (!ValidateDirPath(fnameAndPath.second)) + return 1; + dstFilesAndPaths.push_back(fnameAndPath); + } + + // Create the directories + bool errors = false; + for (const auto &[dstFname, dstPath] : dstFilesAndPaths) { + const std::string fname{dstFname}; + const bool fileExists = gSystem->AccessPathName(fname.c_str(), kFileExists) == 0; + // If the file does not exist we attempt to create it in the following cases: + // 1. dstPath is empty + // 2. dstPath is non-empty and the user passed the -p flag. + if (!fileExists && !(dstPath.empty() || args.fCreateParents)) { + Err() << "cannot create directory '" << dstFname << ":" << dstPath + << "': file does not exist. Use the -p flag if you want to create the file alongside the directories.\n"; + errors = true; + continue; + } else if (fileExists && dstPath.empty() && !args.fCreateParents) { + Err() << "cannot create file '" << fname << "': already exists.\n"; + errors = true; + continue; + } + + auto file = std::unique_ptr(TFile::Open(fname.c_str(), "UPDATE")); + if (!file || file->IsZombie()) { + Err() << "failed to open '" << fname << "' for writing.\n"; + errors = true; + continue; + } else if (!fileExists) { + Info(1) << "created file '" << fname << "'\n"; + } + + if (!dstPath.empty()) + errors |= !MakeDirectory(*file, dstPath, args.fCreateParents); + } + + return errors; +} From 3572d801564b4c44bed35f5c43798426dcd46d0c Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 6 Mar 2026 11:24:12 +0100 Subject: [PATCH 2/3] [main] Replace rootmkdir.py with C++ version --- main/CMakeLists.txt | 2 +- main/python/cmdLineUtils.py | 59 ----------------------------------- main/python/rootmkdir.py | 48 ----------------------------- roottest/main/CMakeLists.txt | 60 ++++++++++++++++++------------------ 4 files changed, 31 insertions(+), 138 deletions(-) delete mode 100755 main/python/rootmkdir.py diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 07bf7cf2eb276..1cef3284ca727 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -138,7 +138,7 @@ if (TARGET Gui) endif() ROOT_EXECUTABLE(rootcp src/rootcp.cxx LIBRARIES RIO Tree Core Rint) ROOT_EXECUTABLE(rootls src/rootls.cxx LIBRARIES RIO Tree Core Rint ROOTNTuple) -ROOT_EXECUTABLE(rootmkdir_cxx src/rootmkdir.cxx LIBRARIES RIO Core Rint) +ROOT_EXECUTABLE(rootmkdir src/rootmkdir.cxx LIBRARIES RIO Core Rint) ROOT_EXECUTABLE(rootrm src/rootrm.cxx LIBRARIES RIO Core Rint) ROOT_ADD_TEST_SUBDIRECTORY(test) diff --git a/main/python/cmdLineUtils.py b/main/python/cmdLineUtils.py index dd50b89bc2d71..0b0f3ff2ba0b9 100644 --- a/main/python/cmdLineUtils.py +++ b/main/python/cmdLineUtils.py @@ -896,65 +896,6 @@ def rootEventselector( # End of ROOTEVENTSELECTOR ########## -########## -# ROOTMKDIR - -MKDIR_ERROR = "cannot create directory '{0}'" - - -def _createDirectories(rootFile, pathSplit, parents): - """Same behaviour as createDirectory but allows the possibility - to build an whole path recursively with the option \"parents\" """ - retcode = 0 - lenPathSplit = len(pathSplit) - if lenPathSplit == 0: - pass - elif parents: - for i in range(lenPathSplit): - currentPathSplit = pathSplit[: i + 1] - if not (isExisting(rootFile, currentPathSplit) and isDirectory(rootFile, currentPathSplit)): - retcode += createDirectory(rootFile, currentPathSplit) - else: - doMkdir = True - for i in range(lenPathSplit - 1): - currentPathSplit = pathSplit[: i + 1] - if not (isExisting(rootFile, currentPathSplit) and isDirectory(rootFile, currentPathSplit)): - doMkdir = False - break - if doMkdir: - retcode += createDirectory(rootFile, pathSplit) - else: - logging.warning(MKDIR_ERROR.format("/".join(pathSplit))) - retcode += 1 - return retcode - - -def _rootMkdirProcessFile(fileName, pathSplitList, parents): - retcode = 0 - rootFile = openROOTFile(fileName, "update") - if not rootFile: - return 1 - for pathSplit in pathSplitList: - retcode += _createDirectories(rootFile, pathSplit, parents) - rootFile.Close() - return retcode - - -def rootMkdir(sourceList, parents=False): - # Check arguments - if sourceList == []: - return 1 - - # Loop on the ROOT files - retcode = 0 - for fileName, pathSplitList in sourceList: - retcode += _rootMkdirProcessFile(fileName, pathSplitList, parents) - return retcode - - -# End of ROOTMKDIR -########## - ########## # ROOTMV diff --git a/main/python/rootmkdir.py b/main/python/rootmkdir.py deleted file mode 100755 index 4675c2029f83b..0000000000000 --- a/main/python/rootmkdir.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env @python@ - -# ROOT command line tools: rootmkdir -# Author: Julien Ripoche -# Mail: julien.ripoche@u-psud.fr -# Date: 20/08/15 - -"""Command line to add directories in ROOT files""" - -import cmdLineUtils -import sys - -# Help strings -description = "Add directories in ROOT files" - -PARENT_HELP = "make parent directories as needed, no error if existing." - -EPILOG="""Examples: -- rootmkdir example.root:dir - Add the directory 'dir' to the ROOT file 'example.root' - -- rootmkdir example.root:dir1/dir2 - Add the directory 'dir2' in 'dir1' which is into the ROOT file 'example.root' - -- rootmkdir -p example.root:dir1/dir2/dir3 - Make parent directories of 'dir3' as needed, no error if existing - -- rootmkdir example.root - Create an empty ROOT file named 'example.root' -""" - -def get_argparse(): - # Collect arguments with the module argparse - parser = cmdLineUtils.getParserFile(description, EPILOG) - parser.prog = 'rootmkdir' - parser.add_argument("-p", "--parents", help=PARENT_HELP, action="store_true") - return parser - -def execute(): - parser = get_argparse() - - # Put arguments in shape - sourceList, optDict = cmdLineUtils.getSourceListOptDict(parser, wildcards = False) - - # Process rootMkdir - return cmdLineUtils.rootMkdir(sourceList, parents=optDict["parents"]) -if __name__ == "__main__": - sys.exit(execute()) diff --git a/roottest/main/CMakeLists.txt b/roottest/main/CMakeLists.txt index 3d15e80411d9a..874c725f77bd7 100644 --- a/roottest/main/CMakeLists.txt +++ b/roottest/main/CMakeLists.txt @@ -395,39 +395,13 @@ endif() ######################################################################### -if(pyroot) - -# We compare the output of some tests against ref. files, and we don't want to -# get noise for Python warnings (for example the warning that the GIL is -# activated because the ROOT Python bindings don't support free threading yet). -set(test_env PYTHONWARNINGS=ignore) - -############################## PATTERN TESTS ############################ -set (TESTPATTERN_EXE ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testPatternToFileNameAndPathSplitList.py) -ROOTTEST_ADD_TEST(SimplePattern1 - COMMAND ${TESTPATTERN_EXE} test.root - OUTREF SimplePattern.ref - ENVIRONMENT ${test_env}) - -ROOTTEST_ADD_TEST(SimplePattern2 - COMMAND ${TESTPATTERN_EXE} test.root:tof - OUTREF SimplePattern2.ref - ENVIRONMENT ${test_env}) - -ROOTTEST_ADD_TEST(SimplePattern3 - COMMAND ${TESTPATTERN_EXE} test.root:* - OUTREF SimplePattern3.ref - ENVIRONMENT ${test_env}) -######################################################################### - - -############################# ROOMKDIR TESTS ############################ +############################# ROOTMKDIR TESTS ############################ ROOTTEST_ADD_TEST(SimpleRootmkdir1PrepareInput COMMAND ${TOOLS_PREFIX}/rootcp${exeext} --recreate -r test.root target1.root FIXTURES_SETUP main-SimpleRootmkdir1PrepareInput-fixture) ROOTTEST_ADD_TEST(SimpleRootmkdir1 - COMMAND ${PY_TOOLS_PREFIX}/rootmkdir${pyext} target1.root:new_directory + COMMAND ${TOOLS_PREFIX}/rootmkdir${exeext} target1.root:new_directory FIXTURES_REQUIRED main-SimpleRootmkdir1PrepareInput-fixture FIXTURES_SETUP main-SimpleRootmkdir1-fixture) @@ -449,7 +423,7 @@ ROOTTEST_ADD_TEST(SimpleRootmkdir2PrepareInput FIXTURES_SETUP main-SimpleRootmkdir2PrepareInput-fixture) ROOTTEST_ADD_TEST(SimpleRootmkdir2 - COMMAND ${PY_TOOLS_PREFIX}/rootmkdir${pyext} target2.root:dir1 target2.root:dir2 + COMMAND ${TOOLS_PREFIX}/rootmkdir${exeext} target2.root:dir1 target2.root:dir2 FIXTURES_REQUIRED main-SimpleRootmkdir2PrepareInput-fixture FIXTURES_SETUP main-SimpleRootmkdir2-fixture) @@ -471,7 +445,7 @@ ROOTTEST_ADD_TEST(SimpleRootmkdir3PrepareInput FIXTURES_SETUP main-SimpleRootmkdir3PrepareInput-fixture) ROOTTEST_ADD_TEST(SimpleRootmkdir3 - COMMAND ${PY_TOOLS_PREFIX}/rootmkdir${pyext} -p target3.root:aa/bb/cc + COMMAND ${TOOLS_PREFIX}/rootmkdir${exeext} -p target3.root:aa/bb/cc FIXTURES_REQUIRED main-SimpleRootmkdir3PrepareInput-fixture FIXTURES_SETUP main-SimpleRootmkdir3-fixture) @@ -485,6 +459,32 @@ ROOTTEST_ADD_TEST(SimpleRootmkdir3CheckOutput ROOTTEST_ADD_TEST(SimpleRootmkdir3Clean COMMAND ${TOOLS_PREFIX}/rootrm${exeext} -r target3.root FIXTURES_REQUIRED main-SimpleRootmkdir3CheckOutput-fixture) + +######################################################################### + +if(pyroot) + +# We compare the output of some tests against ref. files, and we don't want to +# get noise for Python warnings (for example the warning that the GIL is +# activated because the ROOT Python bindings don't support free threading yet). +set(test_env PYTHONWARNINGS=ignore) + +############################## PATTERN TESTS ############################ +set (TESTPATTERN_EXE ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testPatternToFileNameAndPathSplitList.py) +ROOTTEST_ADD_TEST(SimplePattern1 + COMMAND ${TESTPATTERN_EXE} test.root + OUTREF SimplePattern.ref + ENVIRONMENT ${test_env}) + +ROOTTEST_ADD_TEST(SimplePattern2 + COMMAND ${TESTPATTERN_EXE} test.root:tof + OUTREF SimplePattern2.ref + ENVIRONMENT ${test_env}) + +ROOTTEST_ADD_TEST(SimplePattern3 + COMMAND ${TESTPATTERN_EXE} test.root:* + OUTREF SimplePattern3.ref + ENVIRONMENT ${test_env}) ######################################################################### ############################# ROOMV TESTS ############################ From 481d29a745e3b5044eff0ff7d3c532cd0db98c43 Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 6 Mar 2026 11:24:41 +0100 Subject: [PATCH 3/3] [main] add more rootmkdir tests --- roottest/main/CMakeLists.txt | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/roottest/main/CMakeLists.txt b/roottest/main/CMakeLists.txt index 874c725f77bd7..ac9cadcc8e59b 100644 --- a/roottest/main/CMakeLists.txt +++ b/roottest/main/CMakeLists.txt @@ -460,6 +460,55 @@ ROOTTEST_ADD_TEST(SimpleRootmkdir3Clean COMMAND ${TOOLS_PREFIX}/rootrm${exeext} -r target3.root FIXTURES_REQUIRED main-SimpleRootmkdir3CheckOutput-fixture) + +ROOTTEST_ADD_TEST(SimpleRootmkdir4PrepareInput + COMMAND ${TOOLS_PREFIX}/rootrm${exeext} -rf target4.root + FIXTURES_SETUP main-SimpleRootmkdir4PrepareInput-fixture) + +ROOTTEST_ADD_TEST(SimpleRootmkdir4 + COMMAND ${TOOLS_PREFIX}/rootmkdir${exeext} target4.root + FIXTURES_REQUIRED main-SimpleRootmkdir4PrepareInput-fixture + FIXTURES_SETUP main-SimpleRootmkdir4-fixture) + +ROOTTEST_ADD_TEST(SimpleRootmkdir4CheckOutput + COMMAND ${TOOLS_PREFIX}/rootls${exeext} -1 target4.root + FIXTURES_REQUIRED main-SimpleRootmkdir4-fixture + FIXTURES_SETUP main-SimpleRootmkdir4CheckOutput-fixture + PASSRC 0) + +ROOTTEST_ADD_TEST(SimpleRootmkdir4Clean + COMMAND ${TOOLS_PREFIX}/rootrm${exeext} -r target4.root + FIXTURES_REQUIRED main-SimpleRootmkdir4CheckOutput-fixture) + + +ROOTTEST_ADD_TEST(SimpleRootmkdir5PrepareInput + COMMAND ${TOOLS_PREFIX}/rootrm${exeext} -rf target5.root + FIXTURES_SETUP main-SimpleRootmkdir5PrepareInput-fixture) + +ROOTTEST_ADD_TEST(SimpleRootmkdir5 + COMMAND ${TOOLS_PREFIX}/rootmkdir${exeext} target5.root:foo + PASSRC 1) + + +ROOTTEST_ADD_TEST(SimpleRootmkdir6PrepareInput + COMMAND ${TOOLS_PREFIX}/rootrm${exeext} -rf target6.root target6.2.root target6.3.root + FIXTURES_SETUP main-SimpleRootmkdir6PrepareInput-fixture) + +ROOTTEST_ADD_TEST(SimpleRootmkdir6 + COMMAND ${TOOLS_PREFIX}/rootmkdir${exeext} -p target6.root:foo target6.2.root:bar/baz target6.3.root + FIXTURES_REQUIRED main-SimpleRootmkdir6PrepareInput-fixture + FIXTURES_SETUP main-SimpleRootmkdir6-fixture) + +ROOTTEST_ADD_TEST(SimpleRootmkdir6CheckOutput + COMMAND ${TOOLS_PREFIX}/rootls${exeext} -1 target6.root:foo target6.2.root:bar/baz target6.3.root + FIXTURES_REQUIRED main-SimpleRootmkdir6-fixture + FIXTURES_SETUP main-SimpleRootmkdir6CheckOutput-fixture + PASSRC 0) + +ROOTTEST_ADD_TEST(SimpleRootmkdir6Clean + COMMAND ${TOOLS_PREFIX}/rootrm${exeext} -r target6.root target6.2.root target6.3.root + FIXTURES_REQUIRED main-SimpleRootmkdir6CheckOutput-fixture) + ######################################################################### if(pyroot)