yanglint FEATURE extdata cmd for interactive mode

Implementation of --ext-data parameter for interactive yanglint mode.
diff --git a/tools/lint/CMakeLists.txt b/tools/lint/CMakeLists.txt
index 20edd97..0fd2fad 100644
--- a/tools/lint/CMakeLists.txt
+++ b/tools/lint/CMakeLists.txt
@@ -17,6 +17,7 @@
     cmd_load.c
     cmd_print.c
     cmd_searchpath.c
+    cmd_extdata.c
     common.c
 )
 if(YANGLINT_INTERACTIVE)
diff --git a/tools/lint/cmd.c b/tools/lint/cmd.c
index 93994c7..885b689 100644
--- a/tools/lint/cmd.c
+++ b/tools/lint/cmd.c
@@ -90,6 +90,18 @@
 #endif
 
 void
+cmd_free(void)
+{
+    uint16_t i;
+
+    for (i = 0; commands[i].name; i++) {
+        if (commands[i].free_func) {
+            commands[i].free_func();
+        }
+    }
+}
+
+void
 cmd_verb_help(void)
 {
     printf("Usage: verb (error | warning | verbose | debug)\n");
@@ -239,22 +251,23 @@
 
 /* Also keep enum COMMAND_INDEX updated. */
 COMMAND commands[] = {
-    {"help", cmd_help, cmd_help_help, "Display commands description", "h"},
-    {"add", cmd_add, cmd_add_help, "Add a new module from a specific file", "DF:hi"},
-    {"load", cmd_load, cmd_load_help, "Load a new schema from the searchdirs", "F:hi"},
-    {"print", cmd_print, cmd_print_help, "Print a module", "f:hL:o:P:q"},
-    {"data", cmd_data, cmd_data_help, "Load, validate and optionally print instance data", "d:ef:F:hmo:O:R:r:nt:x:"},
-    {"list", cmd_list, cmd_list_help, "List all the loaded modules", "f:h"},
-    {"feature", cmd_feature, cmd_feature_help, "Print all features of module(s) with their state", "haf"},
-    {"searchpath", cmd_searchpath, cmd_searchpath_help, "Print/set the search path(s) for schemas", "ch"},
-    {"clear", cmd_clear, cmd_clear_help, "Clear the context - remove all the loaded modules", "iyh"},
-    {"verb", cmd_verb, cmd_verb_help, "Change verbosity", "h"},
+    {"help", cmd_help, cmd_help_help, NULL, "Display commands description", "h"},
+    {"add", cmd_add, cmd_add_help, NULL, "Add a new module from a specific file", "DF:hi"},
+    {"load", cmd_load, cmd_load_help, NULL, "Load a new schema from the searchdirs", "F:hi"},
+    {"print", cmd_print, cmd_print_help, NULL, "Print a module", "f:hL:o:P:q"},
+    {"data", cmd_data, cmd_data_help, NULL, "Load, validate and optionally print instance data", "d:ef:F:hmo:O:R:r:nt:x:"},
+    {"list", cmd_list, cmd_list_help, NULL, "List all the loaded modules", "f:h"},
+    {"feature", cmd_feature, cmd_feature_help, NULL, "Print all features of module(s) with their state", "haf"},
+    {"searchpath", cmd_searchpath, cmd_searchpath_help, NULL, "Print/set the search path(s) for schemas", "ch"},
+    {"extdata", cmd_extdata, cmd_extdata_help, cmd_extdata_free, "Set the specific data required by an extension", "ch"},
+    {"clear", cmd_clear, cmd_clear_help, NULL, "Clear the context - remove all the loaded modules", "iyh"},
+    {"verb", cmd_verb, cmd_verb_help, NULL, "Change verbosity", "h"},
 #ifndef NDEBUG
-    {"debug", cmd_debug, cmd_debug_help, "Display specific debug message groups", "h"},
+    {"debug", cmd_debug, cmd_debug_help, NULL, "Display specific debug message groups", "h"},
 #endif
-    {"quit", cmd_quit, NULL, "Quit the program", "h"},
+    {"quit", cmd_quit, NULL, NULL, "Quit the program", "h"},
     /* synonyms for previous commands */
-    {"?", cmd_help, NULL, "Display commands description", "h"},
-    {"exit", cmd_quit, NULL, "Quit the program", "h"},
-    {NULL, NULL, NULL, NULL, NULL}
+    {"?", cmd_help, NULL, NULL, "Display commands description", "h"},
+    {"exit", cmd_quit, NULL, NULL, "Quit the program", "h"},
+    {NULL, NULL, NULL, NULL, NULL, NULL}
 };
diff --git a/tools/lint/cmd.h b/tools/lint/cmd.h
index 071348f..5898cfe 100644
--- a/tools/lint/cmd.h
+++ b/tools/lint/cmd.h
@@ -26,6 +26,7 @@
 
     void (*func)(struct ly_ctx **ctx, const char *); /* Function to call to do the command. */
     void (*help_func)(void);                         /* Display command help. */
+    void (*free_func)(void);                         /* Freeing global variables allocated by the command. */
     char *helpstring;                                /* Documentation for this function. */
     char *optstring;                                 /* Option characters used in function getopt_long. */
 } COMMAND;
@@ -47,6 +48,7 @@
     CMD_LIST,
     CMD_FEATURE,
     CMD_SEARCHPATH,
+    CMD_EXTDATA,
     CMD_CLEAR,
     CMD_VERB,
 #ifndef NDEBUG
@@ -54,6 +56,11 @@
 #endif
 };
 
+/**
+ * @brief For each cmd, call the COMMAND.free_func in the variable 'commands'.
+ */
+void cmd_free(void);
+
 /* cmd_add.c */
 void cmd_add(struct ly_ctx **ctx, const char *cmdline);
 void cmd_add_help(void);
@@ -86,4 +93,9 @@
 void cmd_searchpath(struct ly_ctx **ctx, const char *cmdline);
 void cmd_searchpath_help(void);
 
+/* cmd_extdata.c */
+void cmd_extdata(struct ly_ctx **ctx, const char *cmdline);
+void cmd_extdata_help(void);
+void cmd_extdata_free(void);
+
 #endif /* COMMANDS_H_ */
diff --git a/tools/lint/cmd_clear.c b/tools/lint/cmd_clear.c
index cbc5e10..44b5307 100644
--- a/tools/lint/cmd_clear.c
+++ b/tools/lint/cmd_clear.c
@@ -91,6 +91,9 @@
         goto cleanup;
     }
 
+    /* Global variables in commands are also deleted. */
+    cmd_free();
+
     ly_ctx_destroy(*ctx);
     *ctx = ctx_new;
 
diff --git a/tools/lint/cmd_extdata.c b/tools/lint/cmd_extdata.c
new file mode 100644
index 0000000..a3b1cfd
--- /dev/null
+++ b/tools/lint/cmd_extdata.c
@@ -0,0 +1,101 @@
+/**
+ * @file cmd_extdata.c
+ * @author Adam Piecek <piecek@cesnet.cz>
+ * @brief 'extdata' command of the libyang's yanglint tool.
+ *
+ * Copyright (c) 2015-2023 CESNET, z.s.p.o.
+ *
+ * This source code is licensed under BSD 3-Clause License (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://opensource.org/licenses/BSD-3-Clause
+ */
+
+#define _GNU_SOURCE
+#define _POSIX_C_SOURCE 200809L /* strdup */
+
+#include "cmd.h"
+
+#include <getopt.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "libyang.h"
+
+#include "common.h"
+
+char *filename;
+
+void
+cmd_extdata_free(void)
+{
+    free(filename);
+    filename = NULL;
+}
+
+void
+cmd_extdata_help(void)
+{
+    printf("Usage: extdata [--clear] [<extdata-file-path>]\n"
+            "                 File containing the specific data required by an extension. Required by\n"
+            "                 the schema-mount extension, for example, when the operational data are\n"
+            "                 expected in the file. File format is guessed.\n");
+}
+
+void
+cmd_extdata(struct ly_ctx **ctx, const char *cmdline)
+{
+    int argc = 0;
+    char **argv = NULL;
+    int opt, opt_index;
+    struct option options[] = {
+        {"clear", no_argument, NULL, 'c'},
+        {"help", no_argument, NULL, 'h'},
+        {NULL, 0, NULL, 0}
+    };
+    int8_t cleared = 0;
+    int8_t file_count = 0;
+
+    if (parse_cmdline(cmdline, &argc, &argv)) {
+        goto cleanup;
+    }
+
+    while ((opt = getopt_long(argc, argv, commands[CMD_EXTDATA].optstring, options, &opt_index)) != -1) {
+        switch (opt) {
+        case 'c':
+            ly_ctx_set_ext_data_clb(*ctx, NULL, NULL);
+            free(filename);
+            filename = NULL;
+            cleared = 1;
+            break;
+        case 'h':
+            cmd_extdata_help();
+            goto cleanup;
+        default:
+            YLMSG_E("Unknown option.\n");
+            goto cleanup;
+        }
+    }
+
+    file_count = argc - 1 - cleared;
+
+    if (!cleared && (file_count == 0)) {
+        /* no argument - print the current file */
+        printf("%s\n", filename ? filename : "No file set.");
+    } else if (file_count == 1) {
+        /* set callback providing run-time extension instance data */
+        free(filename);
+        filename = strdup(argv[optind]);
+        if (!filename) {
+            YLMSG_E("Memory allocation error.\n");
+            goto cleanup;
+        }
+        ly_ctx_set_ext_data_clb(*ctx, ext_data_clb, filename);
+    } else if (!cleared) {
+        YLMSG_E("Only one file must be entered.\n");
+    }
+
+cleanup:
+    free_cmdline(argv);
+}
diff --git a/tools/lint/common.c b/tools/lint/common.c
index 1ba4a1d..f2ff464 100644
--- a/tools/lint/common.c
+++ b/tools/lint/common.c
@@ -27,6 +27,7 @@
 
 #include "compat.h"
 #include "libyang.h"
+#include "plugins_exts.h"
 
 int
 parse_schema_path(const char *path, char **dir, char **module)
@@ -937,3 +938,19 @@
     free(module_name);
     return parent_node;
 }
+
+LY_ERR
+ext_data_clb(const struct lysc_ext_instance *ext, void *user_data, void **ext_data, ly_bool *ext_data_free)
+{
+    struct ly_ctx *ctx;
+    struct lyd_node *data = NULL;
+
+    ctx = ext->module->ctx;
+    if (user_data) {
+        lyd_parse_data_path(ctx, user_data, LYD_XML, LYD_PARSE_STRICT, LYD_VALIDATE_PRESENT, &data);
+    }
+
+    *ext_data = data;
+    *ext_data_free = 1;
+    return LY_SUCCESS;
+}
diff --git a/tools/lint/common.h b/tools/lint/common.h
index 6f117bc..7e98691 100644
--- a/tools/lint/common.h
+++ b/tools/lint/common.h
@@ -276,4 +276,15 @@
  */
 const struct lysc_node *find_schema_path(const struct ly_ctx *ctx, const char *schema_path);
 
+/**
+ * @brief General callback providing run-time extension instance data.
+ *
+ * @param[in] ext Compiled extension instance.
+ * @param[in] user_data User-supplied callback data.
+ * @param[out] ext_data Provided extension instance data.
+ * @param[out] ext_data_free Whether the extension instance should free @p ext_data or not.
+ * @return LY_ERR value.
+ */
+LY_ERR ext_data_clb(const struct lysc_ext_instance *ext, void *user_data, void **ext_data, ly_bool *ext_data_free);
+
 #endif /* COMMON_H_ */
diff --git a/tools/lint/completion.c b/tools/lint/completion.c
index 2eab01c..db2704d 100644
--- a/tools/lint/completion.c
+++ b/tools/lint/completion.c
@@ -413,6 +413,7 @@
         {CMD_PRINT,       "-o",    linenoisePathCompletion, NULL},
         {CMD_PRINT,       NULL,    NULL, get_model_completion},
         {CMD_SEARCHPATH,  NULL,    linenoisePathCompletion, NULL},
