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:
parent
f9e083086f
commit
f689eb44f1
|
@ -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.
|
||||||
|
|
10
etc/ORG-NEWS
10
etc/ORG-NEWS
|
@ -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
|
||||||
|
|
||||||
|
|
120
lisp/org.el
120
lisp/org.el
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue