[PATCH] cli: add --output=filesandtags to notmuch search

Subject: [PATCH] cli: add --output=filesandtags to notmuch search

Date: Mon, 3 Sep 2018 22:02:56 +0200

To: notmuch@notmuchmail.org

Cc:

From: Vincent Breitmoser


This commit adds a filesandtags output format, which outputs the
filenames of all matching messages together with their tags. Files and
tags are separated by newlines or null-bytes for --format=text or text0
respectively, so that filenames and tags are on alternating lines. The
json and sexp output formats are a list of maps, with a "filename" and
"tags" key each.

The rationale for this output parameter is to have a way of searching
messages with notmuch in a scenario where display of message info is
taken care of by another application based on filenames (e.g. mblaze),
but that also want to make use of related tags.  This use case isn't
covered with any other notmuch search output format, and very cumbersome
with notmuch show.

It's possible to cover this workflow with a trivial python script.
However in a quick test, a query that returned 40 messages was about
three times slower for me with a python script with a hot cache, and
even worse with a cold cache.
---
 NEWS                        |   7 ++
 doc/man1/notmuch-search.rst |  10 ++-
 notmuch-search.c            |  58 +++++++++++-
 test/T090-search-output.sh  | 171 ++++++++++++++++++++++++++++++++++++
 4 files changed, 241 insertions(+), 5 deletions(-)

diff --git a/NEWS b/NEWS
index 240d594b..18e8a08d 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,10 @@
+Command Line Interface
+----------------------
+
+Add the --output=filesandtags option to `notmuch search`
+
+  This option outputs both the filenames and tags of relevant messages.
+
 Notmuch 0.27 (2018-06-13)
 =========================
 
diff --git a/doc/man1/notmuch-search.rst b/doc/man1/notmuch-search.rst
index 654c5f2c..96593096 100644
--- a/doc/man1/notmuch-search.rst
+++ b/doc/man1/notmuch-search.rst
@@ -35,7 +35,7 @@ Supported options for **search** include
     intended for programs that invoke **notmuch(1)** internally. If
     omitted, the latest supported version will be used.
 
-``--output=(summary|threads|messages|files|tags)``
+``--output=(summary|threads|messages|files|tags|filesandtags)``
     **summary**
         Output a summary of each thread with any message matching the
         search terms. The summary includes the thread ID, date, the
@@ -71,6 +71,14 @@ Supported options for **search** include
         in other directories that are included in the output, although
         these files alone would not match the search.
 
+    **filesandtags**
+        Output the filenames of all messages matching the search terms, together
+        with their corresponding tags. Filenames and tags are output as lines in
+        an alternating fashion so that filenames are on odd lines and their tags
+        on the following even line (``--format=text``), as a JSON arrray of
+        objects (``--format=text``), or as an S-Expression list
+        (``--format=sexp``).
+
     **tags**
         Output all tags that appear on any message matching the search
         terms, either one per line (``--format=text``), separated by null
diff --git a/notmuch-search.c b/notmuch-search.c
index 8f467db4..65167afa 100644
--- a/notmuch-search.c
+++ b/notmuch-search.c
@@ -29,12 +29,13 @@ typedef enum {
     OUTPUT_MESSAGES	= 1 << 2,
     OUTPUT_FILES	= 1 << 3,
     OUTPUT_TAGS		= 1 << 4,
+    OUTPUT_FILESANDTAGS = 1 << 5,
 
     /* Address command */
-    OUTPUT_SENDER	= 1 << 5,
-    OUTPUT_RECIPIENTS	= 1 << 6,
-    OUTPUT_COUNT	= 1 << 7,
-    OUTPUT_ADDRESS	= 1 << 8,
+    OUTPUT_SENDER	= 1 << 6,
+    OUTPUT_RECIPIENTS	= 1 << 7,
+    OUTPUT_COUNT	= 1 << 8,
+    OUTPUT_ADDRESS	= 1 << 9,
 } output_t;
 
 typedef enum {
@@ -537,6 +538,7 @@ do_search_messages (search_context_t *ctx)
     notmuch_message_t *message;
     notmuch_messages_t *messages;
     notmuch_filenames_t *filenames;
+    notmuch_tags_t *tags;
     sprinter_t *format = ctx->format;
     int i;
     notmuch_status_t status;
@@ -583,6 +585,52 @@ do_search_messages (search_context_t *ctx)
 	    }
 	    
 	    notmuch_filenames_destroy( filenames );
