Add trim and trim_empty support to strsplit and update all callers

This commit is contained in:
Andrea Mazzoleni 2026-09-19 11:12:49 +02:00
parent 929ba5cebe
commit 8e66b42ceb
5 changed files with 163 additions and 23 deletions

View file

@ -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();

View file

@ -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 */

View file

@ -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;

View file

@ -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,

View file

@ -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;