Stroika Library 3.0d20
 
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#include "Stroika/Foundation/Execution/Module.h"
13
14#include "CommandLine.h"
15
16using namespace Stroika::Foundation;
19using namespace Stroika::Foundation::Common;
20using namespace Stroika::Foundation::Execution;
21using namespace Stroika::Foundation::Traversal;
22
23/*
24 ********************************************************************************
25 ******************* Execution::InvalidCommandLineArgument **********************
26 ********************************************************************************
27 */
28InvalidCommandLineArgument::InvalidCommandLineArgument ()
29 : RuntimeErrorException<>{"Invalid Command Argument"sv}
30{
31}
32InvalidCommandLineArgument::InvalidCommandLineArgument (const String& message)
33 : RuntimeErrorException<>{message}
34{
35}
36InvalidCommandLineArgument::InvalidCommandLineArgument (const String& message, const String& argument)
37 : RuntimeErrorException<>{message}
38 , fArgument{argument}
39{
40}
41
42/*
43 ********************************************************************************
44 ****************** Execution::MatchesCommandLineArgument ***********************
45 ********************************************************************************
46 */
48DISABLE_COMPILER_GCC_WARNING_START ("GCC diagnostic ignored \"-Wdeprecated-declarations\"");
49DISABLE_COMPILER_CLANG_WARNING_START ("clang diagnostic ignored \"-Wdeprecated-declarations\"");
50namespace {
51 String Simplify2Compare_ (const String& actualArg)
52 {
53 return actualArg.StripAll ([] (Characters::Character c) -> bool { return c == '-' or c == '/'; }).ToLowerCase ();
54 }
55}
56
57bool Execution::MatchesCommandLineArgument (const String& actualArg, const String& matchesArgPattern)
58{
59 // Command-line arguments must start with - or / (windows only)
60 if (actualArg.empty ()) {
61 return false;
62 }
63#if qStroika_Foundation_Common_Platform_Windows
64 if (actualArg[0] != '-' and actualArg[0] != '/') {
65 return false;
66 }
67#else
68 if (actualArg[0] != '-') {
69 return false;
70 }
71#endif
72 return Simplify2Compare_ (actualArg) == Simplify2Compare_ (matchesArgPattern);
73}
74
75bool Execution::MatchesCommandLineArgument (const Iterable<String>& argList, const String& matchesArgPattern)
76{
77 return static_cast<bool> (
78 argList.Find ([matchesArgPattern] (String i) -> bool { return Execution::MatchesCommandLineArgument (i, matchesArgPattern); }));
79}
80
81optional<String> Execution::MatchesCommandLineArgumentWithValue ([[maybe_unused]] const String& actualArg, [[maybe_unused]] const String& matchesArgPattern)
82{
83 Require (matchesArgPattern.size () > 0 and matchesArgPattern[matchesArgPattern.size () - 1] == '=');
85 // must first strip everything after the '=' in the actualarg, and then similar to first overload...
86 return nullopt;
87}
88
89optional<String> Execution::MatchesCommandLineArgumentWithValue (const Iterable<String>& argList, const String& matchesArgPattern)
90{
91 auto i = argList.Find ([matchesArgPattern] (const String& i) -> bool { return 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 includeArgName) const
113{
114 if (not this->fSupportsArgument and not this->IsPositionArgument ()) {
115 includeArgName = false;
116 }
117 String argName = this->fHelpArgName.value_or ("ARG"sv);
118 if (fSingleCharName and fLongName) {
119 if (includeArgName) {
120 return "(-{} {}|--{}={})"_f(*fSingleCharName, argName, *fLongName, argName);
121 }
122 else {
123 return "(-{}|--{})"_f(*fSingleCharName, *fLongName);
124 }
125 }
126 else if (this->fSingleCharName) {
127 if (includeArgName) {
128 return "-{} {}"_f(*fSingleCharName, argName);
129 }
130 else {
131 return "-{}"_f(*fSingleCharName);
132 }
133 }
134 else if (fLongName) {
135 if (includeArgName) {
136 return "--"sv + *fLongName + "="sv + argName;
137 }
138 else {
139 return "--"sv + *fLongName;
140 }
141 }
142 else {
143 if (includeArgName) {
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 if (this->IsPositionArgument ()) {
163 sb << "POSITIONAL ARGUMENT,"sv;
164 }
165 sb << "CaseSensitive: "sv << fLongNameCaseSensitive << ","sv;
166 if (not this->IsPositionArgument ()) {
167 sb << "SupportsArgument: "sv << fSupportsArgument << ","sv;
168 sb << "IfSupportsArgumentThenRequired: "sv << fIfSupportsArgumentThenRequired << ","sv;
169 }
170 sb << "Repeatable: "sv << fRepeatable << ","sv;
171 if (fSkipFirstNArguments) {
172 sb << "SkipFirstNArguments: "sv << fSkipFirstNArguments << ","sv;
173 }
174 if (fHelpArgName) {
175 sb << "HelpArgName: "sv << *fHelpArgName << ","sv;
176 }
177 if (fHelpOptionText) {
178 sb << "HelpOptionText: "sv << *fHelpOptionText << ","sv;
179 }
180 sb << "}"sv;
181 return sb;
182}
183
184/*
185 ********************************************************************************
186 ********************************** CommandLine *********************************
187 ********************************************************************************
188 */
189namespace {
190 Sequence<String> ParseArgs_ (const String& cmdLine)
191 {
192 Sequence<String> args;
193 size_t e = cmdLine.length ();
194 StringBuilder curToken;
195 Character endQuoteChar = '\0';
196 for (size_t i = 0; i < e; ++i) {
197 Character c = cmdLine[i];
198 if (endQuoteChar != '\0' and c == endQuoteChar) {
199 args.Append (curToken.str ());
200 endQuoteChar = '\0';
201 curToken.clear ();
202 }
203 else if (c == '\'' or c == '\"') {
204 endQuoteChar = c;
205 }
206 else if (endQuoteChar != '\0') {
207 // in middle of quoted string
208 curToken += c;
209 }
210 else {
211 bool isTokenChar = not c.IsWhitespace ();
212 if (isTokenChar) {
213 curToken += c;
214 }
215 else {
216 if (curToken.size () != 0) {
217 args.Append (curToken.str ());
218 curToken.clear ();
219 }
220 }
221 }
222 }
223 if (curToken.size () != 0) {
224 args.Append (curToken.str ());
225 }
226 return args;
227 }
228}
229CommandLine::CommandLine (const String& cmdLine)
230 : fArgs_{ParseArgs_ (cmdLine)}
231{
232}
233
234CommandLine::CommandLine (WrapInShell wrapInShell, const String& cmdLine)
235{
236 switch (wrapInShell) {
237 case WrapInShell::eBash:
238#if qStroika_Foundation_Common_Platform_Windows
239 {
240 // https://stroika.atlassian.net/browse/STK-1029
241 // Weird bug workaround only needed on Medusa? - unclear why
242 if (optional<filesystem::path> pp = FindExecutableInPath ("bash"sv)) {
243 fArgs_ += String{*pp};
244 }
245 else {
246 Throw (IO::FileSystem::Exception{make_error_code (errc::no_such_file_or_directory), filesystem::path{"bash"sv}});
247 }
248 }
249#else
250 fArgs_ += "bash"sv;
251#endif
252 fArgs_ += "-c"sv;
253 fArgs_ += cmdLine;
254 fShellStyleQuoting_ = StringShellQuoting::eBash;
255 break;
256#if qStroika_Foundation_Common_Platform_Windows
257 case WrapInShell::eWindowsCMD: {
258 // this is the version of CMD.exe to invoke (I think)
259 // https://en.wikipedia.org/wiki/COMSPEC
260 static const String kCOMPSEC_ = [] () -> String {
261 DISABLE_COMPILER_MSC_WARNING_START (4996)
262 if (const char* env_p = std::getenv ("COMSPEC")) {
263 return String::FromNarrowSDKString (env_p);
264 }
265 DISABLE_COMPILER_MSC_WARNING_END (4996)
266 return "C:\\WINDOWS\\system32\\cmd.exe"sv;
267 }();
268 fArgs_ += kCOMPSEC_;
269 // fArgs_ += "/D";
270 // fArgs_ += "/E:OFF";
271 // fArgs_ += "/F:OFF";
272 fArgs_ += "/C"sv; // Carries out the command specified by string and then terminates
273 fArgs_ += cmdLine;
274 fShellStyleQuoting_ = StringShellQuoting::eWindowsCMD;
275 } break;
276#endif
277 default:
279 }
280}
281
282CommandLine::CommandLine (int argc, const char* argv[])
283{
284 for (int i = 0; i < argc; ++i) {
285 fArgs_.push_back (String::FromNarrowSDKString (argv[i]));
286 }
287}
288
289CommandLine::CommandLine (int argc, const wchar_t* argv[])
290{
291 for (int i = 0; i < argc; ++i) {
292 fArgs_.push_back (argv[i]);
293 }
294}
295
296String CommandLine::GenerateUsage (const Iterable<Option>& options) const
297{
298 return GenerateUsage (GetAppName (), options);
299}
300
301String CommandLine::GenerateUsage (const String& exeName, const Iterable<Option>& options)
302{
303 static const String kIndent_ = " "sv;
304 StringBuilder sb;
305 sb << "Usage: "sv << exeName;
306 options.Apply ([&] (const Option& o) {
307 sb << " [" << o.GetArgumentDescription (true) << "]"sv;
308 if (o.fRepeatable) {
309 if (o.fRequired) {
310 sb << "+"sv;
311 }
312 else {
313 sb << "*"sv;
314 }
315 }
316 else if (not o.fRequired) {
317 sb << "?"sv;
318 }
319 });
320 sb << "\n"sv;
321 size_t maxArgDescLen{0}; // used to tab-out descriptions so they align
322 options.Apply ([&] (const Option& o) {
323 if (o.fHelpOptionText) {
324 maxArgDescLen = max (maxArgDescLen, o.GetArgumentDescription ().length ());
325 }
326 });
327 options.Apply ([&] (const Option& o) {
328 if (o.fHelpOptionText) {
329 String argDesc = o.GetArgumentDescription ();
330 sb << kIndent_ << argDesc << " "_k.Repeat (static_cast<unsigned int> (kIndent_.length () + maxArgDescLen - argDesc.size ()))
331 << "/* " << *o.fHelpOptionText << " */\n";
332 }
333 });
334 return sb;
335}
336
337void CommandLine::Validate (const Iterable<Option>& options) const
338{
339 if (auto oe = ValidateQuietly (options)) {
340 Throw (*oe);
341 }
342}
343
344nonvirtual optional<InvalidCommandLineArgument> CommandLine::ValidateQuietly (const Iterable<Option>& options) const
345{
346 Set<Option> all{options};
347 Set<Option> unused{all};
348 for (Iterator<String> argi = fArgs_.begin () + 1; argi != fArgs_.end (); ++argi) {
349 if (not all.First ([&] (const Option& o) {
350 auto oRes = ParseOneArg_ (o, &argi);
351 if (!oRes) {
352 Throw (oRes.error ());
353 }
354 if (*oRes) {
355 unused.RemoveIf (o);
356 return true;
357 }
358 return false;
359 })) {
360 return InvalidCommandLineArgument{"Unrecognized argument: "sv + *argi, *argi};
361 }
362 }
363 if (auto o = unused.First ([] (const Option& o) { return o.fRequired; })) {
364 return InvalidCommandLineArgument{"Required command line argument "sv + o->GetArgumentDescription (true) + " was not provided"sv};
365 }
366 return nullopt;
367}
368
369String CommandLine::GetAppName (bool onlyBaseName) const
370{
371 if (fArgs_.empty ()) {
372 return String{};
373 }
374 if (onlyBaseName) {
375 filesystem::path p = fArgs_[0].As<filesystem::path> ();
376 return String{p.stem ()};
377 }
378 return fArgs_[0];
379}
380
381tuple<bool, Sequence<String>> CommandLine::Get (const Option& o) const
382{
383 bool found = false;
384 size_t nMore2Skip = o.fSkipFirstNArguments.value_or (0);
385 Assert (nMore2Skip == 0 or o.IsPositionArgument ());
386 Sequence<String> arguments;
387 for (Iterator<String> argi = fArgs_.begin () + 1; argi != fArgs_.end (); ++argi) {
388 if (optional<optional<String>> oRes = ThrowIfFailed (ParseOneArg_ (o, &argi))) {
389 found = true;
390 if (nMore2Skip == 0) {
391 if (*oRes) {
392 arguments += **oRes;
393 if (not o.fRepeatable) {
394 break; // no need to keep looking
395 }
396 }
397 }
398 else {
399 --nMore2Skip;
400 }
401 }
402 }
403 if (o.fRequired and not found and arguments.empty ()) {
404 Throw (InvalidCommandLineArgument{"Command line argument '{}' required but not provided"_f(o.GetArgumentDescription ())});
405 }
406 if (found and o.fSupportsArgument and o.fIfSupportsArgumentThenRequired and arguments.empty ()) {
407 Throw (InvalidCommandLineArgument{"Command line argument {} provided, but without required argument"_f(o.GetArgumentDescription ())});
408 }
409 return make_tuple (found, arguments);
410}
411
412String CommandLine::ToString () const
413{
414 return this->As<String> (); // hides some details, but most useful summary typically
415}
416
417StdCompat::expected<optional<optional<String>>, InvalidCommandLineArgument> CommandLine::ParseOneArg_ (const Option& o, Iterator<String>* argi)
418{
419 RequireNotNull (argi);
420 Require (not argi->Done ());
421 using OptionalArgument = optional<String>;
422 using OptionallyHasOption = optional<OptionalArgument>;
425
426 // Sorry api is confusing about these two optionals - not clear how todo better, but its private so no biggie
427 static const RT kMissingArgument_ = RT{OptionallyHasOption{OptionalArgument{}}};
428 static const RT kMissingOption_ = RT{OptionallyHasOption{}};
429
430 String ai = **argi;
431 if (o.fSingleCharName and ai.length () == 2 and ai[0] == '-' and ai[1] == o.fSingleCharName) {
432 if (o.fSupportsArgument) {
433 ++(*argi);
434 if ((*argi).Done ()) {
435 if (o.fIfSupportsArgumentThenRequired) {
436 return ERRT{InvalidCommandLineArgument{
437 "Command line argument requires an argument to it, but none provided (= or following argument)"sv, ai}};
438 }
439 return kMissingArgument_;
440 }
441 else {
442 return RT{OptionallyHasOption{**argi}};
443 }
444 }
445 return kMissingArgument_;
446 }
447
448 String argiUpToEquals = ai;
449 if (optional<size_t> indexOfEquals = argiUpToEquals.Find ('=')) {
450 argiUpToEquals = argiUpToEquals.SubString (0, *indexOfEquals);
451 }
452 if (o.fLongName and ai.length () >= 2 + o.fLongName->size () and ai[0] == '-' and ai[1] == '-' and
453 String::EqualsComparer{o.fLongNameCaseSensitive}(argiUpToEquals.SubString (2), *o.fLongName)) {
454 String restOfArgi = ai.SubString (2 + o.fLongName->size ());
455 if (o.fSupportsArgument) {
456 // see if '=' follows longname
457 if (restOfArgi.size () >= 1 and restOfArgi[0] == '=') {
458 return RT{OptionallyHasOption{restOfArgi.SubString (1)}};
459 }
460 ++(*argi); // Look for argument as string just after --option
461 if ((*argi).Done ()) {
462 if (o.fIfSupportsArgumentThenRequired) {
463 return ERRT{InvalidCommandLineArgument{
464 "Command line argument requires an argument to it, but none provided (= or following argument)"sv, ai}};
465 }
466 return kMissingArgument_;
467 }
468 else {
469 return RT{OptionallyHasOption{**argi}};
470 }
472 }
473 else {
474 return kMissingArgument_;
475 }
477 }
478 // anything that cannot be an option (-x or --y...) is skipped, but anything else - that could be a positional parameter (even a bare '-') is matched as 'argument'
479 if (o.IsPositionArgument () and /* o.fSupportsArgument and */ not(ai.size () >= 2 and ai.StartsWith ("-"sv))) {
480 // note we add the argument, but don't set 'found'
481 return RT{OptionallyHasOption{**argi}};
482 }
483 return kMissingOption_;
484}
485
486template <>
487String CommandLine::As<String> () const
488{
489 return As<String> (this->fShellStyleQuoting_);
490}
491
492template <>
493String CommandLine::As<String> (optional<CommandLine::StringShellQuoting> shellStyle) const
494{
495 // UNCLEAR how to handle quoting of elements inside string - so for now, do (less) harm? DOnt try to
496 // quote the quotes (but still wrap items in quotes).
497 // --LGP 2024-12-07
498 return fArgs_.Join<String> (
499 [&] (const String& i) {
500 // default in Stroika is wrap in double-quotes, and \-quote double-quote characters, and rest leave alone
501 if (shellStyle == nullopt) {
502 if (i.ContainsAny ({' ', '\"'})) {
503 return "\"{}\""_f(i);
504 //return "\"{}\""_f(i.ReplaceAll ("\""sv, "\\\""sv));
505 }
506 else {
507 return i;
508 }
509 }
510 else if (shellStyle == StringShellQuoting::eWindowsCMD) {
511 // @todo - NO IDEA - I think "" replaces ", in cmd shell?
512 if (i.ContainsAny ({' ', '\"'})) {
513 return "\"{}\""_f(i);
514 // return "\"{}\""_f(i.ReplaceAll ("\""sv, "\"\""sv));
515 }
516 else {
517 return i;
518 }
519 }
520 else if (shellStyle == StringShellQuoting::eBash) {
521 // @todo more complex - think I need to quote other stuff, but unclear
522 if (i.ContainsAny ({' ', '\"'})) {
523 return "\"{}\""_f(i);
524 //return "\"{}\""_f(i.ReplaceAll ("\""sv, "\\\""sv));
525 }
526 else {
527 return i;
528 }
529 }
530 return i;
531 },
532 " "sv);
533}
#define AssertNotImplemented()
Definition Assertions.h:401
#define RequireNotReached()
Definition Assertions.h:385
#define RequireNotNull(p)
Definition Assertions.h:347
#define AssertNotReached()
Definition Assertions.h:355
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
nonvirtual optional< size_t > Find(Character c, CompareOptions co=eWithCase) const
Definition String.inl:685
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 optional< InvalidCommandLineArgument > ValidateQuietly(const Iterable< Option > &options) const
like Validate - but return optional< InvalidCommandLineArgument> instead of throwing
nonvirtual void Validate(const Iterable< Option > &options) const
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
EXPECTED::value_type ThrowIfFailed(const EXPECTED &e)
Definition Throw.inl:158
optional< filesystem::path > FindExecutableInPath(const filesystem::path &fn)
If fn refers to an executable - return it (using kPATH, and kPathEXT as appropriate)
Definition Module.cpp:245
STL namespace.