From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!.POSTED.blaine.gmane.org!not-for-mail From: Noam Postavsky Newsgroups: gmane.emacs.bugs Subject: bug#36237: Support (rx (and (regexp EXPR) (regexp-quote EXPR))) Date: Sat, 22 Jun 2019 18:05:58 -0400 Message-ID: <877e9duvg9.fsf@gmail.com> References: <87fto9yawl.fsf@gmail.com> <385FA4F7-7FB5-43A2-B571-CFBA20B24123@acm.org> <87tvcmwe6b.fsf@gmail.com> <87o92tw13b.fsf@gmail.com> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: blaine.gmane.org; posting-host="blaine.gmane.org:195.159.176.226"; logging-data="103024"; mail-complaints-to="usenet@blaine.gmane.org" User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/26.2 (gnu/linux) Cc: Michael Heerdegen , 36237@debbugs.gnu.org, Stefan Monnier , kevin.legouguec@gmail.com To: Mattias =?UTF-8?Q?Engdeg=C3=A5rd?= Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane.org@gnu.org Sun Jun 23 00:07:29 2019 Return-path: Envelope-to: geb-bug-gnu-emacs@m.gmane.org Original-Received: from lists.gnu.org ([209.51.188.17]) by blaine.gmane.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.89) (envelope-from ) id 1heoAQ-000Qa7-1k for geb-bug-gnu-emacs@m.gmane.org; Sun, 23 Jun 2019 00:07:26 +0200 Original-Received: from localhost ([::1]:42544 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.86_2) (envelope-from ) id 1heoAO-0007QQ-Vg for geb-bug-gnu-emacs@m.gmane.org; Sat, 22 Jun 2019 18:07:24 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]:56227) by lists.gnu.org with esmtp (Exim 4.86_2) (envelope-from ) id 1heoA5-0007QH-BD for bug-gnu-emacs@gnu.org; Sat, 22 Jun 2019 18:07:08 -0400 Original-Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1heoA2-0002ex-IJ for bug-gnu-emacs@gnu.org; Sat, 22 Jun 2019 18:07:05 -0400 Original-Received: from debbugs.gnu.org ([209.51.188.43]:39080) by eggs.gnu.org with esmtps (TLS1.0:RSA_AES_128_CBC_SHA1:16) (Exim 4.71) (envelope-from ) id 1heoA2-0002eC-78 for bug-gnu-emacs@gnu.org; Sat, 22 Jun 2019 18:07:02 -0400 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1heoA1-0006Wh-Un for bug-gnu-emacs@gnu.org; Sat, 22 Jun 2019 18:07:02 -0400 X-Loop: help-debbugs@gnu.org Resent-From: Noam Postavsky Original-Sender: "Debbugs-submit" Resent-CC: bug-gnu-emacs@gnu.org Resent-Date: Sat, 22 Jun 2019 22:07:01 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 36237 X-GNU-PR-Package: emacs X-GNU-PR-Keywords: patch Original-Received: via spool by 36237-submit@debbugs.gnu.org id=B36237.156124117025028 (code B ref 36237); Sat, 22 Jun 2019 22:07:01 +0000 Original-Received: (at 36237) by debbugs.gnu.org; 22 Jun 2019 22:06:10 +0000 Original-Received: from localhost ([127.0.0.1]:52624 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1heo9B-0006Vc-2r for submit@debbugs.gnu.org; Sat, 22 Jun 2019 18:06:09 -0400 Original-Received: from mail-io1-f44.google.com ([209.85.166.44]:33908) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1heo98-0006V4-8s for 36237@debbugs.gnu.org; Sat, 22 Jun 2019 18:06:07 -0400 Original-Received: by mail-io1-f44.google.com with SMTP id k8so23608iot.1 for <36237@debbugs.gnu.org>; Sat, 22 Jun 2019 15:06:06 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:references:date:in-reply-to:message-id :user-agent:mime-version; bh=+9lctW79u6hxQa8juAnnKGkzAH/IQCIRwX3daB1MQ1c=; b=XJ2sezFiS+UolIFrsazbStTV9mC/Jhl4zZfpL3DlSZFARZwVsN/p16zQ6W7iMYqH+3 ldSko6MF6HgRFHzXnWRS1TPG2VyzvjtK4ha4CI4vy5LQvwr+I+lP3sXr5v2C/StxICwR 7zgdn3gw5VfA+C6QM1NdEheLq/0mgnmWEKUhFWMBpTy+msvMCsO5paIllupOQHgRNdkZ ZbF6avxis2Z0xMphrD8bA7UeHpwNQytJZi+V+zG5GcPZrcQ2zQgum4h4zeWt9OG9FtGm 9zKU4t/AAq7I/OSBK3hi36KUWUDY6rfHJ8/lxkQa+GMezoC8OdSL4Nipx3vt7iL6k8j6 20rg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:references:date:in-reply-to :message-id:user-agent:mime-version; bh=+9lctW79u6hxQa8juAnnKGkzAH/IQCIRwX3daB1MQ1c=; b=S0olWEvCfwtbomtEZQHPMcBR4KIfWZh0ZeLjyTWsS7swZYW4uX06LG9mJh0A0Z5tbx JN11mEVOOb4ShZbAvJH7r8OA+VSJ5dFgnt46NPk7SlUMoYYIReCwng2rT0UT/adf0Jk6 JU0NKa6Y6oJ8B3mNjI0YwCkFdYuuqscFi0rorYVRWDirVdvgzs9LocgTk07fdjPKaldi I5BoZa9ykfigtyBVH2p/wdYb+RJl08GsnWY/oFvhoG2rhQXg2cCZ4LF9gAHz7ZtLkihZ 2OJ3G1ciuIKe0MXckxtJjJabYt4VAOOvKSKu1SrNXwk91krVoXla3NTIHXvVMUhABATP /b/A== X-Gm-Message-State: APjAAAXL4V2qG3IsjFmCDDxTkiQnv9c/wNi6gPSXwT7g8SdcAiIVxscp 5/gpHo+0PAebVN1X/0iwiGQ= X-Google-Smtp-Source: APXvYqwRVGLUVddX5nFuIi12XzILAaGssRqy9P8i6aAF+VzwSz07MPiTV21ST4u+DitvVBwah3aWLA== X-Received: by 2002:a5d:9c46:: with SMTP id 6mr1944997iof.6.1561241160641; Sat, 22 Jun 2019 15:06:00 -0700 (PDT) Original-Received: from minid (cbl-45-2-119-34.yyz.frontiernetworks.ca. [45.2.119.34]) by smtp.gmail.com with ESMTPSA id p25sm8599389iol.48.2019.06.22.15.05.58 (version=TLS1_2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256/256); Sat, 22 Jun 2019 15:05:59 -0700 (PDT) In-Reply-To: ("Mattias \=\?utf-8\?Q\?Engdeg\=C3\=A5rd\=22's\?\= message of "Thu, 20 Jun 2019 12:26:21 +0200") X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] X-Received-From: 209.51.188.43 X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane.org@gnu.org Original-Sender: "bug-gnu-emacs" Xref: news.gmane.org gmane.emacs.bugs:161065 Archived-At: --=-=-= Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable Mattias Engdeg=C3=A5rd writes: > -;; (rx (and line-start (eval something-else))), statically or > -;; (rx-to-string '(and line-start ,something-else)), dynamically. > +;; (rx (seq line-start (regexp something-else))) > > You can actually drop the `seq' form entirely, since it's implicit in `rx= '. > It was only needed for `rx-to-string' which is now gone. Yeah, that applies to most of the examples actually. Updated (and I found a couple of mistakes in them). > +`(literal STRING)' > + matches STRING literally, where STRING is any lisp > + expression that evaluates to a string. > > It's better to name the metavariable EXPR, STRING-EXPR or LISP-EXPR to > make it clear that it's an arbitrary lisp expression, especially since > STRING is used for a constant string just above. Sure. > The same goes for `regexp', since it can now be a lisp expression; > this should be mentioned in the describing paragraph, using a similar > phrasing. The `literal' item should probably be moved next to > `regexp', since they are closely related. Yeah, I wasn't entirely sure whether `literal' should be considered more related to `regexp' or STRING. I guess since I've added a mention of `literal' and `regexp' in the paragraphs above it makes sense to put them at the end together. > The paragraph on `eval' uses FORM, which is too generic No, it's not generic, see (info "(elisp) Intro Eval"): A Lisp object that is intended for evaluation is called a "form" or "expression"(1). --=-=-= Content-Type: text/plain Content-Disposition: attachment; filename=0001-Support-rx-and-regexp-EXPR-literal-EXPR-Bug-36237.patch Content-Description: patch >From 3346081acbac014ac3ecef468a46c19e60e9dcc0 Mon Sep 17 00:00:00 2001 From: Noam Postavsky Date: Fri, 14 Jun 2019 08:43:17 -0400 Subject: [PATCH] Support (rx (and (regexp EXPR) (literal EXPR))) (Bug#36237) * lisp/emacs-lisp/rx.el (rx-regexp): Allow non-string forms. (rx-constituents): Add literal constituent, which is like a plain STRING form, but allows arbitrary lisp expressions. (rx-literal): New function. (rx-compile-to-lisp): New variable. (rx--subforms): New helper function for handling subforms, including non-constant case. (rx-group-if, rx-and, rx-or, rx-=, rx->=, rx-repeat, rx-submatch) (rx-submatch-n, rx-kleene, rx-atomic-p): Use it to handle non-constant subforms. (rx): Document new form, wrap non-constant forms with concat call. * test/lisp/emacs-lisp/rx-tests.el (rx-tests--match): New macro. (rx-nonstring-expr, rx-nonstring-expr-non-greedy): New tests. * etc/NEWS: Announce changes. squash! Support (rx (and (regexp EXPR) (literal EXPR))) (Bug#36237) --- etc/NEWS | 6 + lisp/emacs-lisp/rx.el | 240 +++++++++++++++++++++++++-------------- test/lisp/emacs-lisp/rx-tests.el | 41 +++++++ 3 files changed, 200 insertions(+), 87 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index 0cfac248a3..dc034a55af 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1400,12 +1400,18 @@ when given in a string. Previously, '(any "\x80-\xff")' would match characters U+0080...U+00FF. Now the expression matches raw bytes in the 128...255 range, as expected. +--- *** The rx 'or' and 'seq' forms no longer require any arguments. (or) produces a regexp that never matches anything, while (seq) matches the empty string, each being an identity for the operation. This also works for their aliases: '|' for 'or'; ':', 'and' and 'sequence' for 'seq'. +--- +*** 'regexp' and new 'literal' accept arbitrary lisp as arguments. +In this case, 'rx' will generate code which produces a regexp string +at runtime, instead of a constant string. + ** Frames +++ diff --git a/lisp/emacs-lisp/rx.el b/lisp/emacs-lisp/rx.el index 8ef78fd69e..c59eb40f08 100644 --- a/lisp/emacs-lisp/rx.el +++ b/lisp/emacs-lisp/rx.el @@ -47,57 +47,58 @@ ;; Rx translates a sexp notation for regular expressions into the ;; usual string notation. The translation can be done at compile-time -;; by using the `rx' macro. It can be done at run-time by calling -;; function `rx-to-string'. See the documentation of `rx' for a -;; complete description of the sexp notation. +;; by using the `rx' macro. The `regexp' and `literal' forms accept +;; non-constant expressions, in which case `rx' will translate to a +;; `concat' expression. Translation can be done fully at run-time by +;; calling function `rx-to-string'. See the documentation of `rx' for +;; a complete description of the sexp notation. ;; ;; Some examples of string regexps and their sexp counterparts: ;; ;; "^[a-z]*" -;; (rx (and line-start (0+ (in "a-z")))) +;; (rx line-start (0+ (in "a-z"))) ;; ;; "\n[^ \t]" -;; (rx (and "\n" (not (any " \t")))) +;; (rx ?\n (not (in " \t"))) ;; ;; "\\*\\*\\* EOOH \\*\\*\\*\n" ;; (rx "*** EOOH ***\n") ;; ;; "\\<\\(catch\\|finally\\)\\>[^_]" -;; (rx (and word-start (submatch (or "catch" "finally")) word-end -;; (not (any ?_)))) +;; (rx word-start (submatch (or "catch" "finally")) word-end +;; (not (in ?_))) ;; ;; "[ \t\n]*:\\([^:]+\\|$\\)" -;; (rx (and (zero-or-more (in " \t\n")) ":" -;; (submatch (or line-end (one-or-more (not (any ?:))))))) +;; (rx (* (in " \t\n")) ":" +;; (submatch (or line-end (+ (not (in ?:)))))) ;; -;; "^content-transfer-encoding:\\(\n?[\t ]\\)*quoted-printable\\(\n?[\t ]\\)*" -;; (rx (and line-start -;; "content-transfer-encoding:" -;; (+ (? ?\n)) (any " \t") -;; "quoted-printable" -;; (+ (? ?\n)) (any " \t")) +;; "^content-transfer-encoding:\\(?:\n?[\t ]\\)*quoted-printable\\(?:\n?[\t ]\\)*" +;; (rx line-start +;; "content-transfer-encoding:" +;; (* (? ?\n) (in " \t")) +;; "quoted-printable" +;; (* (? ?\n) (in " \t"))) ;; ;; (concat "^\\(?:" something-else "\\)") -;; (rx (and line-start (eval something-else))), statically or -;; (rx-to-string '(and line-start ,something-else)), dynamically. +;; (rx line-start (regexp something-else)) ;; ;; (regexp-opt '(STRING1 STRING2 ...)) ;; (rx (or STRING1 STRING2 ...)), or in other words, `or' automatically ;; calls `regexp-opt' as needed. ;; ;; "^;;\\s-*\n\\|^\n" -;; (rx (or (and line-start ";;" (0+ space) ?\n) -;; (and line-start ?\n))) +;; (rx (or (seq line-start ";;" (0+ space) ?\n) +;; (seq line-start ?\n))) ;; ;; "\\$[I]d: [^ ]+ \\([^ ]+\\) " -;; (rx (and "$Id: " -;; (1+ (not (in " "))) -;; " " -;; (submatch (1+ (not (in " ")))) -;; " ")) +;; (rx "$Id: " +;; (1+ (not (in " "))) +;; " " +;; (submatch (1+ (not (in " ")))) +;; " ") ;; ;; "\\\\\\\\\\[\\w+" -;; (rx (and ?\\ ?\\ ?\[ (1+ word))) +;; (rx "\\\\[" (1+ word)) ;; ;; etc. @@ -176,6 +177,7 @@ (defvar rx-constituents ;Not `const' because some modes extend it. (not-syntax . (rx-not-syntax 1 1)) ; sregex (category . (rx-category 1 1 rx-check-category)) (eval . (rx-eval 1 1)) + (literal . (rx-literal 1 1 stringp)) (regexp . (rx-regexp 1 1 stringp)) (regex . regexp) ; sregex (digit . "[[:digit:]]") @@ -302,6 +304,10 @@ (defvar rx-greedy-flag t "Non-nil means produce greedy regular expressions for `zero-or-one', `zero-or-more', and `one-or-more'. Dynamically bound.") +(defvar rx--compile-to-lisp nil + "Nil means return a regexp as a string. +Non-nil means we may return a lisp form which produces a +string (used for `rx' macro).") (defun rx-info (op head) "Return parsing/code generation info for OP. @@ -344,7 +350,7 @@ (defun rx-check (form) (> nargs max-args)) (error "rx form `%s' accepts at most %d args" (car form) max-args)) - (when (not (null type-pred)) + (when type-pred (dolist (sub-form (cdr form)) (unless (funcall type-pred sub-form) (error "rx form `%s' requires args satisfying `%s'" @@ -360,8 +366,9 @@ (defun rx-group-if (regexp group) ;; for concatenation ((eq group ':) (if (rx-atomic-p - (if (string-match - "\\(?:[?*+]\\??\\|\\\\{[0-9]*,?[0-9]*\\\\}\\)\\'" regexp) + (if (and (stringp regexp) + (string-match + "\\(?:[?*+]\\??\\|\\\\{[0-9]*,?[0-9]*\\\\}\\)\\'" regexp)) (substring regexp 0 (match-beginning 0)) regexp)) (setq group nil))) @@ -370,9 +377,10 @@ (defun rx-group-if (regexp group) ;; do anyway ((eq group t)) ((rx-atomic-p regexp t) (setq group nil))) - (if group - (concat "\\(?:" regexp "\\)") - regexp)) + (cond ((and group (stringp regexp)) + (concat "\\(?:" regexp "\\)")) + (group `("\\(?:" ,@regexp "\\)")) + (t regexp))) (defvar rx-parent) @@ -384,7 +392,7 @@ (defun rx-and (form) FORM is of the form `(and FORM1 ...)'." (rx-check form) (rx-group-if - (mapconcat (lambda (x) (rx-form x ':)) (cdr form) nil) + (rx--subforms (cdr form) ':) (and (memq rx-parent '(* t)) rx-parent))) @@ -396,7 +404,7 @@ (defun rx-or (form) ((null (cdr form)) regexp-unmatchable) ((cl-every #'stringp (cdr form)) (regexp-opt (cdr form) nil t)) - (t (mapconcat (lambda (x) (rx-form x '|)) (cdr form) "\\|"))) + (t (rx--subforms (cdr form) '| "\\|"))) (and (memq rx-parent '(: * t)) rx-parent))) @@ -669,7 +677,10 @@ (defun rx-= (form) (unless (and (integerp (nth 1 form)) (> (nth 1 form) 0)) (error "rx `=' requires positive integer first arg")) - (format "%s\\{%d\\}" (rx-form (nth 2 form) '*) (nth 1 form))) + (let ((subform (rx-form (nth 2 form) '*))) + (if (stringp subform) + (format "%s\\{%d\\}" subform (nth 1 form)) + `(,@subform ,(format "\\{%d\\}" (nth 1 form)))))) (defun rx->= (form) @@ -679,7 +690,10 @@ (defun rx->= (form) (unless (and (integerp (nth 1 form)) (> (nth 1 form) 0)) (error "rx `>=' requires positive integer first arg")) - (format "%s\\{%d,\\}" (rx-form (nth 2 form) '*) (nth 1 form))) + (let ((subform (rx-form (nth 2 form) '*))) + (if (stringp subform) + (format "%s\\{%d,\\}" subform (nth 1 form)) + `(,@subform ,(format "\\{%d,\\}" (nth 1 form)))))) (defun rx-** (form) @@ -700,7 +714,10 @@ (defun rx-repeat (form) (unless (and (integerp (nth 1 form)) (> (nth 1 form) 0)) (error "rx `repeat' requires positive integer first arg")) - (format "%s\\{%d\\}" (rx-form (nth 2 form) '*) (nth 1 form))) + (let ((subform (rx-form (nth 2 form) '*))) + (if (stringp subform) + (format "%s\\{%d\\}" subform (nth 1 form)) + `(,@subform ,(format "\\{%d\\}" (nth 1 form)))))) ((or (not (integerp (nth 2 form))) (< (nth 2 form) 0) (not (integerp (nth 1 form))) @@ -708,30 +725,26 @@ (defun rx-repeat (form) (< (nth 2 form) (nth 1 form))) (error "rx `repeat' range error")) (t - (format "%s\\{%d,%d\\}" (rx-form (nth 3 form) '*) - (nth 1 form) (nth 2 form))))) + (let ((subform (rx-form (nth 3 form) '*))) + (if (stringp subform) + (format "%s\\{%d,%d\\}" subform (nth 1 form) (nth 2 form)) + `(,@subform ,(format "\\{%d,%d\\}" (nth 1 form) (nth 2 form)))))))) (defun rx-submatch (form) "Parse and produce code from FORM, which is `(submatch ...)'." - (concat "\\(" - (if (= 2 (length form)) - ;; Only one sub-form. - (rx-form (cadr form)) - ;; Several sub-forms implicitly concatenated. - (mapconcat (lambda (re) (rx-form re ':)) (cdr form) nil)) - "\\)")) + (let ((subforms (rx--subforms (cdr form) ':))) + (if (stringp subforms) + (concat "\\(" subforms "\\)") + `("\\(" ,@subforms "\\)")))) (defun rx-submatch-n (form) "Parse and produce code from FORM, which is `(submatch-n N ...)'." - (let ((n (nth 1 form))) - (concat "\\(?" (number-to-string n) ":" - (if (= 3 (length form)) - ;; Only one sub-form. - (rx-form (nth 2 form)) - ;; Several sub-forms implicitly concatenated. - (mapconcat (lambda (re) (rx-form re ':)) (cddr form) nil)) - "\\)"))) + (let ((n (nth 1 form)) + (subforms (rx--subforms (cddr form) ':))) + (if (stringp subforms) + (concat "\\(?" (number-to-string n) ":" subforms "\\)") + `("\\(?" ,(number-to-string n) ":" ,@subforms "\\)")))) (defun rx-backref (form) "Parse and produce code from FORM, which is `(backref N)'." @@ -759,9 +772,12 @@ (defun rx-kleene (form) (t "?"))) (op (cond ((memq (car form) '(* *? 0+ zero-or-more)) "*") ((memq (car form) '(+ +? 1+ one-or-more)) "+") - (t "?")))) + (t "?"))) + (subform (rx-form (cadr form) '*))) (rx-group-if - (concat (rx-form (cadr form) '*) op suffix) + (if (stringp subform) + (concat subform op suffix) + `(,@subform ,(concat op suffix))) (and (memq rx-parent '(t *)) rx-parent)))) @@ -789,15 +805,18 @@ (defun rx-atomic-p (r &optional lax) be detected without much effort. A guarantee of no false negatives would require a theoretic specification of the set of all atomic regexps." - (let ((l (length r))) - (cond - ((<= l 1)) - ((= l 2) (= (aref r 0) ?\\)) - ((= l 3) (string-match "\\`\\(?:\\\\[cCsS_]\\|\\[[^^]\\]\\)" r)) - ((null lax) + (if (and rx--compile-to-lisp + (not (stringp r))) + nil ;; Runtime value, we must assume non-atomic. + (let ((l (length r))) (cond - ((string-match "\\`\\[\\^?]?\\(?:\\[:[a-z]+:]\\|[^]]\\)*]\\'" r)) - ((string-match "\\`\\\\(\\(?:[^\\]\\|\\\\[^)]\\)*\\\\)\\'" r))))))) + ((<= l 1)) + ((= l 2) (= (aref r 0) ?\\)) + ((= l 3) (string-match "\\`\\(?:\\\\[cCsS_]\\|\\[[^^]\\]\\)" r)) + ((null lax) + (cond + ((string-match "\\`\\[\\^?]?\\(?:\\[:[a-z]+:]\\|[^]]\\)*]\\'" r)) + ((string-match "\\`\\\\(\\(?:[^\\]\\|\\\\[^)]\\)*\\\\)\\'" r)))))))) (defun rx-syntax (form) @@ -853,9 +872,23 @@ (defun rx-greedy (form) (defun rx-regexp (form) "Parse and produce code from FORM, which is `(regexp STRING)'." - (rx-check form) - (rx-group-if (cadr form) rx-parent)) - + (cond ((stringp form) + (rx-group-if (cadr form) rx-parent)) + (rx--compile-to-lisp + ;; Always group non-string forms, since we can't be sure they + ;; are atomic. + (rx-group-if (cdr form) t)) + (t (rx-check form)))) + +(defun rx-literal (form) + "Parse and produce code from FORM, which is `(literal STRING-EXP)'." + (cond ((stringp form) + ;; This is allowed, but makes little sense, you could just + ;; use STRING directly. + (rx-group-if (regexp-quote (cadr form)) rx-parent)) + (rx--compile-to-lisp + (rx-group-if `((regexp-quote ,(cadr form))) rx-parent)) + (t (rx-check form)))) (defun rx-form (form &optional parent) "Parse and produce code for regular expression FORM. @@ -886,12 +919,38 @@ (defun rx-form (form &optional parent) (t (error "rx syntax error at `%s'" form))))) +(defun rx--subforms (subforms &optional parent separator) + "Produce code for regular expressions SUBFORMS. +SUBFORMS is a list of regular expression sexps. +PARENT controls grouping, as in `rx-form'. +Insert SEPARATOR between the code from each of SUBFORMS." + (if (null (cdr subforms)) + ;; Zero or one forms, no need for grouping. + (and subforms (rx-form (car subforms))) + (let ((listify (lambda (x) + (if (listp x) (copy-sequence x) + (list x))))) + (setq subforms (mapcar (lambda (x) (rx-form x parent)) subforms)) + (cond ((or (not rx--compile-to-lisp) + (cl-every #'stringp subforms)) + (mapconcat #'identity subforms separator)) + (separator + (nconc (funcall listify (car subforms)) + (mapcan (lambda (x) + (cons separator (funcall listify x))) + (cdr subforms)))) + (t (mapcan listify subforms)))))) + ;;;###autoload (defun rx-to-string (form &optional no-group) "Parse and produce code for regular expression FORM. FORM is a regular expression in sexp form. -NO-GROUP non-nil means don't put shy groups around the result." +NO-GROUP non-nil means don't put shy groups around the result. + +In contrast to the `rx' macro, subforms `literal' and `regexp' +will not accept non-string arguments, i.e., (literal STRING) +becomes just a more verbose version of STRING." (rx-group-if (rx-form form) (null no-group))) @@ -901,8 +960,12 @@ (defmacro rx (&rest regexps) REGEXPS is a non-empty sequence of forms of the sort listed below. Note that `rx' is a Lisp macro; when used in a Lisp program being -compiled, the translation is performed by the compiler. -See `rx-to-string' for how to do such a translation at run-time. +compiled, the translation is performed by the compiler. The +`literal' and `regexp' forms accept subforms that will evaluate +to strings, in addition to constant strings. If REGEXPS include +such forms, then the result is an expression which returns a +regexp string, rather than a regexp string directly. See +`rx-to-string' for performing translation completely at run-time. The following are valid subforms of regular expressions in sexp notation. @@ -1202,18 +1265,29 @@ (defmacro rx (&rest regexps) `(backref N)' matches what was matched previously by submatch N. +`(literal STRING-EXPR)' + matches STRING-EXPR literally, where STRING-EXPR is any lisp + expression that evaluates to a string. + +`(regexp REGEXP-EXPR)' + include REGEXP-EXPR in string notation in the result, where + REGEXP-EXPR is any lisp expression that evaluates a string + containing a valid regexp. + `(eval FORM)' evaluate FORM and insert result. If result is a string, - `regexp-quote' it. - -`(regexp REGEXP)' - include REGEXP in string notation in the result." - (cond ((null regexps) - (error "No regexp")) - ((cdr regexps) - (rx-to-string `(and ,@regexps) t)) - (t - (rx-to-string (car regexps) t)))) + `regexp-quote' it. Note that FORM is evaluated during + macroexpansion." + (let* ((rx--compile-to-lisp t) + (re (cond ((null regexps) + (error "No regexp")) + ((cdr regexps) + (rx-to-string `(and ,@regexps) t)) + (t + (rx-to-string (car regexps) t))))) + (if (stringp re) + re + `(concat ,@re)))) (pcase-defmacro rx (&rest regexps) @@ -1275,14 +1349,6 @@ (pcase-defmacro rx (&rest regexps) for var in vars collect `(app (match-string ,i) ,var))))) -;; ;; sregex.el replacement - -;; ;;;###autoload (provide 'sregex) -;; ;;;###autoload (autoload 'sregex "rx") -;; (defalias 'sregex 'rx-to-string) -;; ;;;###autoload (autoload 'sregexq "rx" nil nil 'macro) -;; (defalias 'sregexq 'rx) - (provide 'rx) ;;; rx.el ends here diff --git a/test/lisp/emacs-lisp/rx-tests.el b/test/lisp/emacs-lisp/rx-tests.el index 6f392d616d..bab71b522b 100644 --- a/test/lisp/emacs-lisp/rx-tests.el +++ b/test/lisp/emacs-lisp/rx-tests.el @@ -115,5 +115,46 @@ (ert-deftest rx-seq () ;; Test zero-argument `seq'. (should (equal (rx (seq)) ""))) +(defmacro rx-tests--match (regexp string &optional match) + (macroexp-let2 nil strexp string + `(ert-info ((format "Matching %S to %S" ',regexp ,strexp)) + (should (string-match ,regexp ,strexp)) + ,@(when match + `((should (equal (match-string 0 ,strexp) ,match))))))) + +(ert-deftest rx-nonstring-expr () + (let ((bee "b") + (vowel "[aeiou]")) + (rx-tests--match (rx "a" (literal bee) "c") "abc") + (rx-tests--match (rx "a" (regexp bee) "c") "abc") + (rx-tests--match (rx "a" (or (regexp bee) "xy") "c") "abc") + (rx-tests--match (rx "a" (or "xy" (regexp bee)) "c") "abc") + (should-not (string-match (rx (or (regexp bee) "xy")) "")) + (rx-tests--match (rx "a" (= 3 (regexp bee)) "c") "abbbc") + (rx-tests--match (rx "x" (= 3 (regexp vowel)) "z") "xeoez") + (should-not (string-match (rx "x" (= 3 (regexp vowel)) "z") "xe[]z")) + (rx-tests--match (rx "x" (= 3 (literal vowel)) "z") + "x[aeiou][aeiou][aeiou]z") + (rx-tests--match (rx "x" (repeat 1 (regexp vowel)) "z") "xaz") + (rx-tests--match (rx "x" (repeat 1 2 (regexp vowel)) "z") "xaz") + (rx-tests--match (rx "x" (repeat 1 2 (regexp vowel)) "z") "xauz") + (rx-tests--match (rx "x" (>= 1 (regexp vowel)) "z") "xaiiz") + (rx-tests--match (rx "x" (** 1 2 (regexp vowel)) "z") "xaiz") + (rx-tests--match (rx "x" (group (regexp vowel)) "z") "xaz") + (rx-tests--match (rx "x" (group-n 1 (regexp vowel)) "z") "xaz") + (rx-tests--match (rx "x" (? (regexp vowel)) "z") "xz"))) + +(ert-deftest rx-nonstring-expr-non-greedy () + "`rx's greediness can't affect runtime regexp parts." + (let ((ad-min "[ad]*?") + (ad-max "[ad]*") + (ad "[ad]")) + (rx-tests--match (rx "c" (regexp ad-min) "a") "cdaaada" "cda") + (rx-tests--match (rx "c" (regexp ad-max) "a") "cdaaada" "cdaaada") + (rx-tests--match (rx "c" (minimal-match (regexp ad-max)) "a") "cdaaada" "cdaaada") + (rx-tests--match (rx "c" (maximal-match (regexp ad-min)) "a") "cdaaada" "cda") + (rx-tests--match (rx "c" (minimal-match (0+ (regexp ad))) "a") "cdaaada" "cda") + (rx-tests--match (rx "c" (maximal-match (0+ (regexp ad))) "a") "cdaaada" "cdaaada"))) + (provide 'rx-tests) ;; rx-tests.el ends here. -- 2.11.0 --=-=-=--