From 010bf5366214d2c67dc0adc6f3b87e8721b942f1 Mon Sep 17 00:00:00 2001 From: Bastiaan Stougie Date: Sat, 4 Apr 2026 11:16:08 -0400 Subject: [PATCH 1/7] tests: unhide shunit2 test failures shunit2 test with failing subtest incorrectly produced: Start 2: shunit2 2/2 Test #2: shunit2 .......................... Passed 52.70 sec 100% tests passed, 0 tests failed out of 2 now correctly produces: Start 2: shunit2 2/2 Test #2: shunit2 ..........................***Failed 53.68 sec # # Performing tests # ... test_get_option ASSERT:expected: but was: test_get_option_multiline ... # # Test report # tests passed: 111 99% tests failed: 1 1% tests skipped: 0 0% tests total: 112 100% 50% tests passed, 1 tests failed out of 2 Total Test time (real) = 59.67 sec The following tests FAILED: 2 - shunit2 (Failed) Errors while running CTest make: *** [Makefile:74: test] Error 8 make: Leaving directory '/home/user/repos/uci/build' Signed-off-by: Bastiaan Stougie --- tests/shunit2/tests.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/shunit2/tests.sh b/tests/shunit2/tests.sh index 00f56c5..3a54e2a 100755 --- a/tests/shunit2/tests.sh +++ b/tests/shunit2/tests.sh @@ -76,5 +76,8 @@ TMP_DIR="${TMP_DIR}" \ UCI="${UCI}" \ UCI_Q="${UCI_Q}" \ /bin/sh ${FULL_SUITE} +EXITSTATUS=$? rm -rf ${TESTS_DIR} + +exit ${EXITSTATUS} From 6272e4e0ee70f57050273111b99f5eacb5ce1b4c Mon Sep 17 00:00:00 2001 From: Bastiaan Stougie Date: Sat, 4 Apr 2026 11:25:15 -0400 Subject: [PATCH 2/7] uci: optionally parse comments Comment lines before an entry, and the comment at the end of an entry are considered to be associated with that entry. Example: # this comment line is associated with section2 # this comment line is associated with section2 config type section2 # this comment is associated with section 2 # this comment line is associated with opt # this comment line is associated with opt option opt 'val' # this comment is associated with opt # this comment line is not associated with any entry Added code to optionally, for each entry in the configuration file: - parse the associated comments, and store them in buffer 'commentbuf' in the parse context. - empty commentbuf after the associated entry has been processed. Signed-off-by: Bastiaan Stougie --- cli.c | 2 +- file.c | 80 +++++++++++++++++++++++++++++++++++++++++++++++--- libuci.c | 3 ++ uci.h | 3 +- uci_internal.h | 12 ++++++++ 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/cli.c b/cli.c index e899387..d045f7a 100644 --- a/cli.c +++ b/cli.c @@ -697,7 +697,7 @@ int main(int argc, char **argv) return 1; } - while((c = getopt(argc, argv, "c:C:d:f:LmnNp:P:qsSt:X")) != -1) { + while((c = getopt(argc, argv, "c:C:d:f:LkmnNp:P:qsSt:X")) != -1) { switch(c) { case 'c': uci_set_confdir(ctx, optarg); diff --git a/file.c b/file.c index 6840cfc..70d2d69 100644 --- a/file.c +++ b/file.c @@ -113,6 +113,63 @@ static void skip_whitespace(struct uci_context *ctx) pctx->pos += 1; } +static void uci_comment_append(struct uci_context *ctx, const char *comment) +{ + struct uci_parse_context *pctx = ctx->pctx; + const size_t len = strlen(comment); + const size_t requiredsz = pctx->commentlen + len + 1; + + if (requiredsz > pctx->commentbufsz) { + pctx->commentbufsz *= 2; + if (requiredsz > pctx->commentbufsz) + pctx->commentbufsz = requiredsz; + pctx->commentbuf = uci_realloc(ctx, pctx->commentbuf, pctx->commentbufsz); + } + + memcpy(pctx->commentbuf + pctx->commentlen, comment, len + 1); + pctx->commentlen += len; +} + +/* + * parse a comment line before a non-comment line + * pctx position must be at '#' + */ +static void uci_parse_comment_before(struct uci_context *ctx) +{ + UCI_ASSERT(ctx, pctx_cur_char(ctx->pctx) == '#'); + + uci_comment_append(ctx, pctx_cur_str(ctx->pctx)); +} + +/* + * parse a comment at the end of a non-comment line + * pctx position must be at '#' + */ +static void uci_parse_comment_after(struct uci_context *ctx) +{ + struct uci_parse_context *pctx = ctx->pctx; + const size_t len = strlen(pctx_cur_str(pctx)); + + UCI_ASSERT(ctx, pctx_cur_char(pctx) == '#'); + + /* cut off newline if present: this indicates that it's a comment + * at the end of the non-comment line instead of a comment line + * before the non-comment line. */ + if (pctx_cur_str(pctx)[len - 1] == '\n') + pctx_cur_str(pctx)[len - 1] = '\0'; + + uci_comment_append(ctx, pctx_cur_str(pctx)); +} + +static void uci_reset_comment(struct uci_context *ctx) +{ + struct uci_parse_context *pctx = ctx->pctx; + + if (pctx->commentbuf) + pctx->commentbuf[0] = '\0'; + pctx->commentlen = 0; +} + static inline void addc(struct uci_context *ctx, size_t *pos_dest, size_t *pos_src) { struct uci_parse_context *pctx = ctx->pctx; @@ -211,6 +268,8 @@ static void parse_str(struct uci_context *ctx, size_t *target) parse_double_quote(ctx, target); break; case '#': + if (ctx->flags & UCI_FLAG_COMMENTS) + uci_parse_comment_after(ctx); pctx_cur_char(pctx) = 0; /* fall through */ case 0: @@ -401,11 +460,12 @@ static void uci_parse_package(struct uci_context *ctx, bool single) ofs_name = next_arg(ctx, true, true, true); assert_eol(ctx); name = pctx_str(pctx, ofs_name); - if (single) - return; + if (!single) { + ctx->pctx->name = name; + uci_switch_config(ctx); + } - ctx->pctx->name = name; - uci_switch_config(ctx); + uci_reset_comment(ctx); } /* @@ -461,6 +521,8 @@ static void uci_parse_config(struct uci_context *ctx) UCI_NESTED(uci_set, ctx, &ptr); pctx->section = ptr.s; } + + uci_reset_comment(ctx); } /* @@ -500,6 +562,8 @@ static void uci_parse_option(struct uci_context *ctx, bool list) UCI_NESTED(uci_add_list, ctx, &ptr); else UCI_NESTED(uci_set, ctx, &ptr); + + uci_reset_comment(ctx); } /* @@ -512,6 +576,14 @@ static void uci_parse_line(struct uci_context *ctx, bool single) /* Skip whitespace characters at the start of line */ skip_whitespace(ctx); + + /* Keep comment line as is, do not tokenize */ + if (pctx_cur_char(pctx) == '#') { + if (ctx->flags & UCI_FLAG_COMMENTS) + uci_parse_comment_before(ctx); + return; + } + do { word = strtok(pctx_cur_str(pctx), " \t"); if (!word) diff --git a/libuci.c b/libuci.c index 1a6b298..3d94d59 100644 --- a/libuci.c +++ b/libuci.c @@ -142,6 +142,9 @@ __private void uci_cleanup(struct uci_context *ctx) if (pctx->package) uci_free_package(&pctx->package); + if (pctx->commentbuf) + free(pctx->commentbuf); + if (pctx->buf) free(pctx->buf); diff --git a/uci.h b/uci.h index b3ffdf9..c5bcac4 100644 --- a/uci.h +++ b/uci.h @@ -380,7 +380,8 @@ enum uci_flags { UCI_FLAG_STRICT = (1 << 0), /* strict mode for the parser */ UCI_FLAG_PERROR = (1 << 1), /* print parser error messages */ UCI_FLAG_EXPORT_NAME = (1 << 2), /* when exporting, name unnamed sections */ - UCI_FLAG_SAVED_DELTA = (1 << 3), /* store the saved delta in memory as well */ + UCI_FLAG_SAVED_DELTA = (1 << 3), /* store the saved delta in memory as well */ + UCI_FLAG_COMMENTS = (1 << 4), /* store parsed comments in memory */ }; struct uci_element diff --git a/uci_internal.h b/uci_internal.h index ff4ee8c..5145663 100644 --- a/uci_internal.h +++ b/uci_internal.h @@ -27,6 +27,18 @@ struct uci_parse_context /* private: */ struct uci_package *package; + /* + * comment format: [][] + * : comment lines placed before the element line, + * concatenated, each starting with '#' and ending with '\n'. + * : comment at the end of the element line itself, + * starting with '#' and ending without '\n'. + * Example: "#commentlinebefore1\n#commentlinebefore2\n#commentafter" + * NULL and empty string mean no comment. + */ + char *commentbuf; + size_t commentbufsz; + size_t commentlen; struct uci_section *section; bool merge; FILE *file; From 1761d1e05da61f0d8a779c02e04d9e02791fe069 Mon Sep 17 00:00:00 2001 From: Bastiaan Stougie Date: Sat, 4 Apr 2026 15:40:58 -0400 Subject: [PATCH 3/7] uci: preserve comments with option '-k' With option '-k': - uci command 'import' preserves comments of unchanged configuration entries, but does not import comments - uci command 'export' exports entries with their associated comments - uci command 'commit' preserves comments of unchanged configuration entries when commiting changes with uci commands 'add', 'delete', 'set', 'add_list', and/or 'del_list'. - preserved comment lines are auto-indented Limitation: - comments at the end of the file are not preserved, because they are not associated with an entry Signed-off-by: Bastiaan Stougie --- cli.c | 4 ++ delta.c | 6 +-- file.c | 64 +++++++++++++++++++----- list.c | 131 +++++++++++++++++++++++++++++++++++++------------ uci.h | 20 ++++++++ uci_internal.h | 12 +++-- util.c | 15 ++++++ 7 files changed, 202 insertions(+), 50 deletions(-) diff --git a/cli.c b/cli.c index d045f7a..bb166fa 100644 --- a/cli.c +++ b/cli.c @@ -163,6 +163,7 @@ static void uci_usage(void) "\t-C set the search path for config override files (default: "UCI_CONF2DIR")\n" "\t-d set the delimiter for list values in uci show\n" "\t-f use as input instead of stdin\n" + "\t-k keep comments of unchanged entries (export, commit)\n" "\t-m when importing, merge data into an existing package\n" "\t-n name unnamed sections on export (default)\n" "\t-N don't name unnamed sections\n" @@ -721,6 +722,9 @@ int main(int argc, char **argv) return 1; } break; + case 'k': + ctx->flags |= UCI_FLAG_COMMENTS; + break; case 'm': flags |= CLI_FLAG_MERGE; break; diff --git a/delta.c b/delta.c index 5437fc1..8374bb8 100644 --- a/delta.c +++ b/delta.c @@ -42,7 +42,7 @@ uci_add_delta(struct uci_context *ctx, struct uci_list *list, int cmd, const cha if (value) size += strlen(value) + 1; - h = uci_alloc_element(ctx, delta, option, size); + h = uci_alloc_element(ctx, NULL, delta, option, size); ptr = uci_dataptr(h); h->cmd = cmd; h->section = strcpy(ptr, section); @@ -114,7 +114,7 @@ int uci_set_savedir(struct uci_context *ctx, const char *dir) } } if (!exists) - e = uci_alloc_generic(ctx, UCI_TYPE_PATH, dir, sizeof(struct uci_element)); + e = uci_alloc_generic(ctx, NULL, UCI_TYPE_PATH, dir, sizeof(struct uci_element)); uci_list_add(&ctx->delta_path, &e->list); sdir = uci_strdup(ctx, dir); @@ -138,7 +138,7 @@ int uci_add_delta_path(struct uci_context *ctx, const char *dir) UCI_THROW(ctx, UCI_ERR_DUPLICATE); } - e = uci_alloc_generic(ctx, UCI_TYPE_PATH, dir, sizeof(struct uci_element)); + e = uci_alloc_generic(ctx, NULL, UCI_TYPE_PATH, dir, sizeof(struct uci_element)); /* Keep savedir at the end of ctx->delta_path list */ savedir = ctx->delta_path.prev; uci_list_insert(savedir->prev, &e->list); diff --git a/file.c b/file.c index 70d2d69..11aee73 100644 --- a/file.c +++ b/file.c @@ -161,6 +161,31 @@ static void uci_parse_comment_after(struct uci_context *ctx) uci_comment_append(ctx, pctx_cur_str(pctx)); } +static void uci_fprintf_comment_before(struct uci_context const *ctx, FILE *stream, const char *indent, const char *comment) +{ + if ((ctx->flags & UCI_FLAG_COMMENTS) && comment && comment[0]) { + char *newline_ptr = strrchr(comment, '\n'); + if (newline_ptr) { + fprintf(stream, "%s", indent); + for (const char *readptr = comment; readptr <= newline_ptr; readptr++) { + putc(*readptr, stream); + if (*readptr == '\n' && readptr != newline_ptr) + fprintf(stream, "%s", indent); + } + } + } +} + +static void uci_fprintf_comment_after(struct uci_context const *ctx, FILE *stream, const char *comment) +{ + if ((ctx->flags & UCI_FLAG_COMMENTS) && comment && comment[0]) { + const char *ptr = strrchr(comment, '\n'); + ptr = ptr ? (ptr + 1) : comment; + if (*ptr) + fprintf(stream, " %s", ptr); + } +} + static void uci_reset_comment(struct uci_context *ctx) { struct uci_parse_context *pctx = ctx->pctx; @@ -413,7 +438,7 @@ static void assert_eol(struct uci_context *ctx) * switch to a different config, either triggered by uci_load, or by a * 'package <...>' statement in the import file */ -static void uci_switch_config(struct uci_context *ctx) +static void uci_switch_config(struct uci_context *ctx, bool with_comment) { struct uci_parse_context *pctx; struct uci_element *e; @@ -441,7 +466,7 @@ static void uci_switch_config(struct uci_context *ctx) e = uci_lookup_list(&ctx->root, name); if (e) UCI_THROW(ctx, UCI_ERR_DUPLICATE); - pctx->package = uci_alloc_package(ctx, name); + pctx->package = uci_alloc_package(ctx, with_comment ? pctx->commentbuf : NULL, name); } /* @@ -462,7 +487,7 @@ static void uci_parse_package(struct uci_context *ctx, bool single) name = pctx_str(pctx, ofs_name); if (!single) { ctx->pctx->name = name; - uci_switch_config(ctx); + uci_switch_config(ctx, true); } uci_reset_comment(ctx); @@ -484,7 +509,7 @@ static void uci_parse_config(struct uci_context *ctx) if (!ctx->pctx->name) uci_parse_error(ctx, "attempting to import a file without a package name"); - uci_switch_config(ctx); + uci_switch_config(ctx, false); } /* command string null-terminated by strtok */ @@ -503,7 +528,7 @@ static void uci_parse_config(struct uci_context *ctx) if (!name || !name[0]) { ctx->internal = !pctx->merge; - UCI_NESTED(uci_add_section, ctx, pctx->package, type, &pctx->section); + UCI_NESTED(uci_add_section_with_comment, ctx, pctx->package, pctx->commentbuf, type, &pctx->section); } else { uci_fill_ptr(ctx, &ptr, &pctx->package->e); e = uci_lookup_list(&pctx->package->sections, name); @@ -514,6 +539,7 @@ static void uci_parse_config(struct uci_context *ctx) uci_parse_error(ctx, "section of different type overwrites prior section with same name"); } + ptr.comment = pctx->commentbuf; ptr.section = name; ptr.value = type; @@ -554,6 +580,7 @@ static void uci_parse_option(struct uci_context *ctx, bool list) e = uci_lookup_list(&pctx->section->options, name); if (e) ptr.o = uci_to_option(e); + ptr.comment = pctx->commentbuf; ptr.option = name; ptr.value = value; @@ -684,25 +711,38 @@ static void uci_export_package(struct uci_package *p, FILE *stream, bool header) struct uci_context *ctx = p->ctx; struct uci_element *s, *o, *i; - if (header) - fprintf(stream, "package %s\n", uci_escape(ctx, p->e.name)); + if (header) { + uci_fprintf_comment_before(ctx, stream, "", p->e.comment); + fprintf(stream, "package %s", uci_escape(ctx, p->e.name)); + uci_fprintf_comment_after(ctx, stream, p->e.comment); + fprintf(stream, "\n"); + } uci_foreach_element(&p->sections, s) { struct uci_section *sec = uci_to_section(s); - fprintf(stream, "\nconfig %s", uci_escape(ctx, sec->type)); + fprintf(stream, "\n"); + uci_fprintf_comment_before(ctx, stream, "", sec->e.comment); + fprintf(stream, "config %s", uci_escape(ctx, sec->type)); if (!sec->anonymous || (ctx->flags & UCI_FLAG_EXPORT_NAME)) fprintf(stream, " '%s'", uci_escape(ctx, sec->e.name)); + uci_fprintf_comment_after(ctx, stream, sec->e.comment); fprintf(stream, "\n"); uci_foreach_element(&sec->options, o) { struct uci_option *opt = uci_to_option(o); switch(opt->type) { case UCI_TYPE_STRING: + uci_fprintf_comment_before(ctx, stream, "\t", opt->e.comment); fprintf(stream, "\toption %s", uci_escape(ctx, opt->e.name)); - fprintf(stream, " '%s'\n", uci_escape(ctx, opt->v.string)); + fprintf(stream, " '%s'", uci_escape(ctx, opt->v.string)); + uci_fprintf_comment_after(ctx, stream, opt->e.comment); + fprintf(stream, "\n"); break; case UCI_TYPE_LIST: uci_foreach_element(&opt->v.list, i) { + uci_fprintf_comment_before(ctx, stream, "\t", i->comment); fprintf(stream, "\tlist %s", uci_escape(ctx, opt->e.name)); - fprintf(stream, " '%s'\n", uci_escape(ctx, i->name)); + fprintf(stream, " '%s'", uci_escape(ctx, i->name)); + uci_fprintf_comment_after(ctx, stream, i->comment); + fprintf(stream, "\n"); } break; default: @@ -775,14 +815,14 @@ int uci_import(struct uci_context *ctx, FILE *stream, const char *name, struct u } if (!pctx->package && name) - uci_switch_config(ctx); + uci_switch_config(ctx, false); if (package) *package = pctx->package; if (pctx->merge) pctx->package = NULL; pctx->name = NULL; - uci_switch_config(ctx); + uci_switch_config(ctx, false); /* no error happened, we can get rid of the parser context now */ uci_cleanup(ctx); diff --git a/list.c b/list.c index 304c9e1..460fb0a 100644 --- a/list.c +++ b/list.c @@ -35,7 +35,7 @@ static bool uci_list_set_pos(struct uci_list *head, struct uci_list *ptr, int po * payload is appended to the struct to save memory and reduce fragmentation */ __private struct uci_element * -uci_alloc_generic(struct uci_context *ctx, int type, const char *name, int size) +uci_alloc_generic(struct uci_context *ctx, const char *comment, int type, const char *name, int size) { struct uci_element *e; int datalen = size; @@ -43,6 +43,13 @@ uci_alloc_generic(struct uci_context *ctx, int type, const char *name, int size) ptr = uci_malloc(ctx, datalen); e = (struct uci_element *) ptr; + if (comment && comment[0]) { + UCI_TRAP_SAVE(ctx, error); + if (!uci_validate_comment(comment)) + UCI_THROW(ctx, UCI_ERR_INVAL); + e->comment = uci_strdup(ctx, comment); + UCI_TRAP_RESTORE(ctx); + } e->type = type; if (name) { UCI_TRAP_SAVE(ctx, error); @@ -53,6 +60,7 @@ uci_alloc_generic(struct uci_context *ctx, int type, const char *name, int size) goto done; error: + free(e->comment); free(ptr); UCI_THROW(ctx, ctx->err); @@ -60,9 +68,39 @@ uci_alloc_generic(struct uci_context *ctx, int type, const char *name, int size) return e; } +static void +uci_update_str(struct uci_context *ctx, char **pstr, const char *new) +{ + if (*pstr && new && new[0] && strlen(*pstr) >= strlen(new)) { + strcpy(*pstr, new); + } else { + if (*pstr) { + free(*pstr); + *pstr = NULL; + } + + if (new && new[0]) { + UCI_TRAP_SAVE(ctx, error); + *pstr = uci_strdup(ctx, new); + UCI_TRAP_RESTORE(ctx); + } + } + + return; +error: + UCI_THROW(ctx, ctx->err); +} + +static void +uci_update_generic(struct uci_context *ctx, struct uci_element *e, const char *comment) +{ + uci_update_str(ctx, &(e->comment), comment); +} + __private void uci_free_element(struct uci_element *e) { + free(e->comment); free(e->name); if (!uci_list_empty(&e->list)) uci_list_del(&e->list); @@ -70,13 +108,13 @@ uci_free_element(struct uci_element *e) } static struct uci_option * -uci_alloc_option(struct uci_section *s, const char *name, const char *value, struct uci_list *after) +uci_alloc_option(struct uci_section *s, const char *comment, const char *name, const char *value, struct uci_list *after) { struct uci_package *p = s->package; struct uci_context *ctx = p->ctx; struct uci_option *o; - o = uci_alloc_element(ctx, option, name, strlen(value) + 1); + o = uci_alloc_element(ctx, comment, option, name, strlen(value) + 1); o->type = UCI_TYPE_STRING; o->v.string = uci_dataptr(o); o->section = s; @@ -86,6 +124,22 @@ uci_alloc_option(struct uci_section *s, const char *name, const char *value, str return o; } +/* + * Attempts to update the option without having to reallocate. + * Returns false on failure, in which case nothing was updated. + * Returns true on success. + */ +static bool +uci_try_update_option(struct uci_context *ctx, struct uci_option *o, const char *comment, const char *value) +{ + if (o->type != UCI_TYPE_STRING || strlen(o->v.string) < strlen(value)) + return false; + + strcpy(o->v.string, value); + uci_update_generic(ctx, &(o->e), comment); + return true; +} + static inline void uci_free_option(struct uci_option *o) { @@ -109,13 +163,13 @@ uci_free_option(struct uci_option *o) } static struct uci_option * -uci_alloc_list(struct uci_section *s, const char *name, struct uci_list *after) +uci_alloc_list(struct uci_section *s, const char *comment, const char *name, struct uci_list *after) { struct uci_package *p = s->package; struct uci_context *ctx = p->ctx; struct uci_option *o; - o = uci_alloc_element(ctx, option, name, 0); + o = uci_alloc_element(ctx, comment, option, name, 0); o->type = UCI_TYPE_LIST; o->section = s; uci_list_init(&o->v.list); @@ -195,7 +249,7 @@ static void uci_section_transfer_options(struct uci_section *dst, struct uci_sec } static struct uci_section * -uci_alloc_section(struct uci_package *p, const char *type, const char *name, struct uci_list *after) +uci_alloc_section(struct uci_package *p, const char *comment, const char *type, const char *name, struct uci_list *after) { struct uci_context *ctx = p->ctx; struct uci_section *s; @@ -203,7 +257,7 @@ uci_alloc_section(struct uci_package *p, const char *type, const char *name, str if (name && !name[0]) name = NULL; - s = uci_alloc_element(ctx, section, name, strlen(type) + 1); + s = uci_alloc_element(ctx, comment, section, name, strlen(type) + 1); uci_list_init(&s->options); s->type = uci_dataptr(s); s->package = p; @@ -217,6 +271,23 @@ uci_alloc_section(struct uci_package *p, const char *type, const char *name, str return s; } +/* + * Attempts to update the section without having to reallocate. + * Returns false on failure, in which case nothing was updated. + * Returns true on success. + */ +static bool +uci_try_update_section(struct uci_context *ctx, struct uci_section *s, const char *comment, const char *type) +{ + if (strlen(s->type) < strlen(type)) + /* need to realloc entire option */ + return false; + + strcpy(s->type, type); + uci_update_generic(ctx, &(s->e), comment); + return true; +} + static void uci_free_section(struct uci_section *s) { @@ -232,11 +303,11 @@ uci_free_section(struct uci_section *s) } __private struct uci_package * -uci_alloc_package(struct uci_context *ctx, const char *name) +uci_alloc_package(struct uci_context *ctx, const char *comment, const char *name) { struct uci_package *p; - p = uci_alloc_element(ctx, package, name, 0); + p = uci_alloc_element(ctx, comment, package, name, 0); p->ctx = ctx; uci_list_init(&p->sections); uci_list_init(&p->delta); @@ -530,14 +601,15 @@ int uci_reorder_section(struct uci_context *ctx, struct uci_section *s, int pos) return 0; } -int uci_add_section(struct uci_context *ctx, struct uci_package *p, const char *type, struct uci_section **res) +__private int +uci_add_section_with_comment(struct uci_context *ctx, struct uci_package *p, const char *comment, const char *type, struct uci_section **res) { bool internal = ctx && ctx->internal; struct uci_section *s; UCI_HANDLE_ERR(ctx); UCI_ASSERT(ctx, p != NULL); - s = uci_alloc_section(p, type, NULL, NULL); + s = uci_alloc_section(p, comment, type, NULL, NULL); if (s && s->anonymous) uci_fixup_section(ctx, s); *res = s; @@ -547,6 +619,11 @@ int uci_add_section(struct uci_context *ctx, struct uci_package *p, const char * return 0; } +int uci_add_section(struct uci_context *ctx, struct uci_package *p, const char *type, struct uci_section **res) +{ + return uci_add_section_with_comment(ctx, p, NULL, type, res); +} + int uci_delete(struct uci_context *ctx, struct uci_ptr *ptr) { /* NB: pass on internal flag to uci_del_element */ @@ -609,19 +686,19 @@ int uci_add_list(struct uci_context *ctx, struct uci_ptr *ptr) } /* create new item */ - e1 = uci_alloc_generic(ctx, UCI_TYPE_ITEM, ptr->value, sizeof(struct uci_option)); + e1 = uci_alloc_generic(ctx, ptr->comment, UCI_TYPE_ITEM, ptr->value, sizeof(struct uci_option)); if (!ptr->o) { /* create new list */ UCI_TRAP_SAVE(ctx, error); - ptr->o = uci_alloc_list(ptr->s, ptr->option, NULL); + ptr->o = uci_alloc_list(ptr->s, ptr->comment, ptr->option, NULL); UCI_TRAP_RESTORE(ctx); } else if (ptr->o->type == UCI_TYPE_STRING) { /* create new list and add old string value as item to list */ struct uci_option *old = ptr->o; UCI_TRAP_SAVE(ctx, error); - e2 = uci_alloc_generic(ctx, UCI_TYPE_ITEM, old->v.string, sizeof(struct uci_option)); - ptr->o = uci_alloc_list(ptr->s, ptr->option, &old->e.list); + e2 = uci_alloc_generic(ctx, old->e.comment, UCI_TYPE_ITEM, old->v.string, sizeof(struct uci_option)); + ptr->o = uci_alloc_list(ptr->s, ptr->comment, ptr->option, &old->e.list); UCI_TRAP_RESTORE(ctx); uci_list_add(&ptr->o->v.list, &e2->list); @@ -705,31 +782,23 @@ int uci_set(struct uci_context *ctx, struct uci_ptr *ptr) return uci_delete(ctx, ptr); } else if (!ptr->o && ptr->option) { /* new option */ - ptr->o = uci_alloc_option(ptr->s, ptr->option, ptr->value, NULL); + ptr->o = uci_alloc_option(ptr->s, ptr->comment, ptr->option, ptr->value, NULL); } else if (!ptr->s && ptr->section) { /* new section */ - ptr->s = uci_alloc_section(ptr->p, ptr->value, ptr->section, NULL); + ptr->s = uci_alloc_section(ptr->p, ptr->comment, ptr->value, ptr->section, NULL); } else if (ptr->o && ptr->option) { /* update option */ - if (ptr->o->type == UCI_TYPE_STRING && !strcmp(ptr->o->v.string, ptr->value)) - return 0; - - if (ptr->o->type == UCI_TYPE_STRING && strlen(ptr->o->v.string) == strlen(ptr->value)) { - strcpy(ptr->o->v.string, ptr->value); - } else { + /* value, list values, comment may have changed */ + if (!uci_try_update_option(ctx, ptr->o, ptr->comment, ptr->value)) { struct uci_option *old = ptr->o; - ptr->o = uci_alloc_option(ptr->s, ptr->option, ptr->value, &old->e.list); + ptr->o = uci_alloc_option(ptr->s, ptr->comment, ptr->option, ptr->value, &old->e.list); if (ptr->option == old->e.name) ptr->option = ptr->o->e.name; uci_free_option(old); } } else if (ptr->s && ptr->section) { /* update section */ - if (!strcmp(ptr->s->type, ptr->value)) - return 0; - - if (strlen(ptr->s->type) == strlen(ptr->value)) { - strcpy(ptr->s->type, ptr->value); - } else { + /* type, comment may have changed */ + if (!uci_try_update_section(ctx, ptr->s, ptr->comment, ptr->value)) { struct uci_section *old = ptr->s; - ptr->s = uci_alloc_section(ptr->p, ptr->value, old->e.name, &old->e.list); + ptr->s = uci_alloc_section(ptr->p, old->e.comment, ptr->value, old->e.name, &old->e.list); uci_section_transfer_options(ptr->s, old); if (ptr->section == old->e.name) ptr->section = ptr->s->e.name; diff --git a/uci.h b/uci.h index c5bcac4..c0d855e 100644 --- a/uci.h +++ b/uci.h @@ -388,6 +388,16 @@ struct uci_element { struct uci_list list; enum uci_type type; + /* + * comment format: [][] + * : comment lines placed before the element line, + * concatenated, each starting with '#' and ending with '\n'. + * : comment at the end of the element line itself, + * starting with '#' and ending without '\n'. + * Example: "#commentlinebefore1\n#commentlinebefore2\n#commentafter" + * NULL and empty string mean no comment. + */ + char *comment; char *name; }; @@ -508,6 +518,16 @@ struct uci_ptr struct uci_option *o; struct uci_element *last; + /* + * comment format: [][] + * : comment lines placed before the element line, + * concatenated, each starting with '#' and ending with '\n'. + * : comment at the end of the element line itself, + * starting with '#' and ending without '\n'. + * Example: "#commentlinebefore1\n#commentlinebefore2\n#commentafter" + * NULL and empty string mean no comment. + */ + const char *comment; const char *package; const char *section; const char *option; diff --git a/uci_internal.h b/uci_internal.h index 5145663..644859d 100644 --- a/uci_internal.h +++ b/uci_internal.h @@ -54,19 +54,21 @@ struct uci_parse_context #define pctx_char(pctx, i) ((pctx)->buf[(i)]) #define pctx_cur_char(pctx) pctx_char(pctx, pctx_pos(pctx)) -#define uci_alloc_element(ctx, type, name, datasize) \ - uci_to_ ## type (uci_alloc_generic(ctx, uci_type_ ## type, name, sizeof(struct uci_ ## type) + datasize)) +#define uci_alloc_element(ctx, comment, type, name, datasize) \ + uci_to_ ## type (uci_alloc_generic(ctx, comment, uci_type_ ## type, name, sizeof(struct uci_ ## type) + datasize)) extern const char *uci_confdir; extern const char *uci_savedir; +__private int uci_add_section_with_comment(struct uci_context *ctx, struct uci_package *p, const char *comment, const char *type, struct uci_section **res); + __private void *uci_malloc(struct uci_context *ctx, size_t size); __private void *uci_realloc(struct uci_context *ctx, void *ptr, size_t size); __private char *uci_strdup(struct uci_context *ctx, const char *str); __private bool uci_validate_str(const char *str, bool name, bool package); __private void uci_add_delta(struct uci_context *ctx, struct uci_list *list, int cmd, const char *section, const char *option, const char *value); __private void uci_free_delta(struct uci_delta *h); -__private struct uci_package *uci_alloc_package(struct uci_context *ctx, const char *name); +__private struct uci_package *uci_alloc_package(struct uci_context *ctx, const char *comment, const char *name); __private FILE *uci_open_stream(struct uci_context *ctx, const char *filename, const char *origfilename, int pos, bool write, bool create); __private void uci_close_stream(FILE *stream); @@ -78,7 +80,7 @@ __private void uci_alloc_parse_context(struct uci_context *ctx); __private void uci_cleanup(struct uci_context *ctx); __private struct uci_element *uci_lookup_list(struct uci_list *list, const char *name); __private void uci_free_package(struct uci_package **package); -__private struct uci_element *uci_alloc_generic(struct uci_context *ctx, int type, const char *name, int size); +__private struct uci_element *uci_alloc_generic(struct uci_context *ctx, const char *comment, int type, const char *name, int size); __private void uci_free_element(struct uci_element *e); __private struct uci_element *uci_expand_ptr(struct uci_context *ctx, struct uci_ptr *ptr, bool complete); @@ -99,6 +101,8 @@ static inline bool uci_validate_name(const char *str) return uci_validate_str(str, true, false); } +__private bool uci_validate_comment(const char *comment); + /* initialize a list head/item */ static inline void uci_list_init(struct uci_list *ptr) { diff --git a/util.c b/util.c index 61e42cd..2bbd7c0 100644 --- a/util.c +++ b/util.c @@ -101,6 +101,21 @@ bool uci_validate_text(const char *str) return true; } +__private bool uci_validate_comment(const char *comment) +{ + const char *readptr = comment; + + do { + if (*readptr != '#') + return false; + readptr = strchr(readptr, '\n'); + if (readptr) + readptr++; + } while (readptr && *readptr); + + return true; +} + __private void uci_alloc_parse_context(struct uci_context *ctx) { ctx->pctx = (struct uci_parse_context *) uci_malloc(ctx, sizeof(struct uci_parse_context)); From 114b84976d1c58dcbb1ff363650dd98b7969e5fd Mon Sep 17 00:00:00 2001 From: Bastiaan Stougie Date: Sat, 4 Apr 2026 15:43:07 -0400 Subject: [PATCH 4/7] uci: tests for option '-k' Added shunit2 tests for option '-k': keep comments for unchanged entries. Signed-off-by: Bastiaan Stougie --- .../references/add_list_keep_comments.data | 23 +++++++++++++ .../references/add_list_keep_comments.result | 25 ++++++++++++++ .../references/del_list_keep_comments.data | 20 +++++++++++ .../references/del_list_keep_comments.result | 6 ++++ tests/shunit2/references/delete_option.data | 13 +++++++ tests/shunit2/references/delete_option.result | 10 ++++++ .../delete_option_keep_comments.data | 24 +++++++++++++ .../delete_option_keep_comments.result | 17 ++++++++++ tests/shunit2/references/delete_section.data | 12 +++++++ .../shunit2/references/delete_section.result | 9 +++++ .../delete_section_keep_comments.data | 18 ++++++++++ .../delete_section_keep_comments.result | 14 ++++++++ .../references/export_keep_comments.data | 23 +++++++++++++ .../references/export_keep_comments.result | 20 +++++++++++ tests/shunit2/references/import_merge.data | 10 ++++++ tests/shunit2/references/import_merge.result | 17 ++++++++++ .../references/import_merge_import.data | 11 ++++++ .../import_merge_keep_comments.data | 34 +++++++++++++++++++ .../import_merge_keep_comments.result | 34 +++++++++++++++++++ .../import_merge_keep_comments_import.data | 11 ++++++ .../shunit2/references/set_keep_comments.data | 23 +++++++++++++ .../references/set_keep_comments.result | 17 ++++++++++ tests/shunit2/tests.d/000_import | 26 ++++++++++++++ tests/shunit2/tests.d/010_export | 15 ++++++++ tests/shunit2/tests.d/030_set | 22 ++++++++++++ tests/shunit2/tests.d/080_list | 25 ++++++++++++++ tests/shunit2/tests.d/110_delete | 34 +++++++++++++++++++ tests/shunit2/tests.sh | 1 + 28 files changed, 514 insertions(+) create mode 100644 tests/shunit2/references/add_list_keep_comments.data create mode 100644 tests/shunit2/references/add_list_keep_comments.result create mode 100644 tests/shunit2/references/del_list_keep_comments.data create mode 100644 tests/shunit2/references/del_list_keep_comments.result create mode 100644 tests/shunit2/references/delete_option.data create mode 100644 tests/shunit2/references/delete_option.result create mode 100644 tests/shunit2/references/delete_option_keep_comments.data create mode 100644 tests/shunit2/references/delete_option_keep_comments.result create mode 100644 tests/shunit2/references/delete_section.data create mode 100644 tests/shunit2/references/delete_section.result create mode 100644 tests/shunit2/references/delete_section_keep_comments.data create mode 100644 tests/shunit2/references/delete_section_keep_comments.result create mode 100644 tests/shunit2/references/export_keep_comments.data create mode 100644 tests/shunit2/references/export_keep_comments.result create mode 100644 tests/shunit2/references/import_merge.data create mode 100644 tests/shunit2/references/import_merge.result create mode 100644 tests/shunit2/references/import_merge_import.data create mode 100644 tests/shunit2/references/import_merge_keep_comments.data create mode 100644 tests/shunit2/references/import_merge_keep_comments.result create mode 100644 tests/shunit2/references/import_merge_keep_comments_import.data create mode 100644 tests/shunit2/references/set_keep_comments.data create mode 100644 tests/shunit2/references/set_keep_comments.result create mode 100644 tests/shunit2/tests.d/110_delete diff --git a/tests/shunit2/references/add_list_keep_comments.data b/tests/shunit2/references/add_list_keep_comments.data new file mode 100644 index 0000000..ffb6acd --- /dev/null +++ b/tests/shunit2/references/add_list_keep_comments.data @@ -0,0 +1,23 @@ +# before +config named 'section' + # before + option opt 'val' + + option opt2 'val2' # after1 + + #before1 + #before2 + option opt3 'val3' # after2 + +config named 'section2' # after3 + +#before3 +#before4 +config named 'section3' # after4 + +#before5 +#before6 +config named 'section4' # after5 + #before7 + #before8 + option opt3 'val3' # after6 diff --git a/tests/shunit2/references/add_list_keep_comments.result b/tests/shunit2/references/add_list_keep_comments.result new file mode 100644 index 0000000..36e1f48 --- /dev/null +++ b/tests/shunit2/references/add_list_keep_comments.result @@ -0,0 +1,25 @@ + +# before +config named 'section' + # before + option opt 'val' + option opt2 'val2' # after1 + #before1 + #before2 + option opt3 'val3' # after2 + list listopt 'val' + list listopt 'val2' + +config named 'section2' # after3 + +#before3 +#before4 +config named 'section3' # after4 + +#before5 +#before6 +config named 'section4' # after5 + #before7 + #before8 + option opt3 'val3' # after6 + diff --git a/tests/shunit2/references/del_list_keep_comments.data b/tests/shunit2/references/del_list_keep_comments.data new file mode 100644 index 0000000..07b13e8 --- /dev/null +++ b/tests/shunit2/references/del_list_keep_comments.data @@ -0,0 +1,20 @@ + +# comment before section +config named 'section' # comment after section + # comment before opt + option opt 'optval' # comment after opt + #before1 + list listopt 'val' + #before1 + #before2 + list listopt 'val2' + list listopt 'val3' #after + #before1 + list listopt 'val4' #after + #before1 + #before2 + list listopt 'val5' #after + #be\fore1 + #be\fore2 + list listopt 'val6' #af\ter + diff --git a/tests/shunit2/references/del_list_keep_comments.result b/tests/shunit2/references/del_list_keep_comments.result new file mode 100644 index 0000000..4bc9a36 --- /dev/null +++ b/tests/shunit2/references/del_list_keep_comments.result @@ -0,0 +1,6 @@ + +# comment before section +config named 'section' # comment after section + # comment before opt + option opt 'optval' # comment after opt + diff --git a/tests/shunit2/references/delete_option.data b/tests/shunit2/references/delete_option.data new file mode 100644 index 0000000..823924b --- /dev/null +++ b/tests/shunit2/references/delete_option.data @@ -0,0 +1,13 @@ + +config type 'section1' + option opt 'val' + option opt2 'val2' + list listopt 'val3' + list listopt 'val4' + +config type 'section2' + option opt 'val' + option opt2 'val2' + list listopt 'val3' + list listopt 'val4' + diff --git a/tests/shunit2/references/delete_option.result b/tests/shunit2/references/delete_option.result new file mode 100644 index 0000000..0e5075f --- /dev/null +++ b/tests/shunit2/references/delete_option.result @@ -0,0 +1,10 @@ + +config type 'section1' + option opt2 'val2' + +config type 'section2' + option opt 'val' + option opt2 'val2' + list listopt 'val3' + list listopt 'val4' + diff --git a/tests/shunit2/references/delete_option_keep_comments.data b/tests/shunit2/references/delete_option_keep_comments.data new file mode 100644 index 0000000..9fa1142 --- /dev/null +++ b/tests/shunit2/references/delete_option_keep_comments.data @@ -0,0 +1,24 @@ + +# comment 1 +config type 'section1' # comment 2 + # comment 3 + option opt 'val' # comment 4 + option opt2 'val2' # comment 5 +# comment 6 + list listopt 'val3' # comment 7 + # comment 8 + # comment 9 + list listopt 'val4' # comment 10 + +# comment 11 +# + # comment 12 +config type 'section2' # comment 13 + option opt 'val' + # comment 14 + # comment 15 + option opt2 'val2' + list listopt 'val3' # comment 16 + # comment 17 + list listopt 'val4' # comment 18 + diff --git a/tests/shunit2/references/delete_option_keep_comments.result b/tests/shunit2/references/delete_option_keep_comments.result new file mode 100644 index 0000000..8d9197f --- /dev/null +++ b/tests/shunit2/references/delete_option_keep_comments.result @@ -0,0 +1,17 @@ + +# comment 1 +config type 'section1' # comment 2 + option opt2 'val2' # comment 5 + +# comment 11 +# +# comment 12 +config type 'section2' # comment 13 + option opt 'val' + # comment 14 + # comment 15 + option opt2 'val2' + list listopt 'val3' # comment 16 + # comment 17 + list listopt 'val4' # comment 18 + diff --git a/tests/shunit2/references/delete_section.data b/tests/shunit2/references/delete_section.data new file mode 100644 index 0000000..de687e9 --- /dev/null +++ b/tests/shunit2/references/delete_section.data @@ -0,0 +1,12 @@ +config type 'section' + option opt 'val' + option opt2 'val2' + +config type 'section2' + option opt3 'val3' + option opt4 'val4' + + +config type 'section3' + option opt3 'val5' + option opt4 'val6' diff --git a/tests/shunit2/references/delete_section.result b/tests/shunit2/references/delete_section.result new file mode 100644 index 0000000..c12d3ec --- /dev/null +++ b/tests/shunit2/references/delete_section.result @@ -0,0 +1,9 @@ + +config type 'section' + option opt 'val' + option opt2 'val2' + +config type 'section3' + option opt3 'val5' + option opt4 'val6' + diff --git a/tests/shunit2/references/delete_section_keep_comments.data b/tests/shunit2/references/delete_section_keep_comments.data new file mode 100644 index 0000000..e010415 --- /dev/null +++ b/tests/shunit2/references/delete_section_keep_comments.data @@ -0,0 +1,18 @@ +# comment 1 +# comment 2 +config type 'section' # comment 3 + # comment 4 + option opt 'val' # comment 5 + option opt2 'val2' # comment 6 + +# comment 7 +config type 'section2' # comment 8 + option opt3 'val3' # comment 9 + # comment 10 + option opt4 'val4' # comment 11 + + # comment 12 +config type 'section3' + option opt3 'val5' # comment 13 +# comment 14 + option opt4 'val6' diff --git a/tests/shunit2/references/delete_section_keep_comments.result b/tests/shunit2/references/delete_section_keep_comments.result new file mode 100644 index 0000000..590d484 --- /dev/null +++ b/tests/shunit2/references/delete_section_keep_comments.result @@ -0,0 +1,14 @@ + +# comment 1 +# comment 2 +config type 'section' # comment 3 + # comment 4 + option opt 'val' # comment 5 + option opt2 'val2' # comment 6 + +# comment 12 +config type 'section3' + option opt3 'val5' # comment 13 + # comment 14 + option opt4 'val6' + diff --git a/tests/shunit2/references/export_keep_comments.data b/tests/shunit2/references/export_keep_comments.data new file mode 100644 index 0000000..cd8d514 --- /dev/null +++ b/tests/shunit2/references/export_keep_comments.data @@ -0,0 +1,23 @@ + +# lots + # of + # comment +# lines +config 'type' 'section' + option 'opt' 'val' + list 'list_opt' 'val0' + list 'list_opt' 'val1' ###### another comment after val1 +# lots + # more + # comment +# lines + option mul_line_opt_sq 'line 1 +line 2 \ +line 3' + option mul_line_opt_dq "\"Hello\, \ +World.\' +" + +# comment lines +# that are +# lost... diff --git a/tests/shunit2/references/export_keep_comments.result b/tests/shunit2/references/export_keep_comments.result new file mode 100644 index 0000000..23f3c76 --- /dev/null +++ b/tests/shunit2/references/export_keep_comments.result @@ -0,0 +1,20 @@ +package export + +# lots +# of +# comment +# lines +config type 'section' + option opt 'val' + list list_opt 'val0' + list list_opt 'val1' ###### another comment after val1 + # lots + # more + # comment + # lines + option mul_line_opt_sq 'line 1 +line 2 \ +line 3' + option mul_line_opt_dq '"Hello, World.'\'' +' + diff --git a/tests/shunit2/references/import_merge.data b/tests/shunit2/references/import_merge.data new file mode 100644 index 0000000..0a313c7 --- /dev/null +++ b/tests/shunit2/references/import_merge.data @@ -0,0 +1,10 @@ +config type 'section1' + option 'opt' 'val' + + list 'list_opt' 'val0' + list 'list_opt' 'val1' + +config type 'section3' + option 'opt' 'val4' + list 'list_opt' 'val5' + list 'list_opt' 'val6' diff --git a/tests/shunit2/references/import_merge.result b/tests/shunit2/references/import_merge.result new file mode 100644 index 0000000..ff01c3d --- /dev/null +++ b/tests/shunit2/references/import_merge.result @@ -0,0 +1,17 @@ + +config type 'section1' + option opt 'val' + list list_opt 'val0' + list list_opt 'val1' + +config type 'section3' + option opt 'val7' + list list_opt 'val5' + list list_opt 'val6' + option opt2 'val8' + +config type 'section2' + option opt 'val4' + list list_opt 'val5' + list list_opt 'val6' + diff --git a/tests/shunit2/references/import_merge_import.data b/tests/shunit2/references/import_merge_import.data new file mode 100644 index 0000000..6d9cd62 --- /dev/null +++ b/tests/shunit2/references/import_merge_import.data @@ -0,0 +1,11 @@ +package 'import-test' + +config type 'section2' + option 'opt' 'val4' + list 'list_opt' 'val5' + list 'list_opt' 'val6' + +config type 'section3' + option 'opt' 'val7' + option 'opt2' 'val8' + diff --git a/tests/shunit2/references/import_merge_keep_comments.data b/tests/shunit2/references/import_merge_keep_comments.data new file mode 100644 index 0000000..d6f0066 --- /dev/null +++ b/tests/shunit2/references/import_merge_keep_comments.data @@ -0,0 +1,34 @@ +# before config type 'section' + +config type 'section' # after config type 'section' + # before option 'opt' + option 'opt' 'val' # after option 'opt' +# before list 'list_opt' 'val0' line 1 + + # before list 'list_opt' 'val0' line 2 + list 'list_opt' 'val0' # after list 'list_opt' 'val0' + list 'list_opt' 'val1' + option mul_line_opt_sq \''line 1 +line 2 \ +line 3'\' # comment after mul_line_opt_sq + option mul_line_opt_dq "\"Hello, \ +World.\' +" # comment after mul_line_opt_dq + +config type 'section3' # after config type 'section' + # before option 'opt' + option 'opt' 'val4' # after option 'opt' +# before list 'list_opt' 'val5' line 1 + + # before list 'list_opt' 'val5' line 2 + list 'list_opt' 'val5' # after list 'list_opt' 'val5' + list 'list_opt' 'val6' + # comment before mul_line_opt_sq + option mul_line_opt_sq \''line 1 +line 2a \ +line 3a'\' # comment after mul_line_opt_sq 2 + option mul_line_opt_dq "\"Hello, \ +World2.\' +" # comment after mul_line_opt_dq 2 + +# this original comment at the end of the file should not be kept diff --git a/tests/shunit2/references/import_merge_keep_comments.result b/tests/shunit2/references/import_merge_keep_comments.result new file mode 100644 index 0000000..d95bb8c --- /dev/null +++ b/tests/shunit2/references/import_merge_keep_comments.result @@ -0,0 +1,34 @@ + +# before config type 'section' +config type 'section' # after config type 'section' + # before option 'opt' + option opt 'val' # after option 'opt' + # before list 'list_opt' 'val0' line 1 + # before list 'list_opt' 'val0' line 2 + list list_opt 'val0' # after list 'list_opt' 'val0' + list list_opt 'val1' + option mul_line_opt_sq ''\''line 1 +line 2 \ +line 3'\''' # comment after mul_line_opt_sq + option mul_line_opt_dq '"Hello, World.'\'' +' # comment after mul_line_opt_dq + +config type 'section3' + option opt 'val7' + # before list 'list_opt' 'val5' line 1 + # before list 'list_opt' 'val5' line 2 + list list_opt 'val5' # after list 'list_opt' 'val5' + list list_opt 'val6' + # comment before mul_line_opt_sq + option mul_line_opt_sq ''\''line 1 +line 2a \ +line 3a'\''' # comment after mul_line_opt_sq 2 + option mul_line_opt_dq '"Hello, World2.'\'' +' # comment after mul_line_opt_dq 2 + option opt2 'val8' + +config type 'section2' + option opt 'val4' + list list_opt 'val5' + list list_opt 'val6' + diff --git a/tests/shunit2/references/import_merge_keep_comments_import.data b/tests/shunit2/references/import_merge_keep_comments_import.data new file mode 100644 index 0000000..6d9cd62 --- /dev/null +++ b/tests/shunit2/references/import_merge_keep_comments_import.data @@ -0,0 +1,11 @@ +package 'import-test' + +config type 'section2' + option 'opt' 'val4' + list 'list_opt' 'val5' + list 'list_opt' 'val6' + +config type 'section3' + option 'opt' 'val7' + option 'opt2' 'val8' + diff --git a/tests/shunit2/references/set_keep_comments.data b/tests/shunit2/references/set_keep_comments.data new file mode 100644 index 0000000..ffb6acd --- /dev/null +++ b/tests/shunit2/references/set_keep_comments.data @@ -0,0 +1,23 @@ +# before +config named 'section' + # before + option opt 'val' + + option opt2 'val2' # after1 + + #before1 + #before2 + option opt3 'val3' # after2 + +config named 'section2' # after3 + +#before3 +#before4 +config named 'section3' # after4 + +#before5 +#before6 +config named 'section4' # after5 + #before7 + #before8 + option opt3 'val3' # after6 diff --git a/tests/shunit2/references/set_keep_comments.result b/tests/shunit2/references/set_keep_comments.result new file mode 100644 index 0000000..fae3524 --- /dev/null +++ b/tests/shunit2/references/set_keep_comments.result @@ -0,0 +1,17 @@ + +config named 'section' + option opt 'val' + option opt2 'val' + option opt3 'val' + +config named 'section2' + +config named 'section3' + +#before5 +#before6 +config named 'section4' # after5 + #before7 + #before8 + option opt3 'val3' # after6 + diff --git a/tests/shunit2/tests.d/000_import b/tests/shunit2/tests.d/000_import index 45a1448..a429a18 100644 --- a/tests/shunit2/tests.d/000_import +++ b/tests/shunit2/tests.d/000_import @@ -3,3 +3,29 @@ test_import () ${UCI} import < ${REF_DIR}/import.data assertSameFile ${REF_DIR}/import.result ${CONFIG_DIR}/import-test } + +make_abspath() +{ + # this should work with /bin/sh + echo "$(cd -- "$(dirname "$1")"; pwd)/$(basename "$1")" +} + +test_import_merge () +{ + cp "${REF_DIR}/import_merge.data" "${CONFIG_DIR}/import-test" + # if the full path of the config file is specified, 'import' applies + # the changes immediately without requiring a commit. + ABSPATH="$(make_abspath "${CONFIG_DIR}/import-test")" + ${UCI} -m import "${ABSPATH}" < "${REF_DIR}/import_merge_import.data" + assertSameFile "${REF_DIR}/import_merge.result" "${CONFIG_DIR}/import-test" +} + +test_import_merge_keep_comments () +{ + cp "${REF_DIR}/import_merge_keep_comments.data" "${CONFIG_DIR}/import-test" + # if the full path of the config file is specified, 'import' applies + # the changes immediately without requiring a commit. + ABSPATH="$(make_abspath "${CONFIG_DIR}/import-test")" + ${UCI} -k -m import "${ABSPATH}" < "${REF_DIR}/import_merge_keep_comments_import.data" + assertSameFile "${REF_DIR}/import_merge_keep_comments.result" "${CONFIG_DIR}/import-test" +} diff --git a/tests/shunit2/tests.d/010_export b/tests/shunit2/tests.d/010_export index 584bcc8..faf9833 100644 --- a/tests/shunit2/tests.d/010_export +++ b/tests/shunit2/tests.d/010_export @@ -12,3 +12,18 @@ test_export () assertTrue $? assertSameFile ${REF_DIR}/export.result ${TMP_DIR}/export.result } + +test_export_keep_comments () +{ + cp ${REF_DIR}/export_keep_comments.data ${CONFIG_DIR}/export + + ${UCI_Q} -k export nilpackage + assertFalse $? + + ${UCI_Q} -k export export 1>/dev/null 2>&1 + assertTrue $? + + ${UCI} -k export > ${TMP_DIR}/export_keep_comments.result + assertTrue $? + assertSameFile ${REF_DIR}/export_keep_comments.result ${TMP_DIR}/export_keep_comments.result +} diff --git a/tests/shunit2/tests.d/030_set b/tests/shunit2/tests.d/030_set index db3c259..33823dc 100644 --- a/tests/shunit2/tests.d/030_set +++ b/tests/shunit2/tests.d/030_set @@ -37,6 +37,28 @@ test_set_existing_option() assertSameFile ${REF_DIR}/set_existing_option.result ${CHANGES_DIR}/set } +test_set_keep_comments() +{ + cp ${REF_DIR}/set_keep_comments.data ${CONFIG_DIR}/set + + # section with comment lines before + ${UCI} set set.section=named + # section with comment after + ${UCI} set set.section2=named + # section with comment lines before and comment after + ${UCI} set set.section3=named + + # option with comment lines before + ${UCI} set set.section.opt=val + # option with comment after + ${UCI} set set.section.opt2=val + # option with comment lines before and comment after + ${UCI} set set.section.opt3=val + + ${UCI} -k commit + assertSameFile ${REF_DIR}/set_keep_comments.result ${CONFIG_DIR}/set +} + test_set_existing_option_multiline() { cp ${REF_DIR}/set_existing_option.data ${CONFIG_DIR}/set diff --git a/tests/shunit2/tests.d/080_list b/tests/shunit2/tests.d/080_list index e0a910a..ed8b2b9 100644 --- a/tests/shunit2/tests.d/080_list +++ b/tests/shunit2/tests.d/080_list @@ -34,6 +34,16 @@ test_add_list_changes() { assertEquals "$value_list_changes" "$value_list_changes_ref" } +test_add_list_keep_comments() { + cp ${REF_DIR}/add_list_keep_comments.data ${CONFIG_DIR}/test + ${UCI} add_list 'test.section.listopt=val' + ${UCI} add_list 'test.section.listopt=val2' + ${UCI} -k commit + assertSameFile ${REF_DIR}/add_list_keep_comments.result ${CONFIG_DIR}/test + + # add_list can't change a list entry, it adds list entries only. +} + test_del_list() { prepare_list_test ${UCI} commit @@ -50,3 +60,18 @@ test_del_list_multiline() { ${UCI} commit assertSameFile "${REF_DIR}/del_list_multiline_config.result" "$CONFIG_DIR/list_test_config" } + +test_del_list_keep_comments() { + # list entries with comments + cp ${REF_DIR}/del_list_keep_comments.data ${CONFIG_DIR}/test + # test that the listopts and their comments are gone with del_list of all entries individually + ${UCI} del_list test.section.listopt=val + ${UCI} del_list test.section.listopt=val2 + ${UCI} del_list test.section.listopt=val3 + ${UCI} del_list test.section.listopt=val4 + ${UCI} del_list test.section.listopt=val5 + ${UCI} del_list test.section.listopt=val6 + ${UCI} -k commit + assertSameFile ${REF_DIR}/del_list_keep_comments.result ${CONFIG_DIR}/test +} + diff --git a/tests/shunit2/tests.d/110_delete b/tests/shunit2/tests.d/110_delete new file mode 100644 index 0000000..4a2e725 --- /dev/null +++ b/tests/shunit2/tests.d/110_delete @@ -0,0 +1,34 @@ +test_delete_section() +{ + cp "${REF_DIR}/delete_section.data" "${CONFIG_DIR}/test" + ${UCI} del test.section2 + ${UCI} commit + assertSameFile "${REF_DIR}/delete_section.result" "${CONFIG_DIR}/test" +} + +test_delete_option() +{ + cp "${REF_DIR}/delete_option.data" "${CONFIG_DIR}/test" + ${UCI} del test.section1.opt + ${UCI} del test.section1.listopt + ${UCI} commit + assertSameFile "${REF_DIR}/delete_option.result" "${CONFIG_DIR}/test" +} + +test_delete_section_keep_comments() +{ + cp "${REF_DIR}/delete_section_keep_comments.data" "${CONFIG_DIR}/test" + ${UCI} del test.section2 + ${UCI} -k commit + assertSameFile "${REF_DIR}/delete_section_keep_comments.result" "${CONFIG_DIR}/test" +} + +test_delete_option_keep_comments() +{ + cp "${REF_DIR}/delete_option_keep_comments.data" "${CONFIG_DIR}/test" + ${UCI} del test.section1.opt + ${UCI} del test.section1.listopt + ${UCI} -k commit + assertSameFile "${REF_DIR}/delete_option_keep_comments.result" "${CONFIG_DIR}/test" +} + diff --git a/tests/shunit2/tests.sh b/tests/shunit2/tests.sh index 3a54e2a..d6629ef 100755 --- a/tests/shunit2/tests.sh +++ b/tests/shunit2/tests.sh @@ -41,6 +41,7 @@ assertSameFile() { echo "TEST:" cat $test echo "----" + diff -r $ref $test } } assertNotSegFault() From 955fd94b88cd109128a00253b0ac6c4e87e94d57 Mon Sep 17 00:00:00 2001 From: Bastiaan Stougie Date: Sat, 4 Apr 2026 17:47:25 -0400 Subject: [PATCH 5/7] uci: import comments with option '-k' With option '-k': - uci command 'import' now also imports comments associated with imported entries - imported comments are auto-indented Limitation: - comment lines at the end of the file are not imported Signed-off-by: Bastiaan Stougie --- delta.c | 32 +++++++++++++++++++++++++++----- file.c | 7 +++++++ list.c | 16 ++++++++-------- uci_internal.h | 3 ++- util.c | 29 +++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/delta.c b/delta.c index 8374bb8..2abc468 100644 --- a/delta.c +++ b/delta.c @@ -33,7 +33,7 @@ /* record a change that was done to a package */ void -uci_add_delta(struct uci_context *ctx, struct uci_list *list, int cmd, const char *section, const char *option, const char *value) +uci_add_delta(struct uci_context *ctx, struct uci_list *list, int cmd, const char *section, const char *option, const char *value, const char *comment) { struct uci_delta *h; int size = strlen(section) + 1; @@ -42,7 +42,7 @@ uci_add_delta(struct uci_context *ctx, struct uci_list *list, int cmd, const cha if (value) size += strlen(value) + 1; - h = uci_alloc_element(ctx, NULL, delta, option, size); + h = uci_alloc_element(ctx, (ctx->flags & UCI_FLAG_COMMENTS) ? comment : NULL, delta, option, size); ptr = uci_dataptr(h); h->cmd = cmd; h->section = strcpy(ptr, section); @@ -66,6 +66,24 @@ uci_free_delta(struct uci_delta *h) uci_free_element(&h->e); } +static void uci_fprintf_escaped_comment(FILE *f, const char *comment) +{ + if (!comment || !comment[0]) + return; + + fputc('#', f); + for (const char *readptr = comment; *readptr; readptr++) { + if (*readptr == '\\') { + fputc('\\', f); + fputc('\\', f); + } else if (*readptr == '\n') { + fputc('\\', f); + fputc('n', f); + } else + fputc(*readptr, f); + } +} + static void uci_delta_save(struct uci_context *ctx, FILE *f, const char *name, const struct uci_delta *h) { @@ -92,7 +110,9 @@ static void uci_delta_save(struct uci_context *ctx, FILE *f, else fprintf(f, "'\\''"); } - fprintf(f, "'\n"); + fprintf(f, "'"); + uci_fprintf_escaped_comment(f, e->comment); + fprintf(f, "\n"); } } @@ -177,6 +197,8 @@ static inline int uci_parse_delta_tuple(struct uci_context *ctx, struct uci_ptr arg += 1; UCI_INTERNAL(uci_parse_ptr, ctx, ptr, arg); + if (!ptr->comment && pctx->commentbuf) + ptr->comment = pctx->commentbuf; if (!ptr->section) goto error; @@ -221,7 +243,7 @@ static void uci_parse_delta_line(struct uci_context *ctx, struct uci_package *p) goto error; if (ctx->flags & UCI_FLAG_SAVED_DELTA) - uci_add_delta(ctx, &p->saved_delta, cmd, ptr.section, ptr.option, ptr.value); + uci_add_delta(ctx, &p->saved_delta, cmd, ptr.section, ptr.option, ptr.value, ptr.comment); switch(cmd) { case UCI_CMD_REORDER: @@ -393,7 +415,7 @@ static void uci_filter_delta(struct uci_context *ctx, const char *name, const ch if (!match && ptr.section) { uci_add_delta(ctx, &list, c, - ptr.section, ptr.option, ptr.value); + ptr.section, ptr.option, ptr.value, ptr.comment); } } diff --git a/file.c b/file.c index 11aee73..ed1ce65 100644 --- a/file.c +++ b/file.c @@ -374,10 +374,17 @@ int uci_parse_argument(struct uci_context *ctx, FILE *stream, char **str, char * uci_getln(ctx, 0); } + uci_reset_comment(ctx); + ofs_result = next_arg(ctx, false, false, false); *result = pctx_str(ctx->pctx, ofs_result); *str = pctx_cur_str(ctx->pctx); + if (ctx->pctx->commentbuf) { + uci_unescape_comment(ctx->pctx->commentbuf, true); + ctx->pctx->commentlen = strlen(ctx->pctx->commentbuf); + } + return 0; } diff --git a/list.c b/list.c index 460fb0a..0ffed4e 100644 --- a/list.c +++ b/list.c @@ -571,7 +571,7 @@ int uci_rename(struct uci_context *ctx, struct uci_ptr *ptr) UCI_ASSERT(ctx, ptr->value); if (!internal && p->has_delta) - uci_add_delta(ctx, &p->delta, UCI_CMD_RENAME, ptr->section, ptr->option, ptr->value); + uci_add_delta(ctx, &p->delta, UCI_CMD_RENAME, ptr->section, ptr->option, ptr->value, NULL); n = uci_strdup(ctx, ptr->value); free(e->name); @@ -595,7 +595,7 @@ int uci_reorder_section(struct uci_context *ctx, struct uci_section *s, int pos) changed = uci_list_set_pos(&s->package->sections, &s->e.list, pos); if (!internal && p->has_delta && changed) { sprintf(order, "%d", pos); - uci_add_delta(ctx, &p->delta, UCI_CMD_REORDER, s->e.name, NULL, order); + uci_add_delta(ctx, &p->delta, UCI_CMD_REORDER, s->e.name, NULL, order, NULL); } return 0; @@ -614,7 +614,7 @@ uci_add_section_with_comment(struct uci_context *ctx, struct uci_package *p, con uci_fixup_section(ctx, s); *res = s; if (!internal && p->has_delta) - uci_add_delta(ctx, &p->delta, UCI_CMD_ADD, s->e.name, NULL, type); + uci_add_delta(ctx, &p->delta, UCI_CMD_ADD, s->e.name, NULL, type, s->e.comment); return 0; } @@ -646,7 +646,7 @@ int uci_delete(struct uci_context *ctx, struct uci_ptr *ptr) uci_foreach_element_safe(&ptr->o->v.list, tmp, e2) { if (index == 0) { if (!internal && p->has_delta) - uci_add_delta(ctx, &p->delta, UCI_CMD_REMOVE, ptr->section, ptr->option, ptr->value); + uci_add_delta(ctx, &p->delta, UCI_CMD_REMOVE, ptr->section, ptr->option, ptr->value, NULL); uci_free_option(uci_to_option(e2)); return 0; } @@ -657,7 +657,7 @@ int uci_delete(struct uci_context *ctx, struct uci_ptr *ptr) } if (!internal && p->has_delta) - uci_add_delta(ctx, &p->delta, UCI_CMD_REMOVE, ptr->section, ptr->option, NULL); + uci_add_delta(ctx, &p->delta, UCI_CMD_REMOVE, ptr->section, ptr->option, NULL, NULL); uci_free_any(&e1); @@ -712,7 +712,7 @@ int uci_add_list(struct uci_context *ctx, struct uci_ptr *ptr) uci_list_add(&ptr->o->v.list, &e1->list); if (!internal && ptr->p->has_delta) - uci_add_delta(ctx, &ptr->p->delta, UCI_CMD_LIST_ADD, ptr->section, ptr->option, ptr->value); + uci_add_delta(ctx, &ptr->p->delta, UCI_CMD_LIST_ADD, ptr->section, ptr->option, ptr->value, ptr->comment); return 0; error: @@ -744,7 +744,7 @@ int uci_del_list(struct uci_context *ctx, struct uci_ptr *ptr) p = ptr->p; if (!internal && p->has_delta) - uci_add_delta(ctx, &p->delta, UCI_CMD_LIST_DEL, ptr->section, ptr->option, ptr->value); + uci_add_delta(ctx, &p->delta, UCI_CMD_LIST_DEL, ptr->section, ptr->option, ptr->value, NULL); uci_foreach_element_safe(&ptr->o->v.list, tmp, e) { if (!strcmp(ptr->value, uci_to_option(e)->e.name)) { @@ -810,7 +810,7 @@ int uci_set(struct uci_context *ctx, struct uci_ptr *ptr) } if (!internal && ptr->p->has_delta) - uci_add_delta(ctx, &ptr->p->delta, UCI_CMD_CHANGE, ptr->section, ptr->option, ptr->value); + uci_add_delta(ctx, &ptr->p->delta, UCI_CMD_CHANGE, ptr->section, ptr->option, ptr->value, ptr->comment); return 0; } diff --git a/uci_internal.h b/uci_internal.h index 644859d..2435035 100644 --- a/uci_internal.h +++ b/uci_internal.h @@ -60,13 +60,14 @@ struct uci_parse_context extern const char *uci_confdir; extern const char *uci_savedir; +__private void uci_unescape_comment(char *comment, bool skip_hash); __private int uci_add_section_with_comment(struct uci_context *ctx, struct uci_package *p, const char *comment, const char *type, struct uci_section **res); __private void *uci_malloc(struct uci_context *ctx, size_t size); __private void *uci_realloc(struct uci_context *ctx, void *ptr, size_t size); __private char *uci_strdup(struct uci_context *ctx, const char *str); __private bool uci_validate_str(const char *str, bool name, bool package); -__private void uci_add_delta(struct uci_context *ctx, struct uci_list *list, int cmd, const char *section, const char *option, const char *value); +__private void uci_add_delta(struct uci_context *ctx, struct uci_list *list, int cmd, const char *section, const char *option, const char *value, const char *comment); __private void uci_free_delta(struct uci_delta *h); __private struct uci_package *uci_alloc_package(struct uci_context *ctx, const char *comment, const char *name); diff --git a/util.c b/util.c index 2bbd7c0..fcfbaa3 100644 --- a/util.c +++ b/util.c @@ -121,6 +121,35 @@ __private void uci_alloc_parse_context(struct uci_context *ctx) ctx->pctx = (struct uci_parse_context *) uci_malloc(ctx, sizeof(struct uci_parse_context)); } +__private void uci_unescape_comment(char *comment, bool skip_hash) +{ + char *readptr = comment; + char *writeptr = comment; + + if (skip_hash && *readptr == '#') + readptr++; + + while (*readptr) { + if (*readptr != '\\') + *writeptr = *readptr; + else { + if (readptr[1] == '\\') { + *writeptr = '\\'; + readptr++; + } + else if (readptr[1] == 'n') { + *writeptr = '\n'; + readptr++; + } + else + *writeptr = '\\'; + } + readptr++; + writeptr++; + } + *writeptr = '\0'; +} + int uci_parse_ptr(struct uci_context *ctx, struct uci_ptr *ptr, char *str) { char *last = NULL; From 4aa3329096b76274d1fa430e685400fb23f1584e Mon Sep 17 00:00:00 2001 From: Bastiaan Stougie Date: Sun, 5 Apr 2026 01:45:35 -0400 Subject: [PATCH 6/7] uci: tests for import comments with option '-k' Test that 'import' with option '-k' also imports comments associated with imported entries. Signed-off-by: Bastiaan Stougie --- scripts/devel-build.sh | 2 ++ .../import_merge_with_comments.changes1 | 1 + .../import_merge_with_comments.data | 20 ++++++++++++ .../import_merge_with_comments.result1 | 19 ++++++++++++ .../import_merge_with_comments.result2 | 28 +++++++++++++++++ .../references/import_with_comments.data | 20 ++++++++++++ .../references/import_with_comments.result | 15 +++++++++ tests/shunit2/tests.d/000_import | 31 +++++++++++++++++++ 8 files changed, 136 insertions(+) create mode 100644 tests/shunit2/references/import_merge_with_comments.changes1 create mode 100644 tests/shunit2/references/import_merge_with_comments.data create mode 100644 tests/shunit2/references/import_merge_with_comments.result1 create mode 100644 tests/shunit2/references/import_merge_with_comments.result2 create mode 100644 tests/shunit2/references/import_with_comments.data create mode 100644 tests/shunit2/references/import_with_comments.result diff --git a/scripts/devel-build.sh b/scripts/devel-build.sh index d40b273..8d997f3 100755 --- a/scripts/devel-build.sh +++ b/scripts/devel-build.sh @@ -87,6 +87,8 @@ make -C "${BUILDDIR}" test CTEST_OUTPUT_ON_FAILURE=1 set +x echo "✅ Success - uci is available at ${BUILDDIR}" +echo "👷 The test log is available at ${BUILDDIR}/Testing/Temporary/LastTest.log" echo "👷 You can rebuild uci by running 'make -C build'" +echo "" exit 0 diff --git a/tests/shunit2/references/import_merge_with_comments.changes1 b/tests/shunit2/references/import_merge_with_comments.changes1 new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/tests/shunit2/references/import_merge_with_comments.changes1 @@ -0,0 +1 @@ +TODO diff --git a/tests/shunit2/references/import_merge_with_comments.data b/tests/shunit2/references/import_merge_with_comments.data new file mode 100644 index 0000000..ac72ca3 --- /dev/null +++ b/tests/shunit2/references/import_merge_with_comments.data @@ -0,0 +1,20 @@ +package 'import-test' + +# 2before config type 'section' +config type 'section' # 2after config type 'section' + # before option 'opt' + option opt 'optval' # after option 'opt' + # before + list list_opt 'val7' # after + list list_opt 'differentval' + option mul_line_opt_sq ''\''line 1 +line 2 \ +line 3'\''' # comment after mul_line_opt_sq + option mul_line_opt_dq '"Hello, World.'\'' +' # 2comment after mul_line_opt_dq + option newopt 'newval' + +# 1somecomment before +config type 'section2' # 1somecomment after + # 2somecomment before + option opt2 'val2' # 2somecomment after diff --git a/tests/shunit2/references/import_merge_with_comments.result1 b/tests/shunit2/references/import_merge_with_comments.result1 new file mode 100644 index 0000000..7f1c0f2 --- /dev/null +++ b/tests/shunit2/references/import_merge_with_comments.result1 @@ -0,0 +1,19 @@ + +# before config type 'section' +config type 'section' # after config type 'section' + # before option 'opt' + option opt 'val' # after option 'opt' + # before list 'list_opt' 'val0' line 1 + # before list 'list_opt' 'val0' line 2 + list list_opt 'val0' # after list 'list_opt' 'val0' + list list_opt 'val1' + # before list 'list_opt' 'val0' line 1 + # before list 'list_opt' 'val0' line 2 + list list_opt 'val0' # after list 'list_opt' 'val0' + list list_opt 'val1' + option mul_line_opt_sq ''\''line 1 +line 2 \ +line 3'\''' # comment after mul_line_opt_sq + option mul_line_opt_dq '"Hello, World.'\'' +' # comment after mul_line_opt_dq + diff --git a/tests/shunit2/references/import_merge_with_comments.result2 b/tests/shunit2/references/import_merge_with_comments.result2 new file mode 100644 index 0000000..b01eeb1 --- /dev/null +++ b/tests/shunit2/references/import_merge_with_comments.result2 @@ -0,0 +1,28 @@ + +# 2before config type 'section' +config type 'section' # 2after config type 'section' + # before option 'opt' + option opt 'optval' # after option 'opt' + # before list 'list_opt' 'val0' line 1 + # before list 'list_opt' 'val0' line 2 + list list_opt 'val0' # after list 'list_opt' 'val0' + list list_opt 'val1' + # before list 'list_opt' 'val0' line 1 + # before list 'list_opt' 'val0' line 2 + list list_opt 'val0' # after list 'list_opt' 'val0' + list list_opt 'val1' + # before + list list_opt 'val7' # after + list list_opt 'differentval' + option mul_line_opt_sq ''\''line 1 +line 2 \ +line 3'\''' # comment after mul_line_opt_sq + option mul_line_opt_dq '"Hello, World.'\'' +' # 2comment after mul_line_opt_dq + option newopt 'newval' + +# 1somecomment before +config type 'section2' # 1somecomment after + # 2somecomment before + option opt2 'val2' # 2somecomment after + diff --git a/tests/shunit2/references/import_with_comments.data b/tests/shunit2/references/import_with_comments.data new file mode 100644 index 0000000..a4167ce --- /dev/null +++ b/tests/shunit2/references/import_with_comments.data @@ -0,0 +1,20 @@ +# before package 'import-test' line 1 +# before package 'import-test' line 2 +package 'import-test' # after package 'import-test' + +# before config type 'section' + +config type 'section' # after config type 'section' + # before option 'opt' + option 'opt' 'val' # after option 'opt' +# before list 'list_opt' 'val0' line 1 + + # before list 'list_opt' 'val0' line 2 + list 'list_opt' 'val0' # after list 'list_opt' 'val0' + list 'list_opt' 'val1' + option mul_line_opt_sq \''line 1 +line 2 \ +line 3'\' # comment after mul_line_opt_sq + option mul_line_opt_dq "\"Hello, \ +World.\' +" # comment after mul_line_opt_dq diff --git a/tests/shunit2/references/import_with_comments.result b/tests/shunit2/references/import_with_comments.result new file mode 100644 index 0000000..88a9ec0 --- /dev/null +++ b/tests/shunit2/references/import_with_comments.result @@ -0,0 +1,15 @@ + +# before config type 'section' +config type 'section' # after config type 'section' + # before option 'opt' + option opt 'val' # after option 'opt' + # before list 'list_opt' 'val0' line 1 + # before list 'list_opt' 'val0' line 2 + list list_opt 'val0' # after list 'list_opt' 'val0' + list list_opt 'val1' + option mul_line_opt_sq ''\''line 1 +line 2 \ +line 3'\''' # comment after mul_line_opt_sq + option mul_line_opt_dq '"Hello, World.'\'' +' # comment after mul_line_opt_dq + diff --git a/tests/shunit2/tests.d/000_import b/tests/shunit2/tests.d/000_import index a429a18..ac90df7 100644 --- a/tests/shunit2/tests.d/000_import +++ b/tests/shunit2/tests.d/000_import @@ -1,6 +1,7 @@ test_import () { ${UCI} import < ${REF_DIR}/import.data + assertTrue $? assertSameFile ${REF_DIR}/import.result ${CONFIG_DIR}/import-test } @@ -17,6 +18,7 @@ test_import_merge () # the changes immediately without requiring a commit. ABSPATH="$(make_abspath "${CONFIG_DIR}/import-test")" ${UCI} -m import "${ABSPATH}" < "${REF_DIR}/import_merge_import.data" + assertTrue $? assertSameFile "${REF_DIR}/import_merge.result" "${CONFIG_DIR}/import-test" } @@ -27,5 +29,34 @@ test_import_merge_keep_comments () # the changes immediately without requiring a commit. ABSPATH="$(make_abspath "${CONFIG_DIR}/import-test")" ${UCI} -k -m import "${ABSPATH}" < "${REF_DIR}/import_merge_keep_comments_import.data" + assertTrue $? assertSameFile "${REF_DIR}/import_merge_keep_comments.result" "${CONFIG_DIR}/import-test" } + +test_import_with_comments () +{ + ${UCI} -k import < "${REF_DIR}/import_with_comments.data" + assertTrue $? + assertSameFile "${REF_DIR}/import_with_comments.result" "${CONFIG_DIR}/import-test" +} + +test_import_merge_with_comments () +{ + ${UCI} -k import < "${REF_DIR}/import_with_comments.data" + assertTrue $? + assertSameFile ${REF_DIR}/import_with_comments.result ${CONFIG_DIR}/import-test + + # merge the same data again: list entries should be appended again + ${UCI} -k -m import import-test < "${REF_DIR}/import_with_comments.data" + assertTrue $? + ${UCI} -k commit + assertTrue $? + assertSameFile "${REF_DIR}/import_merge_with_comments.result1" "${CONFIG_DIR}/import-test" + + # merge different data + ${UCI} -k -m import import-test < "${REF_DIR}/import_merge_with_comments.data" + assertTrue $? + ${UCI} -k commit + assertTrue $? + assertSameFile "${REF_DIR}/import_merge_with_comments.result2" "${CONFIG_DIR}/import-test" +} From 9b2cba222261b5d490b9269addb09c833eddc95b Mon Sep 17 00:00:00 2001 From: Bastiaan Stougie Date: Sun, 5 Apr 2026 11:47:59 -0400 Subject: [PATCH 7/7] uci: show and set comments with option '-k' - `uci -k get '.
##'` shows the section comment - `uci -k get '.
.