| /** |
| * @file printer_xml.c |
| * @author Michal Vasko <mvasko@cesnet.cz> |
| * @brief XML printer for libyang data structure |
| * |
| * Copyright (c) 2015 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 <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <assert.h> |
| #include <inttypes.h> |
| |
| #include "common.h" |
| #include "parser.h" |
| #include "printer.h" |
| #include "xml_internal.h" |
| #include "tree_data.h" |
| #include "tree_schema.h" |
| #include "resolve.h" |
| #include "tree_internal.h" |
| |
| #define INDENT "" |
| #define LEVEL (level ? level*2-2 : 0) |
| |
| struct mlist { |
| struct mlist *next; |
| struct lys_module *module; |
| } *mlist = NULL, *mlist_new; |
| |
| static int |
| modlist_add(struct mlist **mlist, const struct lys_module *mod) |
| { |
| struct mlist *iter; |
| |
| for (iter = *mlist; iter; iter = iter->next) { |
| if (mod == iter->module) { |
| break; |
| } |
| } |
| |
| if (!iter) { |
| iter = malloc(sizeof *iter); |
| LY_CHECK_ERR_RETURN(!iter, LOGMEM(mod->ctx), EXIT_FAILURE); |
| iter->next = *mlist; |
| iter->module = (struct lys_module *)mod; |
| *mlist = iter; |
| } |
| |
| return EXIT_SUCCESS; |
| } |
| |
| static void |
| xml_print_ns(struct lyout *out, const struct lyd_node *node, int options) |
| { |
| struct lyd_node *next, *cur, *node2; |
| struct lyd_attr *attr; |
| const struct lys_module *wdmod = NULL; |
| struct mlist *mlist = NULL, *miter; |
| int r; |
| |
| assert(out); |
| assert(node); |
| |
| /* add node attribute modules */ |
| for (attr = node->attr; attr; attr = attr->next) { |
| if (!strcmp(node->schema->name, "filter") && |
| (!strcmp(node->schema->module->name, "ietf-netconf") || |
| !strcmp(node->schema->module->name, "notifications"))) { |
| /* exception for NETCONF's filter attributes */ |
| continue; |
| } else { |
| r = modlist_add(&mlist, lys_main_module(attr->annotation->module)); |
| } |
| if (r) { |
| goto print; |
| } |
| } |
| |
| /* add node children nodes and attribute modules */ |
| switch (node->schema->nodetype) { |
| case LYS_LEAFLIST: |
| case LYS_LEAF: |
| if (node->dflt && (options & (LYP_WD_ALL_TAG | LYP_WD_IMPL_TAG))) { |
| /* get with-defaults module and print its namespace */ |
| wdmod = ly_ctx_get_module(node->schema->module->ctx, "ietf-netconf-with-defaults", NULL, 1); |
| if (wdmod && modlist_add(&mlist, wdmod)) { |
| goto print; |
| } |
| } |
| break; |
| case LYS_CONTAINER: |
| case LYS_LIST: |
| case LYS_RPC: |
| case LYS_ACTION: |
| case LYS_NOTIF: |
| if (options & (LYP_WD_ALL_TAG | LYP_WD_IMPL_TAG)) { |
| /* get with-defaults module and print its namespace */ |
| wdmod = ly_ctx_get_module(node->schema->module->ctx, "ietf-netconf-with-defaults", NULL, 1); |
| if (wdmod && modlist_add(&mlist, wdmod)) { |
| goto print; |
| } |
| } |
| |
| LY_TREE_FOR(node->child, node2) { |
| LY_TREE_DFS_BEGIN(node2, next, cur) { |
| for (attr = cur->attr; attr; attr = attr->next) { |
| if (!strcmp(cur->schema->name, "filter") && |
| (!strcmp(cur->schema->module->name, "ietf-netconf") || |
| !strcmp(cur->schema->module->name, "notifications"))) { |
| /* exception for NETCONF's filter attributes */ |
| continue; |
| } else { |
| r = modlist_add(&mlist, lys_main_module(attr->annotation->module)); |
| } |
| if (r) { |
| goto print; |
| } |
| } |
| LY_TREE_DFS_END(node2, next, cur)} |
| } |
| break; |
| default: |
| break; |
| } |
| |
| print: |
| /* print used namespaces */ |
| while (mlist) { |
| miter = mlist; |
| mlist = mlist->next; |
| |
| ly_print(out, " xmlns:%s=\"%s\"", miter->module->prefix, miter->module->ns); |
| free(miter); |
| } |
| } |
| |
| static int |
| xml_print_attrs(struct lyout *out, const struct lyd_node *node, int options) |
| { |
| struct lyd_attr *attr; |
| const char **prefs, **nss; |
| const char *xml_expr = NULL, *mod_name; |
| uint32_t ns_count, i; |
| int rpc_filter = 0; |
| const struct lys_module *wdmod = NULL; |
| char *p; |
| size_t len; |
| |
| /* with-defaults */ |
| if (node->schema->nodetype & (LYS_LEAF | LYS_LEAFLIST)) { |
| if ((node->dflt && (options & (LYP_WD_ALL_TAG | LYP_WD_IMPL_TAG))) || |
| (!node->dflt && (options & LYP_WD_ALL_TAG) && lyd_wd_default((struct lyd_node_leaf_list *)node))) { |
| /* we have implicit OR explicit default node */ |
| /* get with-defaults module */ |
| wdmod = ly_ctx_get_module(node->schema->module->ctx, "ietf-netconf-with-defaults", NULL, 1); |
| if (wdmod) { |
| /* print attribute only if context include with-defaults schema */ |
| ly_print(out, " %s:default=\"true\"", wdmod->prefix); |
| } |
| } |
| } |
| /* technically, check for the extension get-filter-element-attributes from ietf-netconf */ |
| if (!strcmp(node->schema->name, "filter") |
| && (!strcmp(node->schema->module->name, "ietf-netconf") || !strcmp(node->schema->module->name, "notifications"))) { |
| rpc_filter = 1; |
| } |
| |
| for (attr = node->attr; attr; attr = attr->next) { |
| if (rpc_filter) { |
| /* exception for NETCONF's filter's attributes */ |
| if (!strcmp(attr->name, "select")) { |
| /* xpath content, we have to convert the JSON format into XML first */ |
| xml_expr = transform_json2xml(node->schema->module, attr->value_str, 0, &prefs, &nss, &ns_count); |
| if (!xml_expr) { |
| /* error */ |
| ly_print(out, "\"(!error!)\""); |
| return EXIT_FAILURE; |
| } |
| |
| for (i = 0; i < ns_count; ++i) { |
| ly_print(out, " xmlns:%s=\"%s\"", prefs[i], nss[i]); |
| } |
| free(prefs); |
| free(nss); |
| } |
| ly_print(out, " %s=\"", attr->name); |
| } else { |
| ly_print(out, " %s:%s=\"", attr->annotation->module->prefix, attr->name); |
| } |
| |
| switch (attr->value_type) { |
| case LY_TYPE_BINARY: |
| case LY_TYPE_STRING: |
| case LY_TYPE_BITS: |
| case LY_TYPE_ENUM: |
| case LY_TYPE_BOOL: |
| case LY_TYPE_DEC64: |
| case LY_TYPE_INT8: |
| case LY_TYPE_INT16: |
| case LY_TYPE_INT32: |
| case LY_TYPE_INT64: |
| case LY_TYPE_UINT8: |
| case LY_TYPE_UINT16: |
| case LY_TYPE_UINT32: |
| case LY_TYPE_UINT64: |
| if (attr->value_str) { |
| /* xml_expr can contain transformed xpath */ |
| lyxml_dump_text(out, xml_expr ? xml_expr : attr->value_str, LYXML_DATA_ATTR); |
| } |
| break; |
| |
| case LY_TYPE_IDENT: |
| if (!attr->value_str) { |
| break; |
| } |
| p = strchr(attr->value_str, ':'); |
| assert(p); |
| len = p - attr->value_str; |
| mod_name = attr->annotation->module->name; |
| if (!strncmp(attr->value_str, mod_name, len) && !mod_name[len]) { |
| lyxml_dump_text(out, ++p, LYXML_DATA_ATTR); |
| } else { |
| /* avoid code duplication - use instance-identifier printer which gets necessary namespaces to print */ |
| goto printinst; |
| } |
| break; |
| case LY_TYPE_INST: |
| printinst: |
| xml_expr = transform_json2xml(node->schema->module, ((struct lyd_node_leaf_list *)node)->value_str, 1, |
| &prefs, &nss, &ns_count); |
| if (!xml_expr) { |
| /* error */ |
| ly_print(out, "(!error!)"); |
| return EXIT_FAILURE; |
| } |
| |
| for (i = 0; i < ns_count; ++i) { |
| ly_print(out, " xmlns:%s=\"%s\"", prefs[i], nss[i]); |
| } |
| free(prefs); |
| free(nss); |
| |
| lyxml_dump_text(out, xml_expr, LYXML_DATA_ATTR); |
| lydict_remove(node->schema->module->ctx, xml_expr); |
| break; |
| |
| /* LY_TYPE_LEAFREF not allowed */ |
| case LY_TYPE_EMPTY: |
| break; |
| |
| default: |
| /* error */ |
| ly_print(out, "(!error!)"); |
| return EXIT_FAILURE; |
| } |
| |
| ly_print(out, "\""); |
| |
| if (xml_expr) { |
| lydict_remove(node->schema->module->ctx, xml_expr); |
| } |
| } |
| |
| return EXIT_SUCCESS; |
| } |
| |
| static int |
| xml_print_leaf(struct lyout *out, int level, const struct lyd_node *node, int toplevel, int options) |
| { |
| const struct lyd_node_leaf_list *leaf = (struct lyd_node_leaf_list *)node, *iter; |
| const struct lys_type *type; |
| struct lys_tpdf *tpdf; |
| const char *ns, *mod_name; |
| const char **prefs, **nss; |
| const char *xml_expr; |
| uint32_t ns_count, i; |
| LY_DATA_TYPE datatype; |
| char *p; |
| size_t len; |
| enum int_log_opts prev_ilo; |
| |
| if (toplevel || !node->parent || nscmp(node, node->parent)) { |
| /* print "namespace" */ |
| ns = lyd_node_module(node)->ns; |
| ly_print(out, "%*s<%s xmlns=\"%s\"", LEVEL, INDENT, node->schema->name, ns); |
| } else { |
| ly_print(out, "%*s<%s", LEVEL, INDENT, node->schema->name); |
| } |
| |
| if (toplevel) { |
| xml_print_ns(out, node, options); |
| } |
| |
| if (xml_print_attrs(out, node, options)) { |
| return EXIT_FAILURE; |
| } |
| datatype = leaf->value_type; |
| |
| printvalue: |
| switch (datatype) { |
| case LY_TYPE_STRING: |
| ly_ilo_change(NULL, ILO_IGNORE, &prev_ilo, NULL); |
| type = lyd_leaf_type((struct lyd_node_leaf_list *)leaf); |
| ly_ilo_restore(NULL, prev_ilo, NULL, 0); |
| if (type) { |
| for (tpdf = type->der; |
| tpdf->module && (strcmp(tpdf->name, "xpath1.0") || strcmp(tpdf->module->name, "ietf-yang-types")); |
| tpdf = tpdf->type.der); |
| /* special handling of ietf-yang-types xpath1.0 */ |
| if (tpdf->module) { |
| /* avoid code duplication - use instance-identifier printer which gets necessary namespaces to print */ |
| datatype = LY_TYPE_INST; |
| goto printvalue; |
| } |
| } |
| /* fallthrough */ |
| case LY_TYPE_BINARY: |
| case LY_TYPE_BITS: |
| case LY_TYPE_ENUM: |
| case LY_TYPE_BOOL: |
| case LY_TYPE_UNION: |
| case LY_TYPE_DEC64: |
| case LY_TYPE_INT8: |
| case LY_TYPE_INT16: |
| case LY_TYPE_INT32: |
| case LY_TYPE_INT64: |
| case LY_TYPE_UINT8: |
| case LY_TYPE_UINT16: |
| case LY_TYPE_UINT32: |
| case LY_TYPE_UINT64: |
| if (!leaf->value_str || !leaf->value_str[0]) { |
| ly_print(out, "/>"); |
| } else { |
| ly_print(out, ">"); |
| lyxml_dump_text(out, leaf->value_str, LYXML_DATA_ELEM); |
| ly_print(out, "</%s>", node->schema->name); |
| } |
| break; |
| |
| case LY_TYPE_IDENT: |
| if (!leaf->value_str || !leaf->value_str[0]) { |
| ly_print(out, "/>"); |
| break; |
| } |
| p = strchr(leaf->value_str, ':'); |
| assert(p); |
| len = p - leaf->value_str; |
| mod_name = leaf->schema->module->name; |
| if (!strncmp(leaf->value_str, mod_name, len) && !mod_name[len]) { |
| ly_print(out, ">"); |
| lyxml_dump_text(out, ++p, LYXML_DATA_ELEM); |
| ly_print(out, "</%s>", node->schema->name); |
| } else { |
| /* avoid code duplication - use instance-identifier printer which gets necessary namespaces to print */ |
| datatype = LY_TYPE_INST; |
| goto printvalue; |
| } |
| break; |
| case LY_TYPE_INST: |
| xml_expr = transform_json2xml(node->schema->module, ((struct lyd_node_leaf_list *)node)->value_str, 1, |
| &prefs, &nss, &ns_count); |
| if (!xml_expr) { |
| /* error */ |
| ly_print(out, "\"(!error!)\""); |
| return EXIT_FAILURE; |
| } |
| |
| for (i = 0; i < ns_count; ++i) { |
| ly_print(out, " xmlns:%s=\"%s\"", prefs[i], nss[i]); |
| } |
| free(prefs); |
| free(nss); |
| |
| if (xml_expr[0]) { |
| ly_print(out, ">"); |
| lyxml_dump_text(out, xml_expr, LYXML_DATA_ELEM); |
| ly_print(out, "</%s>", node->schema->name); |
| } else { |
| ly_print(out, "/>"); |
| } |
| lydict_remove(node->schema->module->ctx, xml_expr); |
| break; |
| |
| case LY_TYPE_LEAFREF: |
| iter = (struct lyd_node_leaf_list *)leaf->value.leafref; |
| while (iter && (iter->value_type == LY_TYPE_LEAFREF)) { |
| iter = (struct lyd_node_leaf_list *)iter->value.leafref; |
| } |
| if (!iter) { |
| /* unresolved and invalid, but we can learn the correct type anyway */ |
| type = lyd_leaf_type((struct lyd_node_leaf_list *)leaf); |
| if (!type) { |
| /* error */ |
| ly_print(out, "\"(!error!)\""); |
| return EXIT_FAILURE; |
| } |
| datatype = type->base; |
| } else { |
| datatype = iter->value_type; |
| } |
| goto printvalue; |
| |
| case LY_TYPE_EMPTY: |
| ly_print(out, "/>"); |
| break; |
| |
| default: |
| /* error */ |
| ly_print(out, "\"(!error!)\""); |
| return EXIT_FAILURE; |
| } |
| |
| if (level) { |
| ly_print(out, "\n"); |
| } |
| |
| return EXIT_SUCCESS; |
| } |
| |
| static int |
| xml_print_container(struct lyout *out, int level, const struct lyd_node *node, int toplevel, int options) |
| { |
| struct lyd_node *child; |
| const char *ns; |
| |
| if (toplevel || !node->parent || nscmp(node, node->parent)) { |
| /* print "namespace" */ |
| ns = lyd_node_module(node)->ns; |
| ly_print(out, "%*s<%s xmlns=\"%s\"", LEVEL, INDENT, node->schema->name, ns); |
| } else { |
| ly_print(out, "%*s<%s", LEVEL, INDENT, node->schema->name); |
| } |
| |
| if (toplevel) { |
| xml_print_ns(out, node, options); |
| } |
| |
| if (xml_print_attrs(out, node, options)) { |
| return EXIT_FAILURE; |
| } |
| |
| if (!node->child) { |
| ly_print(out, "/>%s", level ? "\n" : ""); |
| return EXIT_SUCCESS; |
| } |
| ly_print(out, ">%s", level ? "\n" : ""); |
| |
| LY_TREE_FOR(node->child, child) { |
| if (xml_print_node(out, level ? level + 1 : 0, child, 0, options)) { |
| return EXIT_FAILURE; |
| } |
| } |
| |
| ly_print(out, "%*s</%s>%s", LEVEL, INDENT, node->schema->name, level ? "\n" : ""); |
| |
| return EXIT_SUCCESS; |
| } |
| |
| static int |
| xml_print_list(struct lyout *out, int level, const struct lyd_node *node, int is_list, int toplevel, int options) |
| { |
| struct lyd_node *child; |
| const char *ns; |
| |
| if (is_list) { |
| /* list print */ |
| if (toplevel || !node->parent || nscmp(node, node->parent)) { |
| /* print "namespace" */ |
| ns = lyd_node_module(node)->ns; |
| ly_print(out, "%*s<%s xmlns=\"%s\"", LEVEL, INDENT, node->schema->name, ns); |
| } else { |
| ly_print(out, "%*s<%s", LEVEL, INDENT, node->schema->name); |
| } |
| |
| if (toplevel) { |
| xml_print_ns(out, node, options); |
| } |
| if (xml_print_attrs(out, node, options)) { |
| return EXIT_FAILURE; |
| } |
| |
| if (!node->child) { |
| ly_print(out, "/>%s", level ? "\n" : ""); |
| return EXIT_SUCCESS; |
| } |
| ly_print(out, ">%s", level ? "\n" : ""); |
| |
| LY_TREE_FOR(node->child, child) { |
| if (xml_print_node(out, level ? level + 1 : 0, child, 0, options)) { |
| return EXIT_FAILURE; |
| } |
| } |
| |
| ly_print(out, "%*s</%s>%s", LEVEL, INDENT, node->schema->name, level ? "\n" : ""); |
| } else { |
| /* leaf-list print */ |
| xml_print_leaf(out, level, node, toplevel, options); |
| } |
| |
| return EXIT_SUCCESS; |
| } |
| |
| static int |
| xml_print_anydata(struct lyout *out, int level, const struct lyd_node *node, int toplevel, int options) |
| { |
| char *buf; |
| struct lyd_node_anydata *any = (struct lyd_node_anydata *)node; |
| struct lyd_node *iter; |
| const char *ns; |
| |
| if (toplevel || !node->parent || nscmp(node, node->parent)) { |
| /* print "namespace" */ |
| ns = lyd_node_module(node)->ns; |
| ly_print(out, "%*s<%s xmlns=\"%s\"", LEVEL, INDENT, node->schema->name, ns); |
| } else { |
| ly_print(out, "%*s<%s", LEVEL, INDENT, node->schema->name); |
| } |
| |
| if (toplevel) { |
| xml_print_ns(out, node, options); |
| } |
| if (xml_print_attrs(out, node, options)) { |
| return EXIT_FAILURE; |
| } |
| if (!(void*)any->value.tree || (any->value_type == LYD_ANYDATA_CONSTSTRING && !any->value.str[0])) { |
| /* no content */ |
| ly_print(out, "/>%s", level ? "\n" : ""); |
| } else { |
| if (any->value_type == LYD_ANYDATA_DATATREE) { |
| /* print namespaces in the anydata data tree */ |
| LY_TREE_FOR(any->value.tree, iter) { |
| xml_print_ns(out, iter, options); |
| } |
| } |
| /* close opening tag ... */ |
| ly_print(out, ">"); |
| /* ... and print anydata content */ |
| switch (any->value_type) { |
| case LYD_ANYDATA_CONSTSTRING: |
| lyxml_dump_text(out, any->value.str, LYXML_DATA_ELEM); |
| break; |
| case LYD_ANYDATA_DATATREE: |
| if (any->value.tree) { |
| if (level) { |
| ly_print(out, "\n"); |
| } |
| LY_TREE_FOR(any->value.tree, iter) { |
| if (xml_print_node(out, level ? level + 1 : 0, iter, 0, (options & ~(LYP_WITHSIBLINGS | LYP_NETCONF)))) { |
| return EXIT_FAILURE; |
| } |
| } |
| } |
| break; |
| case LYD_ANYDATA_XML: |
| lyxml_print_mem(&buf, any->value.xml, (level ? LYXML_PRINT_FORMAT | LYXML_PRINT_NO_LAST_NEWLINE : 0) |
| | LYXML_PRINT_SIBLINGS); |
| ly_print(out, "%s%s", level ? "\n" : "", buf); |
| free(buf); |
| break; |
| case LYD_ANYDATA_SXML: |
| /* print without escaping special characters */ |
| ly_print(out, "%s", any->value.str); |
| break; |
| case LYD_ANYDATA_JSON: |
| case LYD_ANYDATA_LYB: |
| /* JSON and LYB format is not supported */ |
| LOGWRN(node->schema->module->ctx, "Unable to print anydata content (type %d) as XML.", any->value_type); |
| break; |
| case LYD_ANYDATA_STRING: |
| case LYD_ANYDATA_SXMLD: |
| case LYD_ANYDATA_JSOND: |
| case LYD_ANYDATA_LYBD: |
| /* dynamic strings are used only as input parameters */ |
| assert(0); |
| break; |
| } |
| |
| /* closing tag */ |
| ly_print(out, "</%s>%s", node->schema->name, level ? "\n" : ""); |
| } |
| |
| return EXIT_SUCCESS; |
| } |
| |
| int |
| xml_print_node(struct lyout *out, int level, const struct lyd_node *node, int toplevel, int options) |
| { |
| int ret = EXIT_SUCCESS; |
| |
| if (!lyd_wd_toprint(node, options)) { |
| /* wd says do not print */ |
| return EXIT_SUCCESS; |
| } |
| |
| switch (node->schema->nodetype) { |
| case LYS_NOTIF: |
| case LYS_RPC: |
| case LYS_ACTION: |
| case LYS_CONTAINER: |
| ret = xml_print_container(out, level, node, toplevel, options); |
| break; |
| case LYS_LEAF: |
| ret = xml_print_leaf(out, level, node, toplevel, options); |
| break; |
| case LYS_LEAFLIST: |
| ret = xml_print_list(out, level, node, 0, toplevel, options); |
| break; |
| case LYS_LIST: |
| ret = xml_print_list(out, level, node, 1, toplevel, options); |
| break; |
| case LYS_ANYXML: |
| case LYS_ANYDATA: |
| ret = xml_print_anydata(out, level, node, toplevel, options); |
| break; |
| default: |
| LOGINT(node->schema->module->ctx); |
| ret = EXIT_FAILURE; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| int |
| xml_print_data(struct lyout *out, const struct lyd_node *root, int options) |
| { |
| const struct lyd_node *node, *next; |
| struct lys_node *parent = NULL; |
| int level, action_input = 0; |
| |
| if (!root) { |
| if (out->type == LYOUT_MEMORY || out->type == LYOUT_CALLBACK) { |
| ly_print(out, ""); |
| } |
| return EXIT_SUCCESS; |
| } |
| |
| level = (options & LYP_FORMAT ? 1 : 0); |
| |
| if (options & LYP_NETCONF) { |
| if (root->schema->nodetype != LYS_RPC) { |
| /* learn whether we are printing an action */ |
| LY_TREE_DFS_BEGIN(root, next, node) { |
| if (node->schema->nodetype == LYS_ACTION) { |
| break; |
| } |
| LY_TREE_DFS_END(root, next, node); |
| } |
| } else { |
| node = root; |
| } |
| |
| if (node) { |
| if ((node->schema->nodetype & (LYS_LIST | LYS_CONTAINER | LYS_RPC | LYS_NOTIF | LYS_ACTION)) && node->child) { |
| for (parent = lys_parent(node->child->schema); parent && (parent->nodetype == LYS_USES); parent = lys_parent(parent)); |
| } |
| if (parent && (parent->nodetype == LYS_OUTPUT)) { |
| /* rpc/action output - skip the container */ |
| root = node->child; |
| } else if (node->schema->nodetype == LYS_ACTION) { |
| /* action input - print top-level action element */ |
| action_input = 1; |
| } |
| } |
| } |
| |
| if (action_input) { |
| ly_print(out, "%*s<action xmlns=\"urn:ietf:params:xml:ns:yang:1\">%s", LEVEL, INDENT, level ? "\n" : ""); |
| if (level) { |
| ++level; |
| } |
| } |
| |
| /* content */ |
| LY_TREE_FOR(root, node) { |
| if (xml_print_node(out, level, node, 1, options)) { |
| return EXIT_FAILURE; |
| } |
| if (!(options & LYP_WITHSIBLINGS)) { |
| break; |
| } |
| } |
| |
| if (action_input) { |
| if (level) { |
| --level; |
| } |
| ly_print(out, "%*s</action>%s", LEVEL, INDENT, level ? "\n" : ""); |
| } |
| |
| ly_print_flush(out); |
| |
| return EXIT_SUCCESS; |
| } |
| |