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