+        {CMD_EXTDATA,     NULL,    linenoisePathCompletion, NULL},
         {CMD_DATA,        "-t",    NULL, get_data_type_arg},
         {CMD_DATA,        "-O",    linenoisePathCompletion, NULL},
         {CMD_DATA,        "-R",    linenoisePathCompletion, NULL},
diff --git a/tools/lint/main.c b/tools/lint/main.c
index 9f0d027..b3d985a 100644
--- a/tools/lint/main.c
+++ b/tools/lint/main.c
@@ -95,6 +95,9 @@
         free(cmdline);
     }
 
+    /* Global variables in commands are freed. */
+    cmd_free();
+
     store_config();
     ly_ctx_destroy(ctx);
 
diff --git a/tools/lint/main_ni.c b/tools/lint/main_ni.c
index 184ab57..2c0ae12 100644
--- a/tools/lint/main_ni.c
+++ b/tools/lint/main_ni.c
@@ -25,7 +25,6 @@
 #include <sys/stat.h>
 
 #include "libyang.h"
-#include "plugins_exts.h"
 
 #include "common.h"
 #include "out.h"
@@ -363,22 +362,6 @@
 }
 
 static LY_ERR
-ext_data_clb(const struct lysc_ext_instance *ext, void *user_data, void **ext_data, ly_bool *ext_data_free)
-{
-    struct ly_ctx *ctx;
-    struct lyd_node *data = NULL;
-
-    ctx = ext->module->ctx;
-    if (user_data) {
-        lyd_parse_data_path(ctx, user_data, LYD_XML, LYD_PARSE_STRICT, LYD_VALIDATE_PRESENT, &data);
-    }
-
-    *ext_data = data;
-    *ext_data_free = 1;
-    return LY_SUCCESS;
-}
-
-static LY_ERR
 searchpath_strcat(char **searchpaths, const char *path)
 {
     uint64_t len;
diff --git a/tools/lint/tests/interactive/extdata.test b/tools/lint/tests/interactive/extdata.test
new file mode 100644
index 0000000..bef06a7
--- /dev/null
+++ b/tools/lint/tests/interactive/extdata.test
@@ -0,0 +1,45 @@
+source [expr {[info exists ::env(TESTS_DIR)] ? "$env(TESTS_DIR)/interactive/ly.tcl" : "ly.tcl"}]
+
+set mdir "$::env(YANG_MODULES_DIR)"
+set ddir "$::env(TESTS_DIR)/data"
+
+test extdata_set_clear {Set and clear extdata file} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "extdata" "No file set"
+    ly_cmd "extdata $ddir/modsm_ctx_ext.xml"
+    ly_cmd "extdata" "$ddir/modsm_ctx_ext.xml"
+    ly_cmd "extdata -c"
+    ly_cmd "extdata" "No file set"
+}}
+
+test extdata_clear_cmd {Clear extdata file by 'clear' command} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "extdata $ddir/modsm_ctx_ext.xml"
+    ly_cmd "clear"
+    ly_cmd "extdata" "No file set"
+}}
+
+test extdata_one_only {Only one file for extdata} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd_err "extdata $ddir/modsm_ctx_ext.xml $ddir/modsm_ctx_ext.xml" "Only one file must be entered"
+}}
+
+test extdata_schema_mount_tree {Print tree output of a model with Schema Mount} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "clear -y"
+    ly_cmd "searchpath $mdir"
+    ly_cmd "load modsm"
+    ly_cmd "extdata $ddir/modsm_ctx_ext.xml"
+    ly_cmd "print -f tree modsm" "--mp root.*--rw lfl/"
+}}
+
+test ext_data_schema_mount_xml {Validating and printing mounted data} {
+-setup $ly_setup -cleanup $ly_cleanup -body {
+    ly_cmd "clear -y"
+    ly_cmd "searchpath $mdir"
+    ly_cmd "load modsm"
+    ly_cmd "extdata $ddir/modsm_ctx_ext.xml"
+    ly_cmd "data -f xml -t config $ddir/modsm.xml" "</lfl>"
+}}
+
+cleanupTests