// 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&,
             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).
          //
          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_}
      };
    }
  }
}