FEATURE patterns evaluation support

Includes yangre(1) tool and support for parsing and validating string value
including evaluation of their restrictions.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1d7aabf..c3d9160 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -231,8 +231,8 @@
     tools/lint/configuration.c
     tools/lint/linenoise/linenoise.c)
 
-#set(resrc
-#    tools/re/main.c)
+set(resrc
+    tools/re/main.c)
 
 set(headers
     src/libyang.h
@@ -386,11 +386,11 @@
 install(FILES ${PROJECT_SOURCE_DIR}/tools/lint/yanglint.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
 target_include_directories(yanglint BEFORE PRIVATE ${PROJECT_BINARY_DIR}/tools)
 
-#yangre
-#add_executable(yangre ${resrc})
-#target_link_libraries(yangre yang)
-#install(TARGETS yangre DESTINATION ${CMAKE_INSTALL_BINDIR})
-#target_include_directories(yangre BEFORE PRIVATE ${PROJECT_BINARY_DIR}/tools)
+# yangre
+add_executable(yangre ${resrc})
+target_link_libraries(yangre yang)
+install(TARGETS yangre DESTINATION ${CMAKE_INSTALL_BINDIR})
+target_include_directories(yangre BEFORE PRIVATE ${PROJECT_BINARY_DIR}/tools)
 
 if(ENABLE_VALGRIND_TESTS)
     set(ENABLE_BUILD_TESTS ON)
diff --git a/src/parser_xml.c b/src/parser_xml.c
index 21aaf3e..3d86181 100644
--- a/src/parser_xml.c
+++ b/src/parser_xml.c
@@ -235,9 +235,8 @@
                 value = "";
                 value_len = 0;
             }
-            ret = lyd_value_validate((struct lyd_node_term*)cur, value, value_len,
-                                      LY_TYPE_OPTS_VALIDATE | LY_TYPE_OPTS_CANONIZE | (dynamic ? LY_TYPE_OPTS_DYNAMIC : 0) | LY_TYPE_OPTS_STORE);
-            LY_CHECK_GOTO(ret, cleanup);
+            LY_CHECK_ERR_GOTO(ret = lyd_value_parse((struct lyd_node_term*)cur, value, value_len, dynamic),
+                              if (dynamic){free(value);}, cleanup);
         } else if (snode->nodetype & LYD_NODE_INNER) {
             int dynamic = 0;
             char *buffer = NULL, *value;
diff --git a/src/plugins_types.c b/src/plugins_types.c
index efac048..244b7ff 100644
--- a/src/plugins_types.c
+++ b/src/plugins_types.c
@@ -122,17 +122,6 @@
     return LY_EVALID;
 }
 
-/**
- * @brief Convert a string with a decimal64 value into libyang representation:
- * ret = value * 10^fraction-digits
- *
- * @param[in] fraction_digits Fraction-digits of the decimal64 type.
- * @param[in] value Value string to parse.
- * @param[in] value_len Length of the @p value (mandatory parameter).
- * @param[out] ret Parsed decimal64 value representing original value * 10^fraction-digits (optional).
- * @param[out] err Error information in case of failure. The error structure can be freed by ly_err_free().
- * @return LY_ERR value according to the result of the parsing and validation.
- */
 API LY_ERR
 parse_dec64(uint8_t fraction_digits, const char *value, size_t value_len, int64_t *ret, struct ly_err_item **err)
 {
@@ -204,7 +193,10 @@
 
     /* prepare value string without decimal point to easily parse using standard functions */
     valcopy = malloc(size * sizeof *valcopy);
-    LY_CHECK_ERR_GOTO(!valcopy, LOGMEM(NULL); rc = LY_EMEM, error);
+    if (!valcopy) {
+        *err = ly_err_new(LY_LLERR, LY_EMEM, 0, "Memory allocation failed.", NULL, NULL);
+        return LY_EMEM;
+    }
 
     valcopy[size - 1] = '\0';
     if (fraction) {
@@ -231,6 +223,49 @@
     return rc;
 }
 
+API LY_ERR
+ly_type_validate_patterns(struct lysc_pattern **patterns, const char *str, size_t str_len, struct ly_err_item **err)
+{
+    LY_ERR ret = LY_SUCCESS;
+    int rc;
+    unsigned int u;
+    char *errmsg;
+    pcre2_match_data *match_data = NULL;
+
+    LY_CHECK_ARG_RET(NULL, str, err, LY_EINVAL);
+
+    LY_ARRAY_FOR(patterns, u) {
+        match_data = pcre2_match_data_create_from_pattern(patterns[u]->code, NULL);
+        if (!match_data) {
+            *err = ly_err_new(LY_LLERR, LY_EMEM, 0, "Memory allocation failed.", NULL, NULL);
+            return LY_EMEM;
+        }
+
+        rc = pcre2_match(patterns[u]->code, (PCRE2_SPTR)str, str_len, 0, PCRE2_ANCHORED | PCRE2_ENDANCHORED, match_data, NULL);
+        if (rc == PCRE2_ERROR_NOMATCH) {
+            asprintf(&errmsg, "String \"%.*s\" does not conforms to the %u. pattern restriction of its type.",
+                     (int)str_len, str, u + 1);
+            *err = ly_err_new(LY_LLERR, LY_ESYS, 0, errmsg, NULL, NULL);
+            ret = LY_EVALID;
+            goto cleanup;
+        } else if (rc < 0) {
+            /* error */
+            PCRE2_UCHAR pcre2_errmsg[256] = {0};
+            pcre2_get_error_message(rc, pcre2_errmsg, 256);
+            *err = ly_err_new(LY_LLERR, LY_ESYS, 0, strdup((const char*)pcre2_errmsg), NULL, NULL);
+            ret = LY_ESYS;
+            goto cleanup;
+        }
+
+    cleanup:
+        pcre2_match_data_free(match_data);
+        if (ret) {
+            break;
+        }
+    }
+
+    return ret;
+}
 
 API LY_ERR
 ly_type_validate_range(LY_DATA_TYPE basetype, struct lysc_range *range, int64_t value, const char *canonized, struct ly_err_item **err)
@@ -341,15 +376,21 @@
     } else {
         free(str);
     }
