aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2017-02-15 02:06:29 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2017-02-16 21:41:37 +0300
commit9cf682c10cbc46679d361f937b124ed301d8ce25 (patch)
treef0eb5d8e0c259e0e5d3ee775754245f864a12e63
parent9ce514f4ec14a13f11da75a7b90ef67141a5487e (diff)
Add cp builtin
-rw-r--r--build2/target-key1
-rw-r--r--build2/test/script/builtin.cxx235
-rw-r--r--build2/variable1
-rw-r--r--tests/test/script/builtin/buildfile2
-rw-r--r--tests/test/script/builtin/cp.test306
5 files changed, 530 insertions, 15 deletions
diff --git a/build2/target-key b/build2/target-key
index 0eae11f..084885f 100644
--- a/build2/target-key
+++ b/build2/target-key
@@ -6,7 +6,6 @@
#define BUILD2_TARGET_KEY
#include <map>
-#include <functional> // hash
#include <butl/utility> // compare_c_string
diff --git a/build2/test/script/builtin.cxx b/build2/test/script/builtin.cxx
index 3957adb..a971f64 100644
--- a/build2/test/script/builtin.cxx
+++ b/build2/test/script/builtin.cxx
@@ -149,9 +149,9 @@ namespace build2
// cat <file>...
//
// Read files in sequence and write their contents to STDOUT in the same
- // sequence. Read from STDIN if no argumements provided or '-' is
- // specified as a file path. STDIN, STDOUT and file streams are set to
- // binary mode prior to I/O operations.
+ // sequence. Read from STDIN if no arguments provided or '-' is specified
+ // as a file path. STDIN, STDOUT and file streams are set to binary mode
+ // prior to I/O operations.
//
// Note that POSIX doesn't specify if after I/O operation failure the
// command should proceed with the rest of the arguments. The current
@@ -270,6 +270,213 @@ namespace build2
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,
+ const function<error_record()>& fail)
+ {
+ try
+ {
+ bool exists (file_exists (to));
+
+ cpfile (from, to,
+ cpflags::overwrite_permissions | cpflags::overwrite_content);
+
+ if (!exists)
+ sp.clean ({cleanup_type::always, to}, true);
+ }
+ 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,
+ const function<error_record()>& fail)
+ {
+ try
+ {
+ if (try_mkdir (to) == mkdir_status::already_exists)
+ throw system_error (EEXIST, system_category ());
+
+ sp.clean ({cleanup_type::always, to}, true);
+
+ for (const auto& de: dir_iterator (from)) // Can throw.
+ {
+ 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)),
+ fail);
+ else
+ cpfile (sp, f, t, fail);
+ }
+ }
+ catch (const system_error& e)
+ {
+ fail () << "unable to copy directory '" << from << "' to '" << to
+ << "': " << e;
+ }
+ }
+
+ // cp <src-file> <dst-file>
+ // cp -R|-r <src-dir> <dst-dir>
+ // cp <src-file>... <dst-dir>/
+ // cp -R|-r <src-path>... <dst-dir>/
+ //
+ // Copy files and/or directories. The first two forms make a copy of a
+ // single entity at the specified path. The last two copy one or more
+ // entities into the specified directory.
+ //
+ // For details read the builtin description in the manual.
+ //
+ // 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 ();
+
+ auto i (args.begin ());
+ auto e (args.end ());
+
+ // Process options.
+ //
+ bool recursive (false);
+ for (; i != e; ++i)
+ {
+ if (*i == "-R" || *i == "-r")
+ recursive = true;
+ else
+ {
+ if (*i == "--")
+ ++i;
+
+ break;
+ }
+ }
+
+ // Copy files or directories.
+ //
+ 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 fail = [&error] () {return error (true);};
+
+ // 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 (*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 (!recursive)
+ // Synopsis 1: make a file copy at the specified path.
+ //
+ cpfile (sp, src, dst, fail);
+ else
+ // Synopsis 2: make a directory copy at the specified path.
+ //
+ cpdir (sp,
+ path_cast<dir_path> (src), path_cast<dir_path> (dst),
+ fail);
+ }
+ else
+ {
+ for (; i != e; ++i)
+ {
+ path src (parse_path (*i, wd));
+
+ if (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 ()),
+ 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 (), 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.
+ }
+
+ cerr.close ();
+ return r;
+ }
+ catch (const std::exception&)
+ {
+ return 1;
+ }
+
// echo <string>...
//
// Note: must be executed asynchronously.
@@ -377,11 +584,12 @@ namespace build2
out.close ();
auto i (args.begin ());
+ auto e (args.end ());
// Process options.
//
bool parent (false);
- for (; i != args.end (); ++i)
+ for (; i != e; ++i)
{
if (*i == "-p")
parent = true;
@@ -396,10 +604,10 @@ namespace build2
// Create directories.
//
- if (i == args.end ())
+ if (i == e)
error () << "missing directory";
- for (; i != args.end (); ++i)
+ for (; i != e; ++i)
{
dir_path p (path_cast<dir_path> (parse_path (*i, sp.wd_path)));
@@ -487,12 +695,13 @@ namespace build2
out.close ();
auto i (args.begin ());
+ auto e (args.end ());
// Process options.
//
bool dir (false);
bool force (false);
- for (; i != args.end (); ++i)
+ for (; i != e; ++i)
{
if (*i == "-r")
dir = true;
@@ -509,13 +718,13 @@ namespace build2
// Remove entries.
//
- if (i == args.end () && !force)
+ if (i == e && !force)
error () << "missing file";
const dir_path& wd (sp.wd_path);
const dir_path& rwd (sp.root->wd_path);
- for (; i != args.end (); ++i)
+ for (; i != e; ++i)
{
path p (parse_path (*i, wd));
@@ -607,11 +816,12 @@ namespace build2
out.close ();
auto i (args.begin ());
+ auto e (args.end ());
// Process options.
//
bool force (false);
- for (; i != args.end (); ++i)
+ for (; i != e; ++i)
{
if (*i == "-f")
force = true;
@@ -626,13 +836,13 @@ namespace build2
// Remove directories.
//
- if (i == args.end () && !force)
+ if (i == e && !force)
error () << "missing directory";
const dir_path& wd (sp.wd_path);
const dir_path& rwd (sp.root->wd_path);
- for (; i != args.end (); ++i)
+ for (; i != e; ++i)
{
dir_path p (path_cast<dir_path> (parse_path (*i, wd)));
@@ -1178,6 +1388,7 @@ namespace build2
const builtin_map builtins
{
{"cat", &async_impl<&cat>},
+ {"cp", &sync_impl<&cp>},
{"echo", &async_impl<&echo>},
{"false", &false_},
{"mkdir", &sync_impl<&mkdir>},
diff --git a/build2/variable b/build2/variable
index 0f90c48..af7cbef 100644
--- a/build2/variable
+++ b/build2/variable
@@ -7,7 +7,6 @@
#include <map>
#include <set>
-#include <functional> // hash
#include <type_traits> // aligned_storage
#include <unordered_map>
diff --git a/tests/test/script/builtin/buildfile b/tests/test/script/builtin/buildfile
index 2a57c54..f72688d 100644
--- a/tests/test/script/builtin/buildfile
+++ b/tests/test/script/builtin/buildfile
@@ -2,4 +2,4 @@
# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
# license : MIT; see accompanying LICENSE file
-./: test{cat echo mkdir rm rmdir sed test touch} $b
+./: test{cat cp echo mkdir rm rmdir sed test touch} $b
diff --git a/tests/test/script/builtin/cp.test b/tests/test/script/builtin/cp.test
new file mode 100644
index 0000000..4709459
--- /dev/null
+++ b/tests/test/script/builtin/cp.test
@@ -0,0 +1,306 @@
+# file : tests/test/script/builtin/cp.test
+# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
+.include ../common.test
+
+: args
+:
+{
+ : none
+ :
+ $c <'cp 2>"cp: missing arguments" == 1' && $b
+
+ : no-source
+ :
+ $c <'cp -R a 2>"cp: missing source path" == 1' && $b
+
+ : no-trailing-sep
+ :
+ $c <<EOI && $b
+ cp a b c 2>"cp: multiple source paths without trailing separator for destination directory" == 1
+ EOI
+
+ : empty
+ :
+ {
+ : dest
+ :
+ $c <<EOI && $b
+ cp '' 2>"cp: invalid path ''" == 1
+ EOI
+
+ : src1
+ :
+ $c <<EOI && $b
+ cp '' a 2>"cp: invalid path ''" == 1
+ EOI
+
+ : src2
+ :
+ $c <<EOI && $b
+ cp '' a b/ 2>"cp: invalid path ''" == 1
+ EOI
+ }
+}
+
+: file
+:
+: Test synopsis 1: make a file copy at the specified path.
+:
+{
+ : existing
+ :
+ {
+ : to-non-existing
+ :
+ $c <<EOI && $b
+ touch a;
+ cp a b && test -f b
+ EOI
+
+ : to-existing
+ :
+ $c <<EOI && $b
+ touch a b;
+ cp a b
+ EOI
+
+ : to-dir
+ :
+ $c <<EOI && $b
+ touch a;
+ mkdir b;
+ cp a b 2>>/~%EOE% != 0
+ %cp: unable to copy file '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+ }
+
+ : non-existing
+ :
+ {
+ $c <<EOI && $b
+ cp a b 2>>/~%EOE% != 0
+ %cp: unable to copy file '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+ }
+
+ : non-file
+ :
+ {
+ $c <<EOI && $b
+ mkdir a;
+ cp a b 2>>/~%EOE% != 0
+ %cp: unable to copy file '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+ }
+
+ : cleanup
+ :
+ {
+ : existing
+ :
+ : Test that copy over an existing file doesn't register 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.
+ :
+ $c <<EOI && $b
+ +touch b
+ {
+ touch a;
+ cp a ../b
+ }
+ EOI
+ }
+}
+
+: dir
+:
+: Test synopsis 2: make a directory copy at the specified path.
+:
+{
+ : existing
+ :
+ {
+ : to-non-existing
+ :
+ $c <<EOI && $b
+ mkdir a;
+ cp -r a b && test -d b
+ EOI
+
+ : to-existing
+ :
+ $c <<EOI && $b
+ mkdir a b;
+ cp -R a b 2>>/~%EOE% != 0
+ %cp: unable to copy directory '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+
+ : to-file
+ :
+ $c <<EOI && $b
+ mkdir a;
+ touch b;
+ cp -r a b 2>>/~%EOE% != 0
+ %cp: unable to copy directory '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+
+ : recursively
+ :
+ {
+ $c <<EOI && $b
+ mkdir -p a/b/c;
+ touch a/x a/b/y;
+ cp -r a d && test -d d/b/c && test -f d/x && test -f d/b/y
+ EOI
+ }
+ }
+
+ : non-existing
+ :
+ {
+ $c <<EOI && $b
+ cp -r a b 2>>/~%EOE% != 0
+ %cp: unable to copy directory '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+ }
+
+ : non-dir
+ :
+ {
+ $c <<EOI && $b
+ touch a;
+ cp -r a b 2>>/~%EOE% != 0
+ %cp: unable to copy directory '.+/a' to '.+/b': .+%
+ EOE
+ EOI
+ }
+}
+
+: files
+:
+: Test synopsis 3: copy files into the specified directory.
+:
+{
+ : existing
+ :
+ {
+ : into-dir
+ :
+ {
+ : over-non-existing
+ :
+ $c <<EOI && $b
+ mkdir b;
+ touch a;
+ cp a b/ && test -f b/a
+ EOI
+
+ : over-dir
+ :
+ $c <<EOI && $b
+ mkdir -p b/a;
+ touch a;
+ cp a b/ 2>>/~%EOE% != 0
+ %cp: unable to copy file '.+/a' to '.+/b/a': .+%
+ EOE
+ EOI
+
+ : multiple
+ :
+ $c <<EOI && $b
+ touch a b;
+ mkdir c;
+ cp a b c/ && test -f c/a && test -f c/b
+ EOI
+ }
+
+ : into-non-existing-dir
+ :
+ {
+ $c <<EOI && $b
+ touch a;
+ cp a b/ 2>>/~%EOE% != 0
+ %cp: unable to copy file '.+/a' to '.+/b/a': .+%
+ EOE
+ EOI
+ }
+
+ : into-non-dir
+ :
+ {
+ $c <<EOI && $b
+ touch a b;
+ cp a b/ 2>>/~%EOE% != 0
+ %cp: unable to copy file '.+/a' to '.+/b/a': .+%
+ EOE
+ EOI
+ }
+ }
+
+ : non-existing
+ :
+ {
+ $c <<EOI && $b
+ mkdir b;
+ cp a b/ 2>>/~%EOE% != 0
+ %cp: unable to copy file '.+/a' to '.+/b/a': .+%
+ EOE
+ EOI
+ }
+
+ : non-file
+ :
+ {
+ $c <<EOI && $b
+ mkdir a b;
+ cp a b/ 2>>/~%EOE% != 0
+ %cp: unable to copy file '.+/a' to '.+/b/a': .+%
+ EOE
+ EOI
+ }
+}
+
+: filesystem-entries
+:
+: Test synopsis 4: copy filesystem entries into the specified directory.
+:
+{
+ : file
+ :
+ {
+ $c <<EOI && $b
+ mkdir b;
+ touch a;
+ cp -R a b/ && test -f b/a
+ EOI
+ }
+
+ : dir
+ :
+ {
+ : over-non-existing
+ :
+ $c <<EOI && $b
+ mkdir a b;
+ touch a/c;
+ cp -R a b/ && test -f b/a/c
+ EOI
+
+ : over-existing
+ :
+ $c <<EOI && $b
+ mkdir -p a b/a;
+ cp -R a b/ 2>>/~%EOE% != 0
+ %cp: unable to copy directory '.+/a' to '.+/b/a': .+%
+ EOE
+ EOI
+ }
+}