* one
* two
** books
*** IN-PROGRESS book1
*** book2
*** book3
* three
* four
** five
*** books
**** book4
How to get:
- all headings title with todo-'IN-PROGRESS' keyword which are children of heading 'books'
Emacs Stack Exchange is a question and answer site for those using, extending or developing Emacs. It only takes a minute to sign up.
Sign up to join this community* one
* two
** books
*** IN-PROGRESS book1
*** book2
*** book3
* three
* four
** five
*** books
**** book4
How to get:
The simplest thing is to use isearch
and look for IN-PROGRESS
, then eyeball the heading to see if it's in a books
hierarchy. Org mode files are plain text, so everything that applies to text applies to them as well.
But assuming you want an Org mode specific method, here's a simple example implementation in the context of a slightly augmented version (adding a TODO
list at the top and two more instances to test the matching) of your file:
#+TODO: START IN-PROGRESS | DONE
* one
* two
** books
*** IN-PROGRESS book1
*** book2
*** book3
* three
** IN-PROGRESS book-not-3
* four
** five
*** books
**** book4
**** IN-PROGRESS book5
* Code :noexport:
#+begin_src emacs-lisp :results drawer
(defun ndk/select-heading ()
(if (member "books" (org-get-outline-path))
(org-get-heading t t t t)))
(org-map-entries 'ndk/select-heading "/+TODO=\"IN-PROGRESS\"" 'file)
#+end_src
Explanation:
org-map-entries
takes a function and applies it on all entries that match a criterion in a given scope (here, the entries that match the TODO
state IN-PROGRESS
in the scope of the whole file). The matching is explained in the Matchings Tags and Properties section of the Org mode manual. In this case, we match every entry in the file whose TODO
state is IN-PROGRESS
and on each one, we apply the function ndk/select-heading
.
The function ndk/select-heading
is called at the beginning of each matching entry. It first gets the breadcrumbs
of the entry, i.e. the path of headings leading from the top level to the entry as a list, and then checks whether books
is a member of the list, i.e. whether the entry is a sub-entry of a books
entry. If so, it returns the current heading, shorn of all extras (TODO
state, tags, etc).
If you press C-c C-c
on the code block to run it, it will return ("book1" nil "book5")
, the first and last entries matching the state and being sub-entries of books
, while the nil
comes from an entry matching the state but which is not a sub-entry of books
(the entry book-not-3
in the file).
Two more things:
The match specification has a short-cut form: instead of saying "+TODO=\"FOO\""
, you can say /+FOO
. But the match specification also allows the form /+FOO-BAR
to specify that you want to match FOO
but NOT BAR
. That precludes your using the short form in your case because /+IN-PROGRESS
would mean: match IN
but NOT PROGRESS
which is not what you want. So you have to use the long form or rename your states so that they don't contain dashes.
The search above is tailored to your exact criteria at the cost of hardwiring the values you are looking for into the code. It's not hard to write a more general function that takes these things as arguments, but it does complicate the code a bit, so I'll leave it as an exercise for the reader. If you understand the code above and have a little Lisp under your belt, you should be able to write the generalization easily.
It's also easy to get rid of any nil
entries in the result if you don't want them: just say (delete nil (org-map-entries ....))
.
Note that org-map-entries
is a very powerful function: it's a good idea to read its doc string with C-h f org-map-entries
and also read the Using the Mapping API section in the manual.
Another approach could be using org-ql. Here an example that should match your original question:
(require 'org-ql)
(org-ql-query
:select #'org-get-heading
:from "/tmp/so.org"
:where '(and (todo "IN-PROGRESS")
(parent "books")))
:select
key denotes the function that will be called on each entry matching the predicate in the :where
condition/tmp/so.org
is the name of the file where I saved the content prepared by @NickD