-    if (options & LY_TYPE_OPTS_DYNAMIC) {
-        free((char*)value);
-    }
+
     if (options & LY_TYPE_OPTS_STORE) {
         /* save for the store callback */
         *priv = malloc(sizeof i);
+        if (!(*priv)) {
+            *err = ly_err_new(LY_LLERR, LY_EMEM, 0, "Memory allocation failed.", NULL, NULL);
+            return LY_EMEM;
+        }
         *(int64_t*)(*priv) = i;
     }
 
+    if (options & LY_TYPE_OPTS_DYNAMIC) {
+        free((char*)value);
+    }
+
     return LY_SUCCESS;
 }
 
@@ -424,15 +465,20 @@
         free(str);
     }
 
-    if (options & LY_TYPE_OPTS_DYNAMIC) {
-        free((char*)value);
-    }
     if (options & LY_TYPE_OPTS_STORE) {
         /* save for the store callback */
         *priv = malloc(sizeof u);
+        if (!(*priv)) {
+            *err = ly_err_new(LY_LLERR, LY_EMEM, 0, "Memory allocation failed.", NULL, NULL);
+            return LY_EMEM;
+        }
         *(uint64_t*)(*priv) = u;
     }
 
+    if (options & LY_TYPE_OPTS_DYNAMIC) {
+        free((char*)value);
+    }
+
     return LY_SUCCESS;
 }
 
@@ -515,15 +561,20 @@
     if (options & LY_TYPE_OPTS_CANONIZE) {
         *canonized = lydict_insert(ctx, buf, strlen(buf));
     }
-    if (options & LY_TYPE_OPTS_DYNAMIC) {
-        free((char*)value);
-    }
     if (options & LY_TYPE_OPTS_STORE) {
         /* save for the store callback */
         *priv = malloc(sizeof d);
+        if (!(*priv)) {
+            *err = ly_err_new(LY_LLERR, LY_EMEM, 0, "Memory allocation failed.", NULL, NULL);
+            return LY_EMEM;
+        }
         *(int64_t*)(*priv) = d;
     }
 
+    if (options & LY_TYPE_OPTS_DYNAMIC) {
+        free((char*)value);
+    }
+
     return LY_SUCCESS;
 }
 
