org-make-tags-matcher: Add starred property operators, fix quoting

* lisp/org.el (org-make-tags-matcher): Add starred property operators.
Recognize additional operators "==", "!=", "/=".  Clean up and
document match term parsing.  Remove needless and buggy unquoting of
minus characters in property and tag names.
(org-op-to-function): Recognize additional inequality operator "/=".

* doc/org-manual.org (Matching tags and properties): Add documentation
on starred and additional operators.  Document allowed characters in
property names and handling of minus characters in property names.

* testing/lisp/test-org.el (test-org/map-entries): Add tests for
starred and additional operators.  Add tests for property names
containing minus characters.

* etc/ORG-NEWS: (~org-tags-view~ supports more property operators):
Add announcement on starred and additional operators.

Link: https://orgmode.org/list/9132e58f-d89e-f7df-bbe4-43d53a2367d2@vodafonemail.de
This commit is contained in:
Jens Schmidt 2023-08-06 16:38:04 +02:00 committed by Ihor Radchenko
parent f9e083086f
commit f689eb44f1
No known key found for this signature in database
GPG Key ID: 6470762A7DA11D8B
4 changed files with 192 additions and 37 deletions

View File

@ -9246,16 +9246,18 @@ When matching properties, a number of different operators can be used
to test the value of a property. Here is a complex example: to test the value of a property. Here is a complex example:
#+begin_example #+begin_example
+work-boss+PRIORITY="A"+Coffee="unlimited"+Effort<2 +work-boss+PRIORITY="A"+Coffee="unlimited"+Effort<*2
+With={Sarah\|Denny}+SCHEDULED>="<2008-10-11>" +With={Sarah\|Denny}+SCHEDULED>="<2008-10-11>"
#+end_example #+end_example
#+cindex: operator, for property search
#+texinfo: @noindent #+texinfo: @noindent
The type of comparison depends on how the comparison value is written: The type of comparison depends on how the comparison value is written:
- If the comparison value is a plain number, a numerical comparison is - If the comparison value is a plain number, a numerical comparison is
done, and the allowed operators are =<=, ===, =>=, =<==, =>==, and done, and the allowed operators are =<=, ===, =>=, =<==, =>==, and
=<>=. =<>=. As a synonym for the equality operator ===, there is also
====; =!== and =/== are synonyms of the inequality operator =<>=.
- If the comparison value is enclosed in double-quotes, a string - If the comparison value is enclosed in double-quotes, a string
comparison is done, and the same operators are allowed. comparison is done, and the same operators are allowed.
@ -9273,6 +9275,13 @@ The type of comparison depends on how the comparison value is written:
is performed, with === meaning that the regexp matches the property is performed, with === meaning that the regexp matches the property
value, and =<>= meaning that it does not match. value, and =<>= meaning that it does not match.
- All operators may be optionally followed by an asterisk =*=, like in
=<*=, =!=*=, etc. Such /starred operators/ work like their regular,
unstarred counterparts except that they match only headlines where
the tested property is actually present. This is most useful for
search terms that logically exclude results, like the inequality
operator.
So the search string in the example finds entries tagged =work= but So the search string in the example finds entries tagged =work= but
not =boss=, which also have a priority value =A=, a =Coffee= property not =boss=, which also have a priority value =A=, a =Coffee= property
with the value =unlimited=, an =EFFORT= property that is numerically with the value =unlimited=, an =EFFORT= property that is numerically
@ -9280,6 +9289,28 @@ smaller than 2, a =With= property that is matched by the regular
expression =Sarah\|Denny=, and that are scheduled on or after October expression =Sarah\|Denny=, and that are scheduled on or after October
11, 2008. 11, 2008.
Note that the test on the =EFFORT= property uses operator =<*=, so
that the search result will include only entries that actually have an
=EFFORT= property defined and with numerical value smaller than 2.
With the regular =<= operator, the search would handle entries without
an =EFFORT= property as having a zero effort and would include them in
the result as well.
Currently, you can use only property names including alphanumeric
characters, underscores, and minus characters in search strings. In
addition, if you want to search for a property whose name starts with
a minus character, you have to "quote" that leading minus character
with an explicit positive selection plus character, like this:
#+begin_example
+-long-and-twisted-property-name-="foo"
#+end_example
#+texinfo: @noindent
Without that extra plus character, the minus character would be taken
to indicate a negative selection on search term
=long-and-twisted-property-name-="foo"=.
You can configure Org mode to use property inheritance during You can configure Org mode to use property inheritance during
a search, but beware that this can slow down searches considerably. a search, but beware that this can slow down searches considerably.
See [[*Property Inheritance]], for details. See [[*Property Inheritance]], for details.

