Emacs, scripting and anything text oriented.

Looping through Org mode headings

Kaushal Modi

Using 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,

  1. I will give a give introduction to the org-map-entries API.
  2. 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)
Code Snippet 1: Signature of the org-map-entries function

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.
  • 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.

Table 1: String-type MATCH argument examples for org-map-entries
Search stringDescriptionExample
"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)
Code Snippet 2: Dummy example showing how to set a property for all Org headings using org-map-entries
  • It defines a function that parses the Org element at point using org-element-at-point, gets the title property of the element  This function is designed to be called by org-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’s foo 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 #


  1. 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↩︎