Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perhaps another nice utility to also generate (not just consume) cmdlines #54

Open
1 of 7 tasks
xparq opened this issue Nov 5, 2023 · 1 comment
Open
1 of 7 tasks

Comments

@xparq
Copy link
Owner

xparq commented Nov 5, 2023

  • Just dump the current state... Well, and also quote args with spaces... and other scary chars...

    • Umm... Except map doesn't preserve order, so we're fucked with the named ones!... :-o (And no, ordered_map doesn't mean the order of insertion either!... :-/ )
    • Ummmm... And even if some rudimentary quoting is kinda easy, there's still the open-ended problem of command-lines being consumed by shells, so such a generator must actually speak the (quoting-globbing-escaping) language of a certain particular target shell that it prepares the command-line to!... A hard NO to that!
      • Mmm, but actually... ;) OK, well, just that some generic features which are mostly OK for most shells, or are a good baseline for further custom app-level processing, would still be nice.
        (A quoting example is done below, and some escaping callback lambda could also be added, too.)
        -> Also, wtime does something similar, and it's super handy. See its impl. -- with escaping for Win32::CreateProcess -- below, in another comment!
      • Also, BTW, the escaping function can be a callback!
        • ...at the cost of adding <functional> to the comp. burden! :-/
  • And then listvals() (already there in the tests) could be added, too, reusing the same mechanics. The tests use a stream as an output:

    auto listvals(auto const& container, const char* tail = "\n", const char* sep = ", ")
    {
        for (auto v = container.begin(); v != container.end(); ++v)
    	    cout << (v == container.begin() ? "":sep)
    	         << *v
    	         << (v+1 == container.end() ? tail:"");
    }
    

    but it could just as well write to a string (and an improved one could even be nice and precalc. its length first -- and an even nicer one that also does auto-quoting, or an even nicer one that does that with multi-char quotes... :) ):

    #include <string>
    #include <string_view>
    #include <cstring>
    #include <cassert>
    
    #define FOUND(expr) ((expr) != std::string::npos)
    #define CONTAINS(str, chars) FOUND((str).find_first_of(chars))
    string listvals(auto const& container, const char prewrap[] = "", const char postwrap[] = "", const char sep[] = ", ",
        const char* quote = "\"", // not const char[], to hint that it accepts nullptr!
        const char* scary_chars = " \t\n")
    // The pre/post wrapping are optional parts that only get written if not empty,
    // to support cases where callers would otherwise have to add an annoying
    // `if (container.empty())` or two themselves.
    {
        string result;
        if (!container.empty()) {
    	    size_t QLEN = quote ? strlen(quote) : 0;
    	    // Precalc. size... (Note: we're processing cmd args. We got time.)
    	    size_t size = strlen(prewrap) + (container.size() - 1) * strlen(sep) + strlen(postwrap);
    	    for (auto& v : container)
    		    size += v.length()
    			    + (quote && *quote && CONTAINS(v, scary_chars) ? // add quotes...
    				    (QLEN>1 ? QLEN:2) : 0); // special case for 1 (-> pair)!
    	    result.reserve(size);
    	    // Write...
    	    result += prewrap;
    	    for (auto v = container.begin(); v != container.end(); ++v) {
    		    if (quote && *quote && CONTAINS(*v, scary_chars))
    			    { result += string_view(quote, quote + (QLEN/2 ? QLEN/2 : 1)); // special case for 1 quote!
    			      result += *v;
    			      result += string_view(quote + QLEN/2); }
    		    else    { result += *v; }
    		    result += (v+1 == container.end() ? postwrap : sep);
    	    }
    //cout << "\n\n["<<result<<"]: " << "result.length() =? size: " << dec << result.length() << " vs. " << size << "\n\n";
    	    assert(result.length() == size);
        }
        return result;
    }
    #undef FOUND
    #undef CONTAINS
    
  • ...then the args "serializer" could be as simple as (well, but still needs to write to a string, as the other!):

    void dumpargs(Args& args, char prefixchar = '-', const char* longprefix = "--")
    {
        // Named...
        for (auto& [name, val] : args.named()) {
    	    if (name.length() == 1)
    		    cout << prefixchar << name << listvals(val, " ", "", " ");
    	    else
    		    cout << longprefix << name << listvals(val, "=", "", " ");
    	    cout << " ";
        }
        // Positional...
        cout << listvals(args.positional(), "", "", " ");
    }
    
  • Could be extra useful if the named/positional accessors would drop their (pretty orthodox) const (Drop const from named() and positional() (or have it both ways?) #55)! Then you could manipulate the arg set, and then "render" it to a new command line!

@xparq xparq changed the title Perhaps another nice utility to also generate cmdlines, not just consume Perhaps another nice utility to also generate (not just consume) cmdlines Nov 5, 2023
@xparq
Copy link
Owner Author

xparq commented Aug 12, 2024

This is the current cmdline builder in wtime, with quoting/escaping logic specific to Win32::CreateProcess:

class CmdLine
{
public:
    static string escape_win32(const string& arg)
    // Written by Claude 3.5 Sonnet; reviewed by ChatGPT 4o... Only tested with spaces!
    {
	    if (arg.find_first_of(" \t\n\v\"") == string::npos) {
		    return arg;
	    }

	    string escaped = "\"";
	    for (auto it = arg.begin(); ; ++it) {
		    unsigned backslashes = 0;
		    while (it != arg.end() && *it == '\\') {
			    ++it;
			    ++backslashes;
		    }

		    if (it == arg.end()) {
			    escaped.append(backslashes * 2, '\\');
			    break;
		    } else if (*it == '"') {
			    escaped.append(backslashes * 2 + 1, '\\');
			    escaped.push_back(*it);
		    } else {
			    escaped.append(backslashes, '\\');
			    escaped.push_back(*it);
		    }
	    }
	    escaped.push_back('"');
	    return escaped;
    }

    static string build(char const* const* argv, int argc)
    {
	    vector<string> args(argv, argv + argc);
	    string cmdline;
	    for (const auto& arg : args) {
		    if (!cmdline.empty()) cmdline += ' ';
		    cmdline += escape_win32(arg);
	    }
	    return cmdline;
    }
}; // class cmdline

@xparq xparq pinned this issue Aug 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant