diff --git a/Makefile.am b/Makefile.am index e2211be8..6d246825 100644 --- a/Makefile.am +++ b/Makefile.am @@ -42,6 +42,7 @@ snapraid_SOURCES = \ cmdline/io.c \ cmdline/bw.c \ cmdline/util.c \ + cmdline/str.c \ cmdline/stream.c \ cmdline/support.c \ cmdline/memory.c \ @@ -99,6 +100,7 @@ noinst_HEADERS = \ cmdline/io.h \ cmdline/bw.h \ cmdline/util.h \ + cmdline/str.h \ cmdline/stream.h \ cmdline/support.h \ cmdline/memory.h \ @@ -132,6 +134,7 @@ mktest_SOURCES = \ os/unix.c \ cmdline/unixapp.c \ cmdline/support.c \ + cmdline/str.c \ cmdline/memory.c \ tommyds/tommy.c @@ -142,6 +145,7 @@ mkstream_SOURCES = \ os/unix.c \ cmdline/unixapp.c \ cmdline/support.c \ + cmdline/str.c \ cmdline/memory.c \ cmdline/util.c \ cmdline/stream.c \ diff --git a/cmdline/str.c b/cmdline/str.c new file mode 100644 index 00000000..8109e2a4 --- /dev/null +++ b/cmdline/str.c @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 Andrea Mazzoleni + +#include "os/portable.h" + +#include "str.h" +#include "support.h" + +/****************************************************************************/ +/* string */ + +char* strpolish(char* s) +{ + char* i = s; + + while (*i) { + if (isspace((unsigned char)*i) || !isprint((unsigned char)*i)) + *i = ' '; + ++i; + } + + return s; +} + +unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* delimiters) +{ + unsigned mac = 0; + + /* skip initial delimiters */ + str += strspn(str, delimiters); + + while (*str != 0 && mac < split_max) { + /* start of the token */ + split_map[mac] = str; + ++mac; + + /* find the first delimiter or the end of the string */ + str += strcspn(str, delimiters); + + /* put the final terminator if missing */ + if (*str != 0) + *str++ = 0; + + /* skip trailing delimiters */ + str += strspn(str, delimiters); + } + + return mac; +} + +unsigned argsplit(const char** split_map, unsigned split_max, char* str) +{ + unsigned mac = 0; + + /* skip leading whitespace */ + while (*str != 0 && isspace((unsigned char)*str)) + ++str; + + while (*str != 0 && mac < split_max) { + char* dst = str; + split_map[mac++] = dst; + +#ifdef _WIN32 + /* + * cmd.exe rules: + * Unquoted: '^' escapes next char; '"' opens a quoted block. + * Quoted: '"' closes the block; '^' is an ordinary character. + */ + int quoted = 0; + + while (*str != 0 && (quoted || !isspace((unsigned char)*str))) { + if (quoted) { + if (*str == '"') { + quoted = 0; + ++str; + } else { + *dst++ = *str++; + } + } else { + if (*str == '^' && *(str + 1) != 0) { + ++str; + *dst++ = *str++; + } else if (*str == '"') { + quoted = 1; + ++str; + } else { + *dst++ = *str++; + } + } + } + +#else + /* + * bash rules: + * Single-quoted block: everything literal, no exceptions. + * Double-quoted block: '\' only escapes $, `, ", '\', newline; + * other backslashes are copied verbatim. + * Unquoted '\': always escapes the immediately following char. + */ + + /* set of characters that '\' can escape inside double quotes */ + static const char dq_escapable[] = "$`\"\\\n"; + + while (*str != 0 && !isspace((unsigned char)*str)) { + + if (*str == '\'') { + /* single-quoted block: truly everything is literal */ + ++str; + while (*str != 0 && *str != '\'') + *dst++ = *str++; + if (*str == '\'') + ++str; + + } else if (*str == '"') { + /* double-quoted block */ + ++str; + while (*str != 0 && *str != '"') { + if (*str == '\\' && strchr(dq_escapable, *(str + 1))) { + ++str; + } + *dst++ = *str++; + } + if (*str == '"') + ++str; + + } else if (*str == '\\') { + /* unquoted backslash: next character always literal */ + ++str; + if (*str != 0) + *dst++ = *str++; + + } else { + *dst++ = *str++; + } + } +#endif + + if (*str != 0) + ++str; /* step past the delimiter */ + + /* skip whitespace before the next token */ + while (*str != 0 && isspace((unsigned char)*str)) + ++str; + + /* null-terminate after str has already moved on */ + *dst = 0; + } + + return mac; +} + +char* strtrim(char* str) +{ + char* begin; + char* end; + + begin = str; + while (begin[0] && isspace((unsigned char)begin[0])) + ++begin; + + end = begin + strlen(begin); + while (end > begin && isspace((unsigned char)end[-1])) + --end; + + end[0] = 0; + + if (begin != str) + memmove(str, begin, end - begin + 1); + + return str; +} + +char* strlwr(char* str) +{ + char* s = str; + + while (*s) { + *s = tolower((unsigned char)*s); + ++s; + } + + return str; +} + +char* worddigitstr(const char* haystack, const char* needle) +{ + size_t len = strlen(needle); + const char* s; + + if (len == 0) + return NULL; + + for (s = haystack; (s = strstr(s, needle)) != NULL; ++s) { + + /* left boundary */ + if (s == haystack || isspace((unsigned char)s[-1]) || isdigit((unsigned char)s[-1])) { + /* right boundary */ + if (s[len] == 0 || isspace((unsigned char)s[len]) || isdigit((unsigned char)s[len])) { + return (char*)s; + } + } + } + + return NULL; +} + +int strtoi(const char* nptr, char** endptr, int base) +{ + long val; + int save_errno = errno; + + errno = 0; + val = strtol(nptr, endptr, base); + + if (errno == ERANGE) { + return val > 0 ? INT_MAX : INT_MIN; + } + + if (val > INT_MAX) { + errno = ERANGE; + return INT_MAX; + } + + if (val < INT_MIN) { + errno = ERANGE; + return INT_MIN; + } + + if (errno == 0) { + errno = save_errno; + } + + return (int)val; +} + +unsigned strtou(const char* nptr, char** endptr, int base) +{ + const char* p = nptr; + char* end; + unsigned long val; + int neg = 0; + int save_errno = errno; + + while (isspace((unsigned char)*p)) { + ++p; + } + + if (*p == '-') { + neg = 1; + ++p; + } else if (*p == '+') { + ++p; + } + + errno = 0; + val = strtoul(p, &end, base); + + if (end == p) { + /* no conversion was performed */ + if (endptr) { + *endptr = (char*)nptr; + } + errno = save_errno; + return 0; + } + + if (endptr) { + *endptr = end; + } + + if (errno == ERANGE || val > UINT_MAX) { + errno = ERANGE; + return UINT_MAX; + } + + if (errno == 0) { + errno = save_errno; + } + + if (neg) { + return (unsigned)-val; + } + + return (unsigned)val; +} + +/****************************************************************************/ +/* wnmatch */ + +/** + * Helper function for case-insensitive character comparison + */ +static inline int char_match(char p, char t) +{ +#ifdef WIN32 + return tolower((unsigned char)p) == tolower((unsigned char)t); +#else + return p == t; +#endif +} + +/** + * Match character class [...] + * Return 0 if NOT matched + */ +static const char* match_class(const char* p, char t) +{ + int negate = 0; + int matched = 0; + + if (*p == '!' || *p == '^') { + negate = 1; + ++p; + } + + while (*p && *p != ']') { + if (p[1] == '-' && p[2] != ']' && p[2] != 0) { + /* range [a-z] */ + char start = *p; + char end = p[2]; +#ifdef WIN32 + start = tolower((unsigned char)start); + end = tolower((unsigned char)end); + t = tolower((unsigned char)t); +#endif + if (t >= start && t <= end) + matched = 1; + p += 3; + } else { + /* single character */ + if (char_match(*p, t)) + matched = 1; + ++p; + } + } + + if (*p == ']') + ++p; + + if (negate) + matched = !matched; + if (!matched) + return 0; + + return p; +} + +int wnmatch_sub(const char* p, const char* t, int match_sub) +{ + char p1 = 0; /* previous char */ + while (*p) { + char p0 = *p; + switch (p0) { + case '?' : + /* ? matches any single character except / */ + if (*t == 0 || *t == '/') + return 1; + ++p; + ++t; + break; + case '*' : + /* check for ** */ + if (p[1] == '*') { + /* skip the ** */ + p += 2; + + /* munge all * */ + while (*p == '*') + ++p; + + /* if its not near a slash, it's like a single * */ + if (p1 == '/' || *p == '/') { + /* a ** at end matches everything */ + if (*p == 0) + return 0; + + /* + * In between slashes matches to nothing + * + * Check for /##/ or ^##/ (^ start of string) + * + * This is required for: + * "/##/file.txt" matching "/file.txt" + * "##/file.txt" matching "file.txt" + * "x##/file.txt" NOT matching "xfile.txt" + */ + if (*p == '/' && (p1 == 0 || p1 == '/')) { + /* try reducing to nothing */ + if (wnmatch_sub(p + 1, t, match_sub) == 0) + return 0; + /* otherwise / should match in the text */ + } + + /* try matching with 0 or more characters */ + while (*t) { + if (wnmatch_sub(p, t, match_sub) == 0) + return 0; + ++t; + } + + /* try matching at the end */ + return wnmatch_sub(p, t, match_sub); + } + } else { + /* skip the * */ + ++p; + } + + /* a * at end matches rest of segment */ + if (*p == 0) { + while (*t && *t != '/') + ++t; + return *t != 0; + } + + /* try matching with 0 or more characters */ + while (*t && *t != '/') { + if (wnmatch_sub(p, t, match_sub) == 0) + return 0; + ++t; + } + + /* try matching at the end */ + return wnmatch_sub(p, t, match_sub); + case '[' : + /* character class */ + if (*t == 0 || *t == '/') + return 1; + + p = match_class(p + 1, *t); + if (!p) + return 1; + + ++t; + break; + default : + /* literal character */ + if (*t == 0 || !char_match(*p, *t)) + return 1; + ++p; + ++t; + break; + } + p1 = p0; + } + + /* if we match sub directory */ + if (match_sub) { + /* match successfully only if we are at a directory border */ + return *t != '/'; + } else { + /* match successfully if we've consumed all text */ + return *t != 0; + } +} + diff --git a/cmdline/str.h b/cmdline/str.h new file mode 100644 index 00000000..da996e02 --- /dev/null +++ b/cmdline/str.h @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 Andrea Mazzoleni + +#ifndef __STR_H +#define __STR_H + +#include "app.h" +#include "str.h" + +/****************************************************************************/ +/* string */ + +/** + * Polish a string. + * + * Not printable chars are replaced by spaces. + * + * Note that the passed string is modified. + */ +char* strpolish(char* s); + +/** + * Split a string into tokens in-place. + * + * Scans @str for tokens separated by any character in @delimiters, + * null-terminates each token by overwriting its trailing delimiter, + * and stores a pointer to each token's start in @split_map. + * + * Leading and consecutive delimiters are silently skipped, so the + * output never contains empty tokens. + * + * The input string is modified in-place; @split_map entries point + * directly into @str and are only valid for its lifetime. + * + * \param split_map Output array receiving pointers to each token. + * \param split_max Capacity of @split_map; at most this many tokens are stored. + * \param str Input string to tokenise (modified in-place). + * \param delimiters Null-terminated set of delimiter characters. + * \return Number of tokens stored in @split_map. + * + * Example: + * char buf[] = " one,two,,three "; + * char* map[8]; + * unsigned n = strsplit(map, 8, buf, " ,"); + * // n == 3, map[] == { "one", "two", "three" } + */ +unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* delimiters); + +/** + * Split a command-line string into an argument vector, + * applying the quoting rules of the platform's native shell. + * + * The input string is modified in-place: quotes and escape characters + * are stripped and each token is null-terminated. Whitespace is the + * implicit token delimiter on both platforms. + * + * bash quoting (Linux / macOS): + * 'single quotes' - Everything literal; no escapes processed inside. + * "double quotes" - '\' only escapes $, `, ", '\', and newline; + * all other backslashes are kept as-is. + * \x (unquoted) - Next character taken literally (even whitespace). + * + * cmd.exe quoting (Windows): + * "double quotes" - Toggle quoted mode; inside, '^' is NOT special. + * ^x (unquoted) - Next character taken literally. + * Single quotes and '\' carry no special meaning in either mode. + * + * \param split_map Output array receiving pointers to each argument. + * \param split_max Capacity of @split_map; at most this many tokens are stored. + * \param str Input command-line string (modified in-place). + * \return Number of arguments stored in @split_map. + * + * Example (bash): + * char buf[] = "one 'two three' \"say \\\"hi\\\"\" back\\slash"; + * char* map[8]; + * unsigned n = argsplit(map, 8, buf); + * // n==4, map[] == { "one", "two three", "say \"hi\"", "backslash" } + * + * Example (cmd.exe): + * char buf[] = "one \"two three\" ^&safe ^\"quoted^\""; + * char* map[8]; + * unsigned n = argsplit(map, 8, buf); + * // n==4, map[] == { "one", "two three", "&safe", "\"quoted\"" } + */ +unsigned argsplit(const char** split_map, unsigned split_max, char* str); + +/** + * Trim spaces from the start and the end + */ +char* strtrim(char* s); + +/** + * Lower case + */ +char* strlwr(char* s); + +/* + * Find the first occurrence of 'needle' in 'haystack' only if it + * appears as a separate word (space, number or string boundaries). + * + * Returns a pointer to the beginning of the match, or NULL if not found. + */ +char* worddigitstr(const char* haystack, const char* needle); + +/** + * Convert a string to an integer, with the same semantics as strtol. + */ +int strtoi(const char* nptr, char** endptr, int base); + +/** + * Convert a string to an unsigned integer, with the same semantics as strtoul. + */ +unsigned strtou(const char* nptr, char** endptr, int base); + +/****************************************************************************/ +/* match */ + +/* + * Wild match function. + * + * If match_sub is !=0, it matches sub directory. Specifically it matches if the + * string "t" is not fully consumed and the next character to consume is a /. + * + * - ? matches any single character except / + * - * matches any sequence of characters except / + * - ** (nearby a /) matches everything including / + * - ** (not near a /) like * + * - ##/ reduces to nothing in addition to the normal matching of ** (using # instead of * to not mess the C comment) + * - [...] matches character classes with support for ranges and negation lile [!...] or [^...], except / + * + * \return 0 if it matches + */ +int wnmatch_sub(const char* p, const char* t, int match_sub); + +static inline int wnmatch(const char* p, const char* t) +{ + return wnmatch_sub(p, t, 0); +} + +#endif + diff --git a/cmdline/support.c b/cmdline/support.c index 2f04827f..cb22c7e9 100644 --- a/cmdline/support.c +++ b/cmdline/support.c @@ -1028,281 +1028,6 @@ bail: } #endif -char* strpolish(char* s) -{ - char* i = s; - - while (*i) { - if (isspace((unsigned char)*i) || !isprint((unsigned char)*i)) - *i = ' '; - ++i; - } - - return s; -} - -unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* delimiters) -{ - unsigned mac = 0; - - /* skip initial delimiters */ - str += strspn(str, delimiters); - - while (*str != 0 && mac < split_max) { - /* start of the token */ - split_map[mac] = str; - ++mac; - - /* find the first delimiter or the end of the string */ - str += strcspn(str, delimiters); - - /* put the final terminator if missing */ - if (*str != 0) - *str++ = 0; - - /* skip trailing delimiters */ - str += strspn(str, delimiters); - } - - return mac; -} - -unsigned argsplit(const char** split_map, unsigned split_max, char* str) -{ - unsigned mac = 0; - - /* skip leading whitespace */ - while (*str != 0 && isspace((unsigned char)*str)) - ++str; - - while (*str != 0 && mac < split_max) { - char* dst = str; - split_map[mac++] = dst; - -#ifdef _WIN32 - /* - * cmd.exe rules: - * Unquoted: '^' escapes next char; '"' opens a quoted block. - * Quoted: '"' closes the block; '^' is an ordinary character. - */ - int quoted = 0; - - while (*str != 0 && (quoted || !isspace((unsigned char)*str))) { - if (quoted) { - if (*str == '"') { - quoted = 0; - ++str; - } else { - *dst++ = *str++; - } - } else { - if (*str == '^' && *(str + 1) != 0) { - ++str; - *dst++ = *str++; - } else if (*str == '"') { - quoted = 1; - ++str; - } else { - *dst++ = *str++; - } - } - } - -#else - /* - * bash rules: - * Single-quoted block: everything literal, no exceptions. - * Double-quoted block: '\' only escapes $, `, ", '\', newline; - * other backslashes are copied verbatim. - * Unquoted '\': always escapes the immediately following char. - */ - - /* set of characters that '\' can escape inside double quotes */ - static const char dq_escapable[] = "$`\"\\\n"; - - while (*str != 0 && !isspace((unsigned char)*str)) { - - if (*str == '\'') { - /* single-quoted block: truly everything is literal */ - ++str; - while (*str != 0 && *str != '\'') - *dst++ = *str++; - if (*str == '\'') - ++str; - - } else if (*str == '"') { - /* double-quoted block */ - ++str; - while (*str != 0 && *str != '"') { - if (*str == '\\' && strchr(dq_escapable, *(str + 1))) { - ++str; - } - *dst++ = *str++; - } - if (*str == '"') - ++str; - - } else if (*str == '\\') { - /* unquoted backslash: next character always literal */ - ++str; - if (*str != 0) - *dst++ = *str++; - - } else { - *dst++ = *str++; - } - } -#endif - - if (*str != 0) - ++str; /* step past the delimiter */ - - /* skip whitespace before the next token */ - while (*str != 0 && isspace((unsigned char)*str)) - ++str; - - /* null-terminate after str has already moved on */ - *dst = 0; - } - - return mac; -} - -char* strtrim(char* str) -{ - char* begin; - char* end; - - begin = str; - while (begin[0] && isspace((unsigned char)begin[0])) - ++begin; - - end = begin + strlen(begin); - while (end > begin && isspace((unsigned char)end[-1])) - --end; - - end[0] = 0; - - if (begin != str) - memmove(str, begin, end - begin + 1); - - return str; -} - -char* strlwr(char* str) -{ - char* s = str; - - while (*s) { - *s = tolower((unsigned char)*s); - ++s; - } - - return str; -} - -char* worddigitstr(const char* haystack, const char* needle) -{ - size_t len = strlen(needle); - const char* s; - - if (len == 0) - return NULL; - - for (s = haystack; (s = strstr(s, needle)) != NULL; ++s) { - - /* left boundary */ - if (s == haystack || isspace((unsigned char)s[-1]) || isdigit((unsigned char)s[-1])) { - /* right boundary */ - if (s[len] == 0 || isspace((unsigned char)s[len]) || isdigit((unsigned char)s[len])) { - return (char*)s; - } - } - } - - return NULL; -} - -int strtoi(const char* nptr, char** endptr, int base) -{ - long val; - int save_errno = errno; - - errno = 0; - val = strtol(nptr, endptr, base); - - if (errno == ERANGE) { - return val > 0 ? INT_MAX : INT_MIN; - } - - if (val > INT_MAX) { - errno = ERANGE; - return INT_MAX; - } - - if (val < INT_MIN) { - errno = ERANGE; - return INT_MIN; - } - - if (errno == 0) { - errno = save_errno; - } - - return (int)val; -} - -unsigned strtou(const char* nptr, char** endptr, int base) -{ - const char* p = nptr; - char* end; - unsigned long val; - int neg = 0; - int save_errno = errno; - - while (isspace((unsigned char)*p)) { - ++p; - } - - if (*p == '-') { - neg = 1; - ++p; - } else if (*p == '+') { - ++p; - } - - errno = 0; - val = strtoul(p, &end, base); - - if (end == p) { - /* no conversion was performed */ - if (endptr) { - *endptr = (char*)nptr; - } - errno = save_errno; - return 0; - } - - if (endptr) { - *endptr = end; - } - - if (errno == ERANGE || val > UINT_MAX) { - errno = ERANGE; - return UINT_MAX; - } - - if (errno == 0) { - errno = save_errno; - } - - if (neg) { - return (unsigned)-val; - } - - return (unsigned)val; -} - /****************************************************************************/ /* path */ @@ -2846,173 +2571,3 @@ int thread_setspecific(thread_key_t key, void* value) return pthread_setspecific(key, value); } -/****************************************************************************/ -/* wnmatch */ - -/** - * Helper function for case-insensitive character comparison - */ -static inline int char_match(char p, char t) -{ -#ifdef WIN32 - return tolower((unsigned char)p) == tolower((unsigned char)t); -#else - return p == t; -#endif -} - -/** - * Match character class [...] - * Return 0 if NOT matched - */ -static const char* match_class(const char* p, char t) -{ - int negate = 0; - int matched = 0; - - if (*p == '!' || *p == '^') { - negate = 1; - ++p; - } - - while (*p && *p != ']') { - if (p[1] == '-' && p[2] != ']' && p[2] != 0) { - /* range [a-z] */ - char start = *p; - char end = p[2]; -#ifdef WIN32 - start = tolower((unsigned char)start); - end = tolower((unsigned char)end); - t = tolower((unsigned char)t); -#endif - if (t >= start && t <= end) - matched = 1; - p += 3; - } else { - /* single character */ - if (char_match(*p, t)) - matched = 1; - ++p; - } - } - - if (*p == ']') - ++p; - - if (negate) - matched = !matched; - if (!matched) - return 0; - - return p; -} - -int wnmatch_sub(const char* p, const char* t, int match_sub) -{ - char p1 = 0; /* previous char */ - while (*p) { - char p0 = *p; - switch (p0) { - case '?' : - /* ? matches any single character except / */ - if (*t == 0 || *t == '/') - return 1; - ++p; - ++t; - break; - case '*' : - /* check for ** */ - if (p[1] == '*') { - /* skip the ** */ - p += 2; - - /* munge all * */ - while (*p == '*') - ++p; - - /* if its not near a slash, it's like a single * */ - if (p1 == '/' || *p == '/') { - /* a ** at end matches everything */ - if (*p == 0) - return 0; - - /* - * In between slashes matches to nothing - * - * Check for /##/ or ^##/ (^ start of string) - * - * This is required for: - * "/##/file.txt" matching "/file.txt" - * "##/file.txt" matching "file.txt" - * "x##/file.txt" NOT matching "xfile.txt" - */ - if (*p == '/' && (p1 == 0 || p1 == '/')) { - /* try reducing to nothing */ - if (wnmatch_sub(p + 1, t, match_sub) == 0) - return 0; - /* otherwise / should match in the text */ - } - - /* try matching with 0 or more characters */ - while (*t) { - if (wnmatch_sub(p, t, match_sub) == 0) - return 0; - ++t; - } - - /* try matching at the end */ - return wnmatch_sub(p, t, match_sub); - } - } else { - /* skip the * */ - ++p; - } - - /* a * at end matches rest of segment */ - if (*p == 0) { - while (*t && *t != '/') - ++t; - return *t != 0; - } - - /* try matching with 0 or more characters */ - while (*t && *t != '/') { - if (wnmatch_sub(p, t, match_sub) == 0) - return 0; - ++t; - } - - /* try matching at the end */ - return wnmatch_sub(p, t, match_sub); - case '[' : - /* character class */ - if (*t == 0 || *t == '/') - return 1; - - p = match_class(p + 1, *t); - if (!p) - return 1; - - ++t; - break; - default : - /* literal character */ - if (*t == 0 || !char_match(*p, *t)) - return 1; - ++p; - ++t; - break; - } - p1 = p0; - } - - /* if we match sub directory */ - if (match_sub) { - /* match successfully only if we are at a directory border */ - return *t != '/'; - } else { - /* match successfully if we've consumed all text */ - return *t != 0; - } -} - diff --git a/cmdline/support.h b/cmdline/support.h index 279f341c..0867df9d 100644 --- a/cmdline/support.h +++ b/cmdline/support.h @@ -6,6 +6,7 @@ #include "app.h" #include "memory.h" +#include "str.h" /****************************************************************************/ /* lock */ @@ -252,7 +253,7 @@ void printl(const char* str, size_t pad); void printp(double v, size_t pad); /****************************************************************************/ -/* string */ +/* escape */ #define ESC_MAX (PATH_MAX * 2 + 1) #define ESC_POOL_MAX 8 @@ -280,108 +281,6 @@ static inline const char* esc_shell(const char* str, char* buffer) return esc_shell_multi(&str, 1, buffer); } -/** - * Polish a string. - * - * Not printable chars are replaced by spaces. - * - * Note that the passed string is modified. - */ -char* strpolish(char* s); - -/** - * Split a string into tokens in-place. - * - * Scans @str for tokens separated by any character in @delimiters, - * null-terminates each token by overwriting its trailing delimiter, - * and stores a pointer to each token's start in @split_map. - * - * Leading and consecutive delimiters are silently skipped, so the - * output never contains empty tokens. - * - * The input string is modified in-place; @split_map entries point - * directly into @str and are only valid for its lifetime. - * - * \param split_map Output array receiving pointers to each token. - * \param split_max Capacity of @split_map; at most this many tokens are stored. - * \param str Input string to tokenise (modified in-place). - * \param delimiters Null-terminated set of delimiter characters. - * \return Number of tokens stored in @split_map. - * - * Example: - * char buf[] = " one,two,,three "; - * char* map[8]; - * unsigned n = strsplit(map, 8, buf, " ,"); - * // n == 3, map[] == { "one", "two", "three" } - */ -unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* delimiters); - -/** - * Split a command-line string into an argument vector, - * applying the quoting rules of the platform's native shell. - * - * The input string is modified in-place: quotes and escape characters - * are stripped and each token is null-terminated. Whitespace is the - * implicit token delimiter on both platforms. - * - * bash quoting (Linux / macOS): - * 'single quotes' - Everything literal; no escapes processed inside. - * "double quotes" - '\' only escapes $, `, ", '\', and newline; - * all other backslashes are kept as-is. - * \x (unquoted) - Next character taken literally (even whitespace). - * - * cmd.exe quoting (Windows): - * "double quotes" - Toggle quoted mode; inside, '^' is NOT special. - * ^x (unquoted) - Next character taken literally. - * Single quotes and '\' carry no special meaning in either mode. - * - * \param split_map Output array receiving pointers to each argument. - * \param split_max Capacity of @split_map; at most this many tokens are stored. - * \param str Input command-line string (modified in-place). - * \return Number of arguments stored in @split_map. - * - * Example (bash): - * char buf[] = "one 'two three' \"say \\\"hi\\\"\" back\\slash"; - * char* map[8]; - * unsigned n = argsplit(map, 8, buf); - * // n==4, map[] == { "one", "two three", "say \"hi\"", "backslash" } - * - * Example (cmd.exe): - * char buf[] = "one \"two three\" ^&safe ^\"quoted^\""; - * char* map[8]; - * unsigned n = argsplit(map, 8, buf); - * // n==4, map[] == { "one", "two three", "&safe", "\"quoted\"" } - */ -unsigned argsplit(const char** split_map, unsigned split_max, char* str); - -/** - * Trim spaces from the start and the end - */ -char* strtrim(char* s); - -/** - * Lower case - */ -char* strlwr(char* s); - -/* - * Find the first occurrence of 'needle' in 'haystack' only if it - * appears as a separate word (space, number or string boundaries). - * - * Returns a pointer to the beginning of the match, or NULL if not found. - */ -char* worddigitstr(const char* haystack, const char* needle); - -/** - * Convert a string to an integer, with the same semantics as strtol. - */ -int strtoi(const char* nptr, char** endptr, int base); - -/** - * Convert a string to an unsigned integer, with the same semantics as strtoul. - */ -unsigned strtou(const char* nptr, char** endptr, int base); - /****************************************************************************/ /* path */ @@ -576,30 +475,5 @@ void* thread_getspecific(thread_key_t key); int thread_setspecific(thread_key_t key, void* value); #endif -/****************************************************************************/ -/* match */ - -/* - * Wild match function. - * - * If match_sub is !=0, it matches sub directory. Specifically it matches if the - * string "t" is not fully consumed and the next character to consume is a /. - * - * - ? matches any single character except / - * - * matches any sequence of characters except / - * - ** (nearby a /) matches everything including / - * - ** (not near a /) like * - * - ##/ reduces to nothing in addition to the normal matching of ** (using # instead of * to not mess the C comment) - * - [...] matches character classes with support for ranges and negation lile [!...] or [^...], except / - * - * \return 0 if it matches - */ -int wnmatch_sub(const char* p, const char* t, int match_sub); - -static inline int wnmatch(const char* p, const char* t) -{ - return wnmatch_sub(p, t, 0); -} - #endif