CLI11 2.7.2
C++11 Command Line Interface Parser
Loading...
Searching...
No Matches
StringTools_inl.hpp
1// Copyright (c) 2017-2026, University of Cincinnati, developed by Henry Schreiner
2// under NSF AWARD 1414736 and by the respective contributors.
3// All rights reserved.
4//
5// SPDX-License-Identifier: BSD-3-Clause
6
7#pragma once
8
9// IWYU pragma: private, include "CLI/CLI.hpp"
10
11// This include is only needed for IDEs to discover symbols
12#include "../StringTools.hpp"
13
14// [CLI11:public_includes:set]
15#include <algorithm>
16#include <cctype>
17#include <cstddef>
18#include <cstdint>
19#include <cstdlib>
20#include <iomanip>
21#include <iterator>
22#include <locale>
23#include <sstream>
24#include <stdexcept>
25#include <string>
26#include <utility>
27#include <vector>
28// [CLI11:public_includes:end]
29
30namespace CLI {
31// [CLI11:string_tools_inl_hpp:verbatim]
32
33namespace detail {
34
35CLI11_INLINE bool isalpha(const std::string &str) {
36 return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); });
37}
38
39CLI11_INLINE std::string to_lower(std::string str) {
40 std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) {
41 return std::tolower(x, std::locale());
42 });
43 return str;
44}
45
46CLI11_INLINE std::vector<std::string> split(const std::string &s, char delim) {
47 std::vector<std::string> elems;
48 if(s.empty()) {
49 elems.emplace_back();
50 return elems;
51 }
52
53 std::size_t start = 0;
54 std::size_t end = 0;
55
56 while((end = s.find(delim, start)) != std::string::npos) {
57 elems.push_back(s.substr(start, end - start));
58 start = end + 1;
59 }
60 elems.push_back(s.substr(start));
61 return elems;
62}
63
64CLI11_INLINE std::string &ltrim(std::string &str) {
65 auto it = std::find_if(str.begin(), str.end(), [](char ch) { return !std::isspace<char>(ch, std::locale()); });
66 str.erase(str.begin(), it);
67 return str;
68}
69
70CLI11_INLINE std::string &ltrim(std::string &str, const std::string &filter) {
71 auto it = std::find_if(str.begin(), str.end(), [&filter](char ch) { return filter.find(ch) == std::string::npos; });
72 str.erase(str.begin(), it);
73 return str;
74}
75
76CLI11_INLINE std::string &rtrim(std::string &str) {
77 auto it = std::find_if(str.rbegin(), str.rend(), [](char ch) { return !std::isspace<char>(ch, std::locale()); });
78 str.erase(it.base(), str.end());
79 return str;
80}
81
82CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter) {
83 auto it =
84 std::find_if(str.rbegin(), str.rend(), [&filter](char ch) { return filter.find(ch) == std::string::npos; });
85 str.erase(it.base(), str.end());
86 return str;
87}
88
89CLI11_INLINE std::string &remove_quotes(std::string &str) {
90 if(!str.empty() && (str.front() == '"' || str.front() == '\'' || str.front() == '`')) {
91 remove_outer(str, str.front());
92 }
93 return str;
94}
95
96CLI11_INLINE std::string &remove_outer(std::string &str, char key) {
97 if(str.length() > 1 && (str.front() == key)) {
98 if(str.front() == str.back()) {
99 str.pop_back();
100 str.erase(str.begin(), str.begin() + 1);
101 }
102 }
103 return str;
104}
105
106CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input) {
107 std::string::size_type n = 0;
108 while(n != std::string::npos && n < input.size()) {
109 n = input.find_first_of("\r\n", n);
110 if(n != std::string::npos) {
111 input.insert(n + 1, leader);
112 n += leader.size() + 1;
113 }
114 }
115 return input;
116}
117
118CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector<std::string> &aliases, std::size_t wid) {
119 if(!aliases.empty()) {
120 out << std::setw(static_cast<int>(wid)) << " aliases: ";
121 bool front = true;
122 for(const auto &alias : aliases) {
123 if(!front) {
124 out << ", ";
125 } else {
126 front = false;
127 }
128 out << detail::fix_newlines(" ", alias);
129 }
130 out << "\n";
131 }
132 return out;
133}
134
135CLI11_INLINE bool valid_name_string(const std::string &str) {
136 if(str.empty() || !valid_first_char(str[0])) {
137 return false;
138 }
139 auto e = str.end();
140 for(auto c = str.begin() + 1; c != e; ++c)
141 if(!valid_later_char(*c))
142 return false;
143 return true;
144}
145
146CLI11_INLINE std::string get_group_separators() {
147 std::string separators{"_'"};
148#if CLI11_HAS_RTTI != 0
149 char group_separator = std::use_facet<std::numpunct<char>>(std::locale()).thousands_sep();
150 separators.push_back(group_separator);
151#endif
152 return separators;
153}
154
155CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to) {
156
157 std::size_t start_pos = 0;
158
159 while((start_pos = str.find(from, start_pos)) != std::string::npos) {
160 str.replace(start_pos, from.length(), to);
161 start_pos += to.length();
162 }
163
164 return str;
165}
166
167CLI11_INLINE void remove_default_flag_values(std::string &flags) {
168 auto loc = flags.find_first_of('{', 2);
169 while(loc != std::string::npos) {
170 auto finish = flags.find_first_of("},", loc + 1);
171 if((finish != std::string::npos) && (flags[finish] == '}')) {
172 flags.erase(flags.begin() + static_cast<std::ptrdiff_t>(loc),
173 flags.begin() + static_cast<std::ptrdiff_t>(finish) + 1);
174 }
175 loc = flags.find_first_of('{', loc + 1);
176 }
177 flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end());
178}
179
180CLI11_INLINE std::ptrdiff_t
181find_member(std::string name, const std::vector<std::string> &names, bool ignore_case, bool ignore_underscore) {
182 auto it = std::end(names);
183 if(ignore_case) {
184 if(ignore_underscore) {
185 name = detail::to_lower(detail::remove_underscore(name));
186 it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
187 return detail::to_lower(detail::remove_underscore(local_name)) == name;
188 });
189 } else {
190 name = detail::to_lower(name);
191 it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
192 return detail::to_lower(local_name) == name;
193 });
194 }
195
196 } else if(ignore_underscore) {
197 name = detail::remove_underscore(name);
198 it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
199 return detail::remove_underscore(local_name) == name;
200 });
201 } else {
202 it = std::find(std::begin(names), std::end(names), name);
203 }
204
205 return (it != std::end(names)) ? (it - std::begin(names)) : (-1);
206}
207
208CLI11_MODULE_INLINE const std::string &escapedChars() {
209 static const std::string s{"\b\t\n\f\r\"\\"};
210 return s;
211}
212CLI11_MODULE_INLINE const std::string &escapedCharsCode() {
213 static const std::string s{"btnfr\"\\"};
214 return s;
215}
216CLI11_MODULE_INLINE const std::string &bracketChars() {
217 static const std::string s{"\"'`[(<{"};
218 return s;
219}
220CLI11_MODULE_INLINE const std::string &matchBracketChars() {
221 static const std::string s{"\"'`])>}"};
222 return s;
223}
224
225CLI11_INLINE bool has_escapable_character(const std::string &str) {
226 return (str.find_first_of(escapedChars()) != std::string::npos);
227}
228
229CLI11_INLINE std::string add_escaped_characters(const std::string &str) {
230 std::string out;
231 out.reserve(str.size() + 4);
232 for(char s : str) {
233 auto sloc = escapedChars().find_first_of(s);
234 if(sloc != std::string::npos) {
235 out.push_back('\\');
236 out.push_back(escapedCharsCode()[sloc]);
237 } else {
238 out.push_back(s);
239 }
240 }
241 return out;
242}
243
244CLI11_INLINE std::uint32_t hexConvert(char hc) {
245 int hcode{0};
246 if(hc >= '0' && hc <= '9') {
247 hcode = (hc - '0');
248 } else if(hc >= 'A' && hc <= 'F') {
249 hcode = (hc - 'A' + 10);
250 } else if(hc >= 'a' && hc <= 'f') {
251 hcode = (hc - 'a' + 10);
252 } else {
253 hcode = -1;
254 }
255 return static_cast<uint32_t>(hcode);
256}
257
258CLI11_INLINE char make_char(std::uint32_t code) { return static_cast<char>(static_cast<unsigned char>(code)); }
259
260CLI11_INLINE void append_codepoint(std::string &str, std::uint32_t code) {
261 if(code < 0x80) { // ascii code equivalent
262 str.push_back(static_cast<char>(code));
263 } else if(code < 0x800) { // \u0080 to \u07FF
264 // 110yyyyx 10xxxxxx; 0x3f == 0b0011'1111
265 str.push_back(make_char(0xC0 | code >> 6));
266 str.push_back(make_char(0x80 | (code & 0x3F)));
267 } else if(code < 0x10000) { // U+0800...U+FFFF
268 if(0xD800 <= code && code <= 0xDFFF) {
269 throw std::invalid_argument("[0xD800, 0xDFFF] are not valid code points.");
270 }
271 // 1110yyyy 10yxxxxx 10xxxxxx
272 str.push_back(make_char(0xE0 | code >> 12));
273 str.push_back(make_char(0x80 | (code >> 6 & 0x3F)));
274 str.push_back(make_char(0x80 | (code & 0x3F)));
275 } else if(code < 0x110000) { // U+010000 ... U+10FFFF
276 // 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx
277 str.push_back(make_char(0xF0 | code >> 18));
278 str.push_back(make_char(0x80 | (code >> 12 & 0x3F)));
279 str.push_back(make_char(0x80 | (code >> 6 & 0x3F)));
280 str.push_back(make_char(0x80 | (code & 0x3F)));
281 } else { // code points above U+10FFFF are not valid
282 throw std::invalid_argument("values above 0x10FFFF are not valid code points.");
283 }
284}
285
286CLI11_INLINE std::string remove_escaped_characters(const std::string &str) {
287
288 std::string out;
289 out.reserve(str.size());
290 for(auto loc = str.begin(); loc < str.end(); ++loc) {
291 if(*loc == '\\') {
292 if(str.end() - loc < 2) {
293 throw std::invalid_argument("invalid escape sequence " + str);
294 }
295 auto ecloc = escapedCharsCode().find_first_of(*(loc + 1));
296 if(ecloc != std::string::npos) {
297 out.push_back(escapedChars()[ecloc]);
298 ++loc;
299 } else if(*(loc + 1) == 'u') {
300 // must have 4 hex characters
301 if(str.end() - loc < 6) {
302 throw std::invalid_argument("unicode sequence must have 4 hex codes " + str);
303 }
304 std::uint32_t code{0};
305 std::uint32_t mplier{16 * 16 * 16};
306 for(int ii = 2; ii < 6; ++ii) {
307 std::uint32_t res = hexConvert(*(loc + ii));
308 if(res > 0x0F) {
309 throw std::invalid_argument("unicode sequence must have 4 hex codes " + str);
310 }
311 code += res * mplier;
312 mplier = mplier / 16;
313 }
314 append_codepoint(out, code);
315 loc += 5;
316 } else if(*(loc + 1) == 'U') {
317 // must have 8 hex characters
318 if(str.end() - loc < 10) {
319 throw std::invalid_argument("unicode sequence must have 8 hex codes " + str);
320 }
321 std::uint32_t code{0};
322 std::uint32_t mplier{16 * 16 * 16 * 16 * 16 * 16 * 16};
323 for(int ii = 2; ii < 10; ++ii) {
324 std::uint32_t res = hexConvert(*(loc + ii));
325 if(res > 0x0F) {
326 throw std::invalid_argument("unicode sequence must have 8 hex codes " + str);
327 }
328 code += res * mplier;
329 mplier = mplier / 16;
330 }
331 append_codepoint(out, code);
332 loc += 9;
333 } else if(*(loc + 1) == '0') {
334 out.push_back('\0');
335 ++loc;
336 } else {
337 throw std::invalid_argument(std::string("unrecognized escape sequence \\") + *(loc + 1) + " in " + str);
338 }
339 } else {
340 out.push_back(*loc);
341 }
342 }
343 return out;
344}
345
346CLI11_INLINE std::size_t close_string_quote(const std::string &str, std::size_t start, char closure_char) {
347 std::size_t loc{0};
348 for(loc = start + 1; loc < str.size(); ++loc) {
349 if(str[loc] == closure_char) {
350 break;
351 }
352 if(str[loc] == '\\') {
353 // skip the next character for escaped sequences
354 ++loc;
355 }
356 }
357 return loc;
358}
359
360CLI11_INLINE std::size_t close_literal_quote(const std::string &str, std::size_t start, char closure_char) {
361 auto loc = str.find_first_of(closure_char, start + 1);
362 return (loc != std::string::npos ? loc : str.size());
363}
364
365CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char) {
366
367 auto bracket_loc = matchBracketChars().find(closure_char);
368 switch(bracket_loc) {
369 case 0:
370 return close_string_quote(str, start, closure_char);
371 case 1:
372 case 2:
373#if defined(_MSC_VER) && _MSC_VER < 1920
374 case(std::size_t)-1:
375#else
376 case std::string::npos:
377#endif
378 return close_literal_quote(str, start, closure_char);
379 default:
380 break;
381 }
382
383 std::string closures(1, closure_char);
384 auto loc = start + 1;
385
386 while(loc < str.size()) {
387 if(str[loc] == closures.back()) {
388 closures.pop_back();
389 if(closures.empty()) {
390 return loc;
391 }
392 }
393 bracket_loc = bracketChars().find(str[loc]);
394 if(bracket_loc != std::string::npos) {
395 switch(bracket_loc) {
396 case 0:
397 loc = close_string_quote(str, loc, str[loc]);
398 break;
399 case 1:
400 case 2:
401 loc = close_literal_quote(str, loc, str[loc]);
402 break;
403 default:
404 closures.push_back(matchBracketChars()[bracket_loc]);
405 break;
406 }
407 }
408 ++loc;
409 }
410 if(loc > str.size()) {
411 loc = str.size();
412 }
413 return loc;
414}
415
416CLI11_INLINE std::vector<std::string> split_up(std::string str, char delimiter) {
417
418 auto find_ws = [delimiter](char ch) {
419 return (delimiter == '\0') ? std::isspace<char>(ch, std::locale()) : (ch == delimiter);
420 };
421 trim(str);
422
423 std::vector<std::string> output;
424 while(!str.empty()) {
425 if(bracketChars().find_first_of(str[0]) != std::string::npos) {
426 auto bracketLoc = bracketChars().find_first_of(str[0]);
427 auto end = close_sequence(str, 0, matchBracketChars()[bracketLoc]);
428 if(end >= str.size()) {
429 output.push_back(std::move(str));
430 str.clear();
431 } else {
432 output.push_back(str.substr(0, end + 1));
433 // The character following a closing quote/bracket is normally a delimiter and is
434 // consumed. If it is an ordinary character it must be retained (resume from it) so
435 // no characters are silently lost (e.g. `"abc"def` -> {"abc", "def"}). However if it
436 // is itself a quote/bracket opening character, resuming from it would start a fresh
437 // (potentially unterminated) quoted sequence that could swallow later delimiters, so
438 // it is skipped like the original delimiter case to keep splitting well behaved.
439 char follow = str[end + 1];
440 bool skip_follow = find_ws(follow) || (bracketChars().find_first_of(follow) != std::string::npos);
441 auto next = skip_follow ? end + 2 : end + 1;
442 if(next < str.size()) {
443 str = str.substr(next);
444 } else {
445 str.clear();
446 }
447 }
448
449 } else {
450 auto it = std::find_if(std::begin(str), std::end(str), find_ws);
451 if(it != std::end(str)) {
452 std::string value = std::string(str.begin(), it);
453 output.push_back(value);
454 str = std::string(it + 1, str.end());
455 } else {
456 output.push_back(str);
457 str.clear();
458 }
459 }
460 trim(str);
461 }
462 return output;
463}
464
465CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset) {
466 auto next = str[offset + 1];
467 if((next == '\"') || (next == '\'') || (next == '`')) {
468 if(offset == 0) {
469 // nothing precedes the trigger character, so there is nothing to reinterpret
470 return offset + 1;
471 }
472 auto astart = str.find_last_of("-/ \"\'`", offset - 1);
473 if(astart != std::string::npos) {
474 if(str[astart] == ((str[offset] == '=') ? '-' : '/'))
475 str[offset] = ' '; // interpret this as a space so the split_up works properly
476 }
477 }
478 return offset + 1;
479}
480
481CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape, bool force) {
482 // s is our escaped output string
483 std::string escaped_string{};
484 // loop through all characters
485 for(char c : string_to_escape) {
486 // check if a given character is printable
487 // the cast is necessary to avoid undefined behaviour
488 if(isprint(static_cast<unsigned char>(c)) == 0) {
489 std::stringstream stream;
490 // if the character is not printable
491 // we'll convert it to a hex string using a stringstream
492 // note that since char is signed we have to cast it to unsigned first
493 stream << std::hex << static_cast<unsigned int>(static_cast<unsigned char>(c));
494 std::string code = stream.str();
495 escaped_string += std::string("\\x") + (code.size() < 2 ? "0" : "") + code;
496 } else if(c == 'x' || c == 'X') {
497 // need to check for inadvertent binary sequences
498 if(!escaped_string.empty() && escaped_string.back() == '\\') {
499 escaped_string += std::string("\\x") + (c == 'x' ? "78" : "58");
500 } else {
501 escaped_string.push_back(c);
502 }
503
504 } else {
505 escaped_string.push_back(c);
506 }
507 }
508 if(escaped_string != string_to_escape || force) {
509 auto sqLoc = escaped_string.find('\'');
510 while(sqLoc != std::string::npos) {
511 escaped_string[sqLoc] = '\\';
512 escaped_string.insert(sqLoc + 1, "x27");
513 sqLoc = escaped_string.find('\'', sqLoc + 4);
514 }
515 escaped_string.insert(0, "'B\"(");
516 escaped_string.push_back(')');
517 escaped_string.push_back('"');
518 escaped_string.push_back('\'');
519 }
520 return escaped_string;
521}
522
523CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string) {
524 size_t ssize = escaped_string.size();
525 if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) {
526 return true;
527 }
528 return (escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0);
529}
530
531CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string) {
532 std::size_t start{0};
533 std::size_t tail{0};
534 size_t ssize = escaped_string.size();
535 if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) {
536 start = 3;
537 tail = 2;
538 } else if(escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0) {
539 start = 4;
540 tail = 3;
541 }
542
543 if(start == 0) {
544 return escaped_string;
545 }
546 std::string outstring;
547
548 outstring.reserve(ssize - start - tail);
549 std::size_t loc = start;
550 while(loc < ssize - tail) {
551 // ssize-2 to skip )" at the end
552 if(escaped_string[loc] == '\\' && (escaped_string[loc + 1] == 'x' || escaped_string[loc + 1] == 'X')) {
553 auto c1 = escaped_string[loc + 2];
554 auto c2 = escaped_string[loc + 3];
555
556 std::uint32_t res1 = hexConvert(c1);
557 std::uint32_t res2 = hexConvert(c2);
558 if(res1 <= 0x0F && res2 <= 0x0F) {
559 loc += 4;
560 outstring.push_back(static_cast<char>(res1 * 16 + res2));
561 continue;
562 }
563 }
564 outstring.push_back(escaped_string[loc]);
565 ++loc;
566 }
567 return outstring;
568}
569
570CLI11_INLINE void remove_quotes(std::vector<std::string> &args) {
571 for(auto &arg : args) {
572 if(arg.empty()) {
573 continue;
574 }
575 if(arg.front() == '\"' && arg.back() == '\"') {
576 remove_quotes(arg);
577 // only remove escaped for string arguments not literal strings
578 arg = remove_escaped_characters(arg);
579 } else {
580 remove_quotes(arg);
581 }
582 }
583}
584
585CLI11_INLINE void handle_secondary_array(std::string &str) {
586 if(str.size() >= 2 && str.front() == '[' && str.back() == ']') {
587 // handle some special array processing for arguments if it might be interpreted as a secondary array
588 std::string tstr{"[["};
589 for(std::size_t ii = 1; ii < str.size(); ++ii) {
590 tstr.push_back(str[ii]);
591 tstr.push_back(str[ii]);
592 }
593 str = std::move(tstr);
594 }
595}
596
597CLI11_INLINE bool
598process_quoted_string(std::string &str, char string_char, char literal_char, bool disable_secondary_array_processing) {
599 if(str.size() <= 1) {
600 return false;
601 }
602 if(detail::is_binary_escaped_string(str)) {
603 str = detail::extract_binary_string(str);
604 if(!disable_secondary_array_processing)
605 handle_secondary_array(str);
606 return true;
607 }
608 if(str.front() == string_char && str.back() == string_char) {
609 detail::remove_outer(str, string_char);
610 if(str.find_first_of('\\') != std::string::npos) {
611 str = detail::remove_escaped_characters(str);
612 }
613 if(!disable_secondary_array_processing)
614 handle_secondary_array(str);
615 return true;
616 }
617 if((str.front() == literal_char || str.front() == '`') && str.back() == str.front()) {
618 detail::remove_outer(str, str.front());
619 if(!disable_secondary_array_processing)
620 handle_secondary_array(str);
621 return true;
622 }
623 return false;
624}
625
626CLI11_INLINE std::string get_environment_value(const std::string &env_name) {
627 std::string ename_string;
628
629#ifdef _MSC_VER
630 // Windows version
631 char *buffer = nullptr;
632 std::size_t sz = 0;
633 if(_dupenv_s(&buffer, &sz, env_name.c_str()) == 0 && buffer != nullptr) {
634 ename_string = std::string(buffer);
635 free(buffer);
636 }
637#else
638 // This also works on Windows, but gives a warning
639
640 // MISRA static analysis need. MISRACPP2023-25_5_2-a-1
641 const char *buffer = nullptr;
642 buffer = std::getenv(env_name.c_str());
643 if(buffer != nullptr) {
644 ename_string = std::string(buffer);
645 }
646#endif
647 return ename_string;
648}
649
650CLI11_INLINE std::ostream &streamOutAsParagraph(std::ostream &out,
651 const std::string &text,
652 std::size_t paragraphWidth,
653 const std::string &linePrefix,
654 bool skipPrefixOnFirstLine) {
655 std::istringstream lss(text);
656 std::string line = "";
657 while(std::getline(lss, line)) {
658 std::istringstream iss(line);
659 std::string word = "";
660 std::size_t charsWritten = 0;
661
662 if(!skipPrefixOnFirstLine)
663 out << linePrefix;
664 skipPrefixOnFirstLine = false; // subsequent lines always get the prefix
665
666 while(iss >> word) {
667 if(charsWritten > 0 && (word.length() + 1 + charsWritten > paragraphWidth)) {
668 out << '\n' << linePrefix;
669 charsWritten = 0;
670 }
671 if(charsWritten == 0) {
672 out << word;
673 charsWritten += word.length();
674 } else {
675 out << ' ' << word;
676 charsWritten += word.length() + 1;
677 }
678 }
679
680 if(!lss.eof())
681 out << '\n';
682 }
683 return out;
684}
685
686} // namespace detail
687// [CLI11:string_tools_inl_hpp:end]
688} // namespace CLI