Repository: eclipse-bluechi/bluechi (language: python)

Issue:
RFE: list-units --filter=myservice and info-units
## Describe the bug

This is an RFE to add `--filter` option into `list-units` command.  Today grep can be used as workaround.

Example(s):
```
# hirtectl list-units --filter=service1
# hirtectl list-units --filter=service1, service2
```

Please also add a `command alias for this part of the code` called `hirte info-units`, example:
```
# hirtectl info-units service1
# hirtectl info-units service1, service2
```


Discussion / hints:
I think you mean hirtectl, not hirte.
I had assumed 
hirtectl list-units service1 service2 would work. Along with
hirtectl list-unitx service1@node1 service2 would work.
In this case it would list only service1 on node1, but service2 on any node where it exists.

> I think you mean hirtectl, not hirte.

correct Dan, thanks for pointing it.
Sounds good too @rhatdan. Not implemented yet.

```
# hirtectl list-units boot.mount
Failed to issue method call: Unknown object '/org/containers/hirte/node/boot_2emount'.
Call to manager failed: Invalid request descriptor

# hirtectl list-units boot.mount@node1
Failed to issue method call: Unknown object '/org/containers/hirte/node/boot_2emount_40node1'.
Call to manager failed: Invalid request descriptor
```

```
# hirtectl list-units | grep boot.mount
control             |boot.mount                                                 | inactive|     dead
node1               |boot.mount                                                 | inactive|     dead
```


Ok so the --filter makes some sense then. Since third option is considered a node specifier.

Fix patch:
diff --git a/doc/man/hirtectl.1.md b/doc/man/hirtectl.1.md
index 21b5de1140..929f900fb7 100644
--- a/doc/man/hirtectl.1.md
+++ b/doc/man/hirtectl.1.md
@@ -29,3 +29,11 @@ Performs one of the listed lifecycle operations on the given systemd unit for th
 ### **hirtectl** *list-units* [*agent*]
 
 Fetches information about all systemd units on the hirte-agents. If [hirte-agent] is not specified, all agents are queried.
+
+### **hirtectl** *monitor* [*agent*] [*unit1*,*unit2*,*...*]
+
+Creates a monitor on the given agent to observe changes in the specified units. Wildcards **\*** to match all agents and/or units are also supported. 
+
+**Example:**
+
+hirtectl monitor \\\* dbus.service,apache2.service
\ No newline at end of file
diff --git a/src/client/client.c b/src/client/client.c
index 016093124d..0744a4282c 100644
--- a/src/client/client.c
+++ b/src/client/client.c
@@ -1,105 +1,17 @@
 /* SPDX-License-Identifier: GPL-2.0-or-later */
 #include <errno.h>
+#include <getopt.h>
 
 #include "libhirte/bus/utils.h"
+#include "libhirte/common/opt.h"
 #include "libhirte/common/time-util.h"
 #include "libhirte/service/shutdown.h"
 
 #include "client.h"
+#include "method_list_units.h"
+#include "method_monitor.h"
 
