Stroika Library 3.0d16
 
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 */
25Execution::InvalidCommandLineArgument::InvalidCommandLineArgument ()
26 : Execution::RuntimeErrorException<>{"Invalid Command Argument"sv}
27{
28}
29Execution::InvalidCommandLineArgument::InvalidCommandLineArgument (const String& message)
30 : Execution::RuntimeErrorException<>{message.As<wstring> ()}
31 , fMessage{message}
32{
33}
34Execution::InvalidCommandLineArgument::InvalidCommandLineArgument (const String& message, const String& argument)
35 : Execution::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]] {
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 Set<Option> all{options};
322 Set<Option> unused{all};
323 for (Iterator<String> argi = fArgs_.begin () + 1; argi != fArgs_.end (); ++argi) {
324 if (not all.First ([&] (Option o) {
325 if (optional<pair<bool, optional<String>>> oRes = ParseOneArg_ (o, &argi)) {
326 unused.RemoveIf (o);
327 return true;
328 }
329 return false;
330 })) {
331 Execution::Throw (InvalidCommandLineArgument{"Unrecognized argument: "sv + *argi, *argi});
332 }
333 }
334 if (auto o = unused.First ([] (Option o) { return o.fRequired; })) {
335 Execution::Throw (InvalidCommandLineArgument{"Required command line argument "sv + o->GetArgumentDescription () + " was not provided"sv});
336 }
337}
338
339String CommandLine::GetAppName (bool onlyBaseName) const
340{
341 if (fArgs_.empty ()) {
342 return String{};
343 }
344 if (onlyBaseName) {
345 filesystem::path p = fArgs_[0].As<filesystem::path> ();
346 return String{p.stem ()};
347 }
348 return fArgs_[0];
349}
350
351tuple<bool, Sequence<String>> CommandLine::Get (const Option& o) const
352{
353 bool found = false;
354 Sequence<String> arguments;
355 for (Iterator<String> argi = fArgs_.begin () + 1; argi != fArgs_.end (); ++argi) {
356 if (optional<pair<bool, optional<String>>> oRes = ParseOneArg_ (o, &argi)) {
357 if (oRes->first) {
358 found = true;
359 }
360 if (oRes->second) {
361 arguments += *oRes->second;
362 }
363 if (not o.fRepeatable) {
364 break; // no need to keep looking
365 }
366 }
367 }
368 if (o.fRequired and not found and arguments.empty ()) {
369 Execution::Throw (InvalidCommandLineArgument{"Command line argument '{}' required but not provided"_f(o.GetArgumentDescription ())});
370 }
371 if (found and o.fSupportsArgument and o.fIfSupportsArgumentThenRequired and arguments.empty ()) {
372 Execution::Throw (InvalidCommandLineArgument{"Command line argument {} provided, but without required argument"_f(o.GetArgumentDescription ())});
373 }
374 return make_tuple (found, arguments);
375}
376
377String CommandLine::ToString () const
378{
379 return this->As<String> (); // hides some details, but most useful summary typically
380}
381
382optional<pair<bool, optional<String>>> CommandLine::ParseOneArg_ (const Option& o, Iterator<String>* argi)
383{
384 RequireNotNull (argi);
385 Require (not argi->Done ());
386
387 String ai = **argi;
388 if (o.fSingleCharName and ai.length () == 2 and ai[0] == '-' and ai[1] == o.fSingleCharName) {
389 if (o.fSupportsArgument) {
390 ++(*argi);
391 if ((*argi).Done ()) {
392 if (o.fIfSupportsArgumentThenRequired) {
394 "Command line argument requires an argument to it, but none provided (= or following argument)"sv, ai});
395 }
396 return make_pair (true, nullopt);
397 }
398 else {
399 return make_pair (true, **argi);
400 }
401 }
402 return make_pair (true, nullopt);
403 }
404
405 // this isn't right!!! - in case where no argument supported - must match all of string (and if next char not =)
406 // but its CLOSE--LGP 2024-03-05
407 if (o.fLongName and ai.length () >= 2 + o.fLongName->size () and ai[0] == '-' and ai[1] == '-' and
408 String::EqualsComparer{o.fLongNameCaseSensitive}(ai.SubString (2, o.fLongName->size () + 2), *o.fLongName)) {
409 if (o.fSupportsArgument) {
410 // see if '=' follows longname
411 String restOfArgi = ai.SubString (2 + o.fLongName->size ());
412 if (restOfArgi.size () >= 1 and restOfArgi[0] == '=') {
413 return make_pair (true, restOfArgi.SubString (1));
414 }
415 else {
416 ++(*argi);
417 if ((*argi).Done ()) {
418 if (o.fIfSupportsArgumentThenRequired) {
420 "Command line argument requires an argument to it, but none provided (= or following argument)"sv, ai});
421 }
422 return make_pair (true, nullopt);
423 }
424 else {
425 return make_pair (true, **argi);
426 }
427 }
428 }
429 return make_pair (true, nullopt);
430 }
431 // 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'
432 if (not o.fSingleCharName and not o.fLongName and o.fSupportsArgument and not(ai.size () >= 2 and ai.StartsWith ("-"sv))) {
433 // note we add the argument, but don't set 'found'
434 return make_pair (false, **argi);
435 }
436 return nullopt;
437}
438
439template <>
440String CommandLine::As<String> () const
441{
442 return As<String> (this->fShellStyleQuoting_);
443}
444
445template <>
446String CommandLine::As<String> (optional<CommandLine::StringShellQuoting> shellStyle) const
447{
448 // UNCLEAR how to handle quoting of elements inside string - so for now, do (less) harm? DOnt try to
449 // quote the quotes (but still wrap items in quotes).
450 // --LGP 2024-12-07
451 return fArgs_.Join<String> (
452 [&] (const String& i) {
453 // default in Stroika is wrap in double-quotes, and \-quote double-quote characters, and rest leave alone
454 if (shellStyle == nullopt) {
455 if (i.ContainsAny ({' ', '\"'})) {
456 return "\"{}\""_f(i);
457 //return "\"{}\""_f(i.ReplaceAll ("\""sv, "\\\""sv));
458 }
459 else {
460 return i;
461 }
462 }
463 else if (shellStyle == StringShellQuoting::eWindowsCMD) {
464 // @todo - NO IDEA - I think "" replaces ", in cmd shell?
465 if (i.ContainsAny ({' ', '\"'})) {
466 return "\"{}\""_f(i);
467 // return "\"{}\""_f(i.ReplaceAll ("\""sv, "\"\""sv));
468 }
469 else {
470 return i;
471 }
472 }
473 else if (shellStyle == StringShellQuoting::eBash) {
474 // @todo more complex - think I need to quote other stuff, but unclear
475 if (i.ContainsAny ({' ', '\"'})) {
476 return "\"{}\""_f(i);
477 //return "\"{}\""_f(i.ReplaceAll ("\""sv, "\\\""sv));
478 }
479 else {
480 return i;
481 }
482 }
483 return i;
484 },
485 " "sv);
486}
#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:1045
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.
Definition Sequence.h:187
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.
Definition Set.h:105
nonvirtual void Validate(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:306
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.