return ret;
 }
 
+/**
+ * Escapes some special characters in the source string by inserting a '\'
+ * before them. Acts like g_strescape() but does not demage utf8 chars.
+ * Returns a newly allocated string.
+ */
+char *util_strescape(const char *source, const char *exceptions)
+{
+    GString *result = g_string_new(NULL);
+    while (TRUE) {
+        char c = *source++;
+        if ('\0' == c) {
+            goto done;
+        }
+        if (exceptions && !strchr(exceptions, c)) {
+            continue;
+        }
+        switch (c) {
+            case '\n':
+                g_string_append(result, "\\n");
+                break;
+            case '\"':
+                g_string_append(result, "\\\"");
+                break;
+            case '\\':
+                g_string_append(result, "\\\\");
+                break;
+            case '\b':
+                g_string_append(result, "\\b");
+                break;
+            case '\f':
+                g_string_append(result, "\\f");
+                break;
+            case '\r':
+                g_string_append(result, "\\r");
+                break;
+            case '\t':
+                g_string_append(result, "\\t");
+                break;
+            default:
+                g_string_append_c(result, c);
+        }
+    }
+done:
+    return g_string_free(result, FALSE);
+}
+
 static void create_dir_if_not_exists(const char *dirpath)
 {
     if (!g_file_test(dirpath, G_FILE_TEST_IS_DIR)) {
 
 char *util_sanitize_uri(const char *uri_str);
 char *util_strcasestr(const char *haystack, const char *needle);
 char *util_str_replace(const char* search, const char* replace, const char* string);
+char *util_strescape(const char *source, const char *exceptions);
 gboolean util_wildmatch(const char *pattern, const char *subject);
 
 #endif /* end of include guard: _UTIL_H */
 
     g_assert_false(util_wildmatch("foo,?", "fo"));
 }
 
+static void test_strescape(void)
+{
+    unsigned int i;
+    char *value;
+    struct {
+        char *in;
+        char *expected;
+        char *excludes;
+    } data[] = {
+        {"", "", NULL},
+        {"foo", "foo", NULL},
+        {"a\nb\nc", "a\\nb\\nc", NULL},
+        {"foo\"bar", "foo\\bar", NULL},
+        {"s\\t", "s\\\\t", NULL},
+        {"u\bv", "u\\bv", NULL},
+        {"w\fx", "w\\fx", NULL},
+        {"y\rz", "y\\rz", NULL},
+        {"tab\tdi\t", "tab\\tdi\\t", NULL},
+        {"❧äüö\n@foo\t\"bar\"", "❧äüö\\n@foo\\t\\\"bar\\\"", NULL},
+        {"❧äüö\n@foo\t\"bar\"", "❧äüö\\n@foo\\t\"bar\"", "\""},
+        {"❧äüö\n@foo\t\"bar\"", "❧äüö\n@foo\t\\\"bar\\\"", "\n\t"},
+    };
+    for (i = 0; i < LENGTH(data); i++) {
+        value = util_strescape(data->in, data->excludes);
+        g_assert_cmpstr(value, ==, data->expected);
+        g_free(value);
+    }
+}
+
 int main(int argc, char *argv[])
 {
     g_test_init(&argc, &argv, NULL);
     g_test_add_func("/test-util/wildmatch-curlybraces", test_wildmatch_curlybraces);
     g_test_add_func("/test-util/wildmatch-complete", test_wildmatch_complete);
     g_test_add_func("/test-util/wildmatch-multi", test_wildmatch_multi);
+    g_test_add_func("/test-util/strescape", test_strescape);
 
     return g_test_run();
 }