-int fetch_unit_list(
-                Client *client,
-                char *interface,
-                char *typestring,
-                int (*parse_unit_info)(sd_bus_message *, UnitInfo *),
-                UnitList *unit_list) {
-        _cleanup_sd_bus_error_ sd_bus_error error = SD_BUS_ERROR_NULL;
-        _cleanup_sd_bus_message_ sd_bus_message *message = NULL;
-
-        int r = 0;
-
-        r = sd_bus_call_method(
-                        client->api_bus,
-                        HIRTE_INTERFACE_BASE_NAME,
-                        client->object_path,
-                        interface,
-                        "ListUnits",
-                        &error,
-                        &message,
-                        "");
-        if (r < 0) {
-                fprintf(stderr, "Failed to issue method call: %s\n", error.message);
-                return r;
-        }
-
-        r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, typestring);
-        if (r < 0) {
-                fprintf(stderr, "Failed to read sd-bus message: %s\n", strerror(-r));
-                return r;
-        }
-
-        for (;;) {
-                _cleanup_unit_ UnitInfo *info = new_unit();
-
-                r = (*parse_unit_info)(message, info);
-                if (r < 0) {
-                        fprintf(stderr, "Failed to parse unit info: %s\n", strerror(-r));
-                        return r;
-                }
-                if (r == 0) {
-                        break;
-                }
-                LIST_APPEND(units, unit_list->units, unit_ref(info));
-        }
-
-        return r;
-}
-
-int method_list_units_on_all(Client *client) {
-        int r = 0;
-        _cleanup_unit_list_ UnitList *unit_list = new_unit_list();
-
-        r = asprintf(&client->object_path, "%s", HIRTE_OBJECT_PATH);
-        if (r < 0) {
-                return r;
-        }
-
-        r = fetch_unit_list(
-                        client,
-                        MANAGER_INTERFACE,
-                        NODE_AND_UNIT_INFO_STRUCT_TYPESTRING,
-                        &bus_parse_unit_on_node_info,
-                        unit_list);
-        if (r < 0) {
-                return r;
-        }
-
-        print_nodes_unit_list(&client->printer, unit_list);
-
-        return 0;
-}
-
-int method_list_units_on(const char *name, Client *client) {
-        int r = 0;
-        _cleanup_unit_list_ UnitList *unit_list = new_unit_list();
-
-        r = assemble_object_path_string(NODE_OBJECT_PATH_PREFIX, name, &client->object_path);
-        if (r < 0) {
-                return r;
-        }
-
-        r = fetch_unit_list(
-                        client, NODE_INTERFACE, UNIT_INFO_STRUCT_TYPESTRING, &bus_parse_unit_info, unit_list);
-        if (r < 0) {
-                return r;
-        }
-
-        print_unit_list(&client->printer, unit_list);
-
-        return 0;
-}
-
-Client *new_client(char *op, int opargc, char **opargv) {
+Client *new_client(char *op, int opargc, char **opargv, char *opt_filter_glob) {
         Client *client = malloc0(sizeof(Client));
         if (client == NULL) {
                 return NULL;
@@ -109,7 +21,7 @@ Client *new_client(char *op, int opargc, char **opargv) {
         client->op = op;
         client->opargc = opargc;
         client->opargv = opargv;
-        client->printer = get_simple_printer();
+        client->opt_filter_glob = opt_filter_glob;
 
         return client;
 }
@@ -135,37 +47,6 @@ void client_unref(Client *client) {
         free(client);
 }
 
-UnitList *new_unit_list() {
-        _cleanup_unit_list_ UnitList *unit_list = malloc0(sizeof(UnitList));
-
-        if (unit_list == NULL) {
-                return NULL;
-        }
-
-        unit_list->ref_count = 1;
-        LIST_HEAD_INIT(unit_list->units);
-
-        return steal_pointer(&unit_list);
-}
-
-UnitList *unit_list_ref(UnitList *unit_list) {
-        unit_list->ref_count++;
-        return unit_list;
-}
-
-void unit_list_unref(UnitList *unit_list) {
-        unit_list->ref_count--;
-        if (unit_list->ref_count != 0) {
-                return;
-        }
-
-        UnitInfo *unit = NULL, *next_unit = NULL;
-        LIST_FOREACH_SAFE(units, unit, next_unit, unit_list->units) {
-                unit_unref(unit);
-        }
-        free(unit_list);
-}
-
 static sd_bus_message *wait_for_job(Client *client, char *object_path) {
         int r = 0;
 
@@ -438,10 +319,15 @@ int client_call_manager(Client *client) {
                 r = print_client_usage("hirte");
         } else if (streq(client->op, "list-units")) {
                 if (client->opargc == 0) {
-                        r = method_list_units_on_all(client);
-                } else {
-                        r = method_list_units_on(client->opargv[0], client);
+                        r = method_list_units_on_all(
+                                        client->api_bus, print_unit_list_simple, client->opt_filter_glob);
+                        return r;
                 }
+
+                char *node_name = client->opargv[0];
+                return method_list_units_on(
+                                client->api_bus, node_name, print_unit_list_simple, client->opt_filter_glob);
+
         } else if (streq(client->op, "start")) {
                 if (client->opargc != 2) {
                         return -EINVAL;
@@ -490,8 +376,8 @@ int print_client_usage(char *argv) {
         printf("Available command:\n");
         printf("  - help: shows this help message\n");
         printf("    usage: help\n");
-        printf("  - list-units: returns the list of systemd service running on a specific not or on all the nodes\n");
-        printf("    usage: list-units [nodename]\n");
+        printf("  - list-units: returns the list of systemd services running on a specific or on all nodes\n");
+        printf("    usage: list-units [nodename] [--filter=glob]\n");
         printf("  - start: starts a specific systemd service (or timer, or slice) on a specific node\n");
         printf("    usage: start nodename unitname\n");
         printf("  - stop: stop a specific systemd service (or timer, or slice) on a specific node\n");
@@ -504,5 +390,7 @@ int print_client_usage(char *argv) {
         printf("    usage: metrics [enable|disable]\n");
         printf("  - metrics listen: listen and print incoming metrics reports\n");
         printf("    usage: metrics listen\n");
+        printf("  - monitor: creates a monitor on the given node to observe changes in the specified units\n");
+        printf("    usage: monitor [node] [unit1,unit2,...]\n");
         return 0;
 }
diff --git a/src/client/client.h b/src/client/client.h
index 118378297a..db07dacb32 100644
--- a/src/client/client.h
+++ b/src/client/client.h
@@ -4,8 +4,6 @@
 #include "libhirte/bus/utils.h"
 #include "libhirte/common/common.h"
 
-#include "method_monitor.h"
-#include "printer.h"
 #include "types.h"
 
 struct Client {
@@ -15,35 +13,21 @@ struct Client {
         int opargc;
         char **opargv;
 
+        char *opt_filter_glob;
+
         sd_bus *api_bus;
         char *object_path;
 
         char *pending_job_name;
         sd_bus_message *pending_job_result;
-
-        Printer printer;
 };
 
-Client *new_client(char *op, int opargc, char **opargv);
+Client *new_client(char *op, int opargc, char **opargv, char *opt_filter_glob);
 Client *client_ref(Client *client);
 void client_unref(Client *client);
 
-struct UnitList {
-        int ref_count;
-
-        LIST_FIELDS(UnitList, node_units);
-
-        LIST_HEAD(UnitInfo, units);
-};
-
-UnitList *new_unit_list();
-UnitList *unit_list_ref(UnitList *unit_list);
-void unit_list_unref(UnitList *unit_list);
-
 int client_call_manager(Client *client);
 int print_client_usage(char *argv);
 
 DEFINE_CLEANUP_FUNC(Client, client_unref)
 #define _cleanup_client_ _cleanup_(client_unrefp)
-DEFINE_CLEANUP_FUNC(UnitList, unit_list_unref)
-#define _cleanup_unit_list_ _cleanup_(unit_list_unrefp)
diff --git a/src/client/main.c b/src/client/main.c
index 59addde713..5246ff7db4 100644
--- a/src/client/main.c
+++ b/src/client/main.c
@@ -8,9 +8,11 @@
 
 #include "client.h"
 
-const struct option options[] = { { ARG_HELP, no_argument, 0, ARG_HELP_SHORT }, { NULL, 0, 0, '\0' } };
+const struct option options[] = { { ARG_HELP, no_argument, 0, ARG_HELP_SHORT },
+                                  { ARG_FILTER, required_argument, 0, ARG_FILTER_SHORT },
+                                  { NULL, 0, 0, '\0' } };
 
-#define OPTIONS_STR ARG_HELP_SHORT_S
+#define OPTIONS_STR ARG_HELP_SHORT_S ARG_FILTER_SHORT_S
 
 static char *op;
 static char **opargv;
@@ -21,7 +23,7 @@ static void usage(char *argv[]) {
         print_client_usage(argv[0]);
 }
 
-static int get_opts(int argc, char *argv[]) {
+static int get_opts(int argc, char *argv[], char **opt_filter_glob) {
         int opt = 0;
 
         while ((opt = getopt_long(argc, argv, OPTIONS_STR, options, NULL)) != -1) {
@@ -29,6 +31,8 @@ static int get_opts(int argc, char *argv[]) {
                         usage(argv);
                         no_action = true;
                         return 0;
+                } else if (opt == ARG_FILTER_SHORT) {
+                        *opt_filter_glob = strdup(optarg);
                 } else {
                         fprintf(stderr, "Unsupported option %c\n", opt);
                         usage(argv);
@@ -51,9 +55,9 @@ static int get_opts(int argc, char *argv[]) {
 
 int main(int argc, char *argv[]) {
         int r = 0;
-        _cleanup_sd_bus_ sd_bus *bus = NULL;
 
-        r = get_opts(argc, argv);
+        _cleanup_free_ char *opt_filter_glob = NULL;
+        r = get_opts(argc, argv, &opt_filter_glob);
         if (r < 0) {
                 return EXIT_FAILURE;
         }
@@ -61,7 +65,7 @@ int main(int argc, char *argv[]) {
                 return EXIT_SUCCESS;
         }
 
-        _cleanup_client_ Client *client = new_client(op, opargc, opargv);
+        _cleanup_client_ Client *client = new_client(op, opargc, opargv, opt_filter_glob);
         r = client_call_manager(client);
         if (r < 0) {
                 fprintf(stderr, "Call to manager failed: %s\n", strerror(-r));
diff --git a/src/client/meson.build b/src/client/meson.build
index cf44853679..5f6568168e 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -5,8 +5,8 @@
 client_src = [
   'main.c',
   'client.c',
-  'printer.c',
-  'method_monitor.c'
+  'method_monitor.c',
+  'method_list_units.c'
 ]
 
 executable(
diff --git a/src/client/method_list_units.c b/src/client/method_list_units.c
new file mode 100644
index 0000000000..e89b1aa20e
--- /dev/null
+++ b/src/client/method_list_units.c
@@ -0,0 +1,158 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include "libhirte/common/string-util.h"
+
+#include "method_list_units.h"
+
+static int
+                fetch_unit_list(sd_bus *api_bus,
+                                const char *node_name,
+                                const char *object_path,
+                                const char *interface,
+                                const char *typestring,
+                                int (*parse_unit_info)(sd_bus_message *, UnitInfo *),
+                                UnitList *unit_list) {
+
+        int r = 0;
+        _cleanup_sd_bus_error_ sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_sd_bus_message_ sd_bus_message *message = NULL;
+
+        r = sd_bus_call_method(
+                        api_bus,
+                        HIRTE_INTERFACE_BASE_NAME,
+                        object_path,
+                        interface,
+                        "ListUnits",
+                        &error,
+                        &message,
+                        "");
+        if (r < 0) {
+                fprintf(stderr, "Failed to issue method call: %s\n", error.message);
+                return r;
+        }
+
+        r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, typestring);
+        if (r < 0) {
+                fprintf(stderr, "Failed to read sd-bus message: %s\n", strerror(-r));
+                return r;
+        }
+
+        for (;;) {
+                _cleanup_unit_ UnitInfo *info = new_unit();
+
+                r = (*parse_unit_info)(message, info);
+                if (r < 0) {
+                        fprintf(stderr, "Failed to parse unit info: %s\n", strerror(-r));
+                        return r;
+                }
+                if (r == 0) {
+                        break;
+                }
+                if (node_name != NULL) {
+                        info->node = strdup(node_name);
+                }
+
+                LIST_APPEND(units, unit_list->units, unit_ref(info));
+        }
+
+        return r;
+}
+
+int method_list_units_on_all(sd_bus *api_bus, print_unit_list_fn print, const char *glob_filter) {
+        _cleanup_unit_list_ UnitList *unit_list = new_unit_list();
+
+        int r = fetch_unit_list(
+                        api_bus,
+                        NULL,
+                        HIRTE_OBJECT_PATH,
+                        MANAGER_INTERFACE,
+                        NODE_AND_UNIT_INFO_STRUCT_TYPESTRING,
+                        &bus_parse_unit_on_node_info,
+                        unit_list);
+        if (r < 0) {
+                return r;
+        }
+
+        print(unit_list, glob_filter);
+
+        return 0;
+}
+
+int method_list_units_on(
+                sd_bus *api_bus, const char *node_name, print_unit_list_fn print, const char *glob_filter) {
+        int r = 0;
+        _cleanup_unit_list_ UnitList *unit_list = new_unit_list();
+
+        _cleanup_free_ char *object_path = NULL;
+        r = assemble_object_path_string(NODE_OBJECT_PATH_PREFIX, node_name, &object_path);
+        if (r < 0) {
+                return r;
+        }
+
+        r = fetch_unit_list(
+                        api_bus,
+                        node_name,
+                        object_path,
+                        NODE_INTERFACE,
+                        UNIT_INFO_STRUCT_TYPESTRING,
+                        &bus_parse_unit_info,
+                        unit_list);
+        if (r < 0) {
+                return r;
+        }
+
+        print(unit_list, glob_filter);
+
+        return 0;
+}
+
+
+/****************************
+ ******** UnitList **********
+ ****************************/
+
+UnitList *new_unit_list() {
+        _cleanup_unit_list_ UnitList *unit_list = malloc0(sizeof(UnitList));
+
+        if (unit_list == NULL) {
+                return NULL;
+        }
+
+        unit_list->ref_count = 1;
+        LIST_HEAD_INIT(unit_list->units);
+
+        return steal_pointer(&unit_list);
+}
+
+UnitList *unit_list_ref(UnitList *unit_list) {
+        unit_list->ref_count++;
+        return unit_list;
+}
+
+void unit_list_unref(UnitList *unit_list) {
+        unit_list->ref_count--;
+        if (unit_list->ref_count != 0) {
+                return;
+        }
+
+        UnitInfo *unit = NULL, *next_unit = NULL;
+        LIST_FOREACH_SAFE(units, unit, next_unit, unit_list->units) {
+                unit_unref(unit);
+        }
+        free(unit_list);
+}
+
+/* Prints 100-characters-wide table */
+void print_unit_list_simple(UnitList *unit_list, const char *glob_filter) {
+        UnitInfo *unit = NULL;
+        printf("%-20.20s|%-59.59s|%9s|%9s\n", "NODE", "ID", "ACTIVE", "SUB");
+        printf("====================================================================================================\n");
+        LIST_FOREACH(units, unit, unit_list->units) {
+                if (glob_filter == NULL || match_glob(unit->id, glob_filter)) {
+                        printf("%-20.20s|%-59.59s|%9s|%9s\n",
+                               unit->node,
+                               unit->id,
+                               unit->active_state,
+                               unit->sub_state);
+                }
+        }
+}
diff --git a/src/client/method_list_units.h b/src/client/method_list_units.h
new file mode 100644
index 0000000000..170e37e990
--- /dev/null
+++ b/src/client/method_list_units.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include <systemd/sd-bus.h>
+
+#include "libhirte/bus/utils.h"
+#include "libhirte/common/common.h"
+
+#include "types.h"
+
+typedef void (*print_unit_list_fn)(UnitList *unit_list, const char *glob_filter);
+
+int method_list_units_on_all(sd_bus *api_bus, print_unit_list_fn printer, const char *glob_filter);
+int method_list_units_on(
+                sd_bus *api_bus, const char *node_name, print_unit_list_fn printer, const char *glob_filter);
+
+struct UnitList {
+        int ref_count;
+
+        LIST_FIELDS(UnitList, node_units);
+
+        LIST_HEAD(UnitInfo, units);
+};
+
+UnitList *new_unit_list();
+UnitList *unit_list_ref(UnitList *unit_list);
+void unit_list_unref(UnitList *unit_list);
+
+DEFINE_CLEANUP_FUNC(UnitList, unit_list_unref)
+#define _cleanup_unit_list_ _cleanup_(unit_list_unrefp)
+
+/* Prints 100-characters-wide table */
+void print_unit_list_simple(UnitList *unit_list, const char *glob_filter);
\ No newline at end of file
diff --git a/src/client/printer.c b/src/client/printer.c
deleted file mode 100644
index f6a5bcd4b9..0000000000
--- a/src/client/printer.c
+++ /dev/null
@@ -1,44 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include <stdio.h>
-
-#include "libhirte/bus/utils.h"
-#include "libhirte/common/common.h"
-
-#include "client.h"
-#include "printer.h"
-#include "types.h"
-
-/* Prints 100-characters-wide table */
-void print_unit_list_simple(UnitList *unit_list) {
-        UnitInfo *unit = NULL;
-        printf("%-80.80s|%9s|%9s\n", "ID", "ACTIVE", "SUB");
-        printf("====================================================================================================\n");
-        LIST_FOREACH(units, unit, unit_list->units) {
-                printf("%-80.80s|%9s|%9s\n", unit->id, unit->active_state, unit->sub_state);
-        }
-}
-
-/* Prints 100-characters-wide table */
-void print_nodes_unit_list_simple(UnitList *unit_list) {
-        UnitInfo *unit = NULL;
-        printf("%-20.20s|%-59.59s|%9s|%9s\n", "NODE", "ID", "ACTIVE", "SUB");
-        printf("====================================================================================================\n");
-        LIST_FOREACH(units, unit, unit_list->units) {
-                printf("%-20.20s|%-59.59s|%9s|%9s\n", unit->node, unit->id, unit->active_state, unit->sub_state);
-        }
-}
-
-/* We can implement here other types of printers, e.g. json printer */
-Printer get_simple_printer() {
-        Printer p = { .print_unit_list = &print_unit_list_simple,
-                      .print_nodes_unit_list = &print_nodes_unit_list_simple };
-        return p;
-}
-
-void print_unit_list(Printer *p, UnitList *unit_list) {
-        (*p->print_unit_list)(unit_list);
-}
-
-void print_nodes_unit_list(Printer *p, UnitList *unit_list) {
-        (*p->print_nodes_unit_list)(unit_list);
-}
\ No newline at end of file
diff --git a/src/client/printer.h b/src/client/printer.h
deleted file mode 100644
index eb8c92aa93..0000000000
--- a/src/client/printer.h
+++ /dev/null
@@ -1,14 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-#pragma once
-
-#include "types.h"
-
-struct Printer {
-        void (*print_unit_list)(UnitList *);
-        void (*print_nodes_unit_list)(UnitList *);
-};
-
-Printer get_simple_printer();
-
-void print_unit_list(Printer *p, UnitList *unit_list);
-void print_nodes_unit_list(Printer *p, UnitList *nodes_unit_list);
\ No newline at end of file
diff --git a/src/client/types.h b/src/client/types.h
index 805a27a82e..a1dbba2d69 100644
--- a/src/client/types.h
+++ b/src/client/types.h
@@ -2,5 +2,4 @@
 #pragma once
 
 typedef struct Client Client;
-typedef struct UnitList UnitList;
-typedef struct Printer Printer;
\ No newline at end of file
+typedef struct UnitList UnitList;
\ No newline at end of file
diff --git a/src/libhirte/common/opt.h b/src/libhirte/common/opt.h
index f73d9f044f..9134320ec5 100644
--- a/src/libhirte/common/opt.h
+++ b/src/libhirte/common/opt.h
@@ -29,6 +29,10 @@
 #define ARG_NAME_SHORT 'n'
 #define ARG_NAME_SHORT_S "n:"
 
+#define ARG_FILTER "filter"
+#define ARG_FILTER_SHORT 'f'
+#define ARG_FILTER_SHORT_S "f:"
+
 #define ARG_HELP "help"
 #define ARG_HELP_SHORT 'h'
 #define ARG_HELP_SHORT_S "h"
diff --git a/src/libhirte/common/protocol.h b/src/libhirte/common/protocol.h
index ab55ee5e83..7c2d3f51dc 100644
--- a/src/libhirte/common/protocol.h
+++ b/src/libhirte/common/protocol.h
@@ -101,3 +101,5 @@ char *get_hostname();
 
 /* Constants */
 #define SYMBOL_WILDCARD "*"
+#define SYMBOL_GLOB_ALL '*'
+#define SYMBOL_GLOB_ONE '?'
diff --git a/src/libhirte/common/string-util.h b/src/libhirte/common/string-util.h
index e187a8e825..831b2ee2c9 100644
--- a/src/libhirte/common/string-util.h
+++ b/src/libhirte/common/string-util.h
@@ -48,3 +48,25 @@ static inline bool copy_str(char **p, const char *s) {
 static inline bool is_wildcard(const char *in) {
         return streq(in, SYMBOL_WILDCARD);
 }
+
+static inline bool is_glob(const char *s) {
+        return s != NULL && (strchr(s, SYMBOL_GLOB_ALL) != NULL || strchr(s, SYMBOL_GLOB_ONE) != NULL);
+}
+
+// NOLINTBEGIN(misc-no-recursion)
+static inline bool match_glob(const char *str, const char *glob) {
+        if (str == NULL || glob == NULL) {
+                return false;
+        }
+
+        if (*glob == SYMBOL_GLOB_ALL) {
+                return match_glob(str, glob + 1) || (*str && match_glob(str + 1, glob));
+        }
+
+        if (*str) {
+                return ((SYMBOL_GLOB_ONE == *glob) || (*str == *glob)) && match_glob(str + 1, glob + 1);
+        }
+
+        return !*glob;
+}
+// NOLINTEND(misc-no-recursion)


Test patch:
diff --git a/src/libhirte/test/common/meson.build b/src/libhirte/test/common/meson.build
index adbfd938ec..13b8092cc2 100644
--- a/src/libhirte/test/common/meson.build
+++ b/src/libhirte/test/common/meson.build
@@ -3,7 +3,8 @@
 common_src = [
   'cfg_test',
   'list_test',
-  'parse-util_test'
+  'parse-util_test',
+  'string-util_test'
 ]
 
 foreach src : common_src
diff --git a/src/libhirte/test/common/string-util_test.c b/src/libhirte/test/common/string-util_test.c
new file mode 100644
index 0000000000..a8c2093f14
--- /dev/null
+++ b/src/libhirte/test/common/string-util_test.c
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include <assert.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "libhirte/common/string-util.h"
+
+#define bool_to_str(b) b ? "true" : "false"
+
+bool test_is_glob(const char *in, bool expected) {
+        bool result = is_glob(in);
+        if (result == expected) {
+                return true;
+        }
+        fprintf(stdout,
+                "FAILED: is_glob('%s') - Expected %s, but got %s\n",
+                in,
+                bool_to_str(expected),
+                bool_to_str(result));
+        return false;
+}
+
+bool test_match_glob(const char *in, const char *glob, bool expected) {
+        bool result = match_glob(in, glob);
+        if (result == expected) {
+                return true;
+        }
+        fprintf(stdout,
+                "FAILED: match_glob('%s', '%s') - Expected %s, but got %s\n",
+                in,
+                glob,
+                bool_to_str(expected),
+                bool_to_str(result));
+        return false;
+}
+
+int main() {
+        bool result = true;
+
+        result = result && test_is_glob(NULL, false);
+        result = result && test_is_glob("", false);
+        result = result && test_is_glob(".", false);
+        result = result && test_is_glob("noglob", false);
+        result = result && test_is_glob("isglob?", true);
+        result = result && test_is_glob("?isglob", true);
+        result = result && test_is_glob("isglob*", true);
+        result = result && test_is_glob("*isglob", true);
+        result = result && test_is_glob("*isglob?", true);
+        result = result && test_is_glob("isg?ob", true);
+        result = result && test_is_glob("isg*ob", true);
+        result = result && test_is_glob("glob.ch?ck.serv*", true);
+
+        result = result && test_match_glob(NULL, NULL, false);
+        result = result && test_match_glob("", NULL, false);
+        result = result && test_match_glob(NULL, "", false);
+        result = result && test_match_glob("", "", true);
+        result = result && test_match_glob("", "?", false);
+        result = result && test_match_glob("", "*", true);
+        result = result && test_match_glob("x", "?", true);
+        result = result && test_match_glob("x", "*", true);
+        result = result && test_match_glob("xt", "?", false);
+        result = result && test_match_glob("xt", "?t", true);
+        result = result && test_match_glob("xt", "*t", true);
+        result = result && test_match_glob("glob.check.service", "?", false);
+        result = result && test_match_glob("glob.check.service", "*", true);
+        result = result && test_match_glob("glob.check.service", "*.check.*", true);
+        result = result && test_match_glob("glob.check.service", "*.ch??k.*", true);
+        result = result && test_match_glob("glob.check.service", "*.ch?k.*", false);
+        result = result && test_match_glob("glob.check.service", "*.service", true);
+        result = result && test_match_glob("glob.check.service", "*.service*", true);
+        result = result && test_match_glob("glob.check.service", "*.service?", false);
+
+        if (result) {
+                return EXIT_SUCCESS;
+        }
+        return EXIT_FAILURE;
+}