aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build2/test/script/builtin.cxx141
-rw-r--r--tests/test/script/builtin/buildfile2
-rw-r--r--tests/test/script/builtin/rm.test82
-rw-r--r--tests/test/script/builtin/touch.test14
4 files changed, 236 insertions, 3 deletions
diff --git a/build2/test/script/builtin.cxx b/build2/test/script/builtin.cxx
index 2dcfde6..b12ae7e 100644
--- a/build2/test/script/builtin.cxx
+++ b/build2/test/script/builtin.cxx
@@ -370,6 +370,146 @@ namespace build2
return 1;
}
+ // rm [-r] [-f] <path>...
+ //
+ // Remove a file or directory. A path must not be the test scope working
+ // directory or its parent directory. It also must not be outside the
+ // script working directory unless -f option is specified. Note that
+ // directories are not removed by default.
+ //
+ // -r
+ // Remove directories recursively. Must be specified to remove even an
+ // empty directory.
+ //
+ // -f
+ // Do not fail if path doesn't exist or no paths specified. Removing
+ // paths outside the script working directory is not an error.
+ //
+ // 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));
+
+ try
+ {
+ in.close ();
+ out.close ();
+
+ auto i (args.begin ());
+
+ // Process options.
+ //
+ bool dir (false);
+ bool force (false);
+ for (; i != args.end (); ++i)
+ {
+ if (*i == "-r")
+ dir = true;
+ else if (*i == "-f")
+ force = true;
+ else
+ {
+ if (*i == "--")
+ ++i;
+
+ break;
+ }
+ }
+
+ // Remove entries.
+ //
+ if (i == args.end () && !force)
+ {
+ cerr << "rm: missing file" << endl;
+ throw failed ();
+ }
+
+ for (; i != args.end (); ++i)
+ {
+ path p (parse_path (*i, sp.wd_path));
+
+ if (!p.sub (sp.root->wd_path) && !force)
+ {
+ cerr << "rm: '" << p << "' is outside script working directory"
+ << endl;
+ throw failed ();
+ }
+
+ try
+ {
+ dir_path d (path_cast<dir_path> (p));
+
+ if (dir_exists (d))
+ {
+ if (!dir)
+ {
+ cerr << "rm: '" << p << "' is a directory" << endl;
+ throw failed ();
+ }
+
+ if (sp.wd_path.sub (d))
+ {
+ cerr << "rm: '" << p << "' contains scope working directory"
+ << endl;
+ throw failed ();
+ }
+
+ // 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 && !force)
+ throw system_error (ENOENT, system_category ());
+ }
+ catch (const system_error& e)
+ {
+ cerr << "rm: unable to remove '" << p << "': " << e.what ()
+ << endl;
+ throw failed ();
+ }
+ }
+
+ r = 0;
+ }
+ catch (const invalid_path& e)
+ {
+ cerr << "rm: invalid path '" << e.path << "'" << endl;
+ }
+ // Can be thrown while closing in, out or writing to cerr (that's why
+ // need to check its state before writing).
+ //
+ catch (const io_error& e)
+ {
+ if (cerr.good ())
+ cerr << "rm: " << e.what () << endl;
+ }
+ catch (const failed&)
+ {
+ // Diagnostics has already been issued.
+ }
+
+ cerr.close ();
+ return r;
+ }
+ catch (const std::exception&)
+ {
+ return 1;
+ }
+
// touch <file>...
//
// Change file access and modification times to the current time. Create
@@ -548,6 +688,7 @@ namespace build2
{"echo", &async_impl<&echo>},
{"false", &false_},
{"mkdir", &sync_impl<&mkdir>},
+ {"rm", &sync_impl<&rm>},
{"touch", &sync_impl<&touch>},
{"true", &true_}
};
diff --git a/tests/test/script/builtin/buildfile b/tests/test/script/builtin/buildfile
index baa4996..77d74cb 100644
--- a/tests/test/script/builtin/buildfile
+++ b/tests/test/script/builtin/buildfile
@@ -2,4 +2,4 @@
# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
# license : MIT; see accompanying LICENSE file
-./: test{cat echo mkdir touch}
+./: test{cat echo mkdir rm touch}
diff --git a/tests/test/script/builtin/rm.test b/tests/test/script/builtin/rm.test
new file mode 100644
index 0000000..31dfca0
--- /dev/null
+++ b/tests/test/script/builtin/rm.test
@@ -0,0 +1,82 @@
+# file : tests/test/script/runner/rm.test
+# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd
+# license : MIT; see accompanying LICENSE file
+
++if ($cxx.target.class != windows) # @@ TMP trailing separator
+ s = '/'
+else
+ s = '\'
+end
+
+: no-args
+:
+: Removing with no arguments fails.
+:
+rm 2>"rm: missing file" == 1
+
+: no-args-force
+:
+: Removing with no arguments succeeds with -f option.
+:
+rm -f
+
+: empty-path
+:
+: Removing an empty path fails.
+:
+rm '' 2>"rm: invalid path ''" == 1
+
+: file
+:
+: Removing existing file succeeds.
+:
+touch a &!a;
+rm a
+
+: file-not-exists
+:
+: Removing non-existing file fails.
+:
+rm a 2>- == 1 # @@ REGEX
+
+: file-not-exists-force
+:
+: Removing non-existing file succeeds with -f option.
+:
+rm -f a
+
+: dir
+:
+: Removing directory fails by default.
+:
+mkdir a;
+rm a 2>"rm: '$normalize([path] $~/a)' is a directory" == 1
+
+: dir-recursive
+:
+: Removing directory succeeds with -r option.
+:
+mkdir -p a/b &!a &!a/b;
+rm -r a
+
+: scope-dir
+:
+: Removing scope directory fails.
+:
+rm -r ./ 2>"rm: '$~$s' contains scope working directory" == 1
+
+: outside-scope
+:
+: Removing path outside the script working directory fails. Need to use a path
+: that unlikely exists (not to remove something useful).
+:
+:
+rm ../../a/b/c 2>"rm: '$normalize([path] $~/../../a/b/c)' is outside script working directory" == 1
+
+: outside-scope-force
+:
+: Removing path outside the script scope working directory succeeds with -f
+: option. Need to use a path that unlikely exists (not to remove something
+: useful).
+:
+rm -f ../../a/b/c
diff --git a/tests/test/script/builtin/touch.test b/tests/test/script/builtin/touch.test
index ef950ea..0f9e4ef 100644
--- a/tests/test/script/builtin/touch.test
+++ b/tests/test/script/builtin/touch.test
@@ -20,8 +20,18 @@ rm a
cat <"" >>>a;
touch a
-# @@ How we can test that touch of an existing file doesn't register a cleanup?
-#
+: no-cleanup
+:
+: Test that touching 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 first touch would fail.
+:
+{
+ +touch a
+ {
+ touch ../a
+ }
+}
: no-args
: