From 1bcae9a9502ca2b2914cd670954b62f24c5d769e Mon Sep 17 00:00:00 2001 From: Vyacheslav Yamont Date: Tue, 15 Sep 2026 13:19:13 +0200 Subject: [PATCH] Fix CLI parsing and completion of union leafrefs Keep union values within one CLI token and preserve completion priority. Resolve explicit prefixes in the defining module while retaining the consuming namespace for unprefixed names. --- apps/backend/backend_clixon_lib.c | 10 ++- lib/src/clixon_autocli_generate.c | 13 ++++ test/test_cli_union_leafref.sh | 119 ++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) create mode 100755 test/test_cli_union_leafref.sh diff --git a/apps/backend/backend_clixon_lib.c b/apps/backend/backend_clixon_lib.c index 64dd33806..61bebc243 100644 --- a/apps/backend/backend_clixon_lib.c +++ b/apps/backend/backend_clixon_lib.c @@ -389,7 +389,8 @@ from_client_restart_plugin(clixon_handle h, * from y is used. Unless the element is .., . * @param[in,out] cb0 Result XPath as cbuf * @param[in] xpath1 Input XPath - * @param[in] y Yang of xpath1 + * @param[in] y Context node for unprefixed identifiers + * @param[in] ypath Path statement defining explicit prefixes * @param[in,out] nsc Namespace * @retval 0 OK * @retval -1 Error @@ -407,6 +408,7 @@ static int xpath_append(cbuf *cb0, char *xpath1, yang_stmt *y, + yang_stmt *ypath, cvec *nsc) { int retval = -1; @@ -471,7 +473,7 @@ xpath_append(cbuf *cb0, /* If prefix is not in nsc, it needs to be added */ if (prefix && cvec_find(nsc, prefix) == NULL){ ns = NULL; - if ((ret = yang_find_namespace_by_prefix(y, prefix, &ns)) < 0) + if ((ret = yang_find_namespace_by_prefix(ypath, prefix, &ns)) < 0) goto done; if (ret == 0){ clixon_err(OE_DB, 0, "Prefix %s does not have an associated namespace", prefix); @@ -545,7 +547,7 @@ leafref_append_path(yang_stmt *yn, clixon_err(OE_DB, 0, "Leafref %s requires path statement", resolved_name); goto done; } - if (xpath_append(cbxpath, yang_argument_get(ypath), yn, nsc) < 0) + if (xpath_append(cbxpath, yang_argument_get(ypath), yn, ypath, nsc) < 0) goto done; } else if (strcmp(resolved_name, "union") == 0){ @@ -577,7 +579,7 @@ leafref_append_path(yang_stmt *yn, goto done; } cbuf_append_str(cb_one, cbuf_get(cb_orig)); - if (xpath_append(cb_one, yang_argument_get(ypath), yn, nsc) < 0) + if (xpath_append(cb_one, yang_argument_get(ypath), yn, ypath, nsc) < 0) goto done; if (!first) cprintf(cbxpath, " | "); diff --git a/lib/src/clixon_autocli_generate.c b/lib/src/clixon_autocli_generate.c index fd6917fac..e30943912 100644 --- a/lib/src/clixon_autocli_generate.c +++ b/lib/src/clixon_autocli_generate.c @@ -239,6 +239,7 @@ gen_mtpoint_add(cbuf *cb, * @retval -1 Error * @param[in] leafref_refer If set, add "leafref-refer" as a 3rd argument to * expand_dbvar() to signal that leafrefs should be followed. + * @param[in] preference Optional CLIgen preference override (zero uses the default) * @see expand_dbvar This is where the expand string is used * @see CLIXON_AUTOCLI_NS "expand" extension Overrides expand_dbvar() with a custom callback * @note XXX only fraction_digits handled,should also have mincv, maxcv, pattern @@ -251,6 +252,7 @@ cli_expand_var_generate(clixon_handle h, uint8_t fraction_digits, int pre, int leafref_refer, + uint32_t preference, cbuf *cb) { int retval = -1; @@ -271,6 +273,8 @@ cli_expand_var_generate(clixon_handle h, if (pre) cprintf(cb, "|"); cprintf(cb, "<%s:%s", yang_argument_get(ys), cvtypestr); + if (preference) + cprintf(cb, " preference:%u", preference); if (options & YANG_OPTIONS_FRACTION_DIGITS) cprintf(cb, " fraction-digits:%u", fraction_digits); cprintf(cb, " %s(\"candidate\",\"%s\"", @@ -909,6 +913,10 @@ yang2cli_var_union_one(clixon_handle h, else { if (clicon_type2cv(origtype, restype, ys, &cvtype) < 0) goto done; + /* Leafrefs in unions are single values, not the rest of the command. + * The backend validates the value against the referenced type. */ + if (restype && strcmp(restype, "leafref") == 0) + cvtype = CGV_STRING; if ((retval = yang2cli_leaf_var_sub(h, ys, ytype, helptext, cvtype, options, cvv, patterns, fraction_digits, patpref, cb)) < 0) goto done; @@ -1187,11 +1195,16 @@ yang2cli_leaf_var(clixon_handle h, * Note: also set when restype=="leafref" because yang_path_arg may fail to resolve the * referred node client-side (e.g. grouping with submodule prefix scope); the backend * handles leafref resolution independently. */ + /* Preserve child commands, but retain the rest variable's preference + * so expanded values do not compete with explicit union alternatives. */ + if (strcmp(restype, "union") == 0) + cvtypestr = "string"; if ((ret = cli_expand_var_generate(h, ys, cvtypestr, options, fraction_digits, regular_value, leafref_refer || strcmp(restype, "union") == 0 || strcmp(restype, "leafref") == 0, + strcmp(restype, "union") == 0 ? COV_PREF_REST : 0, cb)) < 0) goto done; if (ret == 1) diff --git a/test/test_cli_union_leafref.sh b/test/test_cli_union_leafref.sh new file mode 100755 index 000000000..8aad84997 --- /dev/null +++ b/test/test_cli_union_leafref.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Union leafref keys must preserve child commands and the typedef's prefix scope. + +# Magic line must be first in script (see README.md) +s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi + +APPNAME=example +cfg=$dir/conf_yang.xml + +cat < $dir/example-targets.yang +module example-targets { + yang-version 1.1; + namespace "urn:example:targets"; + prefix t; + list port { + key name; + leaf name { type string; } + } + list lag { + key name; + leaf name { type string; } + } + typedef port-ref { + type leafref { path "/t:port/t:name"; } + } + typedef relative-ref { + type leafref { path "../name"; } + } + typedef endpoint-ref { + type union { + type port-ref; + type leafref { path "/t:lag/t:name"; } + } + } +} +EOF1 + +cat < $dir/example.yang +module example { + yang-version 1.1; + namespace "urn:example:clixon"; + prefix ex; + // The defining module's prefix t is deliberately unavailable here. + import example-targets { prefix target; } + leaf primary { type target:port-ref; } + list endpoint { + key name; + leaf name { type target:endpoint-ref; } + leaf cost { type uint32; } + leaf alias { type target:relative-ref; } + } +} +EOF1 + +AUTOCLI=$(autocli_config 'example*' kw-nokey false | sed 's||true|') +cat < $cfg + + $cfg + ${YANG_INSTALLDIR} + $dir + example + $dir + $dir/backend.sock + $dir/backend.pid + $dir + false + ${AUTOCLI} + +EOF1 + +cat < $dir/example.cli +CLICON_MODE="example"; +set @datamodel, cli_auto_set(); +validate, cli_validate(); +show, cli_show_auto_mode("candidate", "xml", false, false); +quit, cli_quit(); +EOF1 + +new "test params: -f $cfg" +if [ $BE -ne 0 ]; then + new "start backend" + start_backend -s init -f $cfg +fi +new "wait backend" +wait_backend + +new "populate both leafref targets" +expectpart "$($clixon_cli -1 -f $cfg set port p1 2>&1)" 0 '^$' +expectpart "$($clixon_cli -1 -f $cfg set lag lag1 2>&1)" 0 '^$' + +new "completion of imported union leafref typedef" +expectpart "$(printf 'set endpoint ?\025quit\n' | $clixon_cli -f $cfg 2>&1)" 0 'p1' 'lag1' --not-- 'Netconf error' 'Syntax error' + +new "completion of imported ordinary leafref typedef" +expectpart "$(printf 'set primary ?\025quit\n' | $clixon_cli -f $cfg 2>&1)" 0 'p1' --not-- 'Netconf error' 'Syntax error' + +for completion in false true; do + new "union keys retain child commands with completion=$completion" + sed -i "s|.*|$completion|" $cfg + expectpart "$($clixon_cli -1 -f $cfg set endpoint p1 cost 10 2>&1)" 0 '^$' + expectpart "$($clixon_cli -1 -f $cfg set endpoint lag1 cost 20 2>&1)" 0 '^$' + expectpart "$($clixon_cli -1 -f $cfg show 2>&1)" 0 'p110' 'lag120' +done + +new "unprefixed typedef paths use the consuming node namespace" +expectpart "$(printf 'set endpoint p1 alias ?\025quit\n' | $clixon_cli -f $cfg 2>&1)" 0 ' p1' --not-- 'Netconf error' 'Syntax error' + +new "union leafrefs validate against existing targets" +expectpart "$($clixon_cli -1 -f $cfg validate 2>&1)" 0 '^$' +expectpart "$($clixon_cli -1 -f $cfg set endpoint absent cost 30 2>&1)" 0 '^$' +expectpart "$($clixon_cli -1 -f $cfg validate 2>&1)" 255 'instance-required' + +if [ $BE -ne 0 ]; then + new "kill backend" + stop_backend -f $cfg +fi +rm -rf $dir +new "endtest" +endtest