From 5a36f357e174d002722122d2408c57fb43da6e59 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 21 Apr 2017 09:17:01 +0200 Subject: Implement execution of Windows batch files --- butl/process.cxx | 133 +++++++++++++++++++++++++++++++++-------------- tests/process/buildfile | 2 +- tests/process/driver.cxx | 25 ++++++++- tests/process/testscript | 17 ++++++ 4 files changed, 135 insertions(+), 42 deletions(-) create mode 100644 tests/process/testscript diff --git a/butl/process.cxx b/butl/process.cxx index 1efa103..850ba9b 100644 --- a/butl/process.cxx +++ b/butl/process.cxx @@ -13,7 +13,7 @@ # include # include // _get_osfhandle(), _close() -# include // _MAX_PATH, getenv() +# include // _MAX_PATH # include // stat # include // stat(), S_IS* @@ -25,7 +25,7 @@ # define STDERR_FILENO 2 # endif // _MSC_VER -# include // __argv[] +# include // getenv(), __argv[] # include #endif @@ -574,16 +574,16 @@ namespace butl size_t fn (strlen (f)); - // Unless there is already the .exe extension, then we will need to add - // it. Note that running .bat files requires starting cmd.exe and passing - // the batch file as an argument (see CreateProcess() for deails). So - // if/when we decide to support those, it will have to be handled - // differently. + // Unless there is already the .exe/.bat extension, then we will need to + // add it. // bool ext; { const char* e (traits::find_extension (f, fn)); - ext = (e == nullptr || casecmp (e, ".exe") != 0); + ext = (e == nullptr || + (casecmp (e, ".exe") != 0 && + casecmp (e, ".bat") != 0 && + casecmp (e, ".cmd") != 0)); } process_path r (f, path (), path ()); // Make sure it is not empty. @@ -599,9 +599,26 @@ namespace butl return _stat (f, &si) == 0 && S_ISREG (si.st_mode); }; - auto search = [&ep, f, fn, ext, &exists] (const char* d, - size_t dn, - bool norm = false) -> bool + // Check with extensions: .exe, .cmd, and .bat. + // + auto exists_ext = [&exists] (string& s) -> bool + { + size_t i (s.size () + 1); // First extension letter. + + s += ".exe"; + if (exists (s.c_str ())) + return true; + + s[i] = 'c'; s[i + 1] = 'm'; s[i + 2] = 'd'; + if (exists (s.c_str ())) + return true; + + s[i] = 'b'; s[i + 1] = 'a'; s[i + 2] = 't'; + return exists (s.c_str ()); + }; + + auto search = [&ep, f, fn, ext, &exists, &exists_ext] ( + const char* d, size_t dn, bool norm = false) -> bool { string s (move (ep).string ()); // Reuse buffer. @@ -619,12 +636,15 @@ namespace butl if (norm) ep.normalize (); - // Add the .exe extension if necessary. - // - if (ext) - ep += ".exe"; + if (!ext) + return exists (ep.string ().c_str ()); - return exists (ep.string ().c_str ()); + // Try with the extensions. + // + s = move (ep).string (); + bool e (exists_ext (s)); + ep = path (move (s)); + return e; }; // If there is a directory component in the file, then the PATH search @@ -636,13 +656,17 @@ namespace butl { if (traits::absolute (f, fn)) { - if (ext) + bool e; + if (!ext) + e = exists (r.effect_string ()); + else { - ep = path (f, fn); - ep += ".exe"; + string s (f, fn); + e = exists_ext (s); + ep = path (move (s)); } - if (exists (r.effect_string ())) + if (e) return r; } else @@ -869,6 +893,24 @@ namespace butl const process_path& pp, const char* args[], int in, int out, int err) { + // Figure out if this is a batch file since running them requires starting + // cmd.exe and passing the batch file as an argument (see CreateProcess() + // for deails). + // + const char* batch (nullptr); + { + const char* p (pp.effect_string ()); + const char* e (path::traits::find_extension (p, strlen (p))); + if (e != nullptr && (casecmp (e, ".bat") == 0 || + casecmp (e, ".cmd") == 0)) + { + batch = getenv ("COMSPEC"); + + if (batch == nullptr) + batch = "C:\\Windows\\System32\\cmd.exe"; + } + } + fdpipe out_fd; fdpipe in_ofd; fdpipe in_efd; @@ -934,31 +976,44 @@ namespace butl // Serialize the arguments to string. // string cmd_line; - - for (const char* const* p (args); *p != 0; ++p) { - if (p != args) - cmd_line += ' '; + auto append = [&cmd_line] (const string& a) + { + if (!cmd_line.empty ()) + cmd_line += ' '; - // On Windows we need to protect values with spaces using quotes. Since - // there could be actual quotes in the value, we need to escape them. - // - string a (*p); - bool quote (a.empty () || a.find (' ') != string::npos); + // On Windows we need to protect values with spaces using quotes. + // Since there could be actual quotes in the value, we need to escape + // them. + // + bool quote (a.empty () || a.find (' ') != string::npos); + + if (quote) + cmd_line += '"'; - if (quote) - cmd_line += '"'; + for (size_t i (0); i < a.size (); ++i) + { + if (a[i] == '"') + cmd_line += "\\\""; + else + cmd_line += a[i]; + } - for (size_t i (0); i < a.size (); ++i) + if (quote) + cmd_line += '"'; + }; + + if (batch != nullptr) { - if (a[i] == '"') - cmd_line += "\\\""; - else - cmd_line += a[i]; + append (batch); + append ("/c"); + append (pp.effect_string ()); } - if (quote) - cmd_line += '"'; + for (const char* const* p (args + (batch != nullptr ? 1 : 0)); + *p != 0; + ++p) + append (*p); } // Prepare other process information. @@ -1035,7 +1090,7 @@ namespace butl fail ("invalid file descriptor"); if (!CreateProcess ( - pp.effect_string (), + batch != nullptr ? batch : pp.effect_string (), const_cast (cmd_line.c_str ()), 0, // Process security attributes. 0, // Primary thread security attributes. diff --git a/tests/process/buildfile b/tests/process/buildfile index ae765c4..72cbec2 100644 --- a/tests/process/buildfile +++ b/tests/process/buildfile @@ -2,6 +2,6 @@ # copyright : Copyright (c) 2014-2017 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -exe{driver}: cxx{driver} ../../butl/lib{butl} +exe{driver}: cxx{driver} ../../butl/lib{butl} test{testscript} include ../../butl/ diff --git a/tests/process/driver.cxx b/tests/process/driver.cxx index fc9ffe9..ae5015c 100644 --- a/tests/process/driver.cxx +++ b/tests/process/driver.cxx @@ -143,10 +143,11 @@ exec (const path& p, } catch (const process_error& e) { + //cerr << args[0] << ": " << e << endl; + if (e.child) exit (1); - //cerr << args[0] << ": " << e << endl; return false; } } @@ -176,6 +177,7 @@ main (int argc, const char* argv[]) for (; i != argc; ++i) { string v (argv[i]); + if (v == "-c") child = true; else if (v == "-b") @@ -254,6 +256,8 @@ main (int argc, const char* argv[]) return 0; } + dir_path owd (dir_path::current_directory ()); + // Test processes created as "already terminated". // { @@ -309,7 +313,7 @@ main (int argc, const char* argv[]) assert (exec (dir_path (".") / fp.leaf ())); - // Fail for unexistent file path. + // Fail for non-existent file path. // assert (!exec (dir_path (".") / path ("dr"))); @@ -343,4 +347,21 @@ main (int argc, const char* argv[]) pr.handle = reinterpret_cast (-1); assert (!pr.wait (true) && !pr.wait (false)); #endif + + // Test execution of Windows batch files. The test file is in the original + // working directory. + // +#ifdef _WIN32 + { + assert (exec (owd / "test.bat")); + assert (exec (owd / "test")); + + paths = owd.string () + path::traits::path_separator + paths; + assert (_putenv (("PATH=" + paths).c_str ()) == 0); + + assert (exec (path ("test.bat"))); + assert (exec (path ("test"))); + assert (!exec (path ("testX.bat"))); + } +#endif } diff --git a/tests/process/testscript b/tests/process/testscript new file mode 100644 index 0000000..c734a05 --- /dev/null +++ b/tests/process/testscript @@ -0,0 +1,17 @@ +# file : tests/process/testscript +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +if ($cxx.target.class == 'windows') + cat <=test.bat + @echo off + setlocal + goto end + :error + endlocal + exit /b 1 + :end + endlocal + EOI +end; +$* -- cgit v1.1