From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by arlo.cworth.org (Postfix) with ESMTP id A6B596DE0AEA for ; Mon, 3 Sep 2018 13:13:05 -0700 (PDT) X-Virus-Scanned: Debian amavisd-new at cworth.org X-Spam-Flag: NO X-Spam-Score: -0.01 X-Spam-Level: X-Spam-Status: No, score=-0.01 tagged_above=-999 required=5 tests=[HEADER_FROM_DIFFERENT_DOMAINS=0.001, SPF_PASS=-0.001, T_RP_MATCHES_RCVD=-0.01] autolearn=disabled Received: from arlo.cworth.org ([127.0.0.1]) by localhost (arlo.cworth.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id NldV8AxA6CVg for ; Mon, 3 Sep 2018 13:13:04 -0700 (PDT) X-Greylist: delayed 599 seconds by postgrey-1.36 at arlo; Mon, 03 Sep 2018 13:13:04 PDT Received: from mail.mugenguild.com (mugenguild.com [5.135.189.5]) by arlo.cworth.org (Postfix) with ESMTPS id 5006A6DE0AE7 for ; Mon, 3 Sep 2018 13:13:04 -0700 (PDT) Received: from localhost (mue-88-130-57-010.dsl.tropolys.de [88.130.57.10]) by mail.mugenguild.com (Postfix) with ESMTPSA id CEEF05FAAE; Mon, 3 Sep 2018 22:03:01 +0200 (CEST) From: Vincent Breitmoser To: notmuch@notmuchmail.org Subject: [PATCH] cli: add --output=filesandtags to notmuch search Date: Mon, 3 Sep 2018 22:02:56 +0200 Message-Id: <20180903200256.16557-1-look@my.amazin.horse> X-Mailer: git-send-email 2.18.0 X-BeenThere: notmuch@notmuchmail.org X-Mailman-Version: 2.1.26 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 03 Sep 2018 20:13:05 -0000 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 <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 <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