From 16b6f3e8a6d2d6bf49d5f8785708ed18983c0667 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Tue, 30 Jun 2020 09:19:13 +0200 Subject: Expose command_run()'s @-substitution functionality Also add support for different opening and closing substitution characters. --- libbutl/command.cxx | 150 +++++++++++++++++++++++++++-------------------- libbutl/command.mxx | 34 +++++++++++ tests/command/testscript | 6 +- 3 files changed, 124 insertions(+), 66 deletions(-) diff --git a/libbutl/command.cxx b/libbutl/command.cxx index fadd617..c23dfd5 100644 --- a/libbutl/command.cxx +++ b/libbutl/command.cxx @@ -48,6 +48,82 @@ using namespace std; namespace butl { + string + command_substitute (const string& s, size_t sp, + const function& sc, + char open, char close) + { + string r; + size_t p (0); // Current parsing position. + + for (size_t n (s.size ()); sp != string::npos; sp = s.find (open, ++p)) + { + // Append the source string fraction preceding this substitution. + // + r.append (s, p, sp - p); + + // See if this is an escape of the opening substitution character + // (adjacent characters). + // + if (++sp != n && s[sp] == open) + { + p = sp; + r += open; + continue; + } + + // In sp we now have the start of the variable name. Find its end. + // + p = s.find (close, sp); + + // Verify that the variable name is properly terminated, not empty, and + // doesn't contain whitespaces. + // + if (p == string::npos) + throw invalid_argument (string ("unmatched substitution character '") + + open + "'"); + + if (p == sp) + throw invalid_argument ("empty substitution variable"); + + string vn (s, sp, p - sp); + + if (vn.find_first_of (" \t") != string::npos) + throw invalid_argument ("whitespace in substitution variable '" + + vn + "'"); + + // Find the variable and append its value or fail if it's unknown. + // + if (!sc (vn, r)) + throw invalid_argument ("unknown substitution variable '" + vn + "'"); + } + + // Append the source string tail following the last substitution. + // + r.append (s.begin () + p, s.end ()); + + return r; + } + + string + command_substitute (const string& s, size_t sp, + const command_substitution_map& sm, + char o, char c) + { + return command_substitute ( + s, + sp, + [&sm] (const string& vn, string& r) + { + auto i (sm.find (vn)); + if (i == sm.end ()) + return false; + r += i->second; + return true; + }, + o, c); + } + process_exit command_run (const string& cmd_str, const optional& env, @@ -63,72 +139,19 @@ namespace butl vector cmd ( string_parser::parse_quoted (cmd_str, true /* unquote */)); - auto bad_arg = [] (const string& d) {throw invalid_argument (d);}; - if (cmd.empty ()) - bad_arg ("no program path specified"); + throw invalid_argument ("no program path specified"); // Perform substitutions in a string. Throw invalid_argument for a - // malformed substitution or an unknown variable. + // malformed substitution or an unknown variable name. // - auto substitute = [&substitutions, subst, &bad_arg] (string&& s) + auto substitute = [&substitutions, subst] (string&& s) { - if (!substitutions) - return move (s); - - string r; - size_t p (0); // Current parsing position. - - for (size_t sp; (sp = s.find (subst, p)) != string::npos; ++p) - { - // Append the source string fraction preceding this substitution. - // - r.append (s, p, sp - p); - - ++sp; // Start of the variable name. - p = s.find (subst, sp); // End of the variable name. - - // Unescape the substitution character (adjacent substitution - // characters). - // - if (p == sp) - { - r += subst; - continue; - } - - // Verify that the variable name is properly terminated and doesn't - // contain whitespaces. - // - if (p == string::npos) - bad_arg (string ("unmatched substitution character '") + subst + - "' in '" + s + "'"); - - string vn (s, sp, p - sp); - - assert (!vn.empty ()); // Otherwise it would be an escape sequence. - - if (vn.find_first_of (" \t") != string::npos) - bad_arg ("whitespace in variable name '" + vn + "'"); - - // Find the variable and append its value or fail if it's unknown. - // - auto i (substitutions->find (vn)); - - if (i == substitutions->end ()) - bad_arg ("unknown variable '" + vn + "'"); - - r += i->second; - } - - // Append the source string tail, following the last substitution, and - // optimizing for the no-substitutions case. - // - if (p == 0) - return move (s); + size_t sp; + if (substitutions && (sp = s.find (subst)) != string::npos) + return command_substitute (s, sp, *substitutions, subst, subst); - r.append (s.begin () + p, s.end ()); - return r; + return move (s); }; // Perform substitutions in the program path. @@ -163,7 +186,7 @@ namespace butl else // Take the space-separated file path from the next element. { if (++i == e) - bad_arg ("no stdout redirect file specified"); + throw invalid_argument ("no stdout redirect file specified"); a = move (*i); } @@ -174,11 +197,12 @@ namespace butl } catch (const invalid_path& e) { - bad_arg ("invalid stdout redirect file path '" + e.path + "'"); + throw invalid_argument ("invalid stdout redirect file path '" + + e.path + "'"); } if (redir->empty ()) - bad_arg ("empty stdout redirect file path"); + throw invalid_argument ("empty stdout redirect file path"); if (redir->relative () && !cwd.empty ()) redir = cwd / *redir; diff --git a/libbutl/command.mxx b/libbutl/command.mxx index 02c8eb8..143d406 100644 --- a/libbutl/command.mxx +++ b/libbutl/command.mxx @@ -85,4 +85,38 @@ LIBBUTL_MODEXPORT namespace butl const optional& = nullopt, char subst = '@', const std::function& = {}); + + // Reusable substitution utility functions. + // + // Unlike command_run(), these support different opening and closing + // substitution characters (e.g., ). Note that unmatched closing + // characters are treated literally and there is no support for their + // escaping (which would only be necessary if we needed to support variable + // names containing the closing character). + + // Perform substitutions in a string. The second argument should be the + // position of the openning substitution character in the passed string. + // Throw invalid_argument for a malformed substitution or an unknown + // variable name. + // + LIBBUTL_SYMEXPORT std::string + command_substitute (const std::string&, std::size_t, + const command_substitution_map&, + char open, char close); + + // As above but using a callback instead of a map. + // + // Specifically, on success, the callback should substitute the specified + // variable in out by appending its value and returning true. On failure, + // the callback can either throw invalid_argument or return false, in which + // case the standard "unknown substitution variable ..." exception will be + // thrown. + // + using command_substitution_callback = + bool (const std::string& var, std::string& out); + + LIBBUTL_SYMEXPORT std::string + command_substitute (const std::string&, std::size_t, + const std::function&, + char open, char close); } diff --git a/tests/command/testscript b/tests/command/testscript index 40af276..f2b046a 100644 --- a/tests/command/testscript +++ b/tests/command/testscript @@ -114,9 +114,9 @@ end { test.options += -s v=a - $* 'p @a b@' 2>"unmatched substitution character '@' in '@a'" != 0 : unterm-var - $* "p '@a b@'" 2>"whitespace in variable name 'a b'" != 0 : ws-var - $* 'p @x@' 2>"unknown variable 'x'" != 0 : unknown-var + $* 'p @a b@' 2>"unmatched substitution character '@'" != 0 : unterm-var + $* "p '@a b@'" 2>"whitespace in substitution variable 'a b'" != 0 : ws-var + $* 'p @x@' 2>"unknown substitution variable 'x'" != 0 : unknown-var } } -- cgit v1.1