diff --git a/tools/lint/CMakeLists.txt b/tools/lint/CMakeLists.txt index 42d43093b..14d8446c1 100644 --- a/tools/lint/CMakeLists.txt +++ b/tools/lint/CMakeLists.txt @@ -14,6 +14,8 @@ set(lintsrc cmd_help.c cmd_verb.c cmd_debug.c + cmd_sample.c + cmd_ietf.c yl_opt.c yl_schema_features.c common.c diff --git a/tools/lint/cmd.c b/tools/lint/cmd.c index 1f1e791f5..ce2bb5a19 100644 --- a/tools/lint/cmd.c +++ b/tools/lint/cmd.c @@ -72,6 +72,10 @@ COMMAND commands[] = { "data", cmd_data_opt, cmd_data_dep, cmd_data_store, cmd_data_process, cmd_data_help, NULL, "Load, validate and optionally print instance data", "d:ef:F:hmo:O:R:r:nt:x:k:" }, + { + "sample", cmd_sample_opt, cmd_sample_dep, cmd_sample_exec, NULL, cmd_sample_help, NULL, + "Generate a sample data skeleton for a module", "f:ho:" + }, { "list", cmd_list_opt, cmd_list_dep, cmd_list_exec, NULL, cmd_list_help, NULL, "List all the loaded modules", "f:h" @@ -104,6 +108,10 @@ COMMAND commands[] = { "Unsupported for the Release build", "h" #endif }, + { + "ietf", cmd_ietf_opt, cmd_ietf_dep, cmd_ietf_exec, NULL, cmd_ietf_help, NULL, + "Validate a loaded schema module according to IETF rules", "ho:" + }, {"quit", NULL, NULL, cmd_quit_exec, NULL, NULL, NULL, "Quit the program", "h"}, /* synonyms for previous commands */ {"?", NULL, NULL, cmd_help_exec, NULL, NULL, NULL, "Display commands description", "h"}, diff --git a/tools/lint/cmd.h b/tools/lint/cmd.h index e9ee5b492..af79e25c5 100644 --- a/tools/lint/cmd.h +++ b/tools/lint/cmd.h @@ -276,6 +276,27 @@ int cmd_print_dep(struct yl_opt *yo, int posc); int cmd_print_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv); void cmd_print_help(void); +/* cmd_sample.c */ + +/** + * @copydoc cmd_add_opt + */ +int cmd_sample_opt(struct yl_opt *yo, const char *cmdline, char ***posv, int *posc); +/** + * @copydoc cmd_add_dep + */ +int cmd_sample_dep(struct yl_opt *yo, int posc); +void cmd_sample_help(void); +/** + * @brief Print a sample skeleton of module in json or xml. + * + * @param[in,out] ctx context for libyang. + * @param[in] yo context for yanglint. + * @param[in] posv Name of the module to be printed. + * @return 0 on success, 1 on failure. + */ +int cmd_sample_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv); + /* cmd_searchpath.c */ /** @@ -391,4 +412,26 @@ int cmd_debug_store(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv); int cmd_debug_setlog(struct ly_ctx *ctx, struct yl_opt *yo); void cmd_debug_help(void); +/* cmd_ietf.c */ +/** + * @copydoc cmd_add_opt + */ +int cmd_ietf_opt(struct yl_opt *yo, const char *cmdline, char ***posv, int *posc); +/** + * @copydoc cmd_add_dep + */ +int cmd_ietf_dep(struct yl_opt *yo, int posc); +void cmd_ietf_help(void); + +/** + * @brief Execution handler for the 'ietf' command in the yanglint CLI tool. + * + * @param[in,out] ctx A double pointer to the libyang context containing the loaded modules. + * @param[in,out] yo A pointer to the yanglint options structure, which manages global + * @param[in] posv The positional command-line argument provided by the user, representing the target module name (e.g., "ietf-interfaces" or "ietf-interfaces@2014-05-08"). + * @return 0 on successful execution of the validation pipeline (note: this returns 0 even if IETF compliance warnings/errors were printed to the output). + * @return 1 on fatal execution errors (e.g., module not found, module failed to compile, or memory allocation failure). + */ +int cmd_ietf_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv); + #endif /* COMMANDS_H_ */ diff --git a/tools/lint/cmd_add.c b/tools/lint/cmd_add.c index 9f107110a..13ad572b9 100644 --- a/tools/lint/cmd_add.c +++ b/tools/lint/cmd_add.c @@ -130,7 +130,7 @@ store_parsed_module(const char *filepath, struct lys_module *mod, struct yl_opt { assert(!yo->interactive); - if (yo->schema_out_format || yo->feature_param_format) { + if (yo->schema_out_format || yo->feature_param_format || yo->data_out_format || yo->ietf_validation) { if (ly_set_add(&yo->schema_modules, (void *)mod, 1, NULL)) { YLMSG_E("Storing parsed schema module (%s) for print failed.", filepath); return 1; diff --git a/tools/lint/cmd_ietf.c b/tools/lint/cmd_ietf.c new file mode 100644 index 000000000..8096b5d7d --- /dev/null +++ b/tools/lint/cmd_ietf.c @@ -0,0 +1,711 @@ +/** + * @file cmd_ietf.c + * @author Petr Hanzlik + * @brief 'ietf' command of the libyang's yanglint tool. + * + * Copyright (c) 2026 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 + +#include "cmd.h" + +#include +#include +#include +#include +#include +#include + +#include "libyang.h" + +#include "common.h" +#include "yl_opt.h" + +#define IETF_WARN(filepath, rfc, section, out, ...) \ + do { \ + ly_print(out, "%s: warning: RFC %s: %s: ", filepath, rfc, section); \ + ly_print(out, __VA_ARGS__); \ + ly_print(out, "\n"); \ + } while(0) + +#define IETF_ERR(filepath, rfc, section, out, ...) \ + do { \ + ly_print(out, "%s: error: RFC %s: %s: ", filepath, rfc, section); \ + ly_print(out, __VA_ARGS__); \ + ly_print(out, "\n"); \ + } while(0) + +void +cmd_ietf_help(void) +{ + printf("Usage: ietf [-o OUTFILE] [@revision]\n" + " Validate a loaded schema module according to IETF rules (RFC 8407).\n\n" + " -o OUTFILE, --output=OUTFILE\n" + " Write the output to OUTFILE instead of stdout.\n" + " -h, --help\n" + " Display this help message.\n\n" + "--- IETF USAGE GUIDELINES (RFC 8407) ---\n\n" + "The module's or submodule's description statement must contain the\n" + "following text:\n\n" + " Copyright (c) IETF Trust and the persons identified as\n" + " authors of the code. All rights reserved.\n\n" + " Redistribution and use in source and binary forms, with or\n" + " without modification, is permitted pursuant to, and subject to\n" + " the license terms contained in, the Revised BSD License set\n" + " forth in Section 4.c of the IETF Trust's Legal Provisions\n" + " Relating to IETF Documents\n" + " (https://trustee.ietf.org/license-info).\n\n" + "An IETF module (but not an IANA module) must also contain the\n" + "following text:\n\n" + " This version of this YANG module is part of RFC XXXX\n" + " (https://www.rfc-editor.org/info/rfcXXXX); see the RFC itself\n" + " for full legal notices.\n\n" + "If any description statement in the module or submodule contains\n" + "RFC 2119 key words, the module's or submodule's description statement\n" + "must contain the following text:\n\n" + " The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL\n" + " NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED',\n" + " 'MAY', and 'OPTIONAL' in this document are to be interpreted as\n" + " described in BCP 14 (RFC 2119) (RFC 8174) when, and only when,\n" + " they appear in all capitals, as shown here.\n"); +} + +int +cmd_ietf_dep(struct yl_opt *yo, int posc) +{ + if (yo->interactive && !posc) { + YLMSG_E("Missing module name to validate.\n"); + return 1; + } + + return 0; +} + +int +cmd_ietf_opt(struct yl_opt *yo, const char *cmdline, char ***posv, int *posc) +{ + int rc = 0, argc = 0; + int opt, opt_index; + char **argv = NULL; + + struct option options[] = { + {"output", required_argument, NULL, 'o'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + if ((rc = parse_cmdline(cmdline, &argc, &argv))) { + return rc; + } + + optind = 0; + while ((opt = getopt_long(argc, argv, "ho:", options, &opt_index)) != -1) { + switch (opt) { + case 'o': /* --output */ + if (yo->out) { + ly_out_free(yo->out, NULL, 0); + yo->out = NULL; + } + if (ly_out_new_filepath(optarg, &yo->out)) { + YLMSG_E("Unable to open output file %s.\n", optarg); + rc = 1; + goto cleanup; + } + break; + + case 'h': /* --help */ + cmd_ietf_help(); + rc = 1; + goto cleanup; + + default: + YLMSG_E("Unknown option.\n"); + rc = 1; + goto cleanup; + } + } + + *posc = argc - optind; + *posv = &argv[optind]; + + return 0; + +cleanup: + free_cmdline(argv); + return rc; +} + +/** + * @brief Checks if a given text matches a specified regular expression pattern. + * + * @param[in] text The target string to search within. If NULL, the function returns 0. + * @param[in] pattern The POSIX extended regular expression pattern to match against. + * @return 1 if the pattern is successfully found within the text. + * @return 0 if the pattern is not found, if a compilation error occurs, or if invalid NULL arguments are passed. + */ +static int +check_regex(const char *text, const char *pattern) +{ + regex_t regex; + int ret; + + if ((text == NULL) || (pattern == NULL)) { + return 0; + } + + if (regcomp(®ex, pattern, REG_EXTENDED | REG_ICASE | REG_NOSUB) != 0) { + return 0; + } + + ret = regexec(®ex, text, 0, NULL, 0); + regfree(®ex); + + return ret == 0; +} + +/** + * @brief Scans a description string for the presence of strict RFC 2119/8174 requirement keywords. + * + * @param[in] dsc The description text to evaluate. + * @return 1 if at least one RFC 2119 keyword is successfully found in the text. + * @return 0 if no keywords are found, if a compilation error occurs, or if NULL is passed. + */ +static int +check_words(const char *dsc) +{ + regex_t regex; + int ret; + + // The regex pattern checks for exact uppercase words surrounded by word boundaries (spaces, punctuation, or string ends) + const char *pattern = "(^|[^a-zA-Z])(MUST|MUST NOT|REQUIRED|SHALL|SHALL NOT|SHOULD|SHOULD NOT|RECOMMENDED|NOT RECOMMENDED|MAY|OPTIONAL)([^a-zA-Z]|$)"; + + if (dsc == NULL) { + return 0; + } + + if (regcomp(®ex, pattern, REG_EXTENDED | REG_NOSUB) != 0) { + return 0; + } + + ret = regexec(®ex, dsc, 0, NULL, 0); + + regfree(®ex); + return ret == 0; +} + +/** + * @brief Traverses the compiled schema tree to enforce IETF-specific validation rules. + * + * @param[in,out] out The libyang output handler where warnings and errors are printed. + * @param[in] data The first top-level node of the compiled data tree to be inspected. + * @param[in] file_name The name of the file being evaluated (used to prefix the error messages). + */ +static void +check_nodes_ietf(struct ly_out *out, const struct lysc_node *data, const char *file_name) +{ + const struct lysc_node *root, *elem; + char node_path[256]; + + LY_LIST_FOR(data, root) { + + int is_mandatory = 0; + + if (root->flags & LYS_MAND_TRUE) { + is_mandatory = 1; + } else if ((root->nodetype == LYS_LIST) && (((struct lysc_node_list *)root)->min > 0)) { + is_mandatory = 1; + } else if ((root->nodetype == LYS_LEAFLIST) && (((struct lysc_node_leaflist *)root)->min > 0)) { + is_mandatory = 1; + } + + if (is_mandatory) { + IETF_WARN(file_name, "8407", "4.10", out, "top-level node \"%s\" must not be mandatory", root->name); + } + + LYSC_TREE_DFS_BEGIN(root, elem) { + lysc_path(elem, LYSC_PATH_LOG, node_path, sizeof(node_path)); + + switch (elem->nodetype) { + case LYS_CONTAINER: + case LYS_LIST: + case LYS_LEAF: + case LYS_ANYXML: + case LYS_LEAFLIST: + case LYS_CHOICE: + case LYS_RPC: + case LYS_NOTIF: + case LYS_ACTION: + if (elem->dsc == NULL) { + IETF_ERR(file_name, "8407", "4.14", out, "statement \"%s\" must have a \"description\" substatement", lys_nodetype2str(elem->nodetype)); + } + break; + default: + break; + } + + LYSC_TREE_DFS_END(root, elem); + } + } +} + +/** + * @brief Helper macro to validate locally scoped typedefs and groupings inside parsed data nodes. + * + * @param node_ptr Pointer to the parsed node structure containing the `typedefs` and `groupings` arrays. + * @param node_type_str A string literal representing the node type (e.g., "container", "list") for the error message. + * @param node_name The string name of the parent node to provide context in the error message. + * @param found_2119 A pointer to an integer flag that tracks if RFC 2119 keywords are used in the module/submodule. + */ +#define CHECK_INNER_DEFS(node_ptr, node_type_str, node_name, found_2119) \ + do { \ + LY_ARRAY_COUNT_TYPE _i; \ + struct lysp_node_grp *_grp; \ + LY_ARRAY_FOR((node_ptr)->typedefs, _i) { \ + if ((node_ptr)->typedefs[_i].dsc == NULL) { \ + IETF_ERR(file_name, "8407", "4.13, 4.14", out, "statement \"typedef\" inside %s \"%s\" must have a \"description\" substatement", node_type_str, node_name);\ + } else if (check_words((node_ptr)->typedefs[_i].dsc)) {\ + *found_2119 = 1; \ + }\ + } \ + LY_LIST_FOR((node_ptr)->groupings, _grp) { \ + if (_grp->dsc == NULL) { \ + IETF_ERR(file_name, "8407", "4.14", out, "statement \"grouping\" inside %s \"%s\" must have a \"description\" substatement", node_type_str, node_name);\ + } else if (check_words(_grp->dsc)) {\ + *found_2119 = 1; \ + }\ + } \ + } while(0) + +/** + * @brief Recursively traverses and validates the parsed schema tree for IETF compliance. + * + * @param[in,out] out The libyang output handler where warnings and errors are printed. + * @param[in] node The starting node of the parsed data tree (or sibling list) to inspect. + * @param[in] file_name The name of the file being evaluated, used to prefix the log messages. + * @param[in,out] found_2119 Pointer to an integer flag used to record whether any node in this tree uses RFC 2119 keywords. + */ +static void +check_parsed_tree_ietf(struct ly_out *out, const struct lysp_node *node, const char *file_name, int *found_2119) +{ + const struct lysp_node *elem; + LY_ARRAY_COUNT_TYPE i; + + LY_LIST_FOR(node, elem) { + if (elem->dsc && check_words(elem->dsc)) { + *found_2119 = 1; + } + + if (elem->flags & LYS_CONFIG_W) { + IETF_WARN(file_name, "8407", "4.4", out, "statement \"config\" is given with its default value \"true\""); + } + + if (elem->flags & LYS_STATUS_CURR) { + IETF_WARN(file_name, "8407", "4.4", out, "statement \"status\" is given with its default value \"current\""); + } + + if (elem->flags & LYS_MAND_FALSE) { + IETF_WARN(file_name, "8407", "4.4", out, "statement \"mandatory\" is given with its default value \"false\""); + } + + if (elem->flags & LYS_YINELEM_FALSE) { + IETF_WARN(file_name, "8407", "4.4", out, "statement \"yin-elements\" is given with its default value \"false\""); + } + + switch (elem->nodetype) { + case LYS_CONTAINER: { + struct lysp_node_container *cont = (struct lysp_node_container *)elem; + + CHECK_INNER_DEFS(cont, "container", elem->name, found_2119); + if (cont->child) { + check_parsed_tree_ietf(out, cont->child, file_name, found_2119); + } + break; + } + case LYS_LIST: { + struct lysp_node_list *list = (struct lysp_node_list *)elem; + + CHECK_INNER_DEFS(list, "list", elem->name, found_2119); + + if ((elem->flags & LYS_SET_MIN) && (list->min == 0)) { + IETF_WARN(file_name, "8407", "4.4", out, "statement \"min-elements\" is given with its default value \"0\""); + } + + if ((elem->flags & LYS_SET_MAX) && (list->max == 0)) { + IETF_WARN(file_name, "8407", "4.4", out, "statement \"max-elements\" is given with its default value \"unbounded\""); + } + + if (list->child) { + check_parsed_tree_ietf(out, list->child, file_name, found_2119); + } + break; + } + case LYS_LEAFLIST: { + struct lysp_node_leaflist *llist = (struct lysp_node_leaflist *)elem; + + if ((elem->flags & LYS_SET_MIN) && (llist->min == 0)) { + IETF_WARN(file_name, "8407", "4.4", out, "statement \"min-elements\" is given with its default value \"0\""); + } + + if ((elem->flags & LYS_SET_MAX) && (llist->max == 0)) { + IETF_WARN(file_name, "8407", "4.4", out, "statement \"max-elements\" is given with its default value \"unbounded\""); + } + + if (llist->type.enums) { + LY_ARRAY_FOR(llist->type.enums, i) { + if (llist->type.enums[i].dsc == NULL) { + IETF_WARN(file_name, "8407", "4.11.3", out, "statement \"enum\" should have a \"description\" substatement"); + } + } + } + if (llist->type.bits) { + LY_ARRAY_FOR(llist->type.bits, i) { + if (llist->type.bits[i].dsc == NULL) { + IETF_WARN(file_name, "8407", "4.11.3", out, "statement \"bit\" should have a \"description\" substatement"); + } + } + } + break; + } + case LYS_GROUPING: { + struct lysp_node_grp *grp = (struct lysp_node_grp *)elem; + + CHECK_INNER_DEFS(grp, "grouping", elem->name, found_2119); + if (grp->child) { + check_parsed_tree_ietf(out, grp->child, file_name, found_2119); + } + break; + } + case LYS_RPC: + case LYS_ACTION: { + struct lysp_node_action *act = (struct lysp_node_action *)elem; + const char *type_str = (elem->nodetype == LYS_RPC) ? "rpc" : "action"; + + CHECK_INNER_DEFS(act, type_str, elem->name, found_2119); + + if (act->input.typedefs || act->input.groupings) { + CHECK_INNER_DEFS(&act->input, "input of", elem->name, found_2119); + } + if (act->input.child) { + check_parsed_tree_ietf(out, act->input.child, file_name, found_2119); + } + + if (act->output.typedefs || act->output.groupings) { + CHECK_INNER_DEFS(&act->output, "output of", elem->name, found_2119); + } + if (act->output.child) { + check_parsed_tree_ietf(out, act->output.child, file_name, found_2119); + } + break; + } + case LYS_NOTIF: { + struct lysp_node_notif *notif = (struct lysp_node_notif *)elem; + + CHECK_INNER_DEFS(notif, "notification", elem->name, found_2119); + if (notif->child) { + check_parsed_tree_ietf(out, notif->child, file_name, found_2119); + } + break; + } + case LYS_CHOICE: { + struct lysp_node_choice *choice = (struct lysp_node_choice *)elem; + + if (choice->child) { + check_parsed_tree_ietf(out, choice->child, file_name, found_2119); + } + break; + } + case LYS_CASE: { + struct lysp_node_case *cas = (struct lysp_node_case *)elem; + + if (cas->child) { + check_parsed_tree_ietf(out, cas->child, file_name, found_2119); + } + break; + } + case LYS_LEAF: { + struct lysp_node_leaf *leaf = (struct lysp_node_leaf *)elem; + + if (leaf->type.enums) { + LY_ARRAY_FOR(leaf->type.enums, i) { + if (leaf->type.enums[i].dsc == NULL) { + IETF_WARN(file_name, "8407", "4.11.3", out, "statement \"enum\" should have a \"description\" substatement"); + } + } + } + if (leaf->type.bits) { + LY_ARRAY_FOR(leaf->type.bits, i) { + if (leaf->type.bits[i].dsc == NULL) { + IETF_WARN(file_name, "8407", "4.11.3", out, "statement \"bit\" should have a \"description\" substatement"); + } + } + } + break; + } + case LYS_USES: { + struct lysp_node_uses *uses = (struct lysp_node_uses *)elem; + struct lysp_node_augment *aug; + + LY_LIST_FOR(uses->augments, aug) { + if (aug->dsc == NULL) { + IETF_ERR(file_name, "8407", "4.14", out, "statement \"augment\" inside uses \"%s\" must have a \"description\" substatement", uses->name); + } + + if (aug->child) { + check_parsed_tree_ietf(out, aug->child, file_name, found_2119); + } + } + break; + } + default: + break; + } + } +} + +/** + * @brief Validates IETF-mandated boilerplate and top-level descriptions for a parsed module or submodule. + * + * @param[in] name The name of the module or submodule. + * @param[in] filepath The file path of the module (used to prefix error messages). + * @param[in] dsc The top-level `description` text of the module/submodule. + * @param[in] contact The top-level `contact` text. + * @param[in] org The top-level `organization` text. + * @param[in] revs Pointer to the parsed `revision` array. + * @param[in] type Integer flag indicating whether the file is a module (0) or submodule (non-zero). + * @param[in] exts Array of top-level parsed `extension` statements. + * @param[in] feats Array of top-level parsed `feature` statements. + * @param[in] idents Array of top-level parsed `identity` statements. + * @param[in] augments Linked list of top-level parsed `augment` statements. + * @param[in] tpdfs Array of top-level parsed `typedef` statements. + * @param[in] grps Linked list of top-level parsed `grouping` statements. + * @param[in,out] out The libyang output handler where warnings and errors are printed. + * @param[in,out] found_2119 Pointer to a flag tracking if RFC 2119 keywords were found anywhere in this specific file. + */ +static void +check_parsed_boilerplate(const char *name, const char *filepath, const char *dsc, const char *contact, const char *org, struct lysp_revision *revs, int type, struct lysp_ext *exts, + struct lysp_feature *feats, struct lysp_ident *idents, struct lysp_node_augment *augments, struct lysp_tpdf *tpdfs, struct lysp_node_grp *grps, struct ly_out *out, int *found_2119) +{ + const char *file_name = filepath ? strrchr(filepath, '/') + 1 : name; + const char *type_name = type ? "submodule" : "module"; + struct lysp_node_grp *grp; + struct lysp_node_augment *aug; + LY_ARRAY_COUNT_TYPE i; + + if ((strncmp(name, "ietf-", 5) != 0) && (strncmp(name, "iana-", 5) != 0)) { + IETF_WARN(file_name, "8407", "4.1", out, "the module name should start with one of the strings \"ietf-\" or \"iana-\""); + } + + if (contact == NULL) { + IETF_ERR(file_name, "8407", "4.8", out, "statement \"%s\" must have a \"contact\" substatement", type_name); + } + + if (org == NULL) { + IETF_ERR(file_name, "8407", "4.8", out, "statement \"%s\" must have a \"organization\" substatement", type_name); + } + + if (revs == NULL) { + IETF_ERR(file_name, "8407", "4.8", out, "statement \"%s\" must have a \"revision\" substatement", type_name); + } else { + LY_ARRAY_FOR(revs, i) { + if (revs[i].ref == NULL) { + IETF_ERR(file_name, "8407", "4.8", out, "statement \"revision\" %s must have a \"reference\" substatement", revs[i].date); + } + } + } + + if (dsc == NULL) { + IETF_ERR(file_name, "8407", "4.8", out, "statement \"%s\" must have a \"description\" substatement", type_name); + return; + } + + if (check_words(dsc)) { + *found_2119 = 1; + } + + if (!strstr(dsc, "IETF Trust") || + !strstr(dsc, "Revised BSD License") || + !strstr(dsc, "https://trustee.ietf.org/license-info")) { + + IETF_WARN(file_name, "8407", "3.1", out, "The IETF Trust Copyright statement seems to be missing or is not correct"); + } + + if (strncmp(name, "ietf-", 5) == 0) { + if (!check_regex(dsc, "is[[:space:]]+part[[:space:]]+of[[:space:]]+RFC") || + !check_regex(dsc, "see[[:space:]]+the[[:space:]]+RFC[[:space:]]+itself")) { + + IETF_WARN(file_name, "8407", "Appendix B", out, "The text about which RFC this module is part of seems to be missing..."); + } + } + + LY_ARRAY_FOR(exts, i) { + if (exts[i].dsc == NULL) { + IETF_ERR(file_name, "8407", "4.14", out, "statement \"%s\" must have a \"description\" substatement", "extension"); + } else if (check_words(exts[i].dsc)) { + *found_2119 = 1; + } + } + + LY_ARRAY_FOR(feats, i) { + if (feats[i].dsc == NULL) { + IETF_ERR(file_name, "8407", "4.14", out, "statement \"%s\" must have a \"description\" substatement", "feature"); + } else if (check_words(feats[i].dsc)) { + *found_2119 = 1; + } + } + + LY_ARRAY_FOR(idents, i) { + if (idents[i].dsc == NULL) { + IETF_ERR(file_name, "8407", "4.14", out, "statement \"%s\" must have a \"description\" substatement", "identity"); + } else if (check_words(idents[i].dsc)) { + *found_2119 = 1; + } + } + + LY_LIST_FOR(augments, aug) { + if (aug->dsc == NULL) { + IETF_ERR(file_name, "8407", "4.14", out, "statement \"augment\" must have a \"description\" substatement"); + } else if (check_words(aug->dsc)) { + *found_2119 = 1; + } + + if (aug->child) { + check_parsed_tree_ietf(out, aug->child, file_name, found_2119); + } + } + + LY_ARRAY_FOR(tpdfs, i) { + if (tpdfs[i].dsc == NULL) { + IETF_ERR(file_name, "8407", "4.13, 4.14", out, "statement \"typedef\" must have a \"description\" substatement"); + } else if (check_words(tpdfs[i].dsc)) { + *found_2119 = 1; + } + } + + LY_LIST_FOR(grps, grp) { + if (grp->dsc == NULL) { + IETF_ERR(file_name, "8407", "4.14", out, "statement \"grouping\" must have a \"description\" substatement"); + } else if (check_words(grp->dsc)) { + *found_2119 = 1; + } + } + + if (*found_2119) { + if (!check_regex(dsc, "interpreted[[:space:]]+as[[:space:]]+described[[:space:]]+in[[:space:]]+BCP[[:space:]]+14") || + !check_regex(dsc, "\\(RFC[[:space:]]*2119\\)[[:space:]]*\\(RFC[[:space:]]*8174\\)[[:space:]]+when,[[:space:]]+and[[:space:]]+only[[:space:]]+when,[[:space:]]+they[[:space:]]+appear[[:space:]]+in[[:space:]]+all[[:space:]]+capitals")) { + + ly_print(out, "%s: warning: %s\n", file_name, "the module seems to use RFC 2119 keywords, but the required text from RFC 8174 is not found or is not correct"); + } + } +} + +/** + * @brief Orchestrator for IETF RFC 8407 compliance validation on a YANG module. + + * @param[in] mod The libyang module structure containing both the parsed schema + * @param[in,out] out The libyang output handler where compliance warnings and errors are printed. + */ +static void +check_ietf(const struct lys_module *mod, struct ly_out *out) +{ + const char *file_name = mod->filepath ? strrchr(mod->filepath, '/') + 1 : mod->name; + char ns[256]; + LY_ARRAY_COUNT_TYPE i; + int main_found_2119 = 0; + + if (mod->parsed->data) { + check_parsed_tree_ietf(out, mod->parsed->data, file_name, &main_found_2119); + } + if (mod->parsed->rpcs) { + check_parsed_tree_ietf(out, (struct lysp_node *)mod->parsed->rpcs, file_name, &main_found_2119); + } + if (mod->parsed->notifs) { + check_parsed_tree_ietf(out, (struct lysp_node *)mod->parsed->notifs, file_name, &main_found_2119); + } + + check_parsed_boilerplate(mod->name, mod->filepath, mod->dsc, mod->contact, mod->org, mod->parsed->revs, mod->parsed->is_submod, mod->parsed->extensions, mod->parsed->features, mod->parsed->identities, mod->parsed->augments, mod->parsed->typedefs, + mod->parsed->groupings, out, &main_found_2119); + + snprintf(ns, sizeof(ns), "urn:ietf:params:xml:ns:yang:%s", mod->name); + if (strcmp(ns, mod->ns)) { + IETF_WARN(file_name, "8407", "4.9", out, "namespace value should be \"%s\"", ns); + } + + LY_ARRAY_FOR(mod->parsed->includes, i) { + struct lysp_submodule *sub = mod->parsed->includes[i].submodule; + const char *sub_file = sub->filepath ? strrchr(sub->filepath, '/') + 1 : sub->name; + int sub_found_2119 = 0; + + if (mod->parsed->revs && sub->revs) { + if (strcmp(mod->parsed->revs[0].date, sub->revs[0].date) < 0) { + IETF_WARN(file_name, "8407", "4.7", out, "the module's revision %s is older than submodule %s's revision %s", mod->parsed->revs[0].date, sub->name, sub->revs[0].date); + } + } + + if (sub->data) { + check_parsed_tree_ietf(out, sub->data, sub_file, &sub_found_2119); + } + if (sub->rpcs) { + check_parsed_tree_ietf(out, (struct lysp_node *)sub->rpcs, sub_file, &sub_found_2119); + } + if (sub->notifs) { + check_parsed_tree_ietf(out, (struct lysp_node *)sub->notifs, sub_file, &sub_found_2119); + } + + check_parsed_boilerplate(sub->name, sub->filepath, sub->dsc, sub->contact, sub->org, sub->revs, sub->is_submod, sub->extensions, sub->features, sub->identities, sub->augments, sub->typedefs, sub->groupings, out, &sub_found_2119); + } + + if (mod->compiled->data) { + check_nodes_ietf(out, mod->compiled->data, file_name); + } +} + +int +cmd_ietf_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv) +{ + const struct lys_module *mod; + char *revision, *name; + uint8_t out_alloc = 0; + + name = strdup(posv); + revision = strchr(name, '@'); + if (revision) { + revision[0] = '\0'; + ++revision; + } + + mod = revision ? + ly_ctx_get_module(*ctx, name, revision) : + ly_ctx_get_module_latest(*ctx, name); + + if (!mod || !mod->compiled || !mod->parsed) { + YLMSG_E("Error: Module %s not loaded or failed to compile.", name); + free(name); + return 1; + } + + if (!yo->out) { + if (ly_out_new_file(stdout, &yo->out)) { + YLMSG_E("Unable to allocate output handler."); + free(name); + return 1; + } + out_alloc = 1; + } + + check_ietf(mod, yo->out); + + if (out_alloc) { + ly_out_free(yo->out, NULL, 0); + yo->out = NULL; + } + + free(name); + return 0; +} diff --git a/tools/lint/cmd_sample.c b/tools/lint/cmd_sample.c new file mode 100644 index 000000000..deaaab339 --- /dev/null +++ b/tools/lint/cmd_sample.c @@ -0,0 +1,559 @@ +/** + * @file cmd_sample.c + * @author Petr Hanzlik + * @brief 'sample-skeleton' command of the libyang's yanglint tool. + * + * Copyright (c) 2026 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 + +#include "cmd.h" + +#include +#include +#include +#include + +#include "libyang.h" + +#include "common.h" +#include "yl_opt.h" + +/** + * @brief Traverse the compiled schema tree and print XML skeleton tags. + * + * @param[in,out] out The libyang output handler to print to. + * @param[in] node The compiled schema node to begin traversing. + * @param[in] ns The XML namespace URI to apply, or NULL if inherited. + * @param[in,out] space_count Pointer to the current indentation level; dynamically incremented and decremented during traversal. + */ +static void print_xml_nodes(struct ly_out *out, const struct lysc_node *node, const char *ns, int *space_count); + +/** + * @brief Traverse the compiled schema tree and print JSON skeleton tags. + * + * @param[in,out] out The libyang output handler to print to. + * @param[in] node The compiled schema node to begin traversing. + * @param[in] name The name of the module, or NULL if inherited. + * @param[in,out] space_count Pointer to the current indentation level; dynamically incremented and decremented during traversal. + * @param[in,out] need_comma Pointer to the state variable tracking JSON comma formatting. Set to 1 if a trailing comma is required before printing the * next sibling, or 0 if the node is the first element in a block. + */ +static void print_json_nodes(struct ly_out *out, const struct lysc_node *node, const char *name, int *space_count, int *need_comma); + +void +cmd_sample_help(void) +{ + printf("Usage: sample [-f (xml | json)] [-o OUTFILE] [@revision]\n" + " Generate and print a sample data skeleton from a loaded schema module.\n\n" + " -f FORMAT, --format=FORMAT\n" + " Print the sample skeleton in the specified FORMAT.\n" + " Supported formats: xml, json.\n" + " -o OUTFILE, --output=OUTFILE\n" + " Write the output to OUTFILE instead of stdout.\n" + " -h, --help\n" + " Display this help message.\n"); +} + +int +cmd_sample_dep(struct yl_opt *yo, int posc) +{ + if (yo->interactive && !posc) { + YLMSG_E("Missing module name to generate skeleton for.\n"); + return 1; + } + + if (!yo->data_out_format) { + YLMSG_E("Missing required format (-f xml | json).\n"); + return 1; + } + + return 0; +} + +int +cmd_sample_opt(struct yl_opt *yo, const char *cmdline, char ***posv, int *posc) +{ + int rc = 0, argc = 0; + int opt, opt_index; + char **argv = NULL; + struct option options[] = { + {"format", required_argument, NULL, 'f'}, + {"output", required_argument, NULL, 'o'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + if ((rc = parse_cmdline(cmdline, &argc, &argv))) { + return rc; + } + + /* reset getopt */ + optind = 0; + while ((opt = getopt_long(argc, argv, "f:ho:", options, &opt_index)) != -1) { + switch (opt) { + case 'f': /* --format */ + if (yl_opt_update_data_out_format(optarg, yo)) { + rc = 1; + goto cleanup; + } + break; + + case 'o': /* --output */ + if (yo->out) { + ly_out_free(yo->out, NULL, 0); + yo->out = NULL; + } + if (ly_out_new_filepath(optarg, &yo->out)) { + YLMSG_E("Unable to open output file %s.\n", optarg); + rc = 1; + goto cleanup; + } + break; + + case 'h': /* --help */ + cmd_sample_help(); + rc = 1; + goto cleanup; + + default: + YLMSG_E("Unknown option.\n"); + rc = 1; + goto cleanup; + } + } + + *posc = argc - optind; + *posv = &argv[optind]; + + return 0; + +cleanup: + free_cmdline(argv); + return rc; +} + +/** + * @brief Print a specific number of spaces for indentation. + * + * @param[in,out] out The libyang output handler to print to. + * @param[in] count The number of spaces to print. + */ +static void +print_space(struct ly_out *out, int count) +{ + for (int i = 0; i < count; i++) { + ly_print(out, " "); + } +} + +/** + * @brief Handle and print the default values or closing XML tags for a schema node. + * + * @param[in,out] out The libyang output handler to print to. + * @param[in] node The compiled schema node currently being processed. + * @param[in] ns The XML namespace string to apply, or NULL if it is inherited. + * @param[in] space_count Pointer to the current indentation level (number of spaces). + */ +static void +handle_xml_default(struct ly_out *out, const struct lysc_node *node, const char *ns, int *space_count) +{ + switch (node->nodetype) { + case LYS_LEAF: { + struct lysc_node_leaf *leaf = (struct lysc_node_leaf *)node; + + if (leaf->dflt.str != NULL) { + ly_print(out, ">%s\n", leaf->dflt.str, node->name); + } else { + ly_print(out, "/>\n"); + } + break; + } + case LYS_LEAFLIST: { + struct lysc_node_leaflist *llist = (struct lysc_node_leaflist *)node; + + if (llist->dflts != NULL) { + LY_ARRAY_COUNT_TYPE i; + + LY_ARRAY_FOR(llist->dflts, i) { + print_space(out, *space_count); + if (node->parent && (node->module != node->parent->module)) { + ly_print(out, "<%s xmlns=\"%s\">%s\n", llist->name, node->module->ns, llist->dflts[i].str, llist->name); + } else { + ly_print(out, "<%s>%s\n", llist->name, llist->dflts[i].str, llist->name); + } + } + } else { + print_space(out, *space_count); + if (node->parent && (node->module != node->parent->module)) { + ly_print(out, "<%s xmlns=\"%s\">\n", llist->name, node->module->ns, llist->name); + } else { + ly_print(out, "<%s>\n", llist->name, llist->name); + } + } + break; + } + case LYS_CHOICE: { + struct lysc_node_choice *choice = (struct lysc_node_choice *)node; + + if (choice->dflt != NULL) { + print_xml_nodes(out, lysc_node_child((const struct lysc_node *)choice->dflt), ns, space_count); + } else { + if (lysc_node_child(node)) { + print_xml_nodes(out, lysc_node_child(lysc_node_child(node)), ns, space_count); + } + } + break; + } + case LYS_CONTAINER: + case LYS_LIST: + case LYS_ANYDATA: + case LYS_ANYXML: + ly_print(out, "/>\n"); + break; + } +} + +static void +print_xml_nodes(struct ly_out *out, const struct lysc_node *node, const char *ns, int *space_count) +{ + const uint32_t PRINT_MASK = LYS_CONTAINER | LYS_LIST | LYS_LEAF | LYS_ANYXML | LYS_ANYDATA; + int nodetype; + + while (node != NULL) { + nodetype = node->nodetype; + + if (nodetype & (LYS_RPC | LYS_ACTION | LYS_NOTIF)) { + node = node->next; + continue; + } + + if (nodetype & PRINT_MASK) { + print_space(out, *space_count); + + if ((ns != NULL) && (node->parent == NULL)) { + ly_print(out, "<%s xmlns=\"%s\"", node->name, ns); + } else if (node->parent && (node->module != node->parent->module)) { + ly_print(out, "<%s xmlns=\"%s\"", node->name, node->module->ns); + } else { + ly_print(out, "<%s", node->name); + } + } + + if (lysc_node_child(node)) { + if (nodetype & PRINT_MASK) { + ly_print(out, ">\n"); + *space_count += 2; + } + + if (nodetype == LYS_CHOICE) { + handle_xml_default(out, node, ns, space_count); + node = node->next; + continue; + } else { + print_xml_nodes(out, lysc_node_child(node), NULL, space_count); + } + + if (nodetype & PRINT_MASK) { + *space_count -= 2; + print_space(out, *space_count); + ly_print(out, "\n", node->name); + } + } else { + handle_xml_default(out, node, NULL, space_count); + } + + node = node->next; + } +} + +/** + * @brief Initialize and print the top-level XML skeleton structure. + * + * @param[in,out] out The libyang output handler to print to. + * @param[in] root The root compiled schema node of the module being processed. + */ +static void +print_xml_skeleton(struct ly_out *out, struct lysc_node *root) +{ + struct lysc_node *node = root; + int spaces = 2; + + ly_print(out, "\n" + "\n"); + print_xml_nodes(out, node, node->module->ns, &spaces); + ly_print(out, "\n"); + +} + +/** + * @brief Determine if a YANG base type requires string quotes in JSON encoding. + * + * @param[in] basetype The libyang data type to evaluate. + * @return 1 if the type requires quotes (string-like), 0 if it must be printed raw (numeric/boolean). + */ +static int +check_basetype_json(LY_DATA_TYPE basetype) +{ + switch (basetype) { + case LY_TYPE_INT8: + case LY_TYPE_INT16: + case LY_TYPE_INT32: + case LY_TYPE_UINT8: + case LY_TYPE_UINT16: + case LY_TYPE_UINT32: + case LY_TYPE_BOOL: + return 0; + + case LY_TYPE_INT64: + case LY_TYPE_UINT64: + case LY_TYPE_DEC64: + return 1; + + default: + return 1; + } +} + +/** + * @brief Handle and print the default values or closing JSON tags for a schema node. + * + * @param[in,out] out The libyang output handler to print to. + * @param[in] node The compiled schema node currently being processed. + * @param[in] space_count Pointer to the current indentation level (number of spaces). + * @param[in,out] need_comma Pointer to the comma state variable. + */ +static void +handle_json_default(struct ly_out *out, const struct lysc_node *node, int *space_count, int *need_comma) +{ + switch (node->nodetype) { + case LYS_LEAF: { + struct lysc_node_leaf *leaf = (struct lysc_node_leaf *)node; + + if (leaf->dflt.str != NULL) { + if (check_basetype_json(leaf->type->basetype)) { + ly_print(out, " \"%s\"", leaf->dflt.str); + } else { + ly_print(out, " %s", leaf->dflt.str); + } + } else { + if (leaf->type->basetype == LY_TYPE_EMPTY) { + ly_print(out, " [null]"); + } else { + ly_print(out, " null"); + } + } + break; + } + case LYS_LEAFLIST: { + struct lysc_node_leaflist *llist = (struct lysc_node_leaflist *)node; + + if (llist->dflts != NULL) { + LY_ARRAY_COUNT_TYPE i; + + ly_print(out, " [\n"); + *space_count += 2; + + LY_ARRAY_FOR(llist->dflts, i) { + if (i == 0) { + print_space(out, *space_count); + } else { + ly_print(out, ",\n"); + print_space(out, *space_count); + } + + if (check_basetype_json(llist->type->basetype)) { + ly_print(out, "\"%s\"", llist->dflts[i].str); + } else { + ly_print(out, "%s", llist->dflts[i].str); + } + } + *space_count -= 2; + ly_print(out, "\n"); + print_space(out, *space_count); + ly_print(out, "]"); + } else { + ly_print(out, " []"); + } + break; + } + case LYS_CHOICE: { + struct lysc_node_choice *choice = (struct lysc_node_choice *)node; + + if (choice->dflt != NULL) { + print_json_nodes(out, lysc_node_child((const struct lysc_node *)choice->dflt), NULL, space_count, need_comma); + } else { + if (lysc_node_child(node)) { + print_json_nodes(out, lysc_node_child(lysc_node_child(node)), NULL, space_count, need_comma); + } + } + break; + } + case LYS_CONTAINER: + case LYS_ANYDATA: + case LYS_ANYXML: + ly_print(out, " {}"); + break; + case LYS_LIST: + ly_print(out, " []"); + break; + } +} + +static void +print_json_nodes(struct ly_out *out, const struct lysc_node *node, const char *name, int *space_count, int *need_comma) +{ + int nodetype; + const uint32_t PRINT_MASK = LYS_CONTAINER | LYS_LIST | LYS_LEAF | LYS_ANYXML | LYS_ANYDATA | LYS_LEAFLIST; + + while (node != NULL) { + nodetype = node->nodetype; + + if (nodetype & (LYS_RPC | LYS_ACTION | LYS_NOTIF)) { + node = node->next; + continue; + } + + if (nodetype & PRINT_MASK) { + if (*need_comma) { + ly_print(out, ",\n"); + } + + print_space(out, *space_count); + + if ((name != NULL) && (node->parent == NULL)) { + ly_print(out, "\"%s:%s\":", node->module->name, node->name); + } else if (node->parent && (node->module != node->parent->module)) { + ly_print(out, "\"%s:%s\":", node->module->name, node->name); + } else { + ly_print(out, "\"%s\":", node->name); + } + } + + if (lysc_node_child(node)) { + if (nodetype & PRINT_MASK) { + if (nodetype == LYS_LIST) { + ly_print(out, " [\n"); + print_space(out, *space_count + 2); + ly_print(out, "{\n"); + *space_count += 4; + } else { + ly_print(out, " {\n"); + *space_count += 2; + } + *need_comma = 0; + } + + if (nodetype == LYS_CHOICE) { + handle_json_default(out, node, space_count, need_comma); + node = node->next; + continue; + } else { + print_json_nodes(out, lysc_node_child(node), NULL, space_count, need_comma); + } + + if (nodetype & PRINT_MASK) { + if (nodetype == LYS_LIST) { + *space_count -= 2; + ly_print(out, "\n"); + print_space(out, *space_count); + ly_print(out, "}\n"); + *space_count -= 2; + print_space(out, *space_count); + ly_print(out, "]"); + } else { + *space_count -= 2; + ly_print(out, "\n"); + print_space(out, *space_count); + ly_print(out, "}"); + } + *need_comma = 1; + } + } else { + handle_json_default(out, node, space_count, need_comma); + + if (nodetype & PRINT_MASK) { + *need_comma = 1; + } + } + + node = node->next; + } +} + +/** + * @brief Initialize and print the top-level JSON skeleton structure. + * + * @param[in,out] out The libyang output handler to print to. + * @param[in] root The root compiled schema node of the module being processed. + * @param[in] name The name of the module, used to correctly prefix the top-level JSON objects according to RFC 7951 namespace rules. + */ +static void +print_json_skeleton(struct ly_out *out, struct lysc_node *root, const char *name) +{ + struct lysc_node *node = root; + int spaces = 2, need_comma = 0; + + ly_print(out, "{\n"); + print_json_nodes(out, node, name, &spaces, &need_comma); + ly_print(out, "\n}\n"); +} + +int +cmd_sample_exec(struct ly_ctx **ctx, struct yl_opt *yo, const char *posv) +{ + const struct lys_module *mod; + char *revision, *name; + uint8_t out_alloc = 0; + + name = strdup(posv); + revision = strchr(name, '@'); + if (revision) { + revision[0] = '\0'; + ++revision; + } + + mod = revision ? + ly_ctx_get_module(*ctx, name, revision) : + ly_ctx_get_module_latest(*ctx, name); + + if (!mod || !mod->compiled) { + YLMSG_E("Error: Module %s not loaded or has no compiled schema.", name); + free(name); + return 1; + } + + if (!mod->compiled->data) { + free(name); + return 0; + } + + if (!yo->out) { + if (ly_out_new_file(stdout, &yo->out)) { + YLMSG_E("Unable to allocate output handler."); + free(name); + return 1; + } + out_alloc = 1; + } + + if (yo->data_out_format == LYD_XML) { + print_xml_skeleton(yo->out, mod->compiled->data); + } else { + print_json_skeleton(yo->out, mod->compiled->data, mod->name); + } + + if (out_alloc) { + ly_out_free(yo->out, NULL, 0); + yo->out = NULL; + } + + free(name); + return 0; +} diff --git a/tools/lint/main_ni.c b/tools/lint/main_ni.c index dc83c9a86..5df0787d5 100644 --- a/tools/lint/main_ni.c +++ b/tools/lint/main_ni.c @@ -52,6 +52,9 @@ help(int shortout) " yanglint [-f { xml | json }] ... ...\n" " Validates the YANG modeled data (s) according to the (s) optionally\n" " printing them in the specified format.\n\n" + " yanglint -S { xml | json } ...\n" + " Generates and prints a sample data skeleton from the provided YANG (s)\n" + " in the specified format (XML or JSON).\n\n" " yanglint -t (nc-)rpc/notif [-O ] ... \n" " Validates the YANG/NETCONF RPC/notification according to the (s) using\n" " with possible references to the operational datastore data.\n" @@ -78,6 +81,10 @@ help(int shortout) " yang, yin, tree, info and feature-param for schemas,\n" " xml, json, and lyb for data.\n\n"); + printf(" -S FORMAT, --sample-skeleton=FORMAT\n" + " Generate a sample data skeleton from the provided schema.\n" + " Supported formats: xml, json.\n\n"); + printf(" -I FORMAT, --in-format=FORMAT\n" " Load the data in one of the following formats:\n" " xml, json, lyb\n" @@ -227,6 +234,9 @@ help(int shortout) " Unsupported for the Release build\n\n" #endif ); + + printf(" -t, --ietf\n" + " Enable strict IETF validation rules for the loaded schemas.\n\n"); } static void @@ -507,6 +517,8 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx) {"extended-leafref", no_argument, NULL, 'X'}, {"json-null", no_argument, NULL, 'J'}, {"debug", required_argument, NULL, 'G'}, + {"sample-skeleton", required_argument, NULL, 'S'}, + {"ietf", no_argument, NULL, 'T'}, {NULL, 0, NULL, 0} }; uint8_t data_type_set = 0; @@ -518,7 +530,7 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx) yo->line_length = 0; opterr = 0; - while ((opt = getopt_long(argc, argv, "hvVQf:I:p:DF:iP:qs:neE:At:d:lL:o:O:R:myY:XJx:G:", options, &opt_index)) != -1) { + while ((opt = getopt_long(argc, argv, "hvVQf:I:p:DF:iP:qs:neE:At:d:lL:o:O:R:myY:XJx:G:S:T", options, &opt_index)) != -1) { switch (opt) { case 'h': /* --help */ help(0); @@ -711,6 +723,19 @@ process_args(int argc, char *argv[], struct yl_opt *yo, struct ly_ctx **ctx) return -1; } break; + + case 'S': /* --sample-skeleton */ + if (yl_opt_update_data_out_format(optarg, yo)) { + YLMSG_E("Unknown out format %s.", optarg); + help(1); + return -1; + } + break; + + case 'T': /* --ietf */ + yo->ietf_validation = 1; + break; + default: YLMSG_E("Invalid option or missing argument: -%c.", optopt); return -1; @@ -818,6 +843,20 @@ main_ni(int argc, char *argv[]) goto cleanup; } } + } else if (yo.data_out_format) { + for (u = 0; u < yo.schema_modules.count; ++u) { + yo.last_one = (u + 1) == yo.schema_modules.count; + if ((ret = cmd_sample_exec(&ctx, &yo, ((struct lys_module *)yo.schema_modules.objs[u])->name))) { + goto cleanup; + } + } + } else if (yo.ietf_validation) { + for (u = 0; u < yo.schema_modules.count; ++u) { + yo.last_one = (u + 1) == yo.schema_modules.count; + if ((ret = cmd_ietf_exec(&ctx, &yo, ((struct lys_module *)yo.schema_modules.objs[u])->name))) { + goto cleanup; + } + } } /* do the data validation despite the schema was printed */ diff --git a/tools/lint/yl_opt.h b/tools/lint/yl_opt.h index d66ae4de9..7fcd75576 100644 --- a/tools/lint/yl_opt.h +++ b/tools/lint/yl_opt.h @@ -137,6 +137,8 @@ struct yl_opt { /* storage for --data-xpath */ struct ly_set data_xpath; + /* flag for --ietf option */ + uint8_t ietf_validation; char **argv; };