diff options
Diffstat (limited to 'libbuild2/test/script/builtin.cxx')
-rw-r--r-- | libbuild2/test/script/builtin.cxx | 1924 |
1 files changed, 0 insertions, 1924 deletions
diff --git a/libbuild2/test/script/builtin.cxx b/libbuild2/test/script/builtin.cxx deleted file mode 100644 index 06b0cec..0000000 --- a/libbuild2/test/script/builtin.cxx +++ /dev/null @@ -1,1924 +0,0 @@ -// file : libbuild2/test/script/builtin.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include <libbuild2/test/script/builtin.hxx> - -#include <chrono> -#include <locale> -#include <ostream> -#include <sstream> -#include <cstdlib> // strtoull() - -#include <libbutl/regex.mxx> -#include <libbutl/path-io.mxx> // use default operator<< implementation -#include <libbutl/fdstream.mxx> // fdopen_mode, fdstream_mode -#include <libbutl/filesystem.mxx> - -#include <libbuild2/context.hxx> // sched - -#include <libbuild2/test/script/script.hxx> -#include <libbuild2/test/script/builtin-options.hxx> - -// 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; -using namespace butl; - -namespace build2 -{ - namespace test - { - namespace script - { - using builtin_impl = uint8_t (scope&, - const strings& args, - auto_fd in, auto_fd out, auto_fd err); - - // 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 <typename T> - 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_; - }; - - // Parse and normalize a path. Also, unless it is already absolute, make - // the path absolute using the specified directory. Throw invalid_path - // if the path is empty, and on parsing and normalization failures. - // - static path - parse_path (string s, const dir_path& d) - { - path p (move (s)); - - if (p.empty ()) - throw invalid_path (""); - - if (p.relative ()) - p = d / move (p); - - p.normalize (); - return p; - } - - // Builtin commands functions. - // - - // cat <file>... - // - // 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 (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "cat"); - }; - - try - { - ifdstream cin (move (in), fdstream_mode::binary); - ofdstream cout (move (out), fdstream_mode::binary); - - // Parse arguments. - // - cli::vector_scanner scan (args); - cat_options ops (scan); // Makes sure no options passed. - - // 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); - - // Print files. - // - while (scan.more ()) - { - string f (scan.next ()); - - if (f == "-") - { - if (!cin.eof ()) - { - p.clear (); - copy (cin); - } - - continue; - } - - p = parse_path (move (f), sp.wd_path); - - ifdstream is (p, fdopen_mode::binary); - copy (is); - is.close (); - } - } - catch (const io_error& e) - { - error_record d (error ()); - d << "unable to print "; - - if (p.empty ()) - d << "stdin"; - else - d << "'" << p << "'"; - - d << ": " << e; - } - - cin.close (); - cout.close (); - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while creating/closing cin, cout or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // Make a copy of a file at the specified path, preserving permissions, - // and registering a cleanup for a newly created file. The file paths - // must be absolute. Fail if an exception is thrown by the underlying - // copy operation. - // - static void - cpfile (scope& sp, - const path& from, const path& to, - bool overwrite, - bool attrs, - bool cleanup, - const function<error_record()>& fail) - { - try - { - bool exists (file_exists (to)); - - 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 (!exists && cleanup) - sp.clean ({cleanup_type::always, to}, true /* implicit */); - } - catch (const system_error& e) - { - fail () << "unable to copy file '" << from << "' to '" << to - << "': " << e; - } - } - - // Make a copy of a directory at the specified path, registering a - // cleanup for the created directory. The directory paths must be - // absolute. Fail if the destination directory already exists or - // an exception is thrown by the underlying copy operation. - // - static void - cpdir (scope& sp, - const dir_path& from, const dir_path& to, - bool attrs, - bool cleanup, - const function<error_record()>& fail) - { - try - { - if (try_mkdir (to) == mkdir_status::already_exists) - throw_generic_error (EEXIST); - - if (cleanup) - sp.clean ({cleanup_type::always, to}, true /* implicit */); - - 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 (sp, - path_cast<dir_path> (move (f)), - path_cast<dir_path> (move (t)), - attrs, - cleanup, - fail); - else - cpfile (sp, f, t, false /* overwrite */, attrs, cleanup, 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] [--no-cleanup] <src-file> <dst-file> - // cp [-p|--preserve] [--no-cleanup] -R|-r|--recursive <src-dir> <dst-dir> - // cp [-p|--preserve] [--no-cleanup] <src-file>... <dst-dir>/ - // cp [-p|--preserve] [--no-cleanup] -R|-r|--recursive <src-path>... <dst-dir>/ - // - // Note: can be executed synchronously. - // - static uint8_t - cp (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "cp"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - cp_options ops (scan); - - // Copy files or directories. - // - if (!scan.more ()) - error () << "missing arguments"; - - // Note that the arguments semantics depends on the last argument, - // so we read out and cache them. - // - small_vector<string, 2> args; - while (scan.more ()) - args.push_back (scan.next ()); - - const dir_path& wd (sp.wd_path); - - auto i (args.begin ()); - auto j (args.rbegin ()); - path dst (parse_path (move (*j++), wd)); - auto e (j.base ()); - - if (i == e) - error () << "missing source path"; - - auto fail = [&error] () {return error (true);}; - - bool cleanup (!ops.no_cleanup ()); - - // 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)); - - // If there are multiple sources but no trailing separator for the - // destination, then, most likelly, it is missing. - // - if (i != e) - error () << "multiple source paths without trailing separator " - << "for destination directory"; - - if (!ops.recursive ()) - // Synopsis 1: make a file copy at the specified path. - // - cpfile (sp, - src, - dst, - true /* overwrite */, - ops.preserve (), - cleanup, - fail); - else - // Synopsis 2: make a directory copy at the specified path. - // - cpdir (sp, - path_cast<dir_path> (src), path_cast<dir_path> (dst), - ops.preserve (), - cleanup, - fail); - } - else - { - for (; i != e; ++i) - { - path src (parse_path (move (*i), wd)); - - 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 (sp, - path_cast<dir_path> (src), - path_cast<dir_path> (dst / src.leaf ()), - ops.preserve (), - cleanup, - 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 (sp, - src, - dst / src.leaf (), - true /* overwrite */, - ops.preserve (), - cleanup, - fail); - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // echo <string>... - // - // Note: must be executed asynchronously. - // - static uint8_t - echo (scope&, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - try - { - in.close (); - ofdstream cout (move (out)); - - for (auto b (args.begin ()), i (b), e (args.end ()); i != e; ++i) - cout << (i != b ? " " : "") << *i; - - cout << '\n'; - cout.close (); - r = 0; - } - catch (const std::exception& e) - { - cerr << "echo: " << e << endl; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // false - // - // Failure to close the file descriptors is silently ignored. - // - // Note: can be executed synchronously. - // - static builtin - false_ (scope&, uint8_t& r, const strings&, auto_fd, auto_fd, auto_fd) - { - return builtin (r = 1); - } - - // true - // - // Failure to close the file descriptors is silently ignored. - // - // Note: can be executed synchronously. - // - static builtin - true_ (scope&, uint8_t& r, const strings&, auto_fd, auto_fd, auto_fd) - { - return builtin (r = 0); - } - - // Create a symlink to a file or directory at the specified path. The - // paths must be absolute. 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. If requested, created - // filesystem entries are registered for cleanup. 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. As soon as an - // existing and the resulting filesystem entries could be of different - // types, we would end up with canceling an old cleanup and registering - // the new one. 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 (scope& sp, - const path& target, const path& link, - bool cleanup, - const function<error_record()>& fail) - { - // Determine the target type, fail if the target doesn't exist. - // - bool dir (false); - - try - { - pair<bool, entry_stat> 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 - { - mksymlink (target, link, dir); - - if (cleanup) - sp.clean ({cleanup_type::always, link}, true /* implicit */); - } - 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 (cleanup) - sp.clean ({cleanup_type::always, link}, true /* implicit */); - } - 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 (sp, - path_cast<dir_path> (target), path_cast<dir_path> (link), - false, - cleanup, - fail); - else - cpfile (sp, - target, - link, - false /* overwrite */, - true /* attrs */, - cleanup, - fail); - } - } - } - - // ln [--no-cleanup] -s|--symbolic <target-path> <link-path> - // ln [--no-cleanup] -s|--symbolic <target-path>... <link-dir>/ - // - // Note: can be executed synchronously. - // - static uint8_t - ln (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "ln"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - ln_options ops (scan); - - if (!ops.symbolic ()) - error () << "missing -s|--symbolic option"; - - // Create file or directory symlinks. - // - if (!scan.more ()) - error () << "missing arguments"; - - // Note that the arguments semantics depends on the last argument, - // so we read out and cache them. - // - small_vector<string, 2> args; - while (scan.more ()) - args.push_back (scan.next ()); - - const dir_path& wd (sp.wd_path); - - auto i (args.begin ()); - auto j (args.rbegin ()); - path link (parse_path (move (*j++), wd)); - auto e (j.base ()); - - if (i == e) - error () << "missing target path"; - - auto fail = [&error] () {return error (true);}; - - bool cleanup (!ops.no_cleanup ()); - - // 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)); - - // If there are multiple targets but no trailing separator for the - // link, then, most likelly, it is missing. - // - if (i != e) - error () << "multiple target paths with non-directory link path"; - - // Synopsis 1: create a target path symlink at the specified path. - // - mksymlink (sp, target, link, cleanup, fail); - } - else - { - for (; i != e; ++i) - { - path target (parse_path (move (*i), wd)); - - // Synopsis 2: create a target path symlink in the specified - // directory. - // - mksymlink (sp, target, link / target.leaf (), cleanup, fail); - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // Create a directory if not exist and its parent directories if - // necessary. Throw system_error on failure. Register created - // directories for cleanup. The directory path must be absolute. - // - static void - mkdir_p (scope& sp, const dir_path& p, bool cleanup) - { - if (!dir_exists (p)) - { - if (!p.root ()) - mkdir_p (sp, p.directory (), cleanup); - - try_mkdir (p); // Returns success or throws. - - if (cleanup) - sp.clean ({cleanup_type::always, p}, true /* implicit */); - } - } - - // mkdir [--no-cleanup] [-p|--parents] <dir>... - // - // 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 (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "mkdir"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - mkdir_options ops (scan); - - // Create directories. - // - if (!scan.more ()) - error () << "missing directory"; - - bool cleanup (!ops.no_cleanup ()); - - while (scan.more ()) - { - dir_path p (path_cast<dir_path> (parse_path (scan.next (), - sp.wd_path))); - - try - { - if (ops.parents ()) - mkdir_p (sp, p, cleanup); - else if (try_mkdir (p) == mkdir_status::success) - { - if (cleanup) - sp.clean ({cleanup_type::always, p}, true /* implicit */); - } - else // == mkdir_status::already_exists - throw_generic_error (EEXIST); - } - catch (const system_error& e) - { - error () << "unable to create directory '" << p << "': " << e; - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // mv [--no-cleanup] [-f|--force] <src-path> <dst-path> - // mv [--no-cleanup] [-f|--force] <src-path>... <dst-dir>/ - // - // Note: can be executed synchronously. - // - static uint8_t - mv (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "mv"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - mv_options ops (scan); - - // Move filesystem entries. - // - if (!scan.more ()) - error () << "missing arguments"; - - // Note that the arguments semantics depends on the last argument, - // so we read out and cache them. - // - small_vector<string, 2> args; - while (scan.more ()) - args.push_back (scan.next ()); - - const dir_path& wd (sp.wd_path); - - auto i (args.begin ()); - auto j (args.rbegin ()); - path dst (parse_path (move (*j++), wd)); - auto e (j.base ()); - - if (i == e) - error () << "missing source path"; - - auto mv = [ops, &wd, &sp, &error] (const path& from, const path& to) - { - const dir_path& rwd (sp.root.wd_path); - - if (!from.sub (rwd) && !ops.force ()) - error () << "'" << from << "' is out of working directory '" - << rwd << "'"; - - try - { - auto check_wd = [&wd, &error] (const path& p) - { - if (wd.sub (path_cast<dir_path> (p))) - error () << "'" << p << "' contains test working directory '" - << wd << "'"; - }; - - check_wd (from); - check_wd (to); - - bool exists (butl::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) - error () << "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); - - // Unless suppressed, adjust the cleanups that are sub-paths of - // the source path. - // - if (!ops.no_cleanup ()) - { - // "Move" the matching cleanup if the destination path doesn't - // exist and is a sub-path of the working directory. Otherwise - // just remove it. - // - // Note that it's not enough to just change the cleanup paths. - // We also need to make sure that these cleanups happen before - // the destination directory (or any of its parents) cleanup, - // that is potentially registered. To achieve that we can just - // relocate these cleanup entries to the end of the list, - // preserving their mutual order. Remember that cleanups in - // the list are executed in the reversed order. - // - bool mv_cleanups (!exists && to.sub (rwd)); - cleanups cs; - - // Remove the source path sub-path cleanups from the list, - // adjusting/caching them if required (see above). - // - for (auto i (sp.cleanups.begin ()); i != sp.cleanups.end (); ) - { - cleanup& c (*i); - path& p (c.path); - - if (p.sub (from)) - { - if (mv_cleanups) - { - // Note that we need to preserve the cleanup path - // trailing separator which indicates the removal - // method. Also note that leaf(), in particular, does - // that. - // - p = p != from - ? to / p.leaf (path_cast<dir_path> (from)) - : p.to_directory () - ? path_cast<dir_path> (to) - : to; - - cs.push_back (move (c)); - } - - i = sp.cleanups.erase (i); - } - else - ++i; - } - - // Re-insert the adjusted cleanups at the end of the list. - // - sp.cleanups.insert (sp.cleanups.end (), - make_move_iterator (cs.begin ()), - make_move_iterator (cs.end ())); - } - } - catch (const system_error& e) - { - error () << "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)); - - // If there are multiple sources but no trailing separator for the - // destination, then, most likelly, it is missing. - // - if (i != e) - error () << "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)); - mv (src, dst / src.leaf ()); - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // rm [-r|--recursive] [-f|--force] <path>... - // - // 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 (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "rm"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - rm_options ops (scan); - - // Remove entries. - // - if (!scan.more () && !ops.force ()) - error () << "missing file"; - - const dir_path& wd (sp.wd_path); - const dir_path& rwd (sp.root.wd_path); - - while (scan.more ()) - { - path p (parse_path (scan.next (), wd)); - - if (!p.sub (rwd) && !ops.force ()) - error () << "'" << p << "' is out of working directory '" << rwd - << "'"; - - try - { - dir_path d (path_cast<dir_path> (p)); - - if (dir_exists (d)) - { - if (!ops.recursive ()) - error () << "'" << p << "' is a directory"; - - if (wd.sub (d)) - error () << "'" << p << "' contains test working directory '" - << wd << "'"; - - // 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); - } - catch (const system_error& e) - { - error () << "unable to remove '" << p << "': " << e; - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // rmdir [-f|--force] <path>... - // - // Note: can be executed synchronously. - // - static uint8_t - rmdir (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "rmdir"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - rmdir_options ops (scan); - - // Remove directories. - // - if (!scan.more () && !ops.force ()) - error () << "missing directory"; - - const dir_path& wd (sp.wd_path); - const dir_path& rwd (sp.root.wd_path); - - while (scan.more ()) - { - dir_path p (path_cast<dir_path> (parse_path (scan.next (), wd))); - - if (wd.sub (p)) - error () << "'" << p << "' contains test working directory '" - << wd << "'"; - - if (!p.sub (rwd) && !ops.force ()) - error () << "'" << p << "' is out of working directory '" - << rwd << "'"; - - 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); - } - catch (const system_error& e) - { - error () << "unable to remove '" << p << "': " << e; - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // sed [-n|--quiet] [-i|--in-place] -e|--expression <script> [<file>] - // - // Note: must be executed asynchronously. - // - static uint8_t - sed (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "sed"); - }; - - try - { - // Automatically remove a temporary file (used for in place editing) - // on failure. - // - auto_rmfile rm; - - // Do not throw when failbit is set (getline() failed to extract any - // character). - // - ifdstream cin (move (in), ifdstream::badbit); - ofdstream cout (move (out)); - - // Parse arguments. - // - cli::vector_scanner scan (args); - sed_options ops (scan); - - if (ops.expression ().empty ()) - error () << "missing script"; - - // Only a single script is supported. - // - if (ops.expression ().size () != 1) - error () << "multiple scripts"; - - struct - { - string regex; - string replacement; - bool icase = false; - bool global = false; - bool print = false; - } subst; - - { - const string& v (ops.expression ()[0]); - if (v.empty ()) - error () << "empty script"; - - if (v[0] != 's') - error () << "only 's' command supported"; - - // Parse the substitute command. - // - if (v.size () < 2) - error () << "no delimiter for 's' command"; - - char delim (v[1]); - if (delim == '\\' || delim == '\n') - error () << "invalid delimiter for 's' command"; - - size_t p (v.find (delim, 2)); - if (p == string::npos) - error () << "unterminated 's' command regex"; - - subst.regex.assign (v, 2, p - 2); - - // Empty regex matches nothing, so not of much use. - // - if (subst.regex.empty ()) - error () << "empty regex in 's' command"; - - size_t b (p + 1); - p = v.find (delim, b); - if (p == string::npos) - error () << "unterminated 's' command replacement"; - - subst.replacement.assign (v, b, p - b); - - // Parse the substitute command flags. - // - char c; - for (++p; (c = v[p]) != '\0'; ++p) - { - switch (c) - { - case 'i': subst.icase = true; break; - case 'g': subst.global = true; break; - case 'p': subst.print = true; break; - default: - { - error () << "invalid 's' command flag '" << c << "'"; - } - } - } - } - - // Path of a file to edit. An empty path represents stdin. - // - path p; - if (scan.more ()) - { - string f (scan.next ()); - - if (f != "-") - p = parse_path (move (f), sp.wd_path); - } - - if (scan.more ()) - error () << "unexpected argument '" << scan.next () << "'"; - - // Edit file. - // - // If we edit file in place make sure that the file path is specified - // and obtain a temporary file path. We will be writing to the - // temporary file (rather than to stdout) and will move it to the - // original file path afterwards. - // - path tp; - if (ops.in_place ()) - { - if (p.empty ()) - error () << "-i|--in-place option specified while reading from " - << "stdin"; - - try - { - tp = path::temp_path ("build2-sed"); - - cout.close (); // Flush and close. - - cout.open ( - fdopen (tp, - fdopen_mode::out | fdopen_mode::truncate | - fdopen_mode::create, - path_permissions (p))); - } - catch (const io_error& e) - { - error_record d (error ()); - d << "unable to open '" << tp << "': " << e; - } - catch (const system_error& e) - { - error_record d (error ()); - d << "unable to obtain temporary file: " << e; - } - - rm = auto_rmfile (tp); - } - - // Note that ECMAScript is implied if no grammar flag is specified. - // - regex re (subst.regex, - subst.icase ? regex::icase : regex::ECMAScript); - - // Edit a file or STDIN. - // - try - { - // Open a file if specified. - // - if (!p.empty ()) - { - cin.close (); // Flush and close. - cin.open (p); - } - - // Read until failbit is set (throw on badbit). - // - string s; - while (getline (cin, s)) - { - auto r (regex_replace_search ( - s, - re, - subst.replacement, - subst.global - ? regex_constants::format_default - : regex_constants::format_first_only)); - - // Add newline regardless whether the source line is newline- - // terminated or not (in accordance with POSIX). - // - if (!ops.quiet () || (r.second && subst.print)) - cout << r.first << '\n'; - } - - cin.close (); - cout.close (); - - if (ops.in_place ()) - { - mvfile ( - tp, p, - cpflags::overwrite_content | cpflags::overwrite_permissions); - - rm.cancel (); - } - - r = 0; - } - catch (const io_error& e) - { - error_record d (error ()); - d << "unable to edit "; - - if (p.empty ()) - d << "stdin"; - else - d << "'" << p << "'"; - - d << ": " << e; - } - } - catch (const regex_error& e) - { - // Print regex_error description if meaningful (no space). - // - error (false) << "invalid regex" << e; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while creating cin, cout or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const system_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // sleep <seconds> - // - // Note: can be executed synchronously. - // - static uint8_t - sleep (scope& s, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "sleep"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - sleep_options ops (scan); // Makes sure no options passed. - - if (!scan.more ()) - error () << "missing time interval"; - - uint64_t n; - - for (;;) // Breakout loop. - { - string a (scan.next ()); - - // Note: strtoull() allows these. - // - if (!a.empty () && a[0] != '-' && a[0] != '+') - { - char* e (nullptr); - n = strtoull (a.c_str (), &e, 10); // Can't throw. - - if (errno != ERANGE && e == a.c_str () + a.size ()) - break; - } - - error () << "invalid time interval '" << a << "'"; - } - - if (scan.more ()) - error () << "unexpected argument '" << scan.next () << "'"; - - // Sleep. - // - // If/when required we could probably support the precise sleep mode - // (e.g., via an option). - // - s.root.test_target.ctx.sched.sleep (chrono::seconds (n)); - - r = 0; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // test (-f|--file)|(-d|--directory) <path> - // - // Note: can be executed synchronously. - // - static uint8_t - test (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (2); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "test"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - test_options ops (scan); // Makes sure no options passed. - - if (!ops.file () && !ops.directory ()) - error () << "either -f|--file or -d|--directory must be specified"; - - if (ops.file () && ops.directory ()) - error () << "both -f|--file and -d|--directory specified"; - - if (!scan.more ()) - error () << "missing path"; - - path p (parse_path (scan.next (), sp.wd_path)); - - if (scan.more ()) - error () << "unexpected argument '" << scan.next () << "'"; - - try - { - r = (ops.file () ? file_exists (p) : dir_exists (p)) ? 0 : 1; - } - catch (const system_error& e) - { - error () << "cannot test '" << p << "': " << e; - } - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 2; - } - - // touch [--no-cleanup] [--after <ref-file>] <file>... - // - // Note that POSIX doesn't specify the behavior for touching an entry - // other than file. - // - // Also note that POSIX doesn't specify if after a file touch 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 - touch (scope& sp, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) noexcept - try - { - uint8_t r (1); - ofdstream cerr (move (err)); - - auto error = [&cerr] (bool fail = true) - { - return error_record (cerr, fail, "touch"); - }; - - try - { - in.close (); - out.close (); - - // Parse arguments. - // - cli::vector_scanner scan (args); - touch_options ops (scan); - - auto mtime = [&error] (const path& p) -> timestamp - { - try - { - timestamp t (file_mtime (p)); - - if (t == timestamp_nonexistent) - throw_generic_error (ENOENT); - - return t; - } - catch (const system_error& e) - { - error () << "cannot obtain file '" << p - << "' modification time: " << e; - } - assert (false); // Can't be here. - return timestamp (); - }; - - optional<timestamp> after; - if (ops.after_specified ()) - after = mtime (parse_path (ops.after (), sp.wd_path)); - - if (!scan.more ()) - error () << "missing file"; - - // Create files. - // - while (scan.more ()) - { - path p (parse_path (scan.next (), sp.wd_path)); - - try - { - // Note that we don't register (implicit) cleanup for an - // existing path. - // - if (touch_file (p) && !ops.no_cleanup ()) - sp.clean ({cleanup_type::always, p}, true /* implicit */); - - if (after) - { - while (mtime (p) <= *after) - touch_file (p, false /* create */); - } - } - catch (const system_error& e) - { - error () << "cannot create/update '" << p << "': " << e; - } - } - - r = 0; - } - catch (const invalid_path& e) - { - error (false) << "invalid path '" << e.path << "'"; - } - // Can be thrown while closing in, out or writing to cerr. - // - catch (const io_error& e) - { - error (false) << e; - } - catch (const failed&) - { - // Diagnostics has already been issued. - } - catch (const cli::exception& e) - { - error (false) << e; - } - - cerr.close (); - return r; - } - catch (const std::exception&) - { - return 1; - } - - // Run builtin implementation asynchronously. - // - static builtin - async_impl (builtin_impl* fn, - scope& sp, - uint8_t& r, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) - { - return builtin ( - r, - thread ([fn, &sp, &r, &args, - in = move (in), - out = move (out), - err = move (err)] () mutable noexcept - { - r = fn (sp, args, move (in), move (out), move (err)); - })); - } - - template <builtin_impl fn> - static builtin - async_impl (scope& sp, - uint8_t& r, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) - { - return async_impl (fn, sp, r, args, move (in), move (out), move (err)); - } - - // Run builtin implementation synchronously. - // - template <builtin_impl fn> - static builtin - sync_impl (scope& sp, - uint8_t& r, - const strings& args, - auto_fd in, auto_fd out, auto_fd err) - { - r = fn (sp, args, move (in), move (out), move (err)); - return builtin (r, thread ()); - } - - const builtin_map builtins - { - {"cat", &async_impl<&cat>}, - {"cp", &sync_impl<&cp>}, - {"echo", &async_impl<&echo>}, - {"false", &false_}, - {"ln", &sync_impl<&ln>}, - {"mkdir", &sync_impl<&mkdir>}, - {"mv", &sync_impl<&mv>}, - {"rm", &sync_impl<&rm>}, - {"rmdir", &sync_impl<&rmdir>}, - {"sed", &async_impl<&sed>}, - {"sleep", &sync_impl<&sleep>}, - {"test", &sync_impl<&test>}, - {"touch", &sync_impl<&touch>}, - {"true", &true_} - }; - } - } -} |