From f01992c952f6ed931d4a3283b6a601e151e91816 Mon Sep 17 00:00:00 2001 From: q66 Date: Sun, 7 May 2017 21:16:42 +0200 Subject: [PATCH] add command line splitting api (posix, windows) and use it from build --- build.cc | 12 ++--- ostd/process.hh | 78 ++++++++++++++++++++++++++++++ src/process.cc | 126 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 ostd/process.hh create mode 100644 src/process.cc diff --git a/build.cc b/build.cc index 675372b..a14c132 100644 --- a/build.cc +++ b/build.cc @@ -11,7 +11,6 @@ #include #include -#include #include #include "ostd/io.hh" @@ -26,6 +25,7 @@ namespace fs = ostd::filesystem; /* ugly, but do not explicitly compile */ #include "src/io.cc" +#include "src/process.cc" using strvec = std::vector; using pathvec = std::vector; @@ -145,13 +145,9 @@ static std::string get_command(std::string const &cmd, strvec const &args) { } static void add_args(strvec &args, std::string const &cmdl) { - wordexp_t p; - if (wordexp(cmdl.data(), &p, 0)) { - return; - } - for (std::size_t i = 0; i < p.we_wordc; ++i) { - args.push_back(p.we_wordv[i]); - } + auto app = ostd::appender(std::move(args)); + ostd::split_args(app, cmdl); + args = std::move(app.get()); } static fs::path path_with_ext(fs::path const &p, fs::path const &ext) { diff --git a/ostd/process.hh b/ostd/process.hh new file mode 100644 index 0000000..8c37753 --- /dev/null +++ b/ostd/process.hh @@ -0,0 +1,78 @@ +/** @addtogroup Utilities + * @{ + */ + +/** @file process.hh + * + * @brief Portable extensions to process handling. + * + * Provides POSIX and Windows abstractions for process creation and more. + * + * @copyright See COPYING.md in the project tree for further information. + */ + +#ifndef OSTD_PROCESS_HH +#define OSTD_PROCESS_HH + +#include + +#include "ostd/platform.hh" +#include "ostd/string.hh" + +namespace ostd { + +/** @addtogroup Utilities + * @{ + */ + +struct word_error: std::runtime_error { + using std::runtime_error::runtime_error; +}; + +namespace detail { + OSTD_EXPORT void split_args_impl( + string_range const &str, void (*func)(string_range, void *), void *data + ); +} + +/** @brief Splits command line argument string into individual arguments. + * + * The split is done in a platform specific manner, using wordexp on POSIX + * systems and CommandLineToArgvW on Windows. On Windows, the input string + * is assumed to be UTF-8 and the output strings are always UTF-8, on POSIX + * the wordexp implementation is followed (so it's locale specific). + * + * The `out` argument is an output range that takes a single argument at + * a time. The value type is any that can be explicitly constructed from + * an ostd::string_range. However, the string range that is used internally + * during the conversions is just temporary and freed at the end of this + * function, so it's important that the string type holds its own memory; + * an std::string will usually suffice. + * + * The ostd::word_error exception is used to handle failures of this + * function itself. It may also throw other exceptions though, particularly + * those thrown by `out` when putting the strings into it and also + * std::bad_alloc on allocation failures. + * + * @returns The forwarded `out`. + * + * @throws ostd::word_error on failure or anything thrown by `out`. + * @throws std::bad_alloc on alloation failures. + */ +template +OutputRange &&split_args(OutputRange &&out, string_range str) { + detail::split_args_impl(str, [](string_range val, void *outp) { + static_cast *>(outp)->put( + range_value_t>{val} + ); + }, &out); + return std::forward(out); +} + +/** @} */ + +} /* namespace ostd */ + +#endif + +/** @} */ diff --git a/src/process.cc b/src/process.cc new file mode 100644 index 0000000..290d40c --- /dev/null +++ b/src/process.cc @@ -0,0 +1,126 @@ +/* Process handling implementation bits. + * + * This file is part of libostd. See COPYING.md for futher information. + */ + +#include +#include +#include +#include + +#include "ostd/process.hh" + +#ifdef OSTD_PLATFORM_WIN32 +# define WIN32_LEAN_AND_MEAN +# include +#else +# include +# include +#endif + +namespace ostd { +namespace detail { + +OSTD_EXPORT void split_args_impl( + string_range const &str, void (*func)(string_range, void *), void *data +) { +#ifndef OSTD_PLATFORM_WIN32 + std::string strs{str}; + + wordexp_t p; + if (int err; (err = wordexp(strs.data(), &p, 0))) { + switch (err) { + case WRDE_BADCHAR: + throw word_error{"illegal character"}; + case WRDE_BADVAL: + /* no WRDE_UNDEF flag, won't happen */ + throw word_error{"undefined shell variable"}; + case WRDE_CMDSUB: + /* no WRDE_NOCMD flag, won't happen */ + throw word_error{"invalid command substitution"}; + case WRDE_NOSPACE: + throw std::bad_alloc{}; + case WRDE_SYNTAX: + throw word_error{"syntax error"}; + default: + throw word_error{"unknown error"}; + } + } + + /* if the output range throws, make sure stuff gets freed */ + struct wordexp_guard { + void operator()(wordexp_t *fp) const { + wordfree(fp); + } + }; + std::unique_ptr guard{&p}; + + for (std::size_t i = 0; i < p.we_wordc; ++i) { + func(p.we_wordv[i], data); + } +#else + std::unique_ptr wstr{new wchar_t[str.size() + 1]}; + memset(wstr.get(), 0, (str.size() + 1) * sizeof(wchar_t)); + + if (!MultiByteToWideChar( + CP_UTF8, 0, str.data(), str.size(), wstr.get(), str.size() + 1 + )) { + switch (GetLastError()) { + case ERROR_NO_UNICODE_TRANSLATION: + throw word_error{"unicode conversion failed"}; + default: + throw word_error{"unknown error"}; + } + } + + int argc = 0; + wchar_t **pwargs = CommandLineToArgvW(wstr.get(), &argc); + + guard.reset()); + if (!pwargs) { + throw word_error{"command line parsing failed"}; + } + + /* if anything throws, make sure stuff gets freed */ + struct wchar_guard { + void operator()(wchar_t **p) const { + LocalFree(p); + } + }; + std::unique_ptr wguard{pwargs}; + + for (int i = 0; i < argc; ++i) { + wchar_t *arg = pwargs[i]; + std::size_t arglen = wcslen(arg); + + std::size_t req = 0; + if (!(req = WideCharToMultiByte( + CP_UTF8, 0, arg, arglen, nullptr, 0, nullptr, nullptr + ))) { + switch (GetLastError()) { + case ERROR_NO_UNICODE_TRANSLATION: + throw word_error{"unicode conversion failed"}; + default: + throw word_error{"unknown error"}; + } + } + + std::unique_ptr buf{new char[req]}; + if (!WideCharToMultiByte( + CP_UTF8, 0, arg, arglen, buf.get(), req, nullptr, nullptr + )) { + switch (GetLastError()) { + case ERROR_NO_UNICODE_TRANSLATION: + throw word_error{"unicode conversion failed"}; + default: + throw word_error{"unknown error"}; + } + } + + func(string_range{buf.get(), buf.get() + req - 1}, data); + } +#endif +} + +} /* namespace detail */ +} /* namespace ostd */