+	} else if (ctx->output == OUTPUT_FILESANDTAGS) {
+	    int j;
+	    filenames = notmuch_message_get_filenames (message);
+
+	    for (j = 1;
+		    notmuch_filenames_valid (filenames);
+		    notmuch_filenames_move_to_next (filenames), j++)
+	    {
+
+		if (ctx->dupe < 0 || ctx->dupe == j) {
+		    format->begin_map (format);
+		    format->map_key (format, "filename");
+
+		    format->string (format, notmuch_filenames_get (filenames));
+		    if (format->is_text_printer) {
+			format->separator (format);
+		    }
+
+		    format->map_key (format, "tags");
+		    format->begin_list (format);
+
+		    bool first_tag = true;
+		    for (tags = notmuch_message_get_tags (message);
+			    notmuch_tags_valid (tags);
+			    notmuch_tags_move_to_next (tags))
+		    {
+			const char *tag = notmuch_tags_get (tags);
+			if (format->is_text_printer) {
+			    if (first_tag)
+				first_tag = false;
+			    else
+				fputc (' ', stdout);
+			    fputs (tag, stdout);
+			} else { /* Structured Output */
+			    format->string (format, tag);
+			}
+		    }
+		    notmuch_tags_destroy( tags );
+		    format->end (format);
+
+		    format->end (format);
+		    format->separator (format);
+		}
+
+	    }
+	    notmuch_filenames_destroy( filenames );
 
 	} else if (ctx->output == OUTPUT_MESSAGES) {
             /* special case 1 for speed */
@@ -816,6 +864,7 @@ notmuch_search_command (notmuch_config_t *config, int argc, char *argv[])
 				  { "threads", OUTPUT_THREADS },
 				  { "messages", OUTPUT_MESSAGES },
 				  { "files", OUTPUT_FILES },
+				  { "filesandtags", OUTPUT_FILESANDTAGS },
 				  { "tags", OUTPUT_TAGS },
 				  { 0, 0 } } },
         { .opt_keyword = &ctx->exclude, .name = "exclude", .keywords =
@@ -856,6 +905,7 @@ notmuch_search_command (notmuch_config_t *config, int argc, char *argv[])
 	break;
     case OUTPUT_MESSAGES:
     case OUTPUT_FILES:
+    case OUTPUT_FILESANDTAGS:
 	ret = do_search_messages (ctx);
 	break;
     case OUTPUT_TAGS:
diff --git a/test/T090-search-output.sh b/test/T090-search-output.sh
index bf28d220..e294a5cc 100755
--- a/test/T090-search-output.sh
+++ b/test/T090-search-output.sh
@@ -276,6 +276,177 @@ MAIL_DIR/new/04:2,
 EOF
 test_expect_equal_file EXPECTED OUTPUT
 
+test_begin_subtest "--output=filesandtags"
+notmuch search --output=filesandtags '*' | notmuch_search_files_sanitize >OUTPUT
+cat <<EOF >EXPECTED
+MAIL_DIR/cur/52:2,
+inbox unread
+MAIL_DIR/cur/53:2,
+inbox unread
+MAIL_DIR/cur/50:2,
+inbox unread
+MAIL_DIR/cur/49:2,
+inbox unread
+MAIL_DIR/cur/48:2,
+inbox unread
+MAIL_DIR/cur/47:2,
+inbox unread
+MAIL_DIR/cur/46:2,
+inbox unread
+MAIL_DIR/cur/45:2,
+inbox unread
+MAIL_DIR/cur/44:2,
+inbox unread
+MAIL_DIR/cur/43:2,
+inbox unread
+MAIL_DIR/cur/42:2,
+inbox unread
+MAIL_DIR/cur/41:2,
+inbox unread
+MAIL_DIR/cur/40:2,
+inbox unread
+MAIL_DIR/cur/39:2,
+inbox unread
+MAIL_DIR/cur/38:2,
+inbox unread
+MAIL_DIR/cur/37:2,
+inbox unread
+MAIL_DIR/cur/36:2,
+inbox unread
+MAIL_DIR/cur/35:2,
+inbox unread
+MAIL_DIR/cur/34:2,
+inbox unread
+MAIL_DIR/cur/33:2,
+inbox unread
+MAIL_DIR/cur/32:2,
+inbox unread
+MAIL_DIR/cur/31:2,
+inbox unread
+MAIL_DIR/cur/30:2,
+inbox unread
+MAIL_DIR/cur/29:2,
+inbox unread
+MAIL_DIR/bar/baz/new/28:2,
+inbox unread
+MAIL_DIR/bar/baz/new/27:2,
+inbox unread
+MAIL_DIR/bar/baz/cur/26:2,
+inbox unread
+MAIL_DIR/bar/baz/cur/25:2,
+inbox unread
+MAIL_DIR/bar/baz/24:2,
+attachment inbox signed unread
+MAIL_DIR/bar/baz/23:2,
+attachment inbox signed unread
+MAIL_DIR/bar/new/22:2,
+inbox signed unread
+MAIL_DIR/bar/new/21:2,
+attachment inbox unread
+MAIL_DIR/bar/cur/19:2,
+inbox unread
+MAIL_DIR/cur/51:2,
+inbox unread
+MAIL_DIR/bar/18:2,
+inbox unread
+MAIL_DIR/bar/cur/20:2,
+inbox signed unread
+MAIL_DIR/bar/17:2,
+inbox unread
+MAIL_DIR/foo/baz/new/16:2,
+inbox unread
+MAIL_DIR/foo/baz/new/15:2,
+inbox unread
+MAIL_DIR/foo/baz/cur/14:2,
+inbox unread
+MAIL_DIR/foo/baz/cur/13:2,
+inbox unread
+MAIL_DIR/foo/baz/12:2,
+inbox unread
+MAIL_DIR/foo/baz/11:2,
+inbox unread
+MAIL_DIR/foo/new/10:2,
+inbox unread
+MAIL_DIR/foo/new/09:2,
+inbox unread
+MAIL_DIR/foo/cur/08:2,
+inbox signed unread
+MAIL_DIR/foo/06:2,
+inbox unread
+MAIL_DIR/bar/baz/05:2,
+attachment inbox unread
+MAIL_DIR/new/04:2,
+inbox signed unread
+MAIL_DIR/foo/new/03:2,
+inbox signed unread
+MAIL_DIR/foo/cur/07:2,
+inbox unread
+MAIL_DIR/02:2,
+inbox unread
+MAIL_DIR/01:2,
+inbox unread
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "--output=filesandtags --format=json"
+notmuch search --output=filesandtags --format=json '*' | notmuch_search_files_sanitize >OUTPUT
+cat <<EOF >EXPECTED
+[{"filename": "MAIL_DIR/cur/52:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/53:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/50:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/49:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/48:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/47:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/46:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/45:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/44:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/43:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/42:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/41:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/40:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/39:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/38:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/37:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/36:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/35:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/34:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/33:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/32:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/31:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/30:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/29:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/baz/new/28:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/baz/new/27:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/baz/cur/26:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/baz/cur/25:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/baz/24:2,", "tags": ["attachment", "inbox", "signed", "unread"]},
+{"filename": "MAIL_DIR/bar/baz/23:2,", "tags": ["attachment", "inbox", "signed", "unread"]},
+{"filename": "MAIL_DIR/bar/new/22:2,", "tags": ["inbox", "signed", "unread"]},
+{"filename": "MAIL_DIR/bar/new/21:2,", "tags": ["attachment", "inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/cur/19:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/cur/51:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/18:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/cur/20:2,", "tags": ["inbox", "signed", "unread"]},
+{"filename": "MAIL_DIR/bar/17:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/baz/new/16:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/baz/new/15:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/baz/cur/14:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/baz/cur/13:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/baz/12:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/baz/11:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/new/10:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/new/09:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/foo/cur/08:2,", "tags": ["inbox", "signed", "unread"]},
+{"filename": "MAIL_DIR/foo/06:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/bar/baz/05:2,", "tags": ["attachment", "inbox", "unread"]},
+{"filename": "MAIL_DIR/new/04:2,", "tags": ["inbox", "signed", "unread"]},
+{"filename": "MAIL_DIR/foo/new/03:2,", "tags": ["inbox", "signed", "unread"]},
+{"filename": "MAIL_DIR/foo/cur/07:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/02:2,", "tags": ["inbox", "unread"]},
+{"filename": "MAIL_DIR/01:2,", "tags": ["inbox", "unread"]}]
+EOF
+test_expect_equal_file EXPECTED OUTPUT
+
 dup1=$(notmuch search --output=files id:20091117232137.GA7669@griffis1.net | head -n 1 | sed -e "s,$MAIL_DIR,MAIL_DIR,")
 dup2=$(notmuch search --output=files id:20091117232137.GA7669@griffis1.net | tail -n 1 | sed -e "s,$MAIL_DIR,MAIL_DIR,")
 
-- 
2.18.0

_______________________________________________
notmuch mailing list
notmuch@notmuchmail.org
https://notmuchmail.org/mailman/listinfo/notmuch

Thread: