From 4564a26c0b88d684c12c396d7ef5b0e66f686964 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Wed, 13 Oct 2021 20:05:27 +0300 Subject: Add --cwd|-t option to env pseudo-builtin --- doc/testscript.cli | 11 ++- libbuild2/script/parser.cxx | 40 +++++++++- libbuild2/script/parser.hxx | 7 +- libbuild2/script/run.cxx | 46 ++++++++--- libbuild2/script/script.cxx | 10 ++- libbuild2/script/script.hxx | 1 + tests/test/script/runner/driver.cxx | 132 +++++++++++++++++--------------- tests/test/script/runner/env.testscript | 34 ++++++++ 8 files changed, 201 insertions(+), 80 deletions(-) diff --git a/doc/testscript.cli b/doc/testscript.cli index 4721a88..50f975d 100644 --- a/doc/testscript.cli +++ b/doc/testscript.cli @@ -2437,11 +2437,12 @@ with a newline. \h#builtins-env|\c{env}| \ -env [-t ] [-u ]... [-] [=]... -- +env [-t ] [-c ] [-u ]... [-] [=]... -- \ + \ -Run a command limiting its execution time and/or adding/removing the variables -to/from the environment. +Run a command limiting its execution time, changing its working directory, +and/or adding/removing the variables to/from the environment. Note that \c{env} is a \i{pseudo-builtin}. In particular, its name and the \c{--} separator must be specified \i{literally} on the command line. @@ -2463,6 +2464,10 @@ env - --unset=FOO -- $* Terminate the command if it fails to complete within the specified number of seconds. See also the \l{#builtins-timeout \c{timeout}} builtin.| +\li|\n\c{-c|--cwd } + + Change the command's working directory.| + \li|\n\c{-u|--unset } Remove the specified variable from the environment.|| diff --git a/libbuild2/script/parser.cxx b/libbuild2/script/parser.cxx index ebfd5fc..f234d58 100644 --- a/libbuild2/script/parser.cxx +++ b/libbuild2/script/parser.cxx @@ -1028,8 +1028,9 @@ namespace build2 if (prog && tt == type::word && t.value == "env") { parsed_env r (parse_env_builtin (t, tt)); + c.cwd = move (r.cwd); c.variables = move (r.variables); - c.timeout = r.timeout; + c.timeout = r.timeout; env = true; } @@ -1411,10 +1412,16 @@ namespace build2 return r; }; + auto bad = [&i, &o, this] (const string& v) + { + fail (i->second) << "env: invalid value '" << v << "' for option '" + << o << "'"; + }; + // As above but convert the option value to a number and fail on // error. // - auto num = [&i, &o, &str, this] (const char* ln, const char* sn) + auto num = [&str, &bad] (const char* ln, const char* sn) { optional r; if (optional s = str (ln, sn)) @@ -1422,8 +1429,29 @@ namespace build2 r = parse_number (*s); if (!r) - fail (i->second) << "env: invalid value '" << *s - << "' for option '" << o << "'"; + bad (*s); + } + + return r; + }; + + // As above but convert the option value to a directory path and fail + // on error. + // + auto dir = [&str, &bad] (const char* ln, const char* sn) + { + optional r; + if (optional s = str (ln, sn)) + try + { + // Note that we don't need to check that the path is not empty, + // since str() fails for empty values. + // + r = dir_path (move (*s)); + } + catch (const invalid_path& e) + { + bad (e.path); } return r; @@ -1435,6 +1463,10 @@ namespace build2 { r.timeout = chrono::seconds (*v); } + else if (optional v = dir ("--cwd", "-c")) + { + r.cwd = move (*v); + } else if (optional v = str ("--unset", "-u")) { verify_environment_var_name (*v, "env: ", i->second, o.c_str ()); diff --git a/libbuild2/script/parser.hxx b/libbuild2/script/parser.hxx index 9098b3c..2a10311 100644 --- a/libbuild2/script/parser.hxx +++ b/libbuild2/script/parser.hxx @@ -130,15 +130,16 @@ namespace build2 pre_parse_line_start (token&, token_type&, lexer_mode); // Parse the env pseudo-builtin arguments up to the program name. Return - // the program execution timeout, the list of the variables that should - // be unset ("name") and/or set ("name=value") in the command + // the program execution timeout, CWD, the list of the variables that + // should be unset ("name") and/or set ("name=value") in the command // environment, and the token/type that starts the program name. Note // that the variable unsets come first, if present. // struct parsed_env { optional timeout; - environment_vars variables; + optional cwd; + environment_vars variables; }; parsed_env diff --git a/libbuild2/script/run.cxx b/libbuild2/script/run.cxx index 91fc9ac..f3b5cad 100644 --- a/libbuild2/script/run.cxx +++ b/libbuild2/script/run.cxx @@ -1227,6 +1227,8 @@ namespace build2 // const command& c (*bc); + const dir_path& wdir (*env.work_dir.path); + // Register the command explicit cleanups. Verify that the path being // cleaned up is a sub-path of the script working directory. Fail if // this is not the case. @@ -1234,7 +1236,7 @@ namespace build2 for (const auto& cl: c.cleanups) { const path& p (cl.path); - path np (normalize (p, *env.work_dir.path, ll)); + path np (normalize (p, wdir, ll)); const string& ls (np.leaf ().string ()); bool wc (ls == "*" || ls == "**" || ls == "***"); @@ -1320,6 +1322,10 @@ namespace build2 if (!first || !last) fail (ll) << program << " builtin must be the only pipe command"; + if (c.cwd) + fail (ll) << "current working directory cannot be specified for " + << program << " builtin"; + if (!c.variables.empty ()) fail (ll) << "environment variables cannot be (un)set for " << program << " builtin"; @@ -1455,7 +1461,7 @@ namespace build2 } case redirect_type::file: { - isp = normalize (in.file.path, *env.work_dir.path, ll); + isp = normalize (in.file.path, wdir, ll); open_stdin (); break; @@ -1549,9 +1555,9 @@ namespace build2 // or null-device descriptor for merge, pass or null redirects // respectively (not opening any file). // - auto open = [&env, &ll, &std_path] (const redirect& r, - int dfd, - path& p) -> auto_fd + auto open = [&env, &wdir, &ll, &std_path] (const redirect& r, + int dfd, + path& p) -> auto_fd { assert (dfd == 1 || dfd == 2); const char* what (dfd == 1 ? "stdout" : "stderr"); @@ -1592,7 +1598,7 @@ namespace build2 // p = r.file.mode == redirect_fmode::compare ? std_path (what) - : normalize (r.file.path, *env.work_dir.path, ll); + : normalize (r.file.path, wdir, ll); m |= r.file.mode == redirect_fmode::append ? fdopen_mode::at_end @@ -1810,6 +1816,28 @@ namespace build2 } }; + // Derive the process/builtin CWD. + // + // If the process/builtin CWD is specified via the env pseudo-builtin, + // then use that, completing it relative to the script environment work + // directory, if it is relative. Otherwise, use the script environment + // work directory. + // + dir_path completed_cwd; + if (c.cwd && c.cwd->relative ()) + completed_cwd = wdir / *c.cwd; + + const dir_path& cwd (!completed_cwd.empty () ? completed_cwd : + c.cwd ? *c.cwd : + wdir); + + // Unless CWD is the script environment work directory (which always + // exists), verify that it exists and fail if it doesn't. + // + if (&cwd != &wdir && !exists (cwd)) + fail (ll) << "specified working directory " << cwd + << " does not exist"; + // Absent if the process/builtin misses the "unsuccessful" deadline. // optional exit; @@ -2069,7 +2097,7 @@ namespace build2 builtin b (bi->function (r, c.arguments, move (ifd), move (ofd.out), move (efd), - *env.work_dir.path, + cwd, bcs)); pipe_command pc (b, c, ll, prev_cmd); @@ -2159,7 +2187,7 @@ namespace build2 program (path (s, 1, s.size () - 1)); } else - program (*env.work_dir.path / p); + program (wdir / p); } } catch (const invalid_path& e) @@ -2189,7 +2217,7 @@ namespace build2 *pe.path, args.data (), {ifd.get (), -1}, process::pipe (ofd), {-1, efd.get ()}, - env.work_dir.path->string ().c_str (), + cwd.string ().c_str (), pe.vars); // Can't throw. diff --git a/libbuild2/script/script.cxx b/libbuild2/script/script.cxx index 298d71f..9e6eeed 100644 --- a/libbuild2/script/script.cxx +++ b/libbuild2/script/script.cxx @@ -411,7 +411,7 @@ namespace build2 { // Print the env builtin if any of its options/arguments are present. // - if (!c.variables.empty () || c.timeout) + if (c.timeout || c.cwd || !c.variables.empty ()) { o << "env"; @@ -421,6 +421,14 @@ namespace build2 o << " -t " << chrono::duration_cast (*c.timeout).count (); + // CWD. + // + if (c.cwd) + { + o << " -c "; + print_path (*c.cwd); + } + // Variable unsets/sets. // auto b (c.variables.begin ()), i (b), e (c.variables.end ()); diff --git a/libbuild2/script/script.hxx b/libbuild2/script/script.hxx index dd31e33..d162900 100644 --- a/libbuild2/script/script.hxx +++ b/libbuild2/script/script.hxx @@ -324,6 +324,7 @@ namespace build2 process_path program; strings arguments; + optional cwd; // From env builtin. environment_vars variables; // From env builtin. optional timeout; // From env builtin. diff --git a/tests/test/script/runner/driver.cxx b/tests/test/script/runner/driver.cxx index 9c05e27..b84f167 100644 --- a/tests/test/script/runner/driver.cxx +++ b/tests/test/script/runner/driver.cxx @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -45,8 +46,8 @@ main (int argc, char* argv[]) // Usage: driver [-i ] (-o )* (-e )* (-f )* // (-d )* (-v )* [(-t (a|m|s|z)) | (-s )] // - // Execute actions specified by -i, -o, -e, -f, -d, -v, and -l options in - // the order as they appear on the command line. After that terminate + // Execute actions specified by -i, -o, -e, -f, -d, -w, -v, and -l options + // in the order as they appear on the command line. After that terminate // abnormally if -t option is provided, otherwise exit normally with the // status specified by -s option (0 by default). // @@ -67,6 +68,9 @@ main (int argc, char* argv[]) // Create a directory with the path specified. Create parent directories // if required. // + // -w + // Print CWD to stdout. + // // -v // If the specified variable is set then print its value to stdout and // the string '' otherwise. @@ -95,10 +99,7 @@ main (int argc, char* argv[]) for (int i (1); i < argc; ++i) { - string o (argv[i++]); - assert (i < argc); - - string v (argv[i]); + string o (argv[i]); auto toi = [] (const string& s) -> int { @@ -118,69 +119,80 @@ main (int argc, char* argv[]) return r; }; - if (o == "-i") + if (o == "-w") { - assert (ifd == 3); // Make sure is not set yet. - - ifd = toi (v); - assert (ifd >= 0 && ifd < 3); + cout << dir_path::current_directory () << endl; + } + else // Handle options other than flags. + { + ++i; + assert (i < argc); + string v (argv[i]); - if (ifd == 0) - cin.ignore (numeric_limits::max ()); - else if (cin.peek () != istream::traits_type::eof ()) + if (o == "-i") { - ostream& o (ifd == 1 ? cout : cerr); - o << cin.rdbuf (); - o.flush (); + assert (ifd == 3); // Make sure is not set yet. + + ifd = toi (v); + assert (ifd >= 0 && ifd < 3); + + if (ifd == 0) + cin.ignore (numeric_limits::max ()); + else if (cin.peek () != istream::traits_type::eof ()) + { + ostream& o (ifd == 1 ? cout : cerr); + o << cin.rdbuf (); + o.flush (); + } } - } - else if (o == "-o") - { - cout << v << endl; - } - else if (o == "-e") - { - cerr << v << endl; - } - else if (o == "-f") - { - ofdstream os (v); - os.close (); - } - else if (o == "-d") - { - try_mkdir_p (dir_path (v)); - } - else if (o == "-v") - { - optional var (getenv (v)); - cout << (var ? *var : "") << endl; - } - else if (o == "-l") - { - size_t t (toi (v)); + else if (o == "-o") + { + cout << v << endl; + } + else if (o == "-e") + { + cerr << v << endl; + } + else if (o == "-f") + { + ofdstream os (v); + os.close (); + } + else if (o == "-d") + { + try_mkdir_p (dir_path (v)); + } + else if (o == "-v") + { + optional var (getenv (v)); + cout << (var ? *var : "") << endl; + } + else if (o == "-l") + { + size_t t (toi (v)); - // MinGW GCC 4.9 doesn't implement this_thread so use Win32 Sleep(). - // + // MinGW GCC 4.9 doesn't implement this_thread so use Win32 Sleep(). + // #ifndef _WIN32 - this_thread::sleep_for (chrono::seconds (t)); + this_thread::sleep_for (chrono::seconds (t)); #else - Sleep (static_cast (t * 1000)); + Sleep (static_cast (t * 1000)); #endif + } + else if (o == "-t") + { + assert (aterm == '\0' && !status); // Make sure exit method is not set. + assert (v.size () == 1 && v.find_first_of ("amsz") != string::npos); + aterm = v[0]; + } + else if (o == "-s") + { + assert (!status && aterm == '\0'); // Make sure exit method is not set. + status = toi (v); + } + else + assert (false); } - else if (o == "-t") - { - assert (aterm == '\0' && !status); // Make sure exit method is not set. - assert (v.size () == 1 && v.find_first_of ("amsz") != string::npos); - aterm = v[0]; - } - else if (o == "-s") - { - assert (!status && aterm == '\0'); // Make sure exit method is not set. - status = toi (v); - } - else - assert (false); } switch (aterm) diff --git a/tests/test/script/runner/env.testscript b/tests/test/script/runner/env.testscript index ef90c3b..512139a 100644 --- a/tests/test/script/runner/env.testscript +++ b/tests/test/script/runner/env.testscript @@ -3,6 +3,40 @@ .include ../common.testscript +: cwd +: +{ + : not-exist + : + $c <'env -c a -- $* -w' && $b 2>>/~%EOE% != 0 + %testscript:1:1: error: specified working directory .+/a/ does not exist% + info: test id: 1 + EOE + + : process + : + $c </~%.+/a% + EOI + + : builtin + : + $c <