From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from mp0 ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by ms11 with LMTPS id 8IwoHofqsl8PIwAA0tVLHw (envelope-from ) for ; Mon, 16 Nov 2020 21:09:27 +0000 Received: from aspmx1.migadu.com ([2001:41d0:2:4a6f::]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) by mp0 with LMTPS id 0JDfGYfqsl9fCQAA1q6Kng (envelope-from ) for ; Mon, 16 Nov 2020 21:09:27 +0000 Received: from mail.notmuchmail.org (nmbug.tethera.net [IPv6:2607:5300:201:3100::1657]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) server-signature RSA-PSS (2048 bits)) (No client certificate requested) by aspmx1.migadu.com (Postfix) with ESMTPS id 68722940367 for ; Mon, 16 Nov 2020 21:09:26 +0000 (UTC) Received: from nmbug.tethera.net (localhost [127.0.0.1]) by mail.notmuchmail.org (Postfix) with ESMTP id 1463A28824; Mon, 16 Nov 2020 16:09:19 -0500 (EST) Received: from mail.hostpark.net (mail.hostpark.net [212.243.197.30]) by mail.notmuchmail.org (Postfix) with ESMTPS id 4B0BD28818 for ; Mon, 16 Nov 2020 16:09:16 -0500 (EST) Received: from localhost (localhost [127.0.0.1]) by mail.hostpark.net (Postfix) with ESMTP id 60168165C8; Mon, 16 Nov 2020 22:09:15 +0100 (CET) X-Virus-Scanned: by Hostpark/NetZone Mailprotection at hostpark.net Received: from mail.hostpark.net ([127.0.0.1]) by localhost (mail0.hostpark.net [127.0.0.1]) (amavisd-new, port 10124) with ESMTP id H5DxMuDIT2tZ; Mon, 16 Nov 2020 22:09:14 +0100 (CET) Received: from customer (localhost [127.0.0.1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange ECDHE (P-256) server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mail.hostpark.net (Postfix) with ESMTPSA id 1EF6516104; Mon, 16 Nov 2020 22:09:14 +0100 (CET) From: Jonas Bernoulli To: Tom Fitzhenry , notmuch@notmuchmail.org Subject: Re: [PATCH v2] emacs: add notmuch-expr, sexp-style queries In-Reply-To: <20201113120122.26105-1-tom@tom-fitzhenry.me.uk> References: <20200513100024.8474-1-tom@tom-fitzhenry.me.uk> <20201113120122.26105-1-tom@tom-fitzhenry.me.uk> Date: Mon, 16 Nov 2020 22:09:13 +0100 Message-ID: <87wnyl101y.fsf@bernoul.li> MIME-Version: 1.0 Message-ID-Hash: YNND2DTEGQF364OR4JZWIYFBO34F2GVA X-Message-ID-Hash: YNND2DTEGQF364OR4JZWIYFBO34F2GVA X-MailFrom: jonas@bernoul.li X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; header-match-notmuch.notmuchmail.org-0; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; suspicious-header CC: Tom Fitzhenry X-Mailman-Version: 3.2.1 Precedence: list List-Id: "Use and development of the notmuch mail system." List-Help: List-Post: List-Subscribe: List-Unsubscribe: Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit X-Scanner: ns3122888.ip-94-23-21.eu Authentication-Results: aspmx1.migadu.com; dkim=none; dmarc=none; spf=pass (aspmx1.migadu.com: domain of notmuch-bounces@notmuchmail.org designates 2607:5300:201:3100::1657 as permitted sender) smtp.mailfrom=notmuch-bounces@notmuchmail.org X-Spam-Score: -1.01 X-TUID: Ib/AzR1Yn2M5 Tom Fitzhenry writes: > From: Tom Fitzhenry > > notmuch-expr allows you to write notmuch search queries in sexp style like: > > (notmuch-expr > '(and > (to "emacs-devel") > "info manual" > (or > (not (is "spam")) > (is "important")))) > > which will generate the textual query: > > "to:emacs-devel AND (NOT is:spam OR is:important) AND \"info manual\"" > --- > emacs/Makefile.local | 1 + > emacs/notmuch-expr-test.el | 96 ++++++++++++++++++++++++++++ > emacs/notmuch-expr.el | 124 +++++++++++++++++++++++++++++++++++++ > emacs/notmuch.el | 1 + > 4 files changed, 222 insertions(+) > create mode 100644 emacs/notmuch-expr-test.el > create mode 100644 emacs/notmuch-expr.el > > diff --git a/emacs/Makefile.local b/emacs/Makefile.local > index d1b320c3..f68e6e31 100644 > --- a/emacs/Makefile.local > +++ b/emacs/Makefile.local > @@ -22,6 +22,7 @@ emacs_sources := \ > $(dir)/notmuch-version.el \ > $(dir)/notmuch-jump.el \ > $(dir)/notmuch-company.el \ > + $(dir)/notmuch-expr.el \ > $(dir)/notmuch-draft.el > > elpa_sources := ${emacs_sources} $(dir)/notmuch-pkg.el > diff --git a/emacs/notmuch-expr-test.el b/emacs/notmuch-expr-test.el > new file mode 100644 > index 00000000..92029fec > --- /dev/null > +++ b/emacs/notmuch-expr-test.el > @@ -0,0 +1,96 @@ > +(require 'ert) > +(require 'notmuch-expr) > + > +(ert-deftest and () > + (should > + (equal > + "(\"valued\" AND is:unread AND from:spam@example.com)" > + (notmuch-expr > + '(and > + "valued" > + (is "unread") > + (from "spam@example.com")))))) > + > +(ert-deftest body () > + (should > + (equal > + "(body:wallace AND from:gromit)" > + (notmuch-expr > + '(and > + (body "wallace") > + (from "gromit")))))) > + > +(ert-deftest regex () > + (should > + (equal > + "(subject:\"/Ca+sh/\" AND NOT is:important)" > + (notmuch-expr > + '(and > + (subject "/Ca+sh/") > + (not (is "important"))))))) > + > +(ert-deftest precedence () > + (should > + (equal > + "(to:emacs-devel AND (NOT is:spam OR is:important))" > + (notmuch-expr > + '(and > + (to "emacs-devel") > + (or > + (not (is "spam")) > + (is "important"))))))) > + > +(ert-deftest xor () > + (should > + (equal > + "is:inbox XOR is:sent" > + (notmuch-expr > + '(xor > + (is "inbox") > + (is "sent")))))) > + > +(ert-deftest literal () > + (should > + (equal > + "(is:inbox OR from:foo)" > + (notmuch-expr > + '(or > + (is "inbox") > + (literal "from:foo")))))) > + > +(ert-deftest string () > + (should > + (equal > + "(is:inbox OR \"from:foo\")" > + (notmuch-expr > + '(or > + (is "inbox") > + "from:foo"))))) > + > +(ert-deftest tag-with-spaces () > + (should > + (equal > + "is:\"a tag\"" > + (notmuch-expr > + '(tag "a tag"))))) > + > +(ert-deftest quoted-spaces () > + (should > + (equal > + "subject:\"Hello there\"" > + (notmuch-expr > + '(subject "Hello there"))))) > + > +(ert-deftest quoted-backslash () > + (should > + (equal > + "subject:\"A celebration! \\o/ Woo.\"" > + (notmuch-expr > + '(subject "A celebration! \\o/ Woo."))))) > + > +(ert-deftest quoted-quote () > + (should > + (equal > + "subject:\"Gandalf: \\\"Use the force!\\\" 2001\"" > + (notmuch-expr > + '(subject "Gandalf: \"Use the force!\" 2001"))))) > diff --git a/emacs/notmuch-expr.el b/emacs/notmuch-expr.el > new file mode 100644 > index 00000000..f5a3429f > --- /dev/null > +++ b/emacs/notmuch-expr.el > @@ -0,0 +1,124 @@ > +;;; notmuch-expr.el --- An S-exp library for building notmuch search queries -*- lexical-binding: t; -*- > + > +;; Author: Tom Fitzhenry > +;; Package-Requires: ((emacs "24.1")) > +;; URL: https://notmuchmail.org I recently improved consistency of the library headers. They don't confirm to the Emacs conventions but its close enough, and I think internal consistency would be nice to maintain here. Don't specify "URL". "Homepage" would be better, but since this is not an entry point library its better to omit this. I think "notmuch.el" is the only library that specifies this currently. Likewise because "notmuch-expr" is not a standalone _package_ "Package-Requires" should not be specified. By the way, notmuch now requires emacs 25.1, so specifying a lower version would not make sense here anyway. Cheers, Jonas > + > +;;; Commentary: > + > +;; This package provides a way to build notmuch search queries via s-expressions. > +;; > +;; For example, rather than write: > + > +;; "to:emacs-devel AND (NOT is:spam OR is:important) AND \"info manual\"" > +;; > +;; this package allows you to generate the same query via s-expressions: > +;; > +;; (notmuch-expr > +;; '(and > +;; (to "emacs-devel") > +;; "info manual" > +;; (or > +;; (not (is "spam")) > +;; (is "important")))) > +;; > +;; See notmuch-expr-test.el for more examples. > +;; > +;; Some search terms are unsupported. To use those, use the `literal' atom. > +;; For example: (literal "path:spam") > +;; > +;; man page: notmuch-search-terms(7). > +;; The generated search query may change across different versions. > + > +;;; Code: > + > +(defmacro notmuch-expr (query) > + "Compile an sexp QUERY into a textual notmuch query." > + `(notmuch-expr--eval ,query)) > + > +(defun notmuch-expr--eval (expr) > + (pcase expr > + (`(tag ,s) (notmuch-expr--is s)) > + (`(is ,s) (notmuch-expr--is s)) > + (`(from ,s) (notmuch-expr--from s)) > + (`(to ,s) (notmuch-expr--to s)) > + (`(body ,s) (notmuch-expr--body s)) > + (`(subject ,s) (notmuch-expr--subject s)) > + > + ;; Boolean operators. > + (`(and . ,clauses) (notmuch-expr--and clauses)) > + (`(or . ,clauses) (notmuch-expr--or clauses)) > + (`(not ,clause) (notmuch-expr--not clause)) > + (`(xor ,c1 ,c2) (notmuch-expr--xor c1 c2)) > + > + ;; Provide an escape-hatch. > + (`(literal ,s) (notmuch-expr--literal s)) > + > + ;; Otherwise, quote. > + (s (notmuch-expr--quote s)))) > + > +(defun notmuch-expr--and (clauses) > + (concat > + "(" > + (mapconcat 'notmuch-expr--eval clauses " AND ") > + ")")) > + > +(defun notmuch-expr--or (clauses) > + (concat > + "(" > + (mapconcat 'notmuch-expr--eval clauses " OR ") > + ")")) > + > +(defun notmuch-expr--not (clauses) > + (concat "NOT " (notmuch-expr--eval clauses))) > + > +(defun notmuch-expr--xor (c1 c2) > + (concat > + (notmuch-expr--eval c1) > + " XOR " > + (notmuch-expr--eval c2))) > + > +(defun notmuch-expr--body (s) > + (concat "body:" > + (notmuch-expr--leaf s))) > + > +(defun notmuch-expr--subject (s) > + (concat "subject:" > + (notmuch-expr--leaf s))) > + > +(defun notmuch-expr--from (f) > + (concat "from:" > + (notmuch-expr--leaf f))) > + > +(defun notmuch-expr--to (f) > + (concat "to:" > + (notmuch-expr--leaf f))) > + > +(defun notmuch-expr--is (expr) > + (concat "is:" > + (notmuch-expr--leaf expr))) > + > +(defun notmuch-expr--leaf (s) > + (if (string-match-p "^[a-zA-Z0-9.@-]+$" s) > + ;; Avoid ugly quoting. > + ;; This is safe because the string is bound to a prefix > + ;; and thus it won't be misinterpreted by notmuch. > + s > + (notmuch-expr--quote s))) > + > +(defun notmuch-expr--literal (s) > + s) > + > +(defun notmuch-expr--quote (s) > + "Return a quoted version of S." > + (concat "\"" > + (replace-regexp-in-string > + (rx "\"") > + ;; We must double escape the backslash to avoid it being > + ;; interpreted as a back-reference. > + "\\\\\"" > + s) > + "\"")) > + > +(provide 'notmuch-expr) > +;;; notmuch-expr.el ends here > diff --git a/emacs/notmuch.el b/emacs/notmuch.el > index 165aaa43..8c5843e5 100644 > --- a/emacs/notmuch.el > +++ b/emacs/notmuch.el > @@ -79,6 +79,7 @@ > (require 'notmuch-maildir-fcc) > (require 'notmuch-message) > (require 'notmuch-parser) > +(require 'notmuch-expr) > > (defcustom notmuch-search-result-format > `(("date" . "%12s ") > -- > 2.28.0 > _______________________________________________ > notmuch mailing list -- notmuch@notmuchmail.org > To unsubscribe send an email to notmuch-leave@notmuchmail.org