7
\$\begingroup\$

Abstract

The bash shell utility mkdir is a tool used to create directories in most POSIX environments. I thought it would be fun to implement such functionality from C++ and maybe forge in some of my own features in the future. I will be using the C++ library Boost to handle the command line options and the Standard Template Libary (std) to work work with the filesystem. I would appreciate some sustains, nays, and comments on the 50 so lines of code I have written.

Code

mkdir.cpp

#include <boost/program_options.hpp>
namespace po = boost::program_options;

#include <filesystem>
#include <iostream>
#include <string.h>
#include <string>
#include <vector>

// Define the LOG() macro in order to be able to add
// a logger in the future without too much refactoring
//#define LOG(x) BOOST_LOG_TRIVIAL(x)
#define LOG(x) std::cout

int main(int argc, char **argv) {
    // Declare the supported options.
    po::options_description desc("Allowed options");
    desc.add_options()
            ("help,h",                       "produce help message")
            ("parents,p", po::bool_switch(), "no error if existing, make parent directories as needed")
            ("verbose,v", po::bool_switch(), "print a message for each created directory");

    const po::parsed_options parsed = po::command_line_parser(argc, argv)
            .options(desc).allow_unregistered().run();
    const std::vector<std::string> unrecognized =
            collect_unrecognized(parsed.options, po::include_positional);
    po::variables_map vm;
    po::store(parsed, vm);
    po::notify(vm);

    for (const auto &opt : unrecognized) {
        if (opt[0] == '-') {
            LOG(error) << "Unknown option '" << opt << "'\n";
            return 1;
        }
    }
    if (vm.count("help") || argc <= 1) {
        LOG(info) << desc;
        return 1;
    }
    std::error_code ec;
    for (const auto &dir_name : unrecognized) {
        auto path = std::filesystem::absolute(std::filesystem::path(dir_name));
        if (vm["parents"].as<bool>())
            std::filesystem::create_directories(path, ec);
        else
            std::filesystem::create_directory(path, ec);
        if (errno) {
            LOG(error) << "Cannot create directory '" << dir_name
                       << "': " << strerror(errno) << '\n';
            errno = 0;
        } else {
            LOG(info) << "Created directory '" << dir_name << "'\n";
        }
    }
    return 0;
}

Try it!

In order to compile this utility the following packages are needed:

sudo apt install build-essential libboost-program-options-dev

Then you can compile mkdir.cpp with:

g++ mkdir.cpp -o mkdir -lboost_program_options -std=c++2a

Then you can test it out with:

./mkdir <options> <directory names>

\$\endgroup\$
8
  • \$\begingroup\$ I was looking at Boost Log as I was already using boost; I had considered using spdlog or glog but decided to consolidate my sources when writing. \$\endgroup\$
    – Alex Angel
    Nov 1 '21 at 21:27
  • 1
    \$\begingroup\$ I didn't want to post answer as I don't know C++ at all, but.. The -m option is missing, mkdir -p foo succeeds even when foo is a regular file, mkdir -p foo/bar prints an error message but actually works, -lboost_options doesn't work on Ubuntu (-lboost_program_options does), and libboost-program-options-dev takes 145 MB of disk space. \$\endgroup\$ Nov 2 '21 at 7:50
  • \$\begingroup\$ @oguzismail: Probably going to be disappointed, but presumably you could statically link libboost-program-options-dev so it wasn't a runtime dependency, and other users of pre-built binaries would only pay for the disk space consumed by the components actually used? \$\endgroup\$ Nov 2 '21 at 14:23
  • \$\begingroup\$ @ShadowRanger That's a huge improvement. The resulting binary is 2.5 megabytes in size (whereas /bin/mkdir is 88 kilobytes), but yeah, far better than having to install such a ridiculously large library for a program with such limited functionality. Thanks \$\endgroup\$ Nov 2 '21 at 14:45
  • 1
    \$\begingroup\$ @oguzismail: Woo! Always nice to learn the library was written well enough that using parts of it doesn't pull the whole thing in for static linkage. If it provides fat LTO static libs, and the whole program is compiled with LTO enabled it might get even smaller (as it can extract just the functions/data it actually uses, inlining where it makes sense, making it even more granular than the "per-object" rules for non-LTO linkage). \$\endgroup\$ Nov 2 '21 at 15:24
7
\$\begingroup\$

Many filesystems allow directory name to start with -. To support such names (not just in the mkdir context, but in general) POSIX-compliant utility shall treat -- as an "end-of-options" option.

Boost supports this idea. Everything after -- is treated as a positional argument. However, as you include_positional while collecting unrecognized, the program still treat such directories as options, and refuses to process them.


Since you use the error_code overload, you should use ec, rather than errno to report errors. And if you do want to use errno, you should not rely on <cerrno> being magically included, but #include <cerrno> explicitly.

\$\endgroup\$
1
  • \$\begingroup\$ I had no idea about the end of options option! also, I will revise it to use std::error_code \$\endgroup\$
    – Alex Angel
    Nov 1 '21 at 21:15
3
\$\begingroup\$

Exit codes

Invoking the help of commands is usually not considered an error, so an exit code of 0 would be expected.

When the real mkdir fails to create a directory, it exits with a non-zero value.

Decompose work to functions

Currently the main function does everything:

  • Argument parsing
  • Perform appropriate actions

If you plan to build this further, it already makes sense to decompose a bit more now.

Unimplemented features

The verbose flag is not implemented ;-)

\$\endgroup\$
0
2
\$\begingroup\$

Use braces around one line in if blocks. See this Q&A and EXP19-C misra rule.

You do not need return 0; at the end. That is the default return value in C++ programs.

\$\endgroup\$
1
  • \$\begingroup\$ Although return 0 is not needed I would never suggest omitting it. Leaving it in make's it clear to the reader what is happening, rather than relying on them knowing the somewhat esoteric rule that int main doesn't need an explicit return value, but every other function that returns an int does. \$\endgroup\$
    – AShelly
    Nov 2 '21 at 18:21
1
\$\begingroup\$

POSIX mkdir also supports the -m option for setting the permissions of the directory as it is created (avoiding race conditions). Consider supporting that as an enhancement.

Since std::filesystem doesn't give expose that aspect of the mkdir() system call, you will need to call it directly (and perhaps #ifdef to support non-POSIX platforms). The alternative is a suitable call to std::filesystem::permissions() immediately after creating the directory, but that leaves a window of time in which the initial default permissions are effective. That's the kind of thing that leads to security bugs, and a weakness in std::filesystem if I'm correct.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.