@@ -649,6 +700,46 @@
 }
 
 /**
+ * @brief Validate value of the YANG built-in string type.
+ *
+ * Implementation of the ly_type_validate_clb.
+ */
+static LY_ERR
+ly_type_validate_string(struct ly_ctx *ctx, struct lysc_type *type, const char *value, size_t value_len, int options,
+                        const char **canonized, struct ly_err_item **err, void **UNUSED(priv))
+{
+    struct lysc_type_str *type_str = (struct lysc_type_str *)type;
+
+    /* initiate */
+    *err = NULL;
+
+    /* length restriction of the string */
+    if (type_str->length) {
+        char buf[22];
+        snprintf(buf, 22, "%lu", value_len);
+        LY_CHECK_RET(ly_type_validate_range(LY_TYPE_BINARY, type_str->length, value_len, buf, err));
+    }
+
+    /* pattern restrictions */
+    LY_CHECK_RET(ly_type_validate_patterns(type_str->patterns, value, value_len, err));
+
+    if (options & LY_TYPE_OPTS_CANONIZE) {
+        if (options & LY_TYPE_OPTS_DYNAMIC) {
+            *canonized = lydict_insert_zc(ctx, (char*)value);
+            value = NULL;
+        } else {
+            *canonized = lydict_insert(ctx, value_len ? value : "", value_len);
+        }
+    }
+
+    if (options & LY_TYPE_OPTS_DYNAMIC) {
+        free((char*)value);
+    }
+
+    return LY_SUCCESS;
+}
+
+/**
  * @brief Validate and canonize value of the YANG built-in bits type.
  *
  * Implementation of the ly_type_validate_clb.
@@ -878,6 +969,7 @@
     if (options & LY_TYPE_OPTS_CANONIZE) {
         if (options & LY_TYPE_OPTS_DYNAMIC) {
             *canonized = lydict_insert_zc(ctx, (char*)value);
+            value = NULL;
         } else {
             *canonized = lydict_insert(ctx, value_len ? value : "", value_len);
         }
@@ -888,6 +980,10 @@
         *priv = &type_enum->enums[u];
     }
 
+    if (options & LY_TYPE_OPTS_DYNAMIC) {
+        free((char*)value);
+    }
+
     return LY_SUCCESS;
 
 error:
@@ -925,7 +1021,7 @@
     {.type = LY_TYPE_UINT16, .validate = ly_type_validate_uint, .store = ly_type_store_uint, .free = NULL},
     {.type = LY_TYPE_UINT32, .validate = ly_type_validate_uint, .store = ly_type_store_uint, .free = NULL},
     {.type = LY_TYPE_UINT64, .validate = ly_type_validate_uint, .store = ly_type_store_uint, .free = NULL},
-    {0}, /* TODO LY_TYPE_STRING */
+    {.type = LY_TYPE_STRING, .validate = ly_type_validate_string, .store = NULL, .free = NULL},
     {.type = LY_TYPE_BITS, .validate = ly_type_validate_bits, .store = ly_type_store_bits, .free = ly_type_free_bits},
     {0}, /* TODO LY_TYPE_BOOL */
     {.type = LY_TYPE_DEC64, .validate = ly_type_validate_decimal64, .store = ly_type_store_decimal64, .free = NULL},
diff --git a/src/plugins_types.h b/src/plugins_types.h
index 3dff5c6..9c48fd8 100644
--- a/src/plugins_types.h
+++ b/src/plugins_types.h
@@ -178,6 +178,19 @@
                   uint64_t *ret, struct ly_err_item **err);
 
 /**
+ * @brief Convert a string with a decimal64 value into libyang representation:
+ * ret = value * 10^fraction-digits
+ *
+ * @param[in] fraction_digits Fraction-digits of the decimal64 type.
+ * @param[in] value Value string to parse.
+ * @param[in] value_len Length of the @p value (mandatory parameter).
+ * @param[out] ret Parsed decimal64 value representing original value * 10^fraction-digits (optional).
+ * @param[out] err Error information in case of failure. The error structure can be freed by ly_err_free().
+ * @return LY_ERR value according to the result of the parsing and validation.
+ */
+LY_ERR parse_dec64(uint8_t fraction_digits, const char *value, size_t value_len, int64_t *ret, struct ly_err_item **err);
+
+/**
  * @brief Data type validator for a range/length-restricted values.
  *
  * @param[in] basetype Base built-in type of the type with the range specified to get know if the @p range structure represents range or length restriction.
@@ -189,5 +202,18 @@
  */
 LY_ERR ly_type_validate_range(LY_DATA_TYPE basetype, struct lysc_range *range, int64_t value, const char *canonized, struct ly_err_item **err);
 
+/**
+ * @brief Data type validator for pattern-restricted string values.
+ *
+ * @param[in] patterns ([Sized array](@ref sizedarrays)) of the compiled list of pointers to the pattern restrictions.
+ * The array can be found in the lysc_type_str::patterns structure.
+ * @param[in] str String to validate.
+ * @param[in] str_len Length of the string to validate (mandatory).
+ * @param[out] err Error information in case of failure or non-matching @p str. The error structure can be freed by ly_err_free().
+ * @return LY_SUCCESS when @p matches all the patterns.
+ * @return LY_EVALID when @p does not match any of the patterns.
+ * @return LY_ESYS in case of PCRE2 error.
+ */
+LY_ERR ly_type_validate_patterns(struct lysc_pattern **patterns, const char *str, size_t str_len, struct ly_err_item **err);
 
 #endif /* LY_PLUGINS_TYPES_H_ */
diff --git a/src/tree_data.c b/src/tree_data.c
index 5bd7b9f..eb551e2 100644
--- a/src/tree_data.c
+++ b/src/tree_data.c
@@ -251,7 +251,3 @@
 
     return result;
 }
