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