From 266c5c655c88176f817b0df7ce61d37173d6bc42 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Fri, 21 Nov 2008 23:14:42 +0100 Subject: [PATCH] match_regex plugin: Renamed `filter_pcre' to `match_regex'. In order to fit into the new match/target schema, the substitute part of the plugin has been removed for now and will be put in a target plugin in the future. The match_regex now registeres a match with the new infrastructure and uses regular expressions to match certain values based on their identifier. --- configure.in | 4 +- src/Makefile.am | 20 +-- src/filter_pcre.c | 431 ---------------------------------------------- src/match_regex.c | 287 ++++++++++++++++++++++++++++++ 4 files changed, 299 insertions(+), 443 deletions(-) delete mode 100644 src/filter_pcre.c create mode 100644 src/match_regex.c diff --git a/configure.in b/configure.in index 21af0b54..0d1bd258 100644 --- a/configure.in +++ b/configure.in @@ -2945,7 +2945,6 @@ AC_PLUGIN([entropy], [$plugin_entropy], [Entropy statistics]) AC_PLUGIN([exec], [yes], [Execution of external programs]) AC_PLUGIN([filecount], [yes], [Count files in directories]) AC_PLUGIN([filter_ignore], [yes], [Ignore specific values]) -AC_PLUGIN([filter_pcre], [$with_libpcre], [Filter based on PCRE]) AC_PLUGIN([hddtemp], [yes], [Query hddtempd]) AC_PLUGIN([interface], [$plugin_interface], [Interface traffic statistics]) AC_PLUGIN([iptables], [$with_libiptc], [IPTables rule counters]) @@ -2955,6 +2954,7 @@ AC_PLUGIN([irq], [$plugin_irq], [IRQ statistics]) AC_PLUGIN([libvirt], [$plugin_libvirt], [Virtual machine statistics]) AC_PLUGIN([load], [$plugin_load], [System load]) AC_PLUGIN([logfile], [yes], [File logging plugin]) +AC_PLUGIN([match_regex], [yes], [The regex match]) AC_PLUGIN([mbmon], [yes], [Query mbmond]) AC_PLUGIN([memcached], [yes], [memcached statistics]) AC_PLUGIN([memory], [$plugin_memory], [Memory usage]) @@ -3120,7 +3120,6 @@ Configuration: exec . . . . . . . . $enable_exec filecount . . . . . . $enable_filecount filter_ignore . . . . $enable_filter_ignore - filter_pcre . . . . . $enable_filter_pcre hddtemp . . . . . . . $enable_hddtemp interface . . . . . . $enable_interface iptables . . . . . . $enable_iptables @@ -3130,6 +3129,7 @@ Configuration: libvirt . . . . . . . $enable_libvirt load . . . . . . . . $enable_load logfile . . . . . . . $enable_logfile + match_regex . . . . . $enable_match_regex mbmon . . . . . . . . $enable_mbmon memcached . . . . . . $enable_memcached memory . . . . . . . $enable_memory diff --git a/src/Makefile.am b/src/Makefile.am index 960dfe4d..24f06fdb 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -284,16 +284,6 @@ collectd_LDADD += "-dlopen" filter_ignore.la collectd_DEPENDENCIES += filter_ignore.la endif -if BUILD_PLUGIN_FILTER_PCRE -pkglib_LTLIBRARIES += filter_pcre.la -filter_pcre_la_SOURCES = filter_pcre.c -filter_pcre_la_CPPFLAGS = $(BUILD_WITH_LIBPCRE_CFLAGS) -filter_pcre_la_LDFLAGS = -module -avoid-version \ - $(BUILD_WITH_LIBPCRE_LIBS) -collectd_LDADD += "-dlopen" filter_pcre.la -collectd_DEPENDENCIES += filter_pcre.la -endif - if BUILD_PLUGIN_HDDTEMP pkglib_LTLIBRARIES += hddtemp.la hddtemp_la_SOURCES = hddtemp.c @@ -400,6 +390,16 @@ collectd_LDADD += "-dlopen" logfile.la collectd_DEPENDENCIES += logfile.la endif +if BUILD_PLUGIN_MATCH_REGEX +pkglib_LTLIBRARIES += match_regex.la +match_regex_la_SOURCES = match_regex.c +match_regex_la_CPPFLAGS = $(BUILD_WITH_LIBPCRE_CFLAGS) +match_regex_la_LDFLAGS = -module -avoid-version \ + $(BUILD_WITH_LIBPCRE_LIBS) +collectd_LDADD += "-dlopen" match_regex.la +collectd_DEPENDENCIES += match_regex.la +endif + if BUILD_PLUGIN_MBMON pkglib_LTLIBRARIES += mbmon.la mbmon_la_SOURCES = mbmon.c diff --git a/src/filter_pcre.c b/src/filter_pcre.c deleted file mode 100644 index 3b1afbf5..00000000 --- a/src/filter_pcre.c +++ /dev/null @@ -1,431 +0,0 @@ -/** - * collectd - src/filter_pcre.c - * Copyright (C) 2008 Sebastian Harl - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the - * Free Software Foundation; only version 2 of the License is applicable. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - * - * Author: - * Sebastian Harl - **/ - -/* - * This module allows to filter and rewrite value lists based on - * Perl-compatible regular expressions. - */ - -#include "collectd.h" -#include "configfile.h" -#include "plugin.h" -#include "common.h" - -#include "utils_subst.h" - -#include - -#define log_err(...) ERROR ("filter_pcre: " __VA_ARGS__) -#define log_warn(...) WARNING ("filter_pcre: " __VA_ARGS__) - -/* - * private data types - */ - -typedef struct { - /* regular expression */ - pcre *re; - const char *re_str; - - /* extra information from studying the pattern */ - pcre_extra *extra; - - /* replacment text for string substitution */ - const char *replacement; -} c_pcre_t; - -#define C_PCRE_INIT(regex) do { \ - (regex).re = NULL; \ - (regex).re_str = NULL; \ - (regex).extra = NULL; \ - (regex).replacement = NULL; \ - } while (0) - -#define C_PCRE_FREE(regex) do { \ - pcre_free ((regex).re); \ - free ((void *)(regex).re_str); \ - pcre_free ((regex).extra); \ - free ((void *)(regex).replacement); \ - C_PCRE_INIT (regex); \ - } while (0) - -typedef struct { - c_pcre_t host; - c_pcre_t plugin; - c_pcre_t plugin_instance; - c_pcre_t type; - c_pcre_t type_instance; - - int action; -} regex_t; - -typedef struct { - int vec[30]; - int status; -} ovec_t; - -typedef struct { - ovec_t host; - ovec_t plugin; - ovec_t plugin_instance; - ovec_t type; - ovec_t type_instance; -} ovectors_t; - -/* - * private variables - */ - -static regex_t *regexes = NULL; -static int regexes_num = 0; - -/* - * internal helper functions - */ - -/* returns true if string matches the regular expression */ -static int c_pcre_match (c_pcre_t *re, const char *string, ovec_t *ovec) -{ - if ((NULL == re) || (NULL == re->re)) - return 1; - - if (NULL == string) - string = ""; - - ovec->status = pcre_exec (re->re, - /* extra = */ re->extra, - /* subject = */ string, - /* length = */ strlen (string), - /* startoffset = */ 0, - /* options = */ 0, - /* ovector = */ ovec->vec, - /* ovecsize = */ STATIC_ARRAY_SIZE (ovec->vec)); - - if (0 <= ovec->status) - return 1; - - if (PCRE_ERROR_NOMATCH != ovec->status) - log_err ("PCRE matching of string \"%s\" failed with status %d", - string, ovec->status); - return 0; -} /* c_pcre_match */ - -static int c_pcre_subst (c_pcre_t *re, char *string, size_t strlen, - ovec_t *ovec) -{ - char buffer[strlen]; - - if ((NULL == re) || (NULL == re->replacement)) - return 0; - - assert (0 <= ovec->status); - - if (NULL == subst (buffer, sizeof (buffer), string, - ovec->vec[0], ovec->vec[1], re->replacement)) { - log_err ("Substitution in string \"%s\" (using regex \"%s\" and " - "replacement string \"%s\") failed.", - string, re->re_str, re->replacement); - return -1; - } - - sstrncpy (string, buffer, strlen); - return 0; -} /* c_pcre_subst */ - -static regex_t *regex_new (void) -{ - regex_t *re; - regex_t *temp; - - temp = (regex_t *) realloc (regexes, (regexes_num + 1) - * sizeof (*regexes)); - if (NULL == temp) { - log_err ("Out of memory."); - return NULL; - } - regexes = temp; - regexes_num++; - - re = regexes + (regexes_num - 1); - - C_PCRE_INIT (re->host); - C_PCRE_INIT (re->plugin); - C_PCRE_INIT (re->plugin_instance); - C_PCRE_INIT (re->type); - C_PCRE_INIT (re->type_instance); - - re->action = 0; - return re; -} /* regex_new */ - -static void regex_delete (regex_t *re) -{ - if (NULL == re) - return; - - C_PCRE_FREE (re->host); - C_PCRE_FREE (re->plugin); - C_PCRE_FREE (re->plugin_instance); - C_PCRE_FREE (re->type); - C_PCRE_FREE (re->type_instance); - - re->action = 0; -} /* regex_delete */ - -/* returns true if the value list matches the regular expression */ -static int regex_match (regex_t *re, value_list_t *vl, ovectors_t *ovectors) -{ - int matches = 0; - - if (NULL == re) - return 1; - - if (c_pcre_match (&re->host, vl->host, &ovectors->host)) - ++matches; - - if (c_pcre_match (&re->plugin, vl->plugin, &ovectors->plugin)) - ++matches; - - if (c_pcre_match (&re->plugin_instance, vl->plugin_instance, - &ovectors->plugin_instance)) - ++matches; - - if (c_pcre_match (&re->type, vl->type, &ovectors->type)) - ++matches; - - if (c_pcre_match (&re->type_instance, vl->type_instance, - &ovectors->type_instance)) - ++matches; - - if (5 == matches) - return 1; - return 0; -} /* regex_match */ - -static int regex_subst (regex_t *re, value_list_t *vl, ovectors_t *ovectors) -{ - if (NULL == re) - return 0; - - c_pcre_subst (&re->host, vl->host, sizeof (vl->host), - &ovectors->host); - c_pcre_subst (&re->plugin, vl->plugin, sizeof (vl->plugin), - &ovectors->plugin); - c_pcre_subst (&re->plugin_instance, vl->plugin_instance, - sizeof (vl->plugin_instance), &ovectors->plugin_instance); - c_pcre_subst (&re->type, vl->type, sizeof (vl->type), - &ovectors->type); - c_pcre_subst (&re->type_instance, vl->type_instance, - sizeof (vl->type_instance), &ovectors->type_instance); - return 0; -} /* regex_subst */ - -/* - * interface to collectd - */ - -static int c_pcre_filter (const data_set_t *ds, value_list_t *vl) -{ - int i; - - ovectors_t ovectors; - - for (i = 0; i < regexes_num; ++i) - if (regex_match (regexes + i, vl, &ovectors)) { - regex_subst (regexes + i, vl, &ovectors); - return regexes[i].action; - } - return 0; -} /* c_pcre_filter */ - -static int c_pcre_shutdown (void) -{ - int i; - - plugin_unregister_filter ("filter_pcre"); - plugin_unregister_shutdown ("filter_pcre"); - - for (i = 0; i < regexes_num; ++i) - regex_delete (regexes + i); - - sfree (regexes); - regexes_num = 0; - return 0; -} /* c_pcre_shutdown */ - -static int config_set_regex (c_pcre_t *re, oconfig_item_t *ci) -{ - const char *pattern; - const char *errptr; - int erroffset; - - if ((0 != ci->children_num) || (1 != ci->values_num) - || (OCONFIG_TYPE_STRING != ci->values[0].type)) { - log_err (": %s expects a single string argument.", ci->key); - return 1; - } - - pattern = ci->values[0].value.string; - - re->re = pcre_compile (pattern, - /* options = */ 0, - /* errptr = */ &errptr, - /* erroffset = */ &erroffset, - /* tableptr = */ NULL); - - if (NULL == re->re) { - log_err (": PCRE compilation of pattern \"%s\" failed " - "at offset %d: %s", pattern, erroffset, errptr); - return 1; - } - - re->re_str = sstrdup (pattern); - - re->extra = pcre_study (re->re, - /* options = */ 0, - /* errptr = */ &errptr); - - if (NULL != errptr) { - log_err (": PCRE studying of pattern \"%s\" failed: %s", - pattern, errptr); - return 1; - } - return 0; -} /* config_set_regex */ - -static int config_set_replacement (c_pcre_t *re, oconfig_item_t *ci) -{ - if ((0 != ci->children_num) || (1 != ci->values_num) - || (OCONFIG_TYPE_STRING != ci->values[0].type)) { - log_err (": %s expects a single string argument.", ci->key); - return 1; - } - - if (NULL == re->re) { - log_err (": %s without an appropriate regex (%s) " - "is not allowed.", ci->key, ci->key + strlen ("Substitute")); - return 1; - } - - re->replacement = sstrdup (ci->values[0].value.string); - return 0; -} /* config_set_replacement */ - -static int config_set_action (int *action, oconfig_item_t *ci) -{ - const char *action_str; - - if ((0 != ci->children_num) || (1 != ci->values_num) - || (OCONFIG_TYPE_STRING != ci->values[0].type)) { - log_err (": Action expects a single string argument."); - return 1; - } - - action_str = ci->values[0].value.string; - - if (0 == strcasecmp (action_str, "NoWrite")) - *action |= FILTER_NOWRITE; - else if (0 == strcasecmp (action_str, "NoThresholdCheck")) - *action |= FILTER_NOTHRESHOLD_CHECK; - else if (0 == strcasecmp (action_str, "Ignore")) - *action |= FILTER_IGNORE; - else - log_warn (": Ignoring unknown action \"%s\".", action_str); - return 0; -} /* config_set_action */ - -static int c_pcre_config_regex (oconfig_item_t *ci) -{ - regex_t *re; - int i; - - if (0 != ci->values_num) { - log_err (" expects no arguments."); - return 1; - } - - re = regex_new (); - if (NULL == re) - return -1; - - for (i = 0; i < ci->children_num; ++i) { - oconfig_item_t *c = ci->children + i; - int status = 0; - - if (0 == strcasecmp (c->key, "Host")) - status = config_set_regex (&re->host, c); - else if (0 == strcasecmp (c->key, "Plugin")) - status = config_set_regex (&re->plugin, c); - else if (0 == strcasecmp (c->key, "PluginInstance")) - status = config_set_regex (&re->plugin_instance, c); - else if (0 == strcasecmp (c->key, "Type")) - status = config_set_regex (&re->type, c); - else if (0 == strcasecmp (c->key, "TypeInstance")) - status = config_set_regex (&re->type_instance, c); - else if (0 == strcasecmp (c->key, "Action")) - status = config_set_action (&re->action, c); - else if (0 == strcasecmp (c->key, "SubstituteHost")) - status = config_set_replacement (&re->host, c); - else if (0 == strcasecmp (c->key, "SubstitutePlugin")) - status = config_set_replacement (&re->plugin, c); - else if (0 == strcasecmp (c->key, "SubstitutePluginInstance")) - status = config_set_replacement (&re->plugin_instance, c); - else if (0 == strcasecmp (c->key, "SubstituteType")) - status = config_set_replacement (&re->type, c); - else if (0 == strcasecmp (c->key, "SubstituteTypeInstance")) - status = config_set_replacement (&re->type_instance, c); - else - log_warn (": Ignoring unknown config key \"%s\".", c->key); - - if (0 != status) { - log_err ("Ignoring regular expression definition."); - regex_delete (re); - --regexes_num; - } - } - return 0; -} /* c_pcre_config_regex */ - -static int c_pcre_config (oconfig_item_t *ci) -{ - int i; - - for (i = 0; i < ci->children_num; ++i) { - oconfig_item_t *c = ci->children + i; - - if (0 == strcasecmp (c->key, "RegEx")) - c_pcre_config_regex (c); - else - log_warn ("Ignoring unknown config key \"%s\".", c->key); - } - - plugin_register_filter ("filter_pcre", c_pcre_filter); - plugin_register_shutdown ("filter_pcre", c_pcre_shutdown); - return 0; -} /* c_pcre_config */ - -void module_register (void) -{ - plugin_register_complex_config ("filter_pcre", c_pcre_config); -} /* module_register */ - -/* vim: set sw=4 ts=4 tw=78 noexpandtab : */ - diff --git a/src/match_regex.c b/src/match_regex.c new file mode 100644 index 00000000..4353b3a7 --- /dev/null +++ b/src/match_regex.c @@ -0,0 +1,287 @@ +/** + * collectd - src/match_regex.c + * Copyright (C) 2008 Sebastian Harl + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; only version 2 of the License is applicable. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * Authors: + * Sebastian Harl + * Florian Forster + **/ + +/* + * This module allows to filter and rewrite value lists based on + * Perl-compatible regular expressions. + */ + +#include "collectd.h" +#include "filter_chain.h" + +#include +#include + +#define log_err(...) ERROR ("`regex' match: " __VA_ARGS__) +#define log_warn(...) WARNING ("`regex' match: " __VA_ARGS__) + +/* + * private data types + */ + +struct mr_regex_s; +typedef struct mr_regex_s mr_regex_t; +struct mr_regex_s +{ + regex_t re; + char *re_str; + + mr_regex_t *next; +}; + +struct mr_match_s; +typedef struct mr_match_s mr_match_t; +struct mr_match_s +{ + mr_regex_t *host; + mr_regex_t *plugin; + mr_regex_t *plugin_instance; + mr_regex_t *type; + mr_regex_t *type_instance; +}; + +/* + * internal helper functions + */ +static void mr_free_regex (mr_regex_t *r) /* {{{ */ +{ + if (r == NULL) + return; + + regfree (&r->re); + memset (&r->re, 0, sizeof (r->re)); + free (r->re_str); + + if (r->next != NULL) + mr_free_regex (r->next); +} /* }}} void mr_free_regex */ + +static void mr_free_match (mr_match_t *m) /* {{{ */ +{ + if (m == NULL) + return; + + mr_free_regex (m->host); + mr_free_regex (m->plugin); + mr_free_regex (m->plugin_instance); + mr_free_regex (m->type); + mr_free_regex (m->type_instance); + + free (m); +} /* }}} void mr_free_match */ + +static int mr_match_regexen (mr_regex_t *re_head, /* {{{ */ + const char *string) +{ + mr_regex_t *re; + + if (re_head == NULL) + return (FC_MATCH_MATCHES); + + for (re = re_head; re != NULL; re = re->next) + { + int status; + + status = regexec (&re->re, string, + /* nmatch = */ 0, /* pmatch = */ NULL, + /* eflags = */ 0); + if (status == 0) + return (FC_MATCH_MATCHES); + } + + return (FC_MATCH_NO_MATCH); +} /* }}} int mr_match_regexen */ + +static int mr_config_add_regex (mr_regex_t **re_head, /* {{{ */ + oconfig_item_t *ci) +{ + mr_regex_t *re; + int status; + + if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING)) + { + log_warn ("`%s' needs exactly one string argument.", ci->key); + return (-1); + } + + re = (mr_regex_t *) malloc (sizeof (*re)); + if (re == NULL) + { + log_err ("mr_config_add_regex: malloc failed."); + return (-1); + } + memset (re, 0, sizeof (*re)); + re->next = NULL; + + re->re_str = strdup (ci->values[0].value.string); + if (re->re_str) + { + free (re); + log_err ("mr_config_add_regex: strdup failed."); + return (-1); + } + + status = regcomp (&re->re, re->re_str, REG_EXTENDED | REG_NOSUB); + if (status != 0) + { + char errmsg[1024]; + regerror (status, &re->re, errmsg, sizeof (errmsg)); + errmsg[sizeof (errmsg) - 1] = 0; + log_err ("Compiling regex `%s' for `%s' failed: %s.", + re->re_str, ci->key, errmsg); + free (re->re_str); + free (re); + return (-1); + } + + if (*re_head == NULL) + { + *re_head = re; + } + else + { + mr_regex_t *ptr; + + ptr = *re_head; + while (ptr->next != NULL) + ptr = ptr->next; + + ptr->next = re; + } + + return (0); +} /* }}} int mr_config_add_regex */ + +static int mr_create (const oconfig_item_t *ci, void **user_data) /* {{{ */ +{ + mr_match_t *m; + int status; + int i; + + m = (mr_match_t *) malloc (sizeof (*m)); + if (m == NULL) + { + log_err ("mr_create: malloc failed."); + return (-ENOMEM); + } + memset (m, 0, sizeof (*m)); + + status = 0; + for (i = 0; i < ci->children_num; i++) + { + oconfig_item_t *child = ci->children + i; + + if ((strcasecmp ("Host", child->key) == 0) + || (strcasecmp ("Hostname", child->key) == 0)) + status = mr_config_add_regex (&m->host, child); + else if (strcasecmp ("Plugin", child->key) == 0) + status = mr_config_add_regex (&m->plugin, child); + else if (strcasecmp ("PluginInstance", child->key) == 0) + status = mr_config_add_regex (&m->plugin_instance, child); + else if (strcasecmp ("Type", child->key) == 0) + status = mr_config_add_regex (&m->type, child); + else if (strcasecmp ("TypeInstance", child->key) == 0) + status = mr_config_add_regex (&m->type_instance, child); + else + { + log_err ("The `%s' configuration option is not understood and " + "will be ignored.", child->key); + status = 0; + } + + if (status != 0) + break; + } + + /* Additional sanity-checking */ + while (status == 0) + { + if ((m->host == NULL) + && (m->plugin == NULL) + && (m->plugin_instance == NULL) + && (m->type == NULL) + && (m->type_instance == NULL)) + { + log_err ("No (valid) regular expressions have been configured. " + "This match will be ignored."); + status = -1; + } + + break; + } + + if (status != 0) + { + mr_free_match (m); + return (status); + } + + *user_data = m; + return (0); +} /* }}} int mr_create */ + +static int mr_destroy (void **user_data) /* {{{ */ +{ + if ((user_data != NULL) && (*user_data != NULL)) + mr_free_match (*user_data); + return (0); +} /* }}} int mr_destroy */ + +static int mr_match (const data_set_t *ds, const value_list_t *vl, /* {{{ */ + notification_meta_t **meta, void **user_data) +{ + mr_match_t *m; + + if ((user_data == NULL) || (*user_data == NULL)) + return (-1); + + m = *user_data; + + if (mr_match_regexen (m->host, vl->host) == FC_MATCH_NO_MATCH) + return (FC_MATCH_NO_MATCH); + if (mr_match_regexen (m->plugin, vl->plugin) == FC_MATCH_NO_MATCH) + return (FC_MATCH_NO_MATCH); + if (mr_match_regexen (m->plugin_instance, + vl->plugin_instance) == FC_MATCH_NO_MATCH) + return (FC_MATCH_NO_MATCH); + if (mr_match_regexen (m->type, vl->type) == FC_MATCH_NO_MATCH) + return (FC_MATCH_NO_MATCH); + if (mr_match_regexen (m->type_instance, + vl->type_instance) == FC_MATCH_NO_MATCH) + return (FC_MATCH_NO_MATCH); + + return (FC_MATCH_MATCHES); +} /* }}} int mr_match */ + +void module_register (void) +{ + match_proc_t mproc; + + memset (&mproc, 0, sizeof (mproc)); + mproc.create = mr_create; + mproc.destroy = mr_destroy; + mproc.match = mr_match; + fc_register_match ("regex", mproc); +} /* module_register */ + +/* vim: set sw=4 ts=4 tw=78 noexpandtab fdm=marker : */ + -- 2.30.2