Skip to main content
2 of 2
added 214 characters in body
NickD
  • 33.6k
  • 3
  • 30
  • 47

You can do it like this:

#+begin_src elisp :results drawer
  (defun my/org-get-filetags (e)
    (let ((type (org-element-type e))
          (key (org-element-property :key e))
          (value (org-element-property :value e)))
      (if (and (eq type 'keyword) (equal key "FILETAGS"))
          (string-split value ":" t))))

  (org-element-map (org-element-parse-buffer) '(keyword) #'my/org-get-filetags)
#+end_src

#+RESULTS:
:results:
((foo bar baz) (tag1 tag2 tag3))
:end:

The Org mode file I used for the testing has two #+FILETAGS: keywords like this:

#+FILETAGS: :foo:bar:baz:
#+FILETAGS: :tag1:tag2:tag3:

...

That explains the output of the code block.

This asks org-element-map to act on the parse tree of the buffer, selecting only keywords (like #+FILETAGS) and applying the function my/org-get-filetags on each such element.

The function itself selects the key and value properties of the element, checks that the type of the element is keyword and the key is the string "FILETAGS" and, if so, splits the value (a string of the form ":tag1:tag2:tag3:" on the : separator and returns the list of tags (omitting the opening and closing null strings because of the final t argument given to string-split. See its doc string with C-h f string-split for the details.)

org-element-map wraps all the return values in a list, so the result is going to be something like (("foo" "bar" "baz") ("tag1" "tag2" "tag3")) which you might need to massage some more for your purposes, but I'll leave that to you.

If your file is big, then parsing the whole buffer to get the single keyword you are interested in will be relatively slow. You might be better off if you first search for #+FILETAGS:, moving to the beginning of the line and then using (org-element-at-point) which does a partial parse around point. There would be no need for org-element-map then:

#+begin_src elisp :results drawer
  (progn
    (save-excursion
      (goto-char (point-min))
      (search-forward "#+FILETAGS:")
      (beginning-of-line)
      (my/org-get-filetags (org-element-at-point))))
#+end_src

#+RESULTS:
:results:
(foo bar baz)
:end:

Note that we are reusing the same function as in the first code block to get the tags, and that we pass to it explicitly the single element that org-element-at-point returns - in the previous case, org-element-map would call it repeatedly, implicitly passing each element in the parse tree to it. The final result is a list of tags, rather than a list of lists of tags, since org-element-map is not part of this implementation, so there is no wrapping of return values. This assumes there is a single #+FILETAGS: keyword in the file, otherwise it will only find the first one (as in this case).

But you could loop searching for all of them:

#+begin_src elisp :results drawer
  (defun my/org-get-ALL-filetags ()
    (let ((ret))
        (goto-char (point-min))
        (while (search-forward "#+FILETAGS:" nil t)
          (save-excursion
            (beginning-of-line)
            (setq ret (cons (my/org-get-filetags (org-element-at-point)) ret))))
        (nreverse (delq nil ret))))

  (my/org-get-ALL-filetags)

#+end_src

#+RESULTS:
:results:
((foo bar baz) (tag1 tag2 tag3))
:end:

This produces the same list of lists structure as the org-element-map implementation, at the cost of complicating the return value of the function a bit: we cons each list to the front, delete any nils (which might be produced when you have the code block as part of the file, because the search succeeds not only for the "real" #+FILETAGS: keywords but also for the "fake" instance in the code block itself - the search finds the string in the function, but the parsing of the line does not return a keyword type and my/org-get-filetags applied to the element at point returns nil which gets consed to the return value). cons adds each list of tags to the front, so we have to reverse the list to produce the "natural" order.

NickD
  • 33.6k
  • 3
  • 30
  • 47