diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 8c0a59d4..8bc6c628 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -1063,6 +1063,125 @@ static void test_wnmatch(void) } } +static void test_strsplit(void) +{ + char buf[64]; + char* map[8]; + unsigned n; + + /* empty string */ + strcpy(buf, ""); + n = strsplit(map, 8, buf, ",", " ", 1); + if (n != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit empty string test with trim_empty=1\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + strcpy(buf, ""); + n = strsplit(map, 8, buf, ",", " ", 0); + if (n != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit empty string test with trim_empty=0\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + /* trim_empty = 1 with delimiters only */ + strcpy(buf, ",,,"); + n = strsplit(map, 8, buf, ",", " ", 1); + if (n != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit consecutive delimiters with trim_empty=1\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + /* trim_empty = 0 with delimiters only */ + strcpy(buf, ",,,"); + n = strsplit(map, 8, buf, ",", " ", 0); + if (n != 4 || strcmp(map[0], "") != 0 || strcmp(map[1], "") != 0 || strcmp(map[2], "") != 0 || strcmp(map[3], "") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit consecutive delimiters with trim_empty=0\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + /* trim_empty = 1 with mixed spaces and commas */ + strcpy(buf, " one, two,, , three "); + n = strsplit(map, 8, buf, ",", " ", 1); + if (n != 3 || strcmp(map[0], "one") != 0 || strcmp(map[1], "two") != 0 || strcmp(map[2], "three") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit mixed test with trim_empty=1\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + /* trim_empty = 0 with mixed spaces and commas */ + strcpy(buf, " one, two,, , three "); + n = strsplit(map, 8, buf, ",", " ", 0); + if (n != 5 || strcmp(map[0], "one") != 0 || strcmp(map[1], "two") != 0 || strcmp(map[2], "") != 0 || strcmp(map[3], "") != 0 || strcmp(map[4], "three") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit mixed test with trim_empty=0\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + /* trailing delimiter */ + strcpy(buf, "a,"); + n = strsplit(map, 8, buf, ",", 0, 1); + if (n != 1 || strcmp(map[0], "a") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit trailing delimiter with trim_empty=1\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + strcpy(buf, "a,"); + n = strsplit(map, 8, buf, ",", 0, 0); + if (n != 2 || strcmp(map[0], "a") != 0 || strcmp(map[1], "") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit trailing delimiter with trim_empty=0\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + /* leading delimiter */ + strcpy(buf, ",a"); + n = strsplit(map, 8, buf, ",", 0, 1); + if (n != 1 || strcmp(map[0], "a") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit leading delimiter with trim_empty=1\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + strcpy(buf, ",a"); + n = strsplit(map, 8, buf, ",", 0, 0); + if (n != 2 || strcmp(map[0], "") != 0 || strcmp(map[1], "a") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit leading delimiter with trim_empty=0\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + /* capacity limit (split_max) */ + strcpy(buf, "a,b,c"); + n = strsplit(map, 2, buf, ",", 0, 1); + if (n != 2 || strcmp(map[0], "a") != 0 || strcmp(map[1], "b") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit split_max test with trim_empty=1\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + strcpy(buf, "a,b,c"); + n = strsplit(map, 2, buf, ",", 0, 0); + if (n != 2 || strcmp(map[0], "a") != 0 || strcmp(map[1], "b") != 0) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed strsplit split_max test with trim_empty=0\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } +} + static void test_path(void) { char dst[PATH_MAX]; @@ -2801,6 +2920,9 @@ void test(int argc, char* argv[]) /* pathname import and export */ test_path(); + /* string split */ + test_strsplit(); + /* smartctl output parsing */ test_parse_smartctl(); diff --git a/cmdline/state.c b/cmdline/state.c index f83ea6dc..c8b3ffe7 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -837,7 +837,7 @@ void state_config(struct snapraid_state* state, const char* path, const char* co /* LCOV_EXCL_STOP */ } - split_mac = strsplit(split_map, SPLIT_MAX + 1, buffer, ","); + split_mac = strsplit(split_map, SPLIT_MAX + 1, buffer, ",", " \t\r\n", 0); if (split_mac > SPLIT_MAX) { /* LCOV_EXCL_START */ diff --git a/cmdline/str.c b/cmdline/str.c index d338a56b..3d147529 100644 --- a/cmdline/str.c +++ b/cmdline/str.c @@ -22,27 +22,42 @@ char* strpolish(char* s) return s; } -unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* delimiters) +unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* delimiters, const char* trim, int trim_empty) { unsigned mac = 0; - /* skip initial delimiters */ - str += strspn(str, delimiters); + if (*str == 0) + return 0; - while (*str != 0 && mac < split_max) { - /* start of the token */ - split_map[mac] = str; - ++mac; + while (mac < split_max) { + char* begin = str; - /* 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; + char* end = str; - /* skip trailing delimiters */ - str += strspn(str, delimiters); + int has_next = *str != 0; + if (has_next) { + *str = 0; + ++str; + } + + if (trim != 0) { + begin += strspn(begin, trim); + + while (end > begin && strchr(trim, end[-1]) != 0) { + --end; + *end = 0; + } + } + + if (!trim_empty || *begin != 0) { + split_map[mac] = begin; + ++mac; + } + + if (!has_next) + break; } return mac; diff --git a/cmdline/str.h b/cmdline/str.h index e19770e1..e8cf9014 100644 --- a/cmdline/str.h +++ b/cmdline/str.h @@ -26,8 +26,9 @@ char* strpolish(char* s); * 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. + * If @trim_empty is non-zero, leading and consecutive delimiters are + * silently skipped, and tokens trimmed to empty are discarded. + * If @trim_empty is zero, empty tokens are preserved. * * The input string is modified in-place; @split_map entries point * directly into @str and are only valid for its lifetime. @@ -36,15 +37,17 @@ char* strpolish(char* s); * \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. + * \param trim Null-terminated set of characters to trim from the start and end of each token. + * \param trim_empty Whether to trim empty tokens and consecutive delimiters. * \return Number of tokens stored in @split_map. * * Example: - * char buf[] = " one,two,,three "; + * char buf[] = " one, two,, , three "; * char* map[8]; - * unsigned n = strsplit(map, 8, buf, " ,"); + * unsigned n = strsplit(map, 8, buf, ",", " ", 1); * // n == 3, map[] == { "one", "two", "three" } */ -unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* delimiters); +unsigned strsplit(char** split_map, unsigned split_max, char* str, const char* delimiters, const char* trim, int trim_empty); /** * Split a command-line string into an argument vector, diff --git a/cmdline/unixapp.c b/cmdline/unixapp.c index d6db1e5e..a4de9f79 100644 --- a/cmdline/unixapp.c +++ b/cmdline/unixapp.c @@ -873,8 +873,8 @@ static int devdereference_bcachefs(uint64_t device, const char* dir, tommy_list* separator += 3; /* split the line */ - id_mac = strsplit(id_map, 8, s, " \t\n"); - fs_mac = strsplit(fs_map, 8, separator, " \t\n"); + id_mac = strsplit(id_map, 8, s, " \t\r\n", 0, 1); + fs_mac = strsplit(fs_map, 8, separator, " \t\r\n", 0, 1); /* if too short, it's the wrong line */ if (id_mac < 5) @@ -914,7 +914,7 @@ static int devdereference_bcachefs(uint64_t device, const char* dir, tommy_list* /* split the device list */ char* dev_map[64]; - unsigned dev_mac = strsplit(dev_map, 64, device_list, ":"); + unsigned dev_mac = strsplit(dev_map, 64, device_list, ":", 0, 0); for (unsigned i = 0; i < dev_mac; ++i) { /* get major:minor, use stat on the path returned */ struct stat st; @@ -998,7 +998,7 @@ static int extract_zfs(const char* dir, char* dataset, size_t dataset_size, char break; /* split the line */ - mac = strsplit(map, 3, s, "\t\n"); + mac = strsplit(map, 3, s, "\t\r\n", 0, 1); if (mac < 3) continue;