aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build2/test/script/builtin.cxx225
-rw-r--r--doc/testscript.cli63
-rw-r--r--tests/test/script/builtin/mv.test252
3 files changed, 530 insertions, 10 deletions
diff --git a/build2/test/script/builtin.cxx b/build2/test/script/builtin.cxx
index e912079..d0b8b38 100644
--- a/build2/test/script/builtin.cxx
+++ b/build2/test/script/builtin.cxx
@@ -11,7 +11,7 @@
#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> // mkdir_status
+#include <libbutl/filesystem.mxx>
#include <build2/test/script/script.hxx>
@@ -909,6 +909,228 @@ namespace build2
return 1;
}
+ // mv [--no-cleanup] <src-path> <dst-path>
+ // mv [--no-cleanup] <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 ();
+
+ auto i (args.begin ());
+ auto e (args.end ());
+
+ // Process options.
+ //
+ bool no_cleanup (false);
+ bool force (false);
+ for (; i != e; ++i)
+ {
+ const string& o (*i);
+
+ if (o == "--no-cleanup")
+ no_cleanup = true;
+ else if (*i == "-f")
+ force = true;
+ else
+ {
+ if (o == "--")
+ ++i;
+
+ break;
+ }
+ }
+
+ // Move filesystem entries.
+ //
+ if (i == e)
+ error () << "missing arguments";
+
+ const dir_path& wd (sp.wd_path);
+
+ auto j (args.rbegin ());
+ path dst (parse_path (*j++, wd));
+ e = j.base ();
+
+ if (i == e)
+ error () << "missing source path";
+
+ auto mv = [no_cleanup, force, &wd, &sp, &error] (const path& from,
+ const path& to)
+ {
+ const dir_path& rwd (sp.root->wd_path);
+
+ if (!from.sub (rwd) && !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 (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 (!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 (*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 (*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.
+ }
+
+ cerr.close ();
+ return r;
+ }
+ catch (const std::exception&)
+ {
+ return 1;
+ }
+
// rm [-r] [-f] <path>...
//
// The implementation deviates from POSIX in a number of ways. It doesn't
@@ -1630,6 +1852,7 @@ namespace build2
{"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>},
diff --git a/doc/testscript.cli b/doc/testscript.cli
index 2cc843d..ad5557d 100644
--- a/doc/testscript.cli
+++ b/doc/testscript.cli
@@ -2310,7 +2310,7 @@ if the \i{dst-dir/src-name} filesystem entry already exists.
Copy permissions as well as modification and access times.||
-Unless the --no-cleanup option is specified, newly created files and
+Unless the \c{--no-cleanup} option is specified, newly created files and
directories that are inside the script working directory are automatically
registered for cleanup.
@@ -2403,8 +2403,8 @@ creation is not supported. If hard link creation is not supported either,
then \c{ln} falls back to copying the content, recursively in case of a
directory target.
-Unless the --no-cleanup option is specified, created filesystem entries that
-are inside the script working directory are automatically registered for
+Unless the \c{--no-cleanup} option is specified, created filesystem entries
+that are inside the script working directory are automatically registered for
cleanup.
@@ -2424,11 +2424,57 @@ directories must exist and the directory itself must not exist.
Create missing leading directories and ignore directories that already
exist.||
-Unless the --no-cleanup option is specified, newly created directories
+Unless the \c{--no-cleanup} option is specified, newly created directories
(including the leading ones) that are inside the script working directory are
automatically registered for cleanup.
+\h#builtins-mv|\c{mv}|
+
+\
+mv [--no-cleanup] [-f] <src-path> <dst-path>
+mv [--no-cleanup] [-f] <src-path>... <dst-dir>/
+\
+
+Rename or move files and/or directories.
+
+The first form moves an entity to the specified path. The parent directory of
+the destination path must exist. An existing destination entity is replaced
+with the source if they are both either directories or non-directories (files,
+symlinks, etc). In the former case the destination directory must be empty.
+The source and destination paths must not be the same nor be the test working
+directory or its parent directory. The source path must also not be outside
+the script working directory unless the \c{-f} option is specified.
+
+The second form moves one or more entities into the specified directory as if
+by executing the following command for each entity:
+
+\
+mv src-path dst-dir/src-name
+\
+
+Where \i{src-name} is the last path component in \i{src-path}.
+
+\dl|
+
+\li|\n\c{-f}
+
+ Do not fail if a source path is outside the script working directory.||
+
+Unless the \c{--no-cleanup} option is specified, the cleanups registered for
+the source entities are adjusted according to their new names and/or
+locations. If the destination entity already exists or is outside the test
+working directory then the source entity cleanup is canceled. Otherwise the
+source entity cleanup path is replaced with the destination path. If the
+source entity is a directory, then, in addition, cleanups that are sub-paths
+of this directory are made sub-paths of the destination directory.
+
+Note that the implementation deviates from POSIX in a number of ways. It never
+interacts with the user and fails immediately if unable to act on an argument.
+It does not check for dot containment in the path nor considers filesystem
+permissions. In essence, it simply tries to move the filesystem entry.
+
+
\h#builtins-rm|\c{rm}|
\
@@ -2454,10 +2500,9 @@ is specified.
the script working directory.||
Note that the implementation deviates from POSIX in a number of ways. It never
-interacts with the user and fails immediately if unable to act on an
-argument. It does not check for dot containment in the path nor considers
-filesystem permissions. In essence, it simply tries to remove the filesystem
-entry.
+interacts with the user and fails immediately if unable to act on an argument.
+It does not check for dot containment in the path nor considers filesystem
+permissions. In essence, it simply tries to remove the filesystem entry.
\h#builtins-rmdir|\c{rmdir}|
@@ -2643,7 +2688,7 @@ Change file access and modification times to the current time. Create files
that do not exist. Fail if a filesystem entry other than the file exists for
the specified name.
-Unless the --no-cleanup option is specified, newly created files that are
+Unless the \c{--no-cleanup} option is specified, newly created files that are
inside the script working directory are automatically registered for cleanup.
\h#builtins-true|\c{true}|
diff --git a/tests/test/script/builtin/mv.test b/tests/test/script/builtin/mv.test
new file mode 100644
index 0000000..291832e
--- /dev/null
+++ b/tests/test/script/builtin/mv.test
@@ -0,0 +1,252 @@
+# file : tests/test/script/builtin/mv.test
+# copyright : Copyright (c) 2014-2018 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+.include ../common.test
+
+: args
+:
+{
+ : none
+ :
+ $c <'mv 2>"mv: missing arguments" == 1' && $b
+
+ : no-source
+ :
+ $c <'mv a 2>"mv: missing source path" == 1' && $b
+
+ : no-trailing-sep
+ :
+ $c <<EOI && $b
+ mv a b c 2>"mv: multiple source paths without trailing separator for destination directory" == 1
+ EOI
+
+ : empty
+ :
+ {
+ : dest
+ :
+ $c <<EOI && $b
+ mv '' 2>"mv: invalid path ''" == 1
+ EOI
+
+ : src1
+ :
+ $c <<EOI && $b
+ mv '' a 2>"mv: invalid path ''" == 1
+ EOI
+
+ : src2
+ :
+ $c <<EOI && $b
+ mv '' a b/ 2>"mv: invalid path ''" == 1
+ EOI
+ }
+}
+
+: synopsis-1
+:
+: Move an entity to the specified path.
+:
+{
+ : file
+ :
+ {
+ : existing
+ :
+ {
+ : to-non-existing
+ :
+ $c <<EOI && $b
+ touch a;
+ mv a b && test -f b && test -f a == 1
+ EOI
+
+ : to-existing
+ :
+ $c <<EOI && $b
+ touch a b;
+ mv a b && test -f b && test -f a == 1
+ EOI
+
+ : to-self
+ :
+ $c <<EOI && $b
+ touch a;
+ mv a a 2>>/~%EOE% != 0
+ %mv: unable to move entity '.+/a' to itself%
+ EOE
+ EOI
+
+ : to-dir
+ :
+ $c <<EOI && $b
+ touch a;
+ mkdir b;
+ mv a b 2>>/~%EOE% != 0
+ %mv: unable to move entity '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+ }
+
+ : outside-scope
+ :
+ : Need to use a path that unlikely exists (not to remove something useful).
+ :
+ {
+ : fail
+ :
+ : Moving path outside the testscript working directory fails.
+ :
+ $c <<EOI && $b
+ mv ../../a/b/c ./c 2>>/~%EOE% == 1
+ %mv: '.+/fail/a/b/c' is out of working directory '.+/fail/test'%
+ EOE
+ EOI
+
+ : force
+ :
+ : Moving path outside the testscript working directory is allowed with -f
+ : option. We fail after this check succeeds as the source path does not
+ : exist.
+ :
+ $c <<EOI && $b
+ mv -f ../../a/b/c ./c 2>>/~%EOE% == 1
+ %mv: unable to move entity '.+/force/a/b/c' to '.+/c': .+%
+ EOE
+ EOI
+ }
+
+ : cleanup
+ :
+ {
+ : existing
+ :
+ : Test that moving over an existing file does not move the cleanup. If
+ : it does, then the file would be removed while leaving the embedded
+ : scope, and so the cleanup registered by the outer touch would fail. We
+ : also test that the source path cleanup is removed, otherwise it would
+ : fail.
+ :
+ $c <<EOI && $b
+ +touch b
+ {
+ touch a;
+ mv a ../b
+ }
+ EOI
+ }
+ }
+
+ : dir
+ :
+ {
+ : existing
+ :
+ {
+ : to-non-existing
+ :
+ : Note the here we also test that b path is cleaned up as a directory.
+ :
+ $c <<EOI && $b
+ mkdir a;
+ mv a b && test -d b && test -d a == 1
+ EOI
+
+ : to-non-empty
+ :
+ $c <<EOI && $b
+ mkdir a b;
+ touch b/c;
+ mv a b 2>>/~%EOE% != 0
+ %mv: unable to move entity '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+
+ : to-non-dir
+ :
+ $c <<EOI && $b
+ mkdir a;
+ touch b;
+ mv a b 2>>/~%EOE% != 0
+ %mv: unable to move entity '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+ }
+
+ : working-dir
+ :
+ {
+ : src
+ :
+ {
+ $c <<EOI && $b
+ mv $~ b 2>"mv: '([string] $~)' contains test working directory '$~'" != 0
+ EOI
+ }
+
+ : dst
+ :
+ {
+ $c <<EOI && $b
+ mkdir a;
+ mv a "$~" 2>"mv: '$~' contains test working directory '$~'" != 0
+ EOI
+ }
+ }
+
+ : overlap
+ :
+ $c <<EOI && $b
+ mkdir a;
+ mv a a/b 2>>/~%EOE% != 0
+ %mv: unable to move entity '.+/a' to '.+/a/b': .+%
+ EOE
+ EOI
+
+ : cleanup
+ :
+ {
+ : sub-entry
+ :
+ {
+ mkdir a;
+ touch a/b;
+ mv a c
+ }
+
+ : reorder
+ :
+ : Test that a/, that is created before b/ and so should be removed after
+ : it, get removed before b/ after being renamed to b/c.
+ :
+ $c <<EOI && $b
+ mkdir a b;
+ mv a b/c
+ EOI
+ }
+ }
+
+ : non-existing
+ :
+ {
+ $c <<EOI && $b
+ mv a b 2>>/~%EOE% != 0
+ %mv: unable to move entity '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+ }
+}
+
+: synopsis-2
+:
+: Move entities into the specified directory.
+:
+{
+ $c <<EOI && $b
+ mkdir a c;
+ touch a/b b;
+ mv a b c/;
+ test -d c/a && test -f c/a/b && test -f c/b
+ EOI
+}