Looping through Org mode headings
— Kaushal ModiUsing the org-map-entries
API to loop through selected or all
headings in an Org file.
Below question on Mastodon by the user @postroutine inspired me to write this post:
I got a lot of Org-Mode headings and I want to modify their properties (add, remove, edit). Is there a function to do the same modifications on each heading?
I think that the best solution to that question is using the org-map-entries function.
But somehow when replying to that question then, that wasn’t what
first came to my mind! .. when ironically that function is the main
function that enables my preferred subtree-based flow in ox-hugo
😆. So I am writing this post to better ingrain the following
concept in myself ..
If you need to loop through headings in an Org buffer, and especially if you need to modify that buffer in the process, use org-map-entries1.
Next,
- I will give a give introduction to the
org-map-entries
API. - Then provide a super-short solution to the above question.
org-map-entries
API #
I will give only a broad level overview on how to use this function. I would encourage the reader to refer to the resources at the end of this post to learn more about it.
So let’s start by looking at this function’s signature:
(org-map-entries FUNC &optional MATCH SCOPE &rest SKIP)
The org-map-entries
function iterates through all the headings
meeting the MATCH criteria in the determined SCOPE, and then calls
the specified function FUNC at each of those headings.
- The
FUNC
function accepts no arguments and is called at the beginning of each Org heading. - The optional second argument MATCH is either nil,
t
or a search string.- If MATCH is nil or
t
, all headings will be visited by the iteration and FUNC will be called on all of them. - But if MATCH is a string, the headings will first be filtered based on that string and then the FUNC will be called on only those.
- If MATCH is nil or
- For explanations on the optional SCOPE and SKIP arguments, see
Org Info: Using the Mapping API or C-h f
org-map-entries
from within Emacs.
Here’s a typical org-map-entries
call that loops through all the
headings in the visible buffer: (org-map-entries #'some-function)
where all the optional
argument values are nil. Next, we’ll see some examples of
string-type MATCH arguments used for filtering the headings.
MATCH strings #
Below table shows few examples of match string patterns.
Search string | Description | Example |
---|---|---|
"TAG" | Tag name | "foo" matches all headings with that tag |
"{TAG REGEXP}" | Regexp matching tags | "{f.*}" matches all headings whose tags match that regexp |
"TAG1+TAG2+.." | Tag set intersection | "foo+bar" matches all headings with both of those tags |
"TAG1-TAG2+.." | Tag set difference | "foo-bar" matches all headings with foo tag but without bar tag |
"TAG1 |TAG2 |.." | Tag set union or boolean OR | "foo |bar" matches all headings with either of those tags |
"TAG1&TAG2&.." | Tag set intersection or boolean AND | "foo&bar" is same as "foo+bar" |
"PROP =\"STRVAL\"" | Specified property value matching a string | "color =\"blue\"" matches all headings where color property is blue |
"PROP<>\"STRVAL\"" | Specified property value not matching a string | "color<>\"blue\"" matches all headings where color property is not blue |
"PROP ={VAL REGEXP}" | Specified property value matching a regexp | "color={b.*}" matches all headings where color property value matches ‘b.* ’ regexp |
"PROP[OP]NUMVAL" | Specified property value compared with a numeric value | "some_num >=10" matches all headings where some_num property is >=10 |
"LEVEL[OP]VAL" | Check value of headline’s special property LEVEL | "level>2" matches all headlines at levels greater than 2 |
"TODO[OP]\"STRVAL\"" | Check value of headline’s TODO state | "TODO =\"DONE\"" matches all headlines with TODO state set to ‘DONE’ |
Comparison Types #
- If the comparison value is a plain number, a numerical comparison is done, and the allowed operators are ‘<’, ‘=’, ‘>’, ‘<=’, ‘>=’, and ‘<>’.
- If the comparison value is enclosed in double quotes, a string comparison is done, and the same operators are allowed.
- If the comparison value is enclosed in curly braces, a regexp match is performed. For this comparison, only ‘=’ (regexp matches) and ‘<>’ (regexp does not match) operators are allowed.
- Comparison with dates and Group Tags is also possible. See Org Info: Matching tags and properties for more details.
Other notes #
- The property names are case-insensitive. So these all work the
same:
"COLOR<>\"blue\""
,"color<>\"blue\""
,"Color<>\"blue\""
. - The “tag” and “property” matches can be mixed up using the boolean
‘
&
’, ‘|
’, ‘+
’ and ‘-
’ operators. So searching ‘+LEVEL=3+boss-TODO="DONE"
’ lists all level three headlines that have the tag ‘boss’ and are not marked with the TODO keyword ‘DONE’. - ‘
&
’ binds more strongly than ‘|
’. - Grouping of match expressions using parentheses is not supported.
Example: Modifying a property in all headings #
Below is an example solution to the Mastoson question that I referenced in the beginning of this post.
(defun test/set-property-at-heading ()
"Function to be called at the beginning of an Org heading."
(let ((el (org-element-at-point)))
(org-set-property "foo" (org-element-property :title el))))
(org-map-entries #'test/set-property-at-heading)
- It defines a function that parses the Org element at point using
org-element-at-point
, gets thetitle
property of the element This function is designed to be called byorg-map-entries
and so the point at the time of calling this function will always be on a heading. , and sets that to the headline element’sfoo
property. - The
org-map-entries
call now simply calls this function on each heading in the visible scope of the Org buffer.
org-map-entries
References #
- C-h f
org-map-entries
- Org Info: Using the Mapping API
- Org Info: Matching tags and properties
Org mode has another popular mapping/looping API function org-element-map. I won’t go into much detail about that in this post — I’ll just mention that
org-element-map
is not the best choice if you need to modify the original Org buffer. It’s main use is to loop through a parsed AST of an Org buffer and optional modify those elements in memory. ↩︎