From 4bad3a3aa3ffc111197f1d5d4b50baa1c4390444 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Fri, 25 Nov 2016 18:40:03 +0300 Subject: Add rm builtin --- build2/test/script/builtin.cxx | 141 +++++++++++++++++++++++++++++++++++ tests/test/script/builtin/buildfile | 2 +- tests/test/script/builtin/rm.test | 82 ++++++++++++++++++++ tests/test/script/builtin/touch.test | 14 +++- 4 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 tests/test/script/builtin/rm.test 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] ... + // + // 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 (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 ... // // 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 : -- cgit v1.1