-
-
-
-
diff --git a/src/tree_data_helpers.c b/src/tree_data_helpers.c
index 3f385a2..fb9c3da 100644
--- a/src/tree_data_helpers.c
+++ b/src/tree_data_helpers.c
@@ -84,14 +84,14 @@
 }
 
 LY_ERR
-lyd_value_validate(struct lyd_node_term *node, const char *value, size_t value_len, int options)
+lyd_value_parse(struct lyd_node_term *node, const char *value, size_t value_len, int dynamic)
 {
     LY_ERR ret = LY_SUCCESS;
     struct ly_err_item *err = NULL;
     struct ly_ctx *ctx;
     struct lysc_type *type;
     void *priv = NULL;
-
+    int options = LY_TYPE_OPTS_VALIDATE | LY_TYPE_OPTS_CANONIZE | LY_TYPE_OPTS_STORE | (dynamic ? LY_TYPE_OPTS_DYNAMIC : 0);
     assert(node);
 
     ctx = node->schema->module->ctx;
@@ -104,11 +104,13 @@
             ly_err_free(err);
             goto error;
         }
-    } else if (options & LY_TYPE_OPTS_CANONIZE) {
+    } else if (dynamic) {
+        node->value.canonized = lydict_insert_zc(ctx, (char*)value);
+    } else {
         node->value.canonized = lydict_insert(ctx, value, value_len);
     }
 
