From 1c6758009e82c47b5b341d418be2be401ef31482 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Fri, 6 Sep 2019 22:20:46 +0300 Subject: Add builtins support --- libbutl/builtin.cxx | 2111 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2111 insertions(+) create mode 100644 libbutl/builtin.cxx (limited to 'libbutl/builtin.cxx') diff --git a/libbutl/builtin.cxx b/libbutl/builtin.cxx new file mode 100644 index 0000000..3340271 --- /dev/null +++ b/libbutl/builtin.cxx @@ -0,0 +1,2111 @@ +// file : libbutl/builtin.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef __cpp_modules_ts +#include +#endif + +#ifdef _WIN32 +# include +#endif + +#include + +#ifndef __cpp_lib_modules_ts +#include +#include +#include +#include +#include // move(), forward() +#include // uint*_t +#include + +#include +#include +#include +#include +#include +#include // strtoull() +#include // strcmp() +#include +#include + +#endif + +#include + +#ifdef __cpp_modules_ts +module butl.builtin; + +// Only imports additional to interface. +#ifdef __clang__ +#ifdef __cpp_lib_modules_ts +import std.core; +import std.io; +import std.threading; +#endif +import butl.path; +import butl.fdstream; +import butl.timestamp; +#endif + +import butl.regex; +import butl.path_io; +import butl.optional; +import butl.filesystem; +import butl.small_vector; +#else +#include +#include +#include +#include +#include +#endif + +// Strictly speaking a builtin which reads/writes from/to standard streams +// must be asynchronous so that the caller can communicate with it through +// pipes without being blocked on I/O operations. However, as an optimization, +// we allow builtins that only print diagnostics to STDERR to be synchronous +// assuming that their output will always fit the pipe buffer. Synchronous +// builtins must not read from STDIN and write to STDOUT. Later we may relax +// this rule to allow a "short" output for such builtins. +// +using namespace std; + +namespace butl +{ + using strings = vector; + using io_error = ios_base::failure; + + using builtin_impl = uint8_t (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks&); + + // Operation failed, diagnostics has already been issued. + // + struct failed {}; + + // Accumulate an error message, print it atomically in dtor to the provided + // stream and throw failed afterwards if requested. Prefixes the message + // with the builtin name. + // + // Move constructible-only, not assignable (based to diag_record). + // + class error_record + { + public: + template + friend const error_record& + operator<< (const error_record& r, const T& x) + { + r.ss_ << x; + return r; + } + + error_record (ostream& o, bool fail, const char* name) + : os_ (o), fail_ (fail), empty_ (false) + { + ss_ << name << ": "; + } + + // Older versions of libstdc++ don't have the ostringstream move support. + // Luckily, GCC doesn't seem to be actually needing move due to copy/move + // elision. + // +#ifdef __GLIBCXX__ + error_record (error_record&&); +#else + error_record (error_record&& r) + : os_ (r.os_), + ss_ (move (r.ss_)), + fail_ (r.fail_), + empty_ (r.empty_) + { + r.empty_ = true; + } +#endif + + ~error_record () noexcept (false) + { + if (!empty_) + { + // The output stream can be in a bad state (for example as a result of + // unsuccessful attempt to report a previous error), so we check it. + // + if (os_.good ()) + { + ss_.put ('\n'); + os_ << ss_.str (); + os_.flush (); + } + + if (fail_) + throw failed (); + } + } + + private: + ostream& os_; + mutable ostringstream ss_; + + bool fail_; + bool empty_; + }; + + // Call a function returning its resulting value. Fail if an exception is + // thrown by the function. + // + template + static inline auto + call (const function& fail, + const function& fn, + A&&... args) -> decltype (fn (forward (args)...)) + { + assert (fn); + + try + { + return fn (forward (args)...); + } + catch (const std::exception& e) + { + fail () << e; + } + catch (...) + { + fail () << "unknown error"; + } + + assert (false); // Can't be here. + throw failed (); + } + + // Parse builtin options. Call the callback to parse unknown options and + // throw cli::unknown_option if the callback is not specified or doesn't + // parse the option. + // + template + static O + parse (cli::vector_scanner& scan, + const strings& args, + const function& parse, + const function& fail) + { + O ops; + + while (true) + { + // Parse the next chunk of options until we reach an argument, --, + // unknown option, or eos. + // + ops.parse (scan, cli::unknown_mode::stop); + + // Bail out on eos. + // + if (!scan.more ()) + break; + + const char* o (scan.peek ()); + + // Bail out on --. + // + if (strcmp (o, "--") == 0) + { + scan.next (); // Skip --. + break; + } + + // Bail out on an argument. + // + if (!(o[0] == '-' && o[1] != '\0')) + break; + + // Parse the unknown option if the callback is specified and fail if + // that's not the case or the callback doesn't recognize the option + // either. + // + size_t n (parse ? call (fail, parse, args, scan.end ()) : 0); + + if (n == 0) + throw cli::unknown_option (o); + + // Skip the parsed arguments and continue. + // + assert (scan.end () + n <= args.size ()); + scan.reset (scan.end () + n); + } + + return ops; + } + + // Parse and normalize a path. Also, unless it is already absolute, make the + // path absolute using the specified directory (must be an absolute path). + // Fail if the path is empty, and on parsing and normalization errors. + // + static path + parse_path (string s, + const dir_path& d, + const function& fail) + { + assert (d.absolute ()); + + try + { + path p (move (s)); + + if (p.empty ()) + throw invalid_path (""); + + if (p.relative ()) + p = d / move (p); + + p.normalize (); + return p; + } + catch (const invalid_path& e) + { + fail () << "invalid path '" << e.path << "'"; + } + + assert (false); // Can't be here. + return path (); + } + + // Return the current working directory if wd is empty and wd otherwise, + // completed against the current directory if it is relative. Fail if + // std::system_error is thrown by the underlying function call. + // + dir_path + current_directory (const dir_path& wd, const function& fail) + { + try + { + if (wd.empty ()) + return dir_path::current_directory (); + + if (wd.relative ()) + return move (dir_path (wd).complete ()); + } + catch (const system_error& e) + { + fail () << "unable to obtain current directory: " << e; + } + + return wd; + } + + // Builtin commands functions. + // + + // cat ... + // + // Note that POSIX doesn't specify if after I/O operation failure the + // command should proceed with the rest of the arguments. The current + // implementation exits immediatelly in such a case. + // + // @@ Shouldn't we check that we don't print a nonempty regular file to + // itself, as that would merely exhaust the output device? POSIX allows + // (but not requires) such a check and some implementations do this. That + // would require to fstat() file descriptors and complicate the code a + // bit. Was able to reproduce on a big file (should be bigger than the + // stream buffer size) with the test `cat file >+file`. + // + // Note: must be executed asynchronously. + // + static uint8_t + cat (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "cat"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + ifdstream cin (in != nullfd ? move (in) : fddup (stdin_fd ()), + fdstream_mode::binary); + + ofdstream cout (out != nullfd ? move (out) : fddup (stdout_fd ()), + fdstream_mode::binary); + + // Parse arguments. + // + cli::vector_scanner scan (args); + parse (scan, args, cbs.parse_option, fail); + + // Print files. + // + // Copy input stream to STDOUT. + // + auto copy = [&cout] (istream& is) + { + if (is.peek () != ifdstream::traits_type::eof ()) + cout << is.rdbuf (); + + is.clear (istream::eofbit); // Sets eofbit. + }; + + // Path of a file being printed to STDOUT. An empty path represents + // STDIN. Used in diagnostics. + // + path p; + + try + { + // Print STDIN. + // + if (!scan.more ()) + copy (cin); + + dir_path wd; + + // Print files. + // + while (scan.more ()) + { + string f (scan.next ()); + + if (f == "-") + { + if (!cin.eof ()) + { + p.clear (); + copy (cin); + } + + continue; + } + + if (wd.empty () && cwd.relative ()) + wd = current_directory (cwd, fail); + + p = parse_path (move (f), !wd.empty () ? wd : cwd, fail); + + ifdstream is (p, fdopen_mode::binary); + copy (is); + is.close (); + } + } + catch (const io_error& e) + { + error_record d (fail ()); + d << "unable to print "; + + if (p.empty ()) + d << "stdin"; + else + d << "'" << p << "'"; + + d << ": " << e; + } + + cin.close (); + cout.close (); + r = 0; + } + // Can be thrown while creating/closing cin, cout or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // Make a copy of a file at the specified path, preserving permissions, and + // calling the hook for a newly created file. The file paths must be + // absolute and normalized. Fail if an exception is thrown by the underlying + // copy operation. + // + static void + cpfile (const path& from, const path& to, + bool overwrite, + bool attrs, + const builtin_callbacks& cbs, + const function& fail) + { + assert (from.absolute () && from.normalized ()); + assert (to.absolute () && to.normalized ()); + + try + { + if (cbs.create) + call (fail, cbs.create, to, true /* pre */); + + cpflags f (overwrite + ? cpflags::overwrite_permissions | cpflags::overwrite_content + : cpflags::none); + + if (attrs) + f |= cpflags::overwrite_permissions | cpflags::copy_timestamps; + + cpfile (from, to, f); + + if (cbs.create) + call (fail, cbs.create, to, false /* pre */); + } + catch (const system_error& e) + { + fail () << "unable to copy file '" << from << "' to '" << to << "': " + << e; + } + } + + // Make a copy of a directory at the specified path, calling the hook for + // the created filesystem entries. The directory paths must be absolute and + // normalized. Fail if the destination directory already exists or an + // exception is thrown by the underlying copy operation. + // + static void + cpdir (const dir_path& from, const dir_path& to, + bool attrs, + const builtin_callbacks& cbs, + const function& fail) + { + assert (from.absolute () && from.normalized ()); + assert (to.absolute () && to.normalized ()); + + try + { + if (cbs.create) + call (fail, cbs.create, to, true /* pre */); + + if (try_mkdir (to) == mkdir_status::already_exists) + throw_generic_error (EEXIST); + + if (cbs.create) + call (fail, cbs.create, to, false /* pre */); + + for (const auto& de: dir_iterator (from, false /* ignore_dangling */)) + { + path f (from / de.path ()); + path t (to / de.path ()); + + if (de.type () == entry_type::directory) + cpdir (path_cast (move (f)), + path_cast (move (t)), + attrs, + cbs, + fail); + else + cpfile (f, t, false /* overwrite */, attrs, cbs, fail); + } + + // Note that it is essential to copy timestamps and permissions after + // the directory content is copied. + // + if (attrs) + { + path_permissions (to, path_permissions (from)); + dir_time (to, dir_time (from)); + } + } + catch (const system_error& e) + { + fail () << "unable to copy directory '" << from << "' to '" << to + << "': " << e; + } + } + + // cp [-p|--preserve] + // cp [-p|--preserve] -R|-r|--recursive + // cp [-p|--preserve] ... / + // cp [-p|--preserve] -R|-r|--recursive ... / + // + // Note: can be executed synchronously. + // + static uint8_t + cp (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "cp"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + cp_options ops (parse (scan, args, cbs.parse_option, fail)); + + // Copy files or directories. + // + if (!scan.more ()) + fail () << "missing arguments"; + + // Note that the arguments semantics depends on the last argument, so we + // read out and cache them. + // + small_vector args; + while (scan.more ()) + args.push_back (scan.next ()); + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + auto i (args.begin ()); + auto j (args.rbegin ()); + path dst (parse_path (move (*j++), wd, fail)); + auto e (j.base ()); + + if (i == e) + fail () << "missing source path"; + + // If destination is not a directory path (no trailing separator) then + // make a copy of the filesystem entry at the specified path (the only + // source path is allowed in such a case). Otherwise copy the source + // filesystem entries into the destination directory. + // + if (!dst.to_directory ()) + { + path src (parse_path (move (*i++), wd, fail)); + + // If there are multiple sources but no trailing separator for the + // destination, then, most likelly, it is missing. + // + if (i != e) + fail () << "multiple source paths without trailing separator for " + << "destination directory"; + + if (!ops.recursive ()) + // Synopsis 1: make a file copy at the specified path. + // + cpfile (src, dst, true /* overwrite */, ops.preserve (), cbs, fail); + else + // Synopsis 2: make a directory copy at the specified path. + // + cpdir (path_cast (src), path_cast (dst), + ops.preserve (), + cbs, + fail); + } + else + { + for (; i != e; ++i) + { + path src (parse_path (move (*i), wd, fail)); + + if (ops.recursive () && dir_exists (src)) + // Synopsis 4: copy a filesystem entry into the specified + // directory. Note that we handle only source directories here. + // Source files are handled below. + // + cpdir (path_cast (src), + path_cast (dst / src.leaf ()), + ops.preserve (), + cbs, + fail); + else + // Synopsis 3: copy a file into the specified directory. Also, + // here we cover synopsis 4 for the source path being a file. + // + cpfile (src, dst / src.leaf (), + true /* overwrite */, + ops.preserve (), + cbs, + fail); + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // echo ... + // + // Note: must be executed asynchronously. + // + static uint8_t + echo (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path&, + const builtin_callbacks&) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + try + { + in.close (); + ofdstream cout (out != nullfd ? move (out) : fddup (stdout_fd ())); + + for (auto b (args.begin ()), i (b), e (args.end ()); i != e; ++i) + cout << (i != b ? " " : "") << *i; + + cout << '\n'; + cout.close (); + r = 0; + } + // Can be thrown while closing cin or creating, writing to, or closing + // cout. + // + catch (const io_error& e) + { + cerr << "echo: " << e << endl; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // false + // + // Failure to close the file descriptors is silently ignored. + // + // Note: can be executed synchronously. + // + static builtin + false_ (uint8_t& r, + const strings&, + auto_fd, auto_fd, auto_fd, + const dir_path&, + const builtin_callbacks&) + { + return builtin (r = 1); + } + + // true + // + // Failure to close the file descriptors is silently ignored. + // + // Note: can be executed synchronously. + // + static builtin + true_ (uint8_t& r, + const strings&, + auto_fd, auto_fd, auto_fd, + const dir_path&, + const builtin_callbacks&) + { + return builtin (r = 0); + } + + // Create a symlink to a file or directory at the specified path and calling + // the hook for the created filesystem entries. The paths must be absolute + // and normalized. Fall back to creating a hardlink, if symlink creation is + // not supported for the link path. If hardlink creation is not supported + // either, then fall back to copies. Fail if the target filesystem entry + // doesn't exist or an exception is thrown by the underlying filesystem + // operation (specifically for an already existing filesystem entry at the + // link path). + // + // Note that supporting optional removal of an existing filesystem entry at + // the link path (the -f option) tends to get hairy. Also removing non-empty + // directories doesn't look very natural, but would be required if we want + // the behavior on POSIX and Windows to be consistent. + // + static void + mksymlink (const path& target, const path& link, + const builtin_callbacks& cbs, + const function& fail) + { + assert (target.absolute () && target.normalized ()); + assert (link.absolute () && link.normalized ()); + + // Determine the target type, fail if the target doesn't exist. + // + bool dir (false); + + try + { + pair pe (path_entry (target)); + + if (!pe.first) + fail () << "unable to create symlink to '" << target << "': no such " + << "file or directory"; + + dir = pe.second.type == entry_type::directory; + } + catch (const system_error& e) + { + fail () << "unable to stat '" << target << "': " << e; + } + + // First we try to create a symlink. If that fails (e.g., "Windows + // happens"), then we resort to hard links. If that doesn't work out + // either (e.g., not on the same filesystem), then we fall back to copies. + // So things are going to get a bit nested. + // + // Note: similar to mkanylink() but with support for directories. + // + try + { + if (cbs.create) + call (fail, cbs.create, link, true /* pre */); + + mksymlink (target, link, dir); + + if (cbs.create) + call (fail, cbs.create, link, false /* pre */); + } + catch (const system_error& e) + { + // Note that we are not guaranteed (here and below) that the + // system_error exception is of the generic category. + // + int c (e.code ().value ()); + if (!(e.code ().category () == generic_category () && + (c == ENOSYS || // Not implemented. + c == EPERM))) // Not supported by the filesystem(s). + fail () << "unable to create symlink '" << link << "' to '" + << target << "': " << e; + + try + { + mkhardlink (target, link, dir); + + if (cbs.create) + call (fail, cbs.create, link, false /* pre */); + } + catch (const system_error& e) + { + c = e.code ().value (); + if (!(e.code ().category () == generic_category () && + (c == ENOSYS || // Not implemented. + c == EPERM || // Not supported by the filesystem(s). + c == EXDEV))) // On different filesystems. + fail () << "unable to create hardlink '" << link << "' to '" + << target << "': " << e; + + if (dir) + cpdir (path_cast (target), path_cast (link), + false /* attrs */, + cbs, + fail); + else + cpfile (target, link, + false /* overwrite */, + true /* attrs */, + cbs, + fail); + } + } + } + + // ln -s|--symbolic + // ln -s|--symbolic ... / + // + // Note: can be executed synchronously. + // + static uint8_t + ln (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "ln"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + ln_options ops (parse (scan, args, cbs.parse_option, fail)); + + if (!ops.symbolic ()) + fail () << "missing -s|--symbolic option"; + + // Create file or directory symlinks. + // + if (!scan.more ()) + fail () << "missing arguments"; + + // Note that the arguments semantics depends on the last argument, so we + // read out and cache them. + // + small_vector args; + while (scan.more ()) + args.push_back (scan.next ()); + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + auto i (args.begin ()); + auto j (args.rbegin ()); + path link (parse_path (move (*j++), wd, fail)); + auto e (j.base ()); + + if (i == e) + fail () << "missing target path"; + + // If link is not a directory path (no trailing separator), then create + // a symlink to the target path at the specified link path (the only + // target path is allowed in such a case). Otherwise create links to the + // target paths inside the specified directory. + // + if (!link.to_directory ()) + { + path target (parse_path (move (*i++), wd, fail)); + + // If there are multiple targets but no trailing separator for the + // link, then, most likelly, it is missing. + // + if (i != e) + fail () << "multiple target paths with non-directory link path"; + + // Synopsis 1: create a target path symlink at the specified path. + // + mksymlink (target, link, cbs, fail); + } + else + { + for (; i != e; ++i) + { + path target (parse_path (move (*i), wd, fail)); + + // Synopsis 2: create a target path symlink in the specified + // directory. + // + mksymlink (target, link / target.leaf (), cbs, fail); + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // Create a directory if not exist and its parent directories if necessary, + // calling the hook for the created directories. The directory path must be + // absolute and normalized. Throw system_error on failure. + // + static void + mkdir_p (const dir_path& p, + const builtin_callbacks& cbs, + const function& fail) + { + assert (p.absolute () && p.normalized ()); + + if (!dir_exists (p)) + { + if (!p.root ()) + mkdir_p (p.directory (), cbs, fail); + + if (cbs.create) + call (fail, cbs.create, p, true /* pre */); + + try_mkdir (p); // Returns success or throws. + + if (cbs.create) + call (fail, cbs.create, p, false /* pre */); + } + } + + // mkdir [-p|--parents] ... + // + // Note that POSIX doesn't specify if after a directory creation failure the + // command should proceed with the rest of the arguments. The current + // implementation exits immediatelly in such a case. + // + // Note: can be executed synchronously. + // + static uint8_t + mkdir (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "mkdir"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + + mkdir_options ops ( + parse (scan, args, cbs.parse_option, fail)); + + // Create directories. + // + if (!scan.more ()) + fail () << "missing directory"; + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + while (scan.more ()) + { + dir_path p ( + path_cast (parse_path (scan.next (), wd, fail))); + + try + { + if (ops.parents ()) + mkdir_p (p, cbs, fail); + else + { + if (cbs.create) + call (fail, cbs.create, p, true /* pre */); + + if (try_mkdir (p) == mkdir_status::success) + { + if (cbs.create) + call (fail, cbs.create, p, false /* pre */); + } + else // == mkdir_status::already_exists + throw_generic_error (EEXIST); + } + } + catch (const system_error& e) + { + fail () << "unable to create directory '" << p << "': " << e; + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // mv [-f|--force] + // mv [-f|--force] ... / + // + // Note: can be executed synchronously. + // + static uint8_t + mv (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "mv"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + mv_options ops (parse (scan, args, cbs.parse_option, fail)); + + // Move filesystem entries. + // + if (!scan.more ()) + fail () << "missing arguments"; + + // Note that the arguments semantics depends on the last argument, so we + // read out and cache them. + // + small_vector args; + while (scan.more ()) + args.push_back (scan.next ()); + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + auto i (args.begin ()); + auto j (args.rbegin ()); + path dst (parse_path (move (*j++), wd, fail)); + auto e (j.base ()); + + if (i == e) + fail () << "missing source path"; + + auto mv = [ops, &fail, cbs] (const path& from, const path& to) + { + if (cbs.move) + call (fail, cbs.move, from, to, ops.force (), true /* pre */); + + try + { + bool exists (entry_exists (to)); + + // Fail if the source and destination paths are the same. + // + // Note that for mventry() function (that is based on the POSIX + // rename() function) this is a noop. + // + if (exists && to == from) + fail () << "unable to move entity '" << from << "' to itself"; + + // Rename/move the filesystem entry, replacing an existing one. + // + mventry (from, to, + cpflags::overwrite_permissions | + cpflags::overwrite_content); + + if (cbs.move) + call (fail, cbs.move, from, to, ops.force (), false /* pre */); + } + catch (const system_error& e) + { + fail () << "unable to move entity '" << from << "' to '" << to + << "': " << e; + } + }; + + // If destination is not a directory path (no trailing separator) then + // move the filesystem entry to the specified path (the only source path + // is allowed in such a case). Otherwise move the source filesystem + // entries into the destination directory. + // + if (!dst.to_directory ()) + { + path src (parse_path (move (*i++), wd, fail)); + + // If there are multiple sources but no trailing separator for the + // destination, then, most likelly, it is missing. + // + if (i != e) + fail () << "multiple source paths without trailing separator for " + << "destination directory"; + + // Synopsis 1: move an entity to the specified path. + // + mv (src, dst); + } + else + { + // Synopsis 2: move entities into the specified directory. + // + for (; i != e; ++i) + { + path src (parse_path (move (*i), wd, fail)); + mv (src, dst / src.leaf ()); + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // rm [-r|--recursive] [-f|--force] ... + // + // The implementation deviates from POSIX in a number of ways. It doesn't + // interact with a user and fails immediatelly if unable to process an + // argument. It doesn't check for dots containment in the path, and doesn't + // consider files and directory permissions in any way just trying to remove + // a filesystem entry. Always fails if empty path is specified. + // + // Note: can be executed synchronously. + // + static uint8_t + rm (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "rm"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + rm_options ops (parse (scan, args, cbs.parse_option, fail)); + + // Remove entries. + // + if (!scan.more () && !ops.force ()) + fail () << "missing file"; + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + while (scan.more ()) + { + path p (parse_path (scan.next (), wd, fail)); + + if (cbs.remove) + call (fail, cbs.remove, p, ops.force (), true /* pre */); + + try + { + dir_path d (path_cast (p)); + + pair es (path_entry (d)); + if (es.first && es.second.type == entry_type::directory) + { + if (!ops.recursive ()) + fail () << "'" << p << "' is a directory"; + + // The call can result in rmdir_status::not_exist. That's not very + // likelly but there is also nothing bad about it. + // + try_rmdir_r (d); + } + else if (try_rmfile (p) == rmfile_status::not_exist && + !ops.force ()) + throw_generic_error (ENOENT); + + if (cbs.remove) + call (fail, cbs.remove, p, ops.force (), false /* pre */); + } + catch (const system_error& e) + { + fail () << "unable to remove '" << p << "': " << e; + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // rmdir [-f|--force] ... + // + // Note: can be executed synchronously. + // + static uint8_t + rmdir (const strings& args, + auto_fd in, auto_fd out, auto_fd err, + const dir_path& cwd, + const builtin_callbacks& cbs) noexcept + try + { + uint8_t r (1); + ofdstream cerr (err != nullfd ? move (err) : fddup (stderr_fd ())); + + auto error = [&cerr] (bool fail = false) + { + return error_record (cerr, fail, "rmdir"); + }; + + auto fail = [&error] () {return error (true /* fail */);}; + + try + { + in.close (); + out.close (); + + // Parse arguments. + // + cli::vector_scanner scan (args); + + rmdir_options ops ( + parse (scan, args, cbs.parse_option, fail)); + + // Remove directories. + // + if (!scan.more () && !ops.force ()) + fail () << "missing directory"; + + const dir_path& wd (cwd.absolute () + ? cwd + : current_directory (cwd, fail)); + + while (scan.more ()) + { + dir_path p ( + path_cast (parse_path (scan.next (), wd, fail))); + + if (cbs.remove) + call (fail, cbs.remove, p, ops.force (), true /* pre */); + + try + { + rmdir_status s (try_rmdir (p)); + + if (s == rmdir_status::not_empty) + throw_generic_error (ENOTEMPTY); + else if (s == rmdir_status::not_exist && !ops.force ()) + throw_generic_error (ENOENT); + + if (cbs.remove) + call (fail, cbs.remove, p, ops.force (), false /* pre */); + } + catch (const system_error& e) + { + fail () << "unable to remove '" << p << "': " << e; + } + } + + r = 0; + } + // Can be thrown while closing in, out or writing to cerr. + // + catch (const io_error& e) + { + error () << e; + } + catch (const failed&) + { + // Diagnostics has already been issued. + } + catch (const cli::exception& e) + { + error () << e; + } + + cerr.close (); + return r; + } + // In particular, handles io_error exception potentially thrown while + // creating, writing to, or closing cerr. + // + catch (const std::exception&) + { + return 1; + } + + // sed [-n|--quiet] [-i|--in-place] -e|--expression