From 3f2b42c3c01ecfee6a63653172d437aa0e915b68 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Fri, 13 Jul 2018 23:34:10 +0300 Subject: Add testscript mv builtin --- build2/test/script/builtin.cxx | 225 +++++++++++++++++++++++++++++++++- doc/testscript.cli | 63 ++++++++-- tests/test/script/builtin/mv.test | 252 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 530 insertions(+), 10 deletions(-) create mode 100644 tests/test/script/builtin/mv.test 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 #include // use default operator<< implementation #include // fdopen_mode, fdstream_mode -#include // mkdir_status +#include #include @@ -909,6 +909,228 @@ namespace build2 return 1; } + // mv [--no-cleanup] + // mv [--no-cleanup] ... / + // + // 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 (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 (from)) + : p.to_directory () + ? path_cast (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] ... // // 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] +mv [--no-cleanup] [-f] ... / +\ + +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 <"mv: multiple source paths without trailing separator for destination directory" == 1 + EOI + + : empty + : + { + : dest + : + $c <"mv: invalid path ''" == 1 + EOI + + : src1 + : + $c <"mv: invalid path ''" == 1 + EOI + + : src2 + : + $c <"mv: invalid path ''" == 1 + EOI + } +} + +: synopsis-1 +: +: Move an entity to the specified path. +: +{ + : file + : + { + : existing + : + { + : to-non-existing + : + $c <>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to itself% + EOE + EOI + + : to-dir + : + $c <>/~%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 <>/~%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 <>/~%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 <>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to '.+/b': .+% + EOE + EOI + + : to-non-dir + : + $c <>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to '.+/b': .+% + EOE + EOI + } + + : working-dir + : + { + : src + : + { + $c <"mv: '([string] $~)' contains test working directory '$~'" != 0 + EOI + } + + : dst + : + { + $c <"mv: '$~' contains test working directory '$~'" != 0 + EOI + } + } + + : overlap + : + $c <>/~%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 <>/~%EOE% != 0 + %mv: unable to move entity '.+/a' to '.+/b': .+% + EOE + EOI + } +} + +: synopsis-2 +: +: Move entities into the specified directory. +: +{ + $c <