diff --git a/docs/dunst.5.pod b/docs/dunst.5.pod index 48e79c346..5d202a256 100644 --- a/docs/dunst.5.pod +++ b/docs/dunst.5.pod @@ -58,6 +58,22 @@ keyboard focus. =back +=item B (default: false) + +When set to true (recommended), you can use POSIX regular expressions for +filtering rules. It uses the POSIX Extended Regular Expression syntax: +https://en.m.wikibooks.org/wiki/Regular_Expressions/POSIX-Extended_Regular_Expressions. + +If this is set to false (not recommended), dunst will us fnmatch(3) for matching +strings. Dunst doesn't pass any flags to fnmatch, so you cannot make use of +extended patterns. + +The POSIX syntax is more powerful and will eventually become the default. The main +differences between POSIX and fnmatch(3) is that POSIX uses ".*" for wildcards +instead of "*" and POSIX allows for partial matches without needing wildcards. +This means that the pattern "abc" will match all strings that contain "abc", +like "abcdef". + =item B DEPRECATED This setting is deprecated and removed. It's split up into B, B, B, @@ -680,7 +696,15 @@ rule, it may affect if it's being matched by a later rule. =item B -Notifications can be matched for any of the following attributes: +With filtering rules you can match notifications to apply rules to only a subset +of notifications. + +For filtering rules that filter based on strings you can use regular +expressions. It's recommended to set B to true. You can then +use the POSIX Extended Regular Expression syntax: +https://en.m.wikibooks.org/wiki/Regular_Expressions/POSIX-Extended_Regular_Expressions. + +Notifications can be matched for any of the following attributes. =over 4 diff --git a/src/rules.c b/src/rules.c index 3c8cf9bbd..b85b4a608 100644 --- a/src/rules.c +++ b/src/rules.c @@ -5,6 +5,7 @@ #include #include #include +#include #include "dunst.h" #include "utils.h" @@ -140,7 +141,30 @@ struct rule *rule_new(const char *name) static inline bool rule_field_matches_string(const char *value, const char *pattern) { - return !pattern || (value && !fnmatch(pattern, value, 0)); + if (settings.enable_regex) { + if (!pattern) { + return true; + } + if (!value) { + return false; + } + regex_t regex; + + // TODO compile each regex only once + if (regcomp(®ex, pattern, REG_NEWLINE | REG_EXTENDED | REG_NOSUB)) + return false; + + for (int i = 0; ; i++) { + if (regexec(®ex, value, 0, NULL, 0)) + break; + regfree(®ex); + return true; + } + regfree(®ex); + return false; + } else { + return !pattern || (value && !fnmatch(pattern, value, 0)); + } } /* diff --git a/src/settings.h b/src/settings.h index 80d84927c..e0ae01fe9 100644 --- a/src/settings.h +++ b/src/settings.h @@ -133,6 +133,7 @@ struct settings { int max_icon_size; char **icon_theme; // experimental bool enable_recursive_icon_lookup; // experimental + bool enable_regex; // experimental char *icon_path; enum follow_mode f_mode; bool always_run_script; diff --git a/src/settings_data.h b/src/settings_data.h index 43becefd1..2143ce7d7 100644 --- a/src/settings_data.h +++ b/src/settings_data.h @@ -1136,6 +1136,16 @@ static const struct setting allowed_settings[] = { .parser = string_parse_bool, .parser_data = boolean_enum_data, }, + { + .name = "enable_posix_regex", + .section = "global", + .description = "Enable POSIX regex for filtering rules", + .type = TYPE_CUSTOM, + .default_value = "false", + .value = &settings.enable_regex, + .parser = string_parse_bool, + .parser_data = boolean_enum_data, + }, { .name = "frame_width", .section = "global", diff --git a/test/rules.c b/test/rules.c index 8ab19e53f..4e1ad10e6 100644 --- a/test/rules.c +++ b/test/rules.c @@ -1,6 +1,7 @@ #include "../src/rules.c" #include "greatest.h" +#include extern const char *base; @@ -9,24 +10,15 @@ TEST test_pattern_match(void) { // NULL should match everything ASSERT(rule_field_matches_string("anything", NULL)); - // Wildcard matching - ASSERT(rule_field_matches_string("anything", "*")); - ASSERT(rule_field_matches_string("*", "*")); - ASSERT(rule_field_matches_string("", "*")); - ASSERT(rule_field_matches_string("ffffasd", "*asd")); + // Literal matches + ASSERT(rule_field_matches_string("asdf", "asdf")); + ASSERT(rule_field_matches_string("test123", "test123")); - // Single character matching - ASSERT(rule_field_matches_string("a", "?")); - - // special characters that are not special to fnmatch ASSERT(rule_field_matches_string("!", "!")); ASSERT(rule_field_matches_string("!asd", "!asd")); ASSERT(rule_field_matches_string("/as/d", "/as/d")); ASSERT(rule_field_matches_string("/as/d", "/as/d")); - // Match substrings - ASSERT(rule_field_matches_string("asd", "asd")); - // ranges ASSERT(rule_field_matches_string("ac", "[a-z][a-z]")); @@ -35,8 +27,62 @@ TEST test_pattern_match(void) { ASSERT_FALSE(rule_field_matches_string("ffff", "*asd")); ASSERT_FALSE(rule_field_matches_string("ffff", "?")); ASSERT_FALSE(rule_field_matches_string("Ac", "[a-z][a-z]")); + + // Things that differ between fnmatch(3) and regex(3) + + if (settings.enable_regex) { + // Single character matching + ASSERT(rule_field_matches_string("a", ".")); + + // Wildcard matching + ASSERT(rule_field_matches_string("anything", ".*")); + ASSERT(rule_field_matches_string("*", ".*")); + ASSERT(rule_field_matches_string("", ".*")); + ASSERT(rule_field_matches_string("ffffasd", ".*asd")); + + // Substring matching + ASSERT(rule_field_matches_string("asd", "")); + ASSERT(rule_field_matches_string("asd", "sd")); + ASSERT(rule_field_matches_string("asd", "a")); + ASSERT(rule_field_matches_string("asd", "d")); + ASSERT(rule_field_matches_string("asd", "asd")); + + // Match multiple strings + ASSERT(rule_field_matches_string("ghj", "asd|dfg|ghj")); + ASSERT(rule_field_matches_string("asd", "asd|dfg|ghj")); + ASSERT(rule_field_matches_string("dfg", "asd|dfg|ghj")); + ASSERT_FALSE(rule_field_matches_string("azd", "asd|dfg|ghj")); + + // Special characters + ASSERT_FALSE(rule_field_matches_string("{", "{")); + } else { + // Single character matching + ASSERT(rule_field_matches_string("a", "?")); + + // Wildcard matching + ASSERT(rule_field_matches_string("anything", "*")); + ASSERT(rule_field_matches_string("*", "*")); + ASSERT(rule_field_matches_string("", "*")); + ASSERT(rule_field_matches_string("ffffasd", "*asd")); + + // Substring matching + ASSERT_FALSE(rule_field_matches_string("asd", "")); + ASSERT_FALSE(rule_field_matches_string("asd", "sd")); + ASSERT_FALSE(rule_field_matches_string("asd", "a")); + ASSERT_FALSE(rule_field_matches_string("asd", "d")); + ASSERT(rule_field_matches_string("asd", "asd")); + } PASS(); } + SUITE(suite_rules) { + bool store = settings.enable_regex; + + settings.enable_regex = false; RUN_TEST(test_pattern_match); + + settings.enable_regex = true; + RUN_TEST(test_pattern_match); + + settings.enable_regex = store; }