-    if ((options & LY_TYPE_OPTS_STORE) && type->plugin->store) {
+    if (type->plugin->store) {
         ret = type->plugin->store(ctx, type, options, &node->value, &err, &priv);
         if (ret) {
             ly_err_print(err);
diff --git a/src/tree_data_internal.h b/src/tree_data_internal.h
index f6be23c..ec11578 100644
--- a/src/tree_data_internal.h
+++ b/src/tree_data_internal.h
@@ -38,13 +38,15 @@
 LY_ERR lyd_parse_check_options(struct ly_ctx *ctx, int options, const char *func);
 
 /**
- * @brief Validate the given value according to the type's rules.
+ * @brief Validate, canonize and store the given @p value into the node according to the node's type's rules.
  *
- * According to the given options, the value can be also canonized or stored into the node's value structure.
- *
- * @param[in] options [Type validation options ](@ref plugintypeopts).
+ * @param[in] node Data node for with the @p value.
+ * @param[in] value String value to be parsed.
+ * @param[in] value_len Length of the give @p value (mandatory).
+ * @param[in] dynamic Flag if @p value is a dynamically allocated memory and should be directly consumed/freed inside the function.
+ * @return LY_ERR value.
  */
-LY_ERR lyd_value_validate(struct lyd_node_term *node, const char *value, size_t value_len, int options);
+LY_ERR lyd_value_parse(struct lyd_node_term *node, const char *value, size_t value_len, int dynamic);
 
 /**
  * @brief Parse XML string as YANG data tree.
diff --git a/src/tree_schema_compile.c b/src/tree_schema_compile.c
index 4e3bed6..2d73a01 100644
--- a/src/tree_schema_compile.c
+++ b/src/tree_schema_compile.c
@@ -1616,12 +1616,6 @@
 
     ptr = perl_regex;
 
-    if (strncmp(pattern + strlen(pattern) - 2, ".*", 2)) {
-        /* we will add line-end anchoring */
-        ptr[0] = '(';
-        ++ptr;
-    }
-
     for (orig_ptr = pattern; orig_ptr[0]; ++orig_ptr) {
         if (orig_ptr[0] == '$') {
             ptr += sprintf(ptr, "\\$");
@@ -1632,13 +1626,8 @@
             ++ptr;
         }
     }
-
-    if (strncmp(pattern + strlen(pattern) - 2, ".*", 2)) {
-        ptr += sprintf(ptr, ")$");
-    } else {
-        ptr[0] = '\0';
-        ++ptr;
-    }
+    ptr[0] = '\0';
+    ++ptr;
 
     /* substitute Unicode Character Blocks with exact Character Ranges */
     while ((ptr = strstr(perl_regex, "\\p{Is"))) {
@@ -1692,7 +1681,8 @@
     }
 
     /* must return 0, already checked during parsing */
-    code_local = pcre2_compile((PCRE2_SPTR)perl_regex, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_ANCHORED | PCRE2_DOLLAR_ENDONLY | PCRE2_NO_AUTO_CAPTURE,
+    code_local = pcre2_compile((PCRE2_SPTR)perl_regex, PCRE2_ZERO_TERMINATED,
+                               PCRE2_UTF | PCRE2_ANCHORED | PCRE2_ENDANCHORED | PCRE2_DOLLAR_ENDONLY | PCRE2_NO_AUTO_CAPTURE,
                            &err_code, &err_offset, NULL);
     if (!code_local) {
         PCRE2_UCHAR err_msg[256] = {0};
diff --git a/tests/features/test_types.c b/tests/features/test_types.c
index 6588d0f..329c150 100644
--- a/tests/features/test_types.c
+++ b/tests/features/test_types.c
@@ -70,7 +70,9 @@
             "leaf bits {type bits {bit zero; bit one {if-feature f;} bit two;}}"
             "leaf enums {type enumeration {enum white; enum yellow {if-feature f;}}}"
             "leaf dec64 {type decimal64 {fraction-digits 1; range 1.5..10;}}"
-            "leaf dec64-norestr {type decimal64 {fraction-digits 18;}}}";
+            "leaf dec64-norestr {type decimal64 {fraction-digits 18;}}"
+            "leaf str {type string {length 8..10; pattern '[a-z ]*';}}"
+            "leaf str-norestr {type string;}}";
 
     s = calloc(1, sizeof *s);
     assert_non_null(s);
@@ -295,6 +297,42 @@
 }
 
 static void
+test_string(void **state)
+{
+    struct state_s *s = (struct state_s*)(*state);
+    s->func = test_string;
+
+    struct lyd_node *tree;
+    struct lyd_node_term *leaf;
+
+    const char *data = "<str xmlns=\"urn:tests:types\">teststring</str>";
+
+    /* valid data */
+    assert_non_null(tree = lyd_parse_mem(s->ctx, data, LYD_XML, 0));
+    assert_int_equal(LYS_LEAF, tree->schema->nodetype);
+    assert_string_equal("str", tree->schema->name);
+    leaf = (struct lyd_node_term*)tree;
+    assert_string_equal("teststring", leaf->value.canonized);
+    lyd_free_all(tree);
+
+    /* invalid length */
+    data = "<str xmlns=\"urn:tests:types\">short</str>";
+    assert_null(lyd_parse_mem(s->ctx, data, LYD_XML, 0));
+    logbuf_assert("Length \"5\" does not satisfy the length constraint. /");
+
+    data = "<str xmlns=\"urn:tests:types\">tooooo long</str>";
+    assert_null(lyd_parse_mem(s->ctx, data, LYD_XML, 0));
+    logbuf_assert("Length \"11\" does not satisfy the length constraint. /");
+
+    /* invalid pattern */
+    data = "<str xmlns=\"urn:tests:types\">string15</str>";
+    assert_null(lyd_parse_mem(s->ctx, data, LYD_XML, 0));
+    logbuf_assert("String \"string15\" does not conforms to the 1. pattern restriction of its type. /");
+
+    s->func = NULL;
+}
+
+static void
 test_bits(void **state)
 {
     struct state_s *s = (struct state_s*)(*state);
@@ -476,6 +514,7 @@
         cmocka_unit_test_setup_teardown(test_int, setup, teardown),
         cmocka_unit_test_setup_teardown(test_uint, setup, teardown),
         cmocka_unit_test_setup_teardown(test_dec64, setup, teardown),
+        cmocka_unit_test_setup_teardown(test_string, setup, teardown),
         cmocka_unit_test_setup_teardown(test_bits, setup, teardown),
         cmocka_unit_test_setup_teardown(test_enums, setup, teardown),
         cmocka_unit_test_setup_teardown(test_binary, setup, teardown),
diff --git a/tools/re/main.c b/tools/re/main.c
new file mode 100644
index 0000000..96cc961
--- /dev/null
+++ b/tools/re/main.c
@@ -0,0 +1,313 @@
+/**
+ * @file main.c
+ * @author Radek Krejci <rkrejci@cesnet.cz>
+ * @brief libyang's YANG Regular Expression tool
+ *
+ * Copyright (c) 2017 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
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <string.h>
+#include <getopt.h>
+#include <unistd.h>
+
+#include "context.h"
+#include "plugins_types.h"
+
+void
+help(void)
+{
+    fprintf(stdout, "YANG Regular Expressions processor.\n");
+    fprintf(stdout, "Usage:\n");
+    fprintf(stdout, "    yangre [-hv]\n");
+    fprintf(stdout, "    yangre [-V] -p <regexp1> [-i] [-p <regexp2> [-i] ...] <string>\n");
+    fprintf(stdout, "    yangre [-V] -f <file>\n");
+    fprintf(stdout, "Returns 0 if string matches the pattern(s), 1 if not and -1 on error.\n\n");
+    fprintf(stdout, "Options:\n"
+        "  -h, --help              Show this help message and exit.\n"
+        "  -v, --version           Show version number and exit.\n"
+        "  -V, --verbose           Print the processing information.\n"
+        "  -i, --invert-match      Invert-match modifier for the closest preceding\n"
+        "                          pattern.\n"
+        "  -p, --pattern=\"REGEXP\"  Regular expression including the quoting,\n"
+        "                          which is applied the same way as in a YANG module.\n"
+        "  -f, --file=\"FILE\"     List of patterns and the <string> (separated by an\n"
+        "                          empty line) are taken from <file>. Invert-match is\n"
+        "                          indicated by the single space character at the \n"
+        "                          beginning of the pattern line. YANG quotation around\n"
+        "                          patterns is still expected, but that avoids issues with\n"
+        "                          reading quotation by shell. Avoid newline at the end\n"
+        "                          of the string line to represent empty <string>.");
+    fprintf(stdout, "Examples:\n"
+        "  pattern \"[0-9a-fA-F]*\";      -> yangre -p '\"[0-9a-fA-F]*\"' '1F'\n"
+        "  pattern '[a-zA-Z0-9\\-_.]*';  -> yangre -p \"'[a-zA-Z0-9\\-_.]*'\" 'a-b'\n"
+        "  pattern [xX][mM][lL].*;      -> yangre -p '[xX][mM][lL].*' 'xml-encoding'\n\n");
+    fprintf(stdout, "Note that to pass YANG quoting through your shell, you are supposed to use\n"
+                    "the other quotation around. For not-quoted patterns, use single quotes.\n\n");
+}
+
+void
+version(void)
+{
+    fprintf(stdout, "yangre %s\n", PROJECT_VERSION);
+}
+
+void
+pattern_error(LY_LOG_LEVEL level, const char *msg, const char *path)
+{
+    (void) path; /* unused */
+
+    if (level == LY_LLERR && strcmp(msg, "Module \"yangre\" parsing failed.")) {
+        fprintf(stderr, "yangre error: %s\n", msg);
+    }
+}
+
+static const char *module_start = "module yangre {"
+    "yang-version 1.1;"
+    "namespace urn:cesnet:libyang:yangre;"
+    "prefix re;"
+    "leaf pattern {"
+    "  type string {";
+static const char *module_invertmatch = " { modifier invert-match; }";
+static const char *module_match = ";";
+static const char *module_end = "}}}";
+
+static int
+add_pattern(char ***patterns, int **inverts, int *counter, char *pattern)
+{
+    void *reallocated1, *reallocated2;
+
+    (*counter)++;
+    reallocated1 = realloc(*patterns, *counter * sizeof **patterns);
+    reallocated2 = realloc(*inverts, *counter * sizeof **inverts);
+    if (!reallocated1 || !reallocated2) {
+        fprintf(stderr, "yangre error: memory allocation error.\n");
+        free(reallocated1);
+        free(reallocated2);
+        return EXIT_FAILURE;
+    }
+    (*patterns) = reallocated1;
+    (*patterns)[*counter - 1] = strdup(pattern);
+    (*inverts) = reallocated2;
+    (*inverts)[*counter - 1] = 0;
+
+    return EXIT_SUCCESS;
+}
+
+int
+main(int argc, char* argv[])
+{
+    LY_ERR match;
+    int i, opt_index = 0, ret = -1, verbose = 0, blankline = 0;
+    struct option options[] = {
+        {"help",             no_argument,       NULL, 'h'},
+        {"file",             required_argument, NULL, 'f'},
+        {"invert-match",     no_argument,       NULL, 'i'},
+        {"pattern",          required_argument, NULL, 'p'},
+        {"version",          no_argument,       NULL, 'v'},
+        {"verbose",          no_argument,       NULL, 'V'},
+        {NULL,               0,                 NULL, 0}
+    };
+    char **patterns = NULL, *str = NULL, *modstr = NULL, *s;
+    int *invert_match = NULL;
+    int patterns_count = 0;
+    struct ly_ctx *ctx = NULL;
+    const struct lys_module *mod;
+    FILE *infile = NULL;
+    size_t len = 0;
+    ssize_t l;
+    struct lysc_type *type;
+    struct ly_err_item *err;
+
+    opterr = 0;
+    while ((i = getopt_long(argc, argv, "hf:ivVp:", options, &opt_index)) != -1) {
+        switch (i) {
+        case 'h':
+            help();
+            ret = -2; /* continue to allow printing version and help at once */
+            break;
+        case 'f':
+            if (infile) {
+                help();
+                fprintf(stderr, "yangre error: multiple input files are not supported.\n");
+                goto cleanup;
+            } else if (patterns_count) {
+                help();
+                fprintf(stderr, "yangre error: command line patterns cannot be mixed with file input.\n");
+                goto cleanup;
+            }
+            infile = fopen(optarg, "r");
+            if (!infile) {
+                fprintf(stderr, "yangre error: unable to open input file %s (%s).\n", optarg, strerror(errno));
+                goto cleanup;
+            }
+
+            while((l = getline(&str, &len, infile)) != -1) {
+                if (!blankline && str[0] == '\n') {
+                    /* blank line */
+                    blankline = 1;
+                    continue;
+                }
+                if (str[0] != '\n' && str[l - 1] == '\n') {
+                    /* remove ending newline */
+                    str[l - 1] = '\0';
+                }
+                if (blankline) {
+                    /* done - str is now the string to check */
+                    blankline = 0;
+                    break;
+                    /* else read the patterns */
+                } else if (add_pattern(&patterns, &invert_match, &patterns_count,
+                                       str[0] == ' ' ? &str[1] : str)) {
+                    goto cleanup;
+                }
+                if (str[0] == ' ') {
+                    /* set invert-match */
+                    invert_match[patterns_count - 1] = 1;
+                }
+            }
+            if (blankline) {
+                /* corner case, no input after blankline meaning the pattern to check is empty */
+                if (str != NULL) {
+                    free(str);
+                }
+                str = malloc(sizeof(char));
+                str[0] = '\0';
+            }
+            break;
+        case 'i':
+            if (!patterns_count || invert_match[patterns_count - 1]) {
+                help();
+                fprintf(stderr, "yangre error: invert-match option must follow some pattern.\n");
+                goto cleanup;
+            }
+            invert_match[patterns_count - 1] = 1;
+            break;
+        case 'p':
+            if (infile) {
+                help();
+                fprintf(stderr, "yangre error: command line patterns cannot be mixed with file input.\n");
+                goto cleanup;
+            }
+            if (add_pattern(&patterns, &invert_match, &patterns_count, optarg)) {
+                goto cleanup;
+            }
+            break;
+        case 'v':
+            version();
+            ret = -2; /* continue to allow printing version and help at once */
+            break;
+        case 'V':
+            verbose = 1;
+            break;
+        default:
+            help();
+            if (optopt) {
+                fprintf(stderr, "yangre error: invalid option: -%c\n", optopt);
+            } else {
+                fprintf(stderr, "yangre error: invalid option: %s\n", argv[optind - 1]);
+            }
+            goto cleanup;
+        }
+    }
+
+    if (ret == -2) {
+        goto cleanup;
+    }
+
+    if (!str) {
+        /* check options compatibility */
+        if (optind >= argc) {
+            help();
+            fprintf(stderr, "yangre error: missing <string> parameter to process.\n");
+            goto cleanup;
+        } else if (!patterns_count) {
+            help();
+            fprintf(stderr, "yangre error: missing pattern parameter to use.\n");
+            goto cleanup;
+        }
+        str = argv[optind];
+    }
+
+    for (modstr = (char*)module_start, i = 0; i < patterns_count; i++) {
+        if (asprintf(&s, "%s pattern %s%s", modstr, patterns[i], invert_match[i] ? module_invertmatch : module_match) == -1) {
+            fprintf(stderr, "yangre error: memory allocation failed.\n");
+            goto cleanup;
+        }
+        if (modstr != module_start) {
+            free(modstr);
+        }
+        modstr = s;
+    }
+    if (asprintf(&s, "%s%s", modstr, module_end) == -1) {
+        fprintf(stderr, "yangre error: memory allocation failed.\n");
+        goto cleanup;
+    }
+    if (modstr != module_start) {
+        free(modstr);
+    }
+    modstr = s;
+
+    if (ly_ctx_new(NULL, 0, &ctx)) {
+        goto cleanup;
+    }
+
+    ly_set_log_clb(pattern_error, 0);
+    mod = lys_parse_mem(ctx, modstr, LYS_IN_YANG);
+    if (!mod || !mod->compiled || !mod->compiled->data) {
+        goto cleanup;
+    }
+
+    type = ((struct lysc_node_leaf*)mod->compiled->data)->type;
+    match = type->plugin->validate(ctx, type, str, strlen(str), LY_TYPE_OPTS_VALIDATE, NULL, &err, NULL);
+    if (verbose) {
+        for (i = 0; i < patterns_count; i++) {
+            fprintf(stdout, "pattern  %d: %s\n", i + 1, patterns[i]);
+            fprintf(stdout, "matching %d: %s\n", i + 1, invert_match[i] ? "inverted" : "regular");
+        }
+        fprintf(stdout, "string    : %s\n", str);
+        if (match == LY_SUCCESS) {
+            fprintf(stdout, "result    : matching\n");
+        } else if (match == LY_EVALID) {
+            fprintf(stdout, "result    : not matching\n");
+        } else {
+            fprintf(stdout, "result    : error (%s)\n", err->msg);
+        }
+    }
+    if (match == LY_SUCCESS) {
+        ret = 0;
+    } else if (match == LY_EVALID) {
+        ret = 1;
+    } else {
+        ret = -1;
+    }
+
+cleanup:
+    ly_ctx_destroy(ctx, NULL);
+    for (i = 0; i < patterns_count; i++) {
+        free(patterns[i]);
+    }
+    free(patterns);
+    free(invert_match);
+    free(modstr);
+    if (infile) {
+        fclose(infile);
+        free(str);
+    }
+    ly_err_free(err);
+
+    return ret;
+}
diff --git a/tools/re/yangre.1 b/tools/re/yangre.1
new file mode 100644
index 0000000..e7b572b
--- /dev/null
+++ b/tools/re/yangre.1
@@ -0,0 +1,118 @@
+.\" Manpage for yanglint.
+.\" Process this file with
+.\" groff -man -Tascii yangre.1
+.\"
+
+.TH YANGRE 1 "2018-11-09" "libyang"
+.SH NAME
+yangre \- YANG regular expression processor
+.
+.SH SYNOPSIS
+.B yangre
+[\-V] \-p \fIREGEXP\fP [\-i] [\-p \fIREGEXP\fP [\-i]...] \fISTRING\fP
+.br
+.B yangre
+[\-V] \-f \fIFILE\fP
+.
+.SH DESCRIPTION
+\fByangre\fP is a command-line tool to test and evaluate regular expressions
+for use in YANG schemas.  Supported regular expressions are defined by the
+W3C's XML-Schema standard.
+
+\fByangre\fP can be used either with regular expressions and a target string
+on the command line or with input from a file.  The latter is particularly
+useful to avoid dealing with proper shell escaping of regular expression
+patterns, which can be somewhat tricky.
+.
+.SH GENERAL OPTIONS
+.TP
+.BR "\-h\fR,\fP \-\^\-help"
+.br
+Outputs usage help and exits.
+.TP
+.BR "\-v\fR,\fP \-\^\-version"
+.br
+Outputs the version number and exits.
+.TP
+.BR "\-V\fR,\fP \-\^\-verbose"
+Increases the verbosity level. If not specified, only errors are printed, with
+each appearance it adds: warnings, verbose messages, debug messages (if compiled
+with debug information).
+.SH COMMAND LINE INPUT
+.TP
+.BR "\-p \fIREGEXP\fP\fR,\fP \-\^\-pattern=\fIREGEXP\fP"
+.br
+One or more regular expression patterns to be tested against the input
+string.  Supplied expressions are tested in the order they appear on the
+command line.  Testing is aborted when an expression does not match (or
+does match, if the \fB-i\fP option is used.)
+.TP
+.BR "\-i\fR,\fP \-\^\-invert-match"
+.br
+Reverse match condition for the previous pattern.  If the pattern matches,
+an error is printed and evaluation is aborted.
+.TP
+.BR "\fISTRING\fP"
+.br
+Target text input to match the regular expression(s) against.  The same
+text is used for all regular expressions.  Note that only the first
+argument is used by \fByangre\fP, if it contains spaces or other shell
+metacharacters they must be properly escaped.  Additional arguments are
+silently ignored.
+.SH FILE INPUT
+.TP
+.BR "\-f \fIFILE\fP\fR,\fP \-\^\-file=\fIFILE\fP"
+Read both patterns and target text from the specified input file.
+
+\fIFILE\fP must consist of one or more YANG regular expressions, each on
+their own line, followed by a blank line and one line of target text.  No
+preprocessing is done on file input, there are no comment lines and
+whitespace is not stripped.  A single space character at the beginning of
+a pattern line inverts the match condition for the pattern on that line.
+Patterns must still be properly quoted as mandated by the YANG standard.
+.SH RETURN VALUES
+.TP
+0
+.I Successful match
+.br
+The target text matched for all patterns.
+.TP
+1
+.I Pattern mismatch
+.br
+One or more patterns did not match the target text.  An error message is
+printed to stderr describing which pattern was the first not to match.
+.TP
+255
+.I Other error
+.br
+One or more patterns could not be processed or some other error occurred that
+precluded processing.
+.SH EXAMPLES
+.IP \[bu] 2
+Test a single pattern:
+    yangre -p 'te.*xt' text_text
+.IP \[bu]
+Test multiple patterns:
+    yangre -p '.*pat1' -p 'pat2.*' -p 'notpat' -i pat2testpat1
+.IP \[bu]
+Input from a file:
+    cat > /tmp/patterns <<EOF
+    .*pat1
+    pat2.*
+     notpat
+
+    pat2testpat1
+    EOF
+    yangre -f /tmp/patterns
+
+.SH SEE ALSO
+https://github.com/CESNET/libyang (libyang homepage and Git repository)
+.
+.SH AUTHORS
+Radek Krejci <rkrejci@cesnet.cz>, Michal Vasko <mvasko@cesnet.cz>
+.br
+This man page was written by David Lamparter <equinox@diac24.net>
+.
+.SH COPYRIGHT
+Copyright \(co 2015-2018 CESNET, a.l.e.