Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 src/rootmkdir.cxx LIBRARIES RIO Core Rint)
ROOT_EXECUTABLE(rootrm src/rootrm.cxx LIBRARIES RIO Core Rint)

ROOT_ADD_TEST_SUBDIRECTORY(test)
59 changes: 0 additions & 59 deletions main/python/cmdLineUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
# http://stackoverflow.com/questions/4675728/redirect-stdout-to-a-file-in-python/22434262#22434262
# Thanks J.F. Sebastian !!

from contextlib import contextmanager
import os
import sys
from time import sleep

Check failure on line 19 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F401)

main/python/cmdLineUtils.py:19:18: F401 `time.sleep` imported but unused help: Remove unused import: `time.sleep`
from itertools import zip_longest

Check failure on line 20 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F401)

main/python/cmdLineUtils.py:20:23: F401 `itertools.zip_longest` imported but unused help: Remove unused import: `itertools.zip_longest`

Check failure on line 20 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (I001)

main/python/cmdLineUtils.py:16:1: I001 Import block is un-sorted or un-formatted help: Organize imports

def fileno(file_or_fd):
"""
Expand Down Expand Up @@ -81,10 +81,10 @@
ROOT.PyConfig.IgnoreCommandLineOptions = True
ROOT.gROOT.GetVersion()

import argparse

Check failure on line 84 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E402)

main/python/cmdLineUtils.py:84:1: E402 Module level import not at top of file
import glob

Check failure on line 85 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E402)

main/python/cmdLineUtils.py:85:1: E402 Module level import not at top of file
import fnmatch

Check failure on line 86 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E402)

main/python/cmdLineUtils.py:86:1: E402 Module level import not at top of file
import logging

Check failure on line 87 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E402)

main/python/cmdLineUtils.py:87:1: E402 Module level import not at top of file

Check failure on line 87 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (I001)

main/python/cmdLineUtils.py:84:1: I001 Import block is un-sorted or un-formatted help: Organize imports

LOG_FORMAT = "%(levelname)s: %(message)s"
logging.basicConfig(format=LOG_FORMAT)
Expand Down Expand Up @@ -328,12 +328,12 @@
Open a ROOT file (like openROOTFile) with the possibility
to change compression settings
"""
if compress != None and os.path.isfile(fileName):

Check failure on line 331 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E711)

main/python/cmdLineUtils.py:331:20: E711 Comparison to `None` should be `cond is not None` help: Replace with `cond is not None`
logging.warning("can't change compression settings on existing file")
return None
mode = "recreate" if recreate else "update"
theFile = openROOTFile(fileName, mode)
if compress != None:

Check failure on line 336 in main/python/cmdLineUtils.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E711)

main/python/cmdLineUtils.py:336:20: E711 Comparison to `None` should be `cond is not None` help: Replace with `cond is not None`
theFile.SetCompressionSettings(compress)
return theFile

Expand Down Expand Up @@ -896,65 +896,6 @@
# 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

Expand Down
48 changes: 0 additions & 48 deletions main/python/rootmkdir.py

This file was deleted.

246 changes: 246 additions & 0 deletions main/src/rootmkdir.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/// \file rootmkdir.cxx
///
/// Command line tool to create directories in ROOT files
///
/// \author Giacomo Parolini <giacomo.parolini@cern.ch>
/// \date 2026-03-06
#include <ROOT/RLogger.hxx>

#include "logging.hxx"
#include "optparse.hxx"
#include "RootObjTree.hxx"
#include "RootObjTree.cxx"

#include <TClass.h>
#include <TClassRef.h>
#include <TError.h>
#include <TFile.h>
#include <TROOT.h>
#include <TSystem.h>

#include <iostream>
#include <memory>
#include <string_view>
#include <vector>

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<std::string> 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<const std::string>{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<TDirectory>();
} 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<const char **>(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<std::pair<std::string_view, std::string_view>> 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>(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;
}
Loading
Loading