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