View File

@ -125,7 +125,7 @@ New functions to retrieve and set (via ~setf~) commonly used element properties:
- =:contents-post-affiliated= :: ~org-element-post-affiliated~ - =:contents-post-affiliated= :: ~org-element-post-affiliated~
- =:contents-post-blank= :: ~org-element-post-blank~ - =:contents-post-blank= :: ~org-element-post-blank~
- =:parent= :: ~org-element-parent~ - =:parent= :: ~org-element-parent~
***** New macro ~org-element-with-enabled-cache~ ***** New macro ~org-element-with-enabled-cache~
The macro arranges the element cache to be active during =BODY= execution. The macro arranges the element cache to be active during =BODY= execution.
@ -558,6 +558,14 @@ special repeaters ~++~ and ~.+~ are skipped.
A capture template can target ~(here)~ which is the equivalent of A capture template can target ~(here)~ which is the equivalent of
invoking a capture template with a zero prefix. invoking a capture template with a zero prefix.
*** ~org-tags-view~ supports more property operators
It supports inequality operators ~!=~ and ~/=~ in addition to the less
common (BASIC? Pascal? SQL?) ~<>~. And it supports starred versions
of all relational operators (~<*~, ~=*~, ~!=*~, etc.) that work like
the regular, unstarred operators but match a headline only if the
tested property is actually present.
** New functions and changes in function arguments ** New functions and changes in function arguments
*** =TYPES= argument in ~org-element-lineage~ can now be a symbol *** =TYPES= argument in ~org-element-lineage~ can now be a symbol

View File

