Stroika Library 3.0d18
 
Loading...
Searching...
No Matches
CommandLine.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
7#include "Stroika/Foundation/Characters/SDKString.h"
10#include "Stroika/Foundation/Containers/Set.h"
11
12#include "CommandLine.h"
13
14using namespace Stroika::Foundation;
17using namespace Stroika::Foundation::Execution;
18using namespace Stroika::Foundation::Traversal;
19
20/*
21 ********************************************************************************
22 ******************* Execution::InvalidCommandLineArgument **********************
23 ********************************************************************************
24 */
25InvalidCommandLineArgument::InvalidCommandLineArgument ()
26 : RuntimeErrorException<>{"Invalid Command Argument"sv}
27{
28}
29InvalidCommandLineArgument::InvalidCommandLineArgument (const String& message)
30 : RuntimeErrorException<>{message.As<wstring> ()}
31 , fMessage{message}
32{
33}
34InvalidCommandLineArgument::InvalidCommandLineArgument (const String& message, const String& argument)
35 : RuntimeErrorException<> (message.As<wstring> ())
36 , fMessage{message}
37 , fArgument{argument}
38{
39}
40
41/*
42 ********************************************************************************
43 ****************** Execution::MatchesCommandLineArgument ***********************
44 ********************************************************************************
45 */
47DISABLE_COMPILER_GCC_WARNING_START ("GCC diagnostic ignored \"-Wdeprecated-declarations\"");
48DISABLE_COMPILER_CLANG_WARNING_START ("clang diagnostic ignored \"-Wdeprecated-declarations\"");
49namespace {
50 String Simplify2Compare_ (const String& actualArg)
51 {
52 return actualArg.StripAll ([] (Characters::Character c) -> bool { return c == '-' or c == '/'; }).ToLowerCase ();
53 }
54}
55
56bool Execution::MatchesCommandLineArgument (const String& actualArg, const String& matchesArgPattern)
57{
58 // Command-line arguments must start with - or / (windows only)
59 if (actualArg.empty ()) {
60 return false;
61 }
62#if qStroika_Foundation_Common_Platform_Windows
63 if (actualArg[0] != '-' and actualArg[0] != '/') {
64 return false;
65 }
66#else
67 if (actualArg[0] != '-') {
68 return false;
69 }
70#endif
71 return Simplify2Compare_ (actualArg) == Simplify2Compare_ (matchesArgPattern);
72}
73
74bool Execution::MatchesCommandLineArgument (const Iterable<String>& argList, const String& matchesArgPattern)
75{
76 return static_cast<bool> (
77 argList.Find ([matchesArgPattern] (String i) -> bool { return Execution::MatchesCommandLineArgument (i, matchesArgPattern); }));
78}
79
80optional<String> Execution::MatchesCommandLineArgumentWithValue ([[maybe_unused]] const String& actualArg, [[maybe_unused]] const String& matchesArgPattern)
81{
82 Require (matchesArgPattern.size () > 0 and matchesArgPattern[matchesArgPattern.size () - 1] == '=');
84 // must first strip everything after the '=' in the actualarg, and then similar to first overload...
85 return nullopt;
86}
87
88optional<String> Execution::MatchesCommandLineArgumentWithValue (const Iterable<String>& argList, const String& matchesArgPattern)
89{
90 auto i =
91 argList.Find ([matchesArgPattern] (const String& i) -> bool { return Execution::MatchesCommandLineArgument (i, matchesArgPattern); });
92 if (i != argList.end ()) {
93 ++i;
94 if (i == argList.end ()) [[unlikely]] {
95 Execution::Throw (InvalidCommandLineArgument{});
96 }
97 else {
98 return optional<String>{*i};
99 }
100 }
101 return nullopt;
102}
103DISABLE_COMPILER_MSC_WARNING_END (4996);
104DISABLE_COMPILER_GCC_WARNING_END ("GCC diagnostic ignored \"-Wdeprecated-declarations\"");
105DISABLE_COMPILER_CLANG_WARNING_END ("clang diagnostic ignored \"-Wdeprecated-declarations\"");
106
107/*
108 ********************************************************************************
109 ************************** CommandLine::Option *********************************
110 ********************************************************************************
111 */
112String CommandLine::Option::GetArgumentDescription (bool includeArg) const
113{
114 if (not this->fSupportsArgument) {
115 includeArg = false;
116 }
117 String argName = this->fHelpArgName.value_or ("ARG"sv);
118 if (fSingleCharName and fLongName) {
119 if (includeArg) {
120 return "(-{} {}|--{}={})"_f(*fSingleCharName, argName, *fLongName, argName);
121 }
122 else {
123 return "(-{}|--{})"_f(*fSingleCharName, *fLongName);
124 }
125 }
126 else if (this->fSingleCharName) {
127 if (includeArg) {
128 return "-{} {}"_f(*fSingleCharName, argName);
129 }
130 else {
131 return "-{}"_f(*fSingleCharName);
132 }
133 }
134 else if (fLongName) {
135 if (includeArg) {
136 return "--"sv + *fLongName + "="sv + argName;
137 }
138 else {
139 return "--"sv + *fLongName;
140 }
141 }
142 else {
143 if (includeArg) {
144 return argName;
145 }
146 else {
147 return String{};
148 }
149 }
150}
151
152String CommandLine::Option::ToString () const
153{
154 StringBuilder sb;
155 sb << "{"sv;
156 if (fSingleCharName) {
157 sb << "SingleCharName: "sv << *fSingleCharName << ","sv;
158 }
159 if (fLongName) {
160 sb << "LongName: "sv << *fLongName << ","sv;
161 }
162 sb << "CaseSensitive: "sv << fLongNameCaseSensitive << ","sv;
163 sb << "SupportsArgument: "sv << fSupportsArgument << ","sv;
164 sb << "IfSupportsArgumentThenRequired: "sv << fIfSupportsArgumentThenRequired << ","sv;
165 sb << "SupportsArgument: "sv << fSupportsArgument << ","sv;
166 sb << "Repeatable: "sv << fRepeatable << ","sv;
167 if (fHelpArgName) {
168 sb << "HelpArgName: "sv << *fHelpArgName << ","sv;
169 }
170 if (fHelpOptionText) {
171 sb << "HelpOptionText: "sv << *fHelpOptionText << ","sv;
172 }
173 sb << "}"sv;
174 return sb;
175}
176
177/*
178 ********************************************************************************
179 ********************************** CommandLine *********************************
180 ********************************************************************************
181 */
182namespace {
183 Sequence<String> ParseArgs_ (const String& cmdLine)
184 {
185 Sequence<String> args;
186 size_t e = cmdLine.length ();
187 StringBuilder curToken;
188 Character endQuoteChar = '\0';
189 for (size_t i = 0; i < e; ++i) {
190 Character c = cmdLine[i];
191 if (endQuoteChar != '\0' and c == endQuoteChar) {
192 args.Append (curToken.str ());
193 endQuoteChar = '\0';
194 curToken.clear ();
195 }
196 else if (c == '\'' or c == '\"') {
197 endQuoteChar = c;
198 }
199 else if (endQuoteChar != '\0') {
200 // in middle of quoted string
201 curToken += c;
202 }
203 else {
204 bool isTokenChar = not c.IsWhitespace ();
205 if (isTokenChar) {
206 curToken += c;
207 }
208 else {
209 if (curToken.size () != 0) {
210 args.Append (curToken.str ());
211 curToken.clear ();
212 }
213 }
214 }
215 }
216 if (curToken.size () != 0) {
217 args.Append (curToken.str ());
218 }
219 return args;
220 }
221}
222CommandLine::CommandLine (const String& cmdLine)
223 : fArgs_{ParseArgs_ (cmdLine)}
224{
225}
226
227CommandLine ::CommandLine (WrapInShell wrapInShell, const String& cmdLine)
228{
229#if qStroika_Foundation_Common_Platform_Windows
230 // this is the version of CMD.exe to invoke (I think)
231 // https://en.wikipedia.org/wiki/COMSPEC
232 static const String kCOMPSEC_ = [] () -> String {
234 if (const char* env_p = std::getenv ("COMSPEC")) {
235 return String::FromNarrowSDKString (env_p);
236 }
237 DISABLE_COMPILER_MSC_WARNING_END (4996)
238 return "C:\\WINDOWS\\system32\\cmd.exe"sv;
239 }();
240#endif
241 switch (wrapInShell) {
242 case WrapInShell::eBash:
243 fArgs_ += "bash"sv;
244 fArgs_ += "-c"sv;
245 fArgs_ += cmdLine;
246 fShellStyleQuoting_ = StringShellQuoting::eBash;
247 break;
248#if qStroika_Foundation_Common_Platform_Windows
249 case WrapInShell::eWindowsCMD:
250 fArgs_ += kCOMPSEC_;
251 // fArgs_ += "/D";
252 // fArgs_ += "/E:OFF";
253 // fArgs_ += "/F:OFF";
254 fArgs_ += "/C"sv; // Carries out the command specified by string and then terminates
255 fArgs_ += cmdLine;
256 fShellStyleQuoting_ = StringShellQuoting::eWindowsCMD;
257 break;
258#endif
259 default:
261 }
262}
263
264CommandLine::CommandLine (int argc, const char* argv[])
265{
266 for (int i = 0; i < argc; ++i) {
267 fArgs_.push_back (String::FromNarrowSDKString (argv[i]));
268 }
269}
270
271CommandLine::CommandLine (int argc, const wchar_t* argv[])
272{
273 for (int i = 0; i < argc; ++i) {
274 fArgs_.push_back (argv[i]);
275 }
276}
277
278String CommandLine::GenerateUsage (const Iterable<Option>& options) const
279{
280 return GenerateUsage (GetAppName (), options);
281}
282
283String CommandLine::GenerateUsage (const String& exeName, const Iterable<Option>& options)
284{
285 const String kIndent_ = " "sv;
286 StringBuilder sb;
287 sb << "Usage: "sv << exeName;
288 options.Apply ([&] (Option o) {
289 sb << " [" << o.GetArgumentDescription (true) << "]"sv;
290 if (o.fRepeatable) {
291 if (o.fRequired) {
292 sb << "+"sv;
293 }
294 else {
295 sb << "*"sv;
296 }
297 }
298 else if (not o.fRequired) {
299 sb << "?"sv;
300 }
301 });
302 sb << "\n"sv;
303 size_t maxArgDescLen{0}; // used to tab-out descriptions so they align
304 options.Apply ([&] (const Option& o) {
305 if (o.fHelpOptionText) {
306 maxArgDescLen = max (maxArgDescLen, o.GetArgumentDescription ().length ());
307 }
308 });
309 options.Apply ([&] (const Option& o) {
310 if (o.fHelpOptionText) {
311 String argDesc = o.GetArgumentDescription ();
312 sb << kIndent_ << argDesc << " "_k.Repeat (static_cast<unsigned int> (kIndent_.length () + maxArgDescLen - argDesc.size ()))
313 << "/* " << *o.fHelpOptionText << " */\n";
314 }
315 });
316 return sb;
317}
318
320{
321 if (auto oe = ValidateQuietly (options)) {
322 Throw (*oe);
323 }
324}
325
326nonvirtual optional<InvalidCommandLineArgument> CommandLine::ValidateQuietly (Iterable<Option> options) const
327{
328 Set<Option> all{options};
329 Set<Option> unused{all};
330 for (Iterator<String> argi = fArgs_.begin () + 1; argi != fArgs_.end (); ++argi) {
331 if (not all.First ([&] (Option o) {
332 if (optional<pair<bool, optional<String>>> oRes = ParseOneArg_ (o, &argi)) {
333 unused.RemoveIf (o);
334 return true;
335 }
336 return false;
337 })) {
338 Execution::Throw (InvalidCommandLineArgument{"Unrecognized argument: "sv + *argi, *argi});
339 }
340 }
341 if (auto o = unused.First ([] (Option o) { return o.fRequired; })) {
342 return InvalidCommandLineArgument{"Required command line argument "sv + o->GetArgumentDescription () + " was not provided"sv};
343 }
344 return nullopt;
345}
346
347String CommandLine::GetAppName (bool onlyBaseName) const
348{
349 if (fArgs_.empty ()) {
350 return String{};
351 }
352 if (onlyBaseName) {
353 filesystem::path p = fArgs_[0].As<filesystem::path> ();
354 return String{p.stem ()};
355 }
356 return fArgs_[0];
357}
358
359tuple<bool, Sequence<String>> CommandLine::Get (const Option& o) const
360{
361 bool found = false;
362 Sequence<String> arguments;
363 for (Iterator<String> argi = fArgs_.begin () + 1; argi != fArgs_.end (); ++argi) {
364 if (optional<pair<bool, optional<String>>> oRes = ParseOneArg_ (o, &argi)) {
365 if (oRes->first) {
366 found = true;
367 }
368 if (oRes->second) {
369 arguments += *oRes->second;
370 }
371 if (not o.fRepeatable) {
372 break; // no need to keep looking
373 }
374 }
375 }
376 if (o.fRequired and not found and arguments.empty ()) {
377 Throw (InvalidCommandLineArgument{"Command line argument '{}' required but not provided"_f(o.GetArgumentDescription ())});
378 }
379 if (found and o.fSupportsArgument and o.fIfSupportsArgumentThenRequired and arguments.empty ()) {
380 Throw (InvalidCommandLineArgument{"Command line argument {} provided, but without required argument"_f(o.GetArgumentDescription ())});
381 }
382 return make_tuple (found, arguments);
383}
384
385String CommandLine::ToString () const
386{
387 return this->As<String> (); // hides some details, but most useful summary typically
388}
389
390optional<pair<bool, optional<String>>> CommandLine::ParseOneArg_ (const Option& o, Iterator<String>* argi)
391{
392 RequireNotNull (argi);
393 Require (not argi->Done ());
394
395 String ai = **argi;
396 if (o.fSingleCharName and ai.length () == 2 and ai[0] == '-' and ai[1] == o.fSingleCharName) {
397 if (o.fSupportsArgument) {
398 ++(*argi);
399 if ((*argi).Done ()) {
400 if (o.fIfSupportsArgumentThenRequired) {
401 Throw (InvalidCommandLineArgument{"Command line argument requires an argument to it, but none provided (= or following argument)"sv, ai});
402 }
403 return make_pair (true, nullopt);
404 }
405 else {
406 return make_pair (true, **argi);
407 }
408 }
409 return make_pair (true, nullopt);
410 }
411
412 // this isn't right!!! - in case where no argument supported - must match all of string (and if next char not =)
413 // but its CLOSE--LGP 2024-03-05
414 if (o.fLongName and ai.length () >= 2 + o.fLongName->size () and ai[0] == '-' and ai[1] == '-' and
415 String::EqualsComparer{o.fLongNameCaseSensitive}(ai.SubString (2, o.fLongName->size () + 2), *o.fLongName)) {
416 if (o.fSupportsArgument) {
417 // see if '=' follows longname
418 String restOfArgi = ai.SubString (2 + o.fLongName->size ());
419 if (restOfArgi.size () >= 1 and restOfArgi[0] == '=') {
420 return make_pair (true, restOfArgi.SubString (1));
421 }
422 else {
423 ++(*argi);
424 if ((*argi).Done ()) {
425 if (o.fIfSupportsArgumentThenRequired) {
427 "Command line argument requires an argument to it, but none provided (= or following argument)"sv, ai});
428 }
429 return make_pair (true, nullopt);
430 }
431 else {
432 return make_pair (true, **argi);
433 }
434 }
435 }
436 return make_pair (true, nullopt);
437 }
438 // anything that cannot be an option (-x or --y...) is skipped, but anything else - that could be a plain filename (even a bare '-') is matched as 'argument'
439 if (not o.fSingleCharName and not o.fLongName and o.fSupportsArgument and not(ai.size () >= 2 and ai.StartsWith ("-"sv))) {
440 // note we add the argument, but don't set 'found'
441 return make_pair (false, **argi);
442 }
443 return nullopt;
444}
445
446template <>
447String CommandLine::As<String> () const
448{
449 return As<String> (this->fShellStyleQuoting_);
450}
451
452template <>
453String CommandLine::As<String> (optional<CommandLine::StringShellQuoting> shellStyle) const
454{
455 // UNCLEAR how to handle quoting of elements inside string - so for now, do (less) harm? DOnt try to
456 // quote the quotes (but still wrap items in quotes).
457 // --LGP 2024-12-07
458 return fArgs_.Join<String> (
459 [&] (const String& i) {
460 // default in Stroika is wrap in double-quotes, and \-quote double-quote characters, and rest leave alone
461 if (shellStyle == nullopt) {
462 if (i.ContainsAny ({' ', '\"'})) {
463 return "\"{}\""_f(i);
464 //return "\"{}\""_f(i.ReplaceAll ("\""sv, "\\\""sv));
465 }
466 else {
467 return i;
468 }
469 }
470 else if (shellStyle == StringShellQuoting::eWindowsCMD) {
471 // @todo - NO IDEA - I think "" replaces ", in cmd shell?
472 if (i.ContainsAny ({' ', '\"'})) {
473 return "\"{}\""_f(i);
474 // return "\"{}\""_f(i.ReplaceAll ("\""sv, "\"\""sv));
475 }
476 else {
477 return i;
478 }
479 }
480 else if (shellStyle == StringShellQuoting::eBash) {
481 // @todo more complex - think I need to quote other stuff, but unclear
482 if (i.ContainsAny ({' ', '\"'})) {
483 return "\"{}\""_f(i);
484 //return "\"{}\""_f(i.ReplaceAll ("\""sv, "\\\""sv));
485 }
486 else {
487 return i;
488 }
489 }
490 return i;
491 },
492 " "sv);
493}
#define AssertNotImplemented()
Definition Assertions.h:401
#define RequireNotReached()
Definition Assertions.h:385
#define RequireNotNull(p)
Definition Assertions.h:347
constexpr bool IsWhitespace() const noexcept
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
nonvirtual size_t size() const noexcept
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual size_t length() const noexcept
Definition String.inl:1051
static String FromNarrowSDKString(const char *from)
Definition String.inl:470
nonvirtual size_t size() const noexcept
Definition String.inl:534
nonvirtual String SubString(SZ from) const
nonvirtual bool StartsWith(const Character &c, CompareOptions co=eWithCase) const
Definition String.cpp:1059
nonvirtual String StripAll(bool(*removeCharIf)(Character)) const
Definition String.cpp:1664
A generalization of a vector: a container whose elements are keyed by the natural numbers.
nonvirtual void Append(ArgByValueType< value_type > item)
Definition Sequence.inl:330
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
nonvirtual void Validate(Iterable< Option > options) const
nonvirtual optional< InvalidCommandLineArgument > ValidateQuietly(Iterable< Option > options) const
like Validate - but return optional< InvalidCommandLineArgument> instead of throwing
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:237
nonvirtual void Apply(const function< void(ArgByValueType< T > item)> &doToElement, Execution::SequencePolicy seq=Execution::SequencePolicy::eDEFAULT) const
Run the argument function (or lambda) on each element of the container.
nonvirtual Iterator< T > Find(THAT_FUNCTION &&that, Execution::SequencePolicy seq=Execution::SequencePolicy::eDEFAULT) const
Run the argument bool-returning function (or lambda) on each element of the container,...
nonvirtual bool empty() const
Returns true iff size() == 0.
Definition Iterable.inl:308
static constexpr default_sentinel_t end() noexcept
Support for ranged for, and STL syntax in general.
An Iterator<T> is a copyable object which allows traversing the contents of some container....
Definition Iterator.h:225
nonvirtual bool Done() const
Done () means there is nothing left in this iterator (a synonym for (it == container....
Definition Iterator.inl:147
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43
STL namespace.