@ -11304,15 +11304,50 @@ See also `org-scan-tags'."
"Match: " "Match: "
'org-tags-completion-function nil nil nil 'org-tags-history)))) 'org-tags-completion-function nil nil nil 'org-tags-history))))
(let ((match0 match) (let* ((match0 match)
(re (concat (opre "[<=>]=?\\|[!/]=\\|<>")
"^&?\\([-+:]\\)?\\({[^}]+}\\|LEVEL\\([<=>]\\{1,2\\}\\)" (re (concat
"\\([0-9]+\\)\\|\\(\\(?:[[:alnum:]_]+\\(?:\\\\-\\)*\\)+\\)" "^"
"\\([<>=]\\{1,2\\}\\)" ;; implicit AND operator (OR is done by global splitting)
"\\({[^}]+}\\|\"[^\"]*\"\\|-?[.0-9]+\\(?:[eE][-+]?[0-9]+\\)?\\)" "&?"
"\\|" org-tag-re "\\)")) ;; exclusion and inclusion (the latter being implicit)
(start 0) "\\(?1:[-+:]\\)?"
tagsmatch todomatch tagsmatcher todomatcher) ;; query term
"\\(?2:"
;; tag regexp match
"{[^}]+}\\|"
;; LEVEL property match. For sake of consistency,
;; recognize starred operators here as well. We do
;; not need to process them below, however, since
;; the LEVEL property is always present.
"LEVEL\\(?3:" opre "\\)\\*?\\(?4:[0-9]+\\)\\|"
;; regular property match
"\\(?:"
;; property name [1]
"\\(?5:[[:alnum:]_-]+\\)"
;; operator, optionally starred
"\\(?6:" opre "\\)\\(?7:\\*\\)?"
;; operand (regexp, double-quoted string,
;; number)
"\\(?8:"
"{[^}]+}\\|"
"\"[^\"]*\"\\|"
"-?[.0-9]+\\(?:[eE][-+]?[0-9]+\\)?"
"\\)"
"\\)\\|"
;; exact tag match
org-tag-re
"\\)"))
(start 0)
tagsmatch todomatch tagsmatcher todomatcher)
;; [1] The minus characters in property names do *not* conflict
;; with the exclusion operator above, since the mandatory
;; following operator distinguishes these both cases.
;; Accordingly, minus characters do not need any special quoting,
;; even if https://orgmode.org/list/87jzv67k3p.fsf@localhost and
;; commit 19b0e03f32c6032a60150fc6cb07c6f766cb3f6c suggest
;; otherwise.
;; Expand group tags. ;; Expand group tags.
(setq match (org-tags-expand match)) (setq match (org-tags-expand match))
@ -11352,15 +11387,16 @@ See also `org-scan-tags'."
(let* ((rest (substring term (match-end 0))) (let* ((rest (substring term (match-end 0)))
(minus (and (match-end 1) (minus (and (match-end 1)
(equal (match-string 1 term) "-"))) (equal (match-string 1 term) "-")))
(tag (save-match-data ;; Bind the whole query term to `tag' and use that
(replace-regexp-in-string ;; variable for a tag regexp match in [2] or as an
"\\\\-" "-" (match-string 2 term)))) ;; exact tag match in [3].
(tag (match-string 2 term))
(regexp (eq (string-to-char tag) ?{)) (regexp (eq (string-to-char tag) ?{))
(levelp (match-end 4)) (levelp (match-end 4))
(propp (match-end 5)) (propp (match-end 5))
(mm (mm
(cond (cond
(regexp (regexp ; [2]
`(with-syntax-table org-mode-tags-syntax-table `(with-syntax-table org-mode-tags-syntax-table
(org-match-any-p ,(substring tag 1 -1) tags-list))) (org-match-any-p ,(substring tag 1 -1) tags-list)))
(levelp (levelp
@ -11368,28 +11404,46 @@ See also `org-scan-tags'."
level level
,(string-to-number (match-string 4 term)))) ,(string-to-number (match-string 4 term))))
(propp (propp
(let* ((gv (pcase (upcase (match-string 5 term)) (let* (;; Convert property name to an Elisp
;; accessor for that property (aka. as
;; getter value).
(gv (pcase (upcase (match-string 5 term))
("CATEGORY" ("CATEGORY"
'(org-get-category (point))) '(org-get-category (point)))
("TODO" 'todo) ("TODO" 'todo)
(p `(org-cached-entry-get nil ,p)))) (p `(org-cached-entry-get nil ,p))))
(pv (match-string 7 term)) ;; Determine operand (aka. property
;; value).
(pv (match-string 8 term))
;; Determine type of operand. Note that
;; these are not exclusive: Any TIMEP is
;; also STRP.
(regexp (eq (string-to-char pv) ?{)) (regexp (eq (string-to-char pv) ?{))
(strp (eq (string-to-char pv) ?\")) (strp (eq (string-to-char pv) ?\"))
(timep (string-match-p "^\"[[<]\\(?:[0-9]+\\|now\\|today\\|tomorrow\\|[+-][0-9]+[dmwy]\\).*[]>]\"$" pv)) (timep (string-match-p "^\"[[<]\\(?:[0-9]+\\|now\\|today\\|tomorrow\\|[+-][0-9]+[dmwy]\\).*[]>]\"$" pv))
;; Massage operand. TIMEP must come
;; before STRP.
(pv (cond (regexp (substring pv 1 -1))
(timep (org-matcher-time
(substring pv 1 -1)))
(strp (substring pv 1 -1))
(t pv)))
;; Convert operator to Elisp.
(po (org-op-to-function (match-string 6 term) (po (org-op-to-function (match-string 6 term)
(if timep 'time strp)))) (if timep 'time strp)))
(setq pv (if (or regexp strp) (substring pv 1 -1) pv)) ;; Convert whole property term to Elisp.
(when timep (setq pv (org-matcher-time pv))) (pt (cond ((and regexp (eq po '/=))
(cond ((and regexp (eq po '/=)) `(not (string-match ,pv (or ,gv ""))))
`(not (string-match ,pv (or ,gv "")))) (regexp `(string-match ,pv (or ,gv "")))
(regexp `(string-match ,pv (or ,gv ""))) (strp `(,po (or ,gv "") ,pv))
(strp `(,po (or ,gv "") ,pv)) (t
(t `(,po
`(,po (string-to-number (or ,gv ""))
(string-to-number (or ,gv "")) ,(string-to-number pv)))))
,(string-to-number pv)))))) ;; Respect the star after the operand.
(t `(member ,tag tags-list))))) (pt (if (match-end 7) `(and ,gv ,pt) pt)))
pt))
(t `(member ,tag tags-list))))) ; [3]
(push (if minus `(not ,mm) mm) tagsmatcher) (push (if minus `(not ,mm) mm) tagsmatcher)
(setq term rest))) (setq term rest)))
(push `(and ,@tagsmatcher) orlist) (push `(and ,@tagsmatcher) orlist)
@ -11520,12 +11574,12 @@ the list of tags in this group."
"Turn an operator into the appropriate function." "Turn an operator into the appropriate function."
(setq op (setq op
(cond (cond
((equal op "<" ) '(< org-string< org-time<)) ((equal op "<" ) '(< org-string< org-time<))
((equal op ">" ) '(> org-string> org-time>)) ((equal op ">" ) '(> org-string> org-time>))
((member op '("<=" "=<")) '(<= org-string<= org-time<=)) ((member op '("<=" "=<" )) '(<= org-string<= org-time<=))
((member op '(">=" "=>")) '(>= org-string>= org-time>=)) ((member op '(">=" "=>" )) '(>= org-string>= org-time>=))
((member op '("=" "==")) '(= string= org-time=)) ((member op '("=" "==" )) '(= string= org-time=))
((member op '("<>" "!=")) '(/= org-string<> org-time<>)))) ((member op '("<>" "!=" "/=")) '(/= org-string<> org-time<>))))
(nth (if (eq stringp 'time) 2 (if stringp 1 0)) op)) (nth (if (eq stringp 'time) 2 (if stringp 1 0)) op))
(defvar org-add-colon-after-tag-completion nil) ;; dynamically scoped param (defvar org-add-colon-after-tag-completion nil) ;; dynamically scoped param

View File

@ -2833,6 +2833,11 @@ test <point>
(equal '(11) (equal '(11)
(org-test-with-temp-text "* Level 1\n** Level 2" (org-test-with-temp-text "* Level 1\n** Level 2"
(let (org-odd-levels-only) (org-map-entries #'point "LEVEL>1"))))) (let (org-odd-levels-only) (org-map-entries #'point "LEVEL>1")))))
;; Level match with (ignored) starred operator.
(should
(equal '(11)
(org-test-with-temp-text "* Level 1\n** Level 2"
(let (org-odd-levels-only) (org-map-entries #'point "LEVEL>*1")))))
;; Tag match. ;; Tag match.
(should (should
(equal '(11) (equal '(11)
@ -2845,12 +2850,17 @@ test <point>
(should (should
(equal '(11 23) (equal '(11 23)
(org-test-with-temp-text "* H1 :no:\n* H2 :yes1:\n* H3 :yes2:" (org-test-with-temp-text "* H1 :no:\n* H2 :yes1:\n* H3 :yes2:"
(org-map-entries #'point "{yes?}")))) (org-map-entries #'point "{yes.?}"))))
;; Priority match. ;; Priority match.
(should (should
(equal '(1) (equal '(1)
(org-test-with-temp-text "* [#A] H1\n* [#B] H2" (org-test-with-temp-text "* [#A] H1\n* [#B] H2"
(org-map-entries #'point "PRIORITY=\"A\"")))) (org-map-entries #'point "PRIORITY=\"A\""))))
;; Negative priority match.
(should
(equal '(11)
(org-test-with-temp-text "* [#A] H1\n* [#B] H2"
(org-map-entries #'point "PRIORITY/=\"A\""))))
;; Date match. ;; Date match.
(should (should
(equal '(36) (equal '(36)
@ -2881,6 +2891,58 @@ SCHEDULED: <2014-03-04 tue.>"
:TEST: 2 :TEST: 2
:END:" :END:"
(org-map-entries #'point "TEST=1")))) (org-map-entries #'point "TEST=1"))))
;; Regular negative property match.
(should
(equal '(35 68)
(org-test-with-temp-text "
* H1
:PROPERTIES:
:TEST: 1
:END:
* H2
:PROPERTIES:
:TEST: 2
:END:
* H3"
(org-map-entries #'point "TEST!=1"))))
;; Starred negative property match.
(should
(equal '(35)
(org-test-with-temp-text "
* H1
:PROPERTIES:
:TEST: 1
:END:
* H2
:PROPERTIES:
:TEST: 2
:END:
* H3"
(org-map-entries #'point "TEST!=*1"))))
;; Property matches on names including minus characters.
(org-test-with-temp-text
"
* H1 :BAR:
:PROPERTIES:
:TEST-FOO: 1
:END:
* H2 :FOO:
:PROPERTIES:
:TEST-FOO: 2
:END:
* H3 :BAR:
:PROPERTIES:
:-FOO: 1
:END:
* H4 :FOO:
:PROPERTIES:
:-FOO: 2
:END:
* H5"
(should (equal '(2) (org-map-entries #'point "TEST-FOO!=*0-FOO")))
(should (equal '(2) (org-map-entries #'point "-FOO+TEST-FOO!=*0")))
(should (equal '(88) (org-map-entries #'point "+-FOO!=*0-FOO")))
(should (equal '(88) (org-map-entries #'point "-FOO+-FOO!=*0"))))
;; Multiple criteria. ;; Multiple criteria.
(should (should
(equal '(23) (equal '(23)