1

Due to a encoding snafu on my part in which I inadvertently added line returns to each line in an org-mode buffer and for which emacs org-id added a :PROPERTIES: drawer right after each headline when I saved the buffer to file. I didn't notice this at the time. I compounded this error doing this several times before I realized what happened. The result is that I now have multiple :PROPERTIES: drawers one after the other where I originally only had one.

So where I originally had this in my org file (both snippets shown are with the drawers unexpanded):

* TODO Sample Headline
  CLOSED: [2023-09-14 Thu 11:25]
  :PROPERTIES:
  :LOGBOOK:
  [2023-09-14 Thu 08:59]

I now have this:

* TODO Sample Headline
  CLOSED: [2023-09-14 Thu 11:25]
  :PROPERTIES:
  :PROPERTIES:
  :PROPERTIES:
  :PROPERTIES:
  :LOGBOOK:
  [2023-09-14 Thu 08:59]

For example these PROPERTIES drawers only contain an ID property all of which are unique. Here's what one of these drawers looks like when expanded:

:PROPERTIES:  
:ID: 05dd1bef-3d89-42dc-a052-60db7b525407  
:END:

Given that I use these org-ids to reference org-mode headers throughout my file, I simply can't delete them all and start over. How do I get rid of multiple :PROPERTIES: drawers and only keep the original one?

I'm using emacs on a windows machine running emacs v28.0.50. The 'org-version' command shows version 9.4.6.

Note: the original drawer is the furthest down from the headline.

7
  • An example would help. Do you have headlines with multiple property drawers and identical contents? That's a bug that should not happen. Which version of Org mode are you using? How do you know which drawer is the oldest? Are there property drawers containing other things than just an ID?
    – NickD
    Nov 21, 2023 at 20:42
  • I think the last from the top is the 'oldest' - ie the original property drawer. All ids are unique. I've done this before. Think the file it was in was relatively small so I manually deleted the redundant drawers. This time the file is huge - so I can't do that. I'm still googling for an answer. I suspect I'll have to write some sort of elisp script - which I'm not anywhere close to proficient at. I guess I wonder what others do in this situation - assuming others have encountered it. Nov 21, 2023 at 20:46
  • So what did you do to get the multiple ID drawers? What is the "encoding snafu" that you mention? Will deleting all the property drawers with just an ID property in them work as an automated solution or should the code be more careful than that? Is this a one-off problem that could e.g. be solved with a quick-and dirty regexp-replace or is it a recurring problem that needs a more formal solution? Finally, why does it happen? I don't think this is a common problem, so I wonder if it's a bug in your version of Org mode.
    – NickD
    Nov 21, 2023 at 21:50
  • Please answer all the questions in both of my comments above, by editing your question and adding all the relevant information. A recipe to reproduce the problem would be ideal. Thanks!
    – NickD
    Nov 21, 2023 at 21:51
  • 1
    Can't delete all of them. It is a one-off but I have done it previously. I think that org-id has to have the :PROPERTIES: drawer right below the headline (or :CLOSED: line in the above example); if that's not the case then it adds it. All you have to do to replicate it is to add a blank line right after the :CLOSED: line in the above example and when you now go to save the file, org-id will add another :PROPERTIES: drawer. At least that's what happens in the version of emacs listed above. Nov 21, 2023 at 21:58

1 Answer 1

3

What is wrong:

  • You should not have an empty line between the headline and any scheduling information or drawers that follow it. An empty line signals the beginning of ordinary text, so that's presumably why you end up with multiple property drawers, but who adds the empty line? Even if you start with an empty line after the headline and a property drawer under it, org-id-get-create will create another drawer, but unless you insert an empty line above that, no more such drawers will be added. It seems that you are inserting empty lines manually or through some other function.

  • You can run M-x org-lint in your file. If it finds an empty line between the headline and the property drawer (possibly with other stuff in-between), it will complain with the (somewhat mysterious and misleading) message " Incorrect contents for PROPERTIES drawer". It will also identify a bunch of other common problems, so it's a good idea to run it occasionally on your Org mode files. It also indicates the line number(s) where it thinks there is a problem.

How to fix it

The following Org file documents my attempt to come up with a solution. It contains the necessary code (although you should heed the WARNINGS in the description: it is NOT turn-key code). It also contains a fairly long description of how to test the code before you unleash it on your file and how to go about unleashing it on your file.

Hope it helps!

* Example file

** DONE Sample Headline
CLOSED: [2023-11-24 Fri 12:16]



:PROPERTIES:
  :ID:       09038df1-77b4-4ca7-9549-be6dbb23e78f
  :VISIBILITY: folded
  :END:

:PROPERTIES:
  :ID:       acecf527-7c1e-4786-8ff0-011dbab1c19d
  :END:
:PROPERTIES:
  :ID:       c2201624-653e-4a84-ae64-df60317af2e6
  :END:
:PROPERTIES:
  :ID:       6a3cfa56-2433-4365-9d55-c3a1b8c1e968
  :END:
    
:LOGBOOK:
:END:
[2023-09-14 Thu 08:59]

Some text (1)

** Another headline with messed-up priorities - multiple drawers but no empty line above.
:PROPERTIES:
:ID:       abaebf32-e061-4019-ad86-02d11be0d33e
:END:

:PROPERTIES:
:ID:       e1379898-de1b-476c-9e72-e512120b8a87
:END:

Some text(2)

** Another headline - just one drawer, so no problem here, so munging should leave this one unchanged
:PROPERTIES:
:ID:       bc7a595a-13ec-48b1-9928-cefae2561433
:END:

Some text (3)

** Another headline - no properties drawer here - copy the whole thing as-is

Some text (4)
  

* Code                                                                                                        :noexport:

The first code block below and the `org-map-entries' call in
`ndk/fix-priorities' is specific to my testing: I consider
the tree with the headline "Example file" above (which is at the
beginning of the buffer) and process all the headlines at level 2 of
that tree.

You need to do `C-c C-c' on the second code block to define the
functions used: that only needs to be done once unless modifications
are made to the functions. After a change, do `C-c C-c' again on the
second code block to load the new definition(s).

To test the four scenarios above, do `C-c C-c' on the first code block
and look at the file `munged-properties.org' in the current
directory. To retest (e.g. after adding another scenario as a level-2
headline above), delete the `munged-properties-copy.org' file from the
current directory (and kill its buffer if there is one), then rerun
the whole thing with `C-c C-c' on the first code block and check the
file again.

WARNING: This code should be considered throw-away code. You might
learn something by studying it and the techniques are applicable
widely, but the code itself is intended for the particular situation
described in the question and is probably not useful as-is in other
situations (it remains to be seen how useful it is even for *this*
situation :-))

WARNING: This is not a turn-key solution: you will have to adapt the
code to your circumstances. However, it is safe to experiment with it:
it does not modify the original file.

For the "real" run, I would recommend putting the code in a file, say
`fix-properties.el', modify the function `ndk/fix-priorities' for your
circumstances (see the comments in the function for some
possibilities - if none of them meet your requirements, follow up with
a comment explaining why and I will try to respond), load the file
with `M-x load-file RET fix-properties.el', execute the `setq' of the
destination buffer below *once* with

`M-: (setq destination-buffer (find-file-noselect "munged-properties-copy.org")) RET'

Then open your problematic file, run the (suitably modified) function
`ndk/fix-priorities' with

`M-: (ndk/fix-priorities)RET'

and save the destination buffer with

`M-: (with-current-buffer destination-buffer (save-buffer))RET'

Check the `munged-properties-copy.org' file and if it does not look
good, delete it and rerun the last two steps after whatever necessary
changes are made. If you modify the functions, reload the
`fix-priorities.el' file to get the new definitions. Lather, rinse,
repeat until you are happy with the result.  Then replace your
original file with the `munged-properties-copy.el' file (but keep a
copy of the original just in case).

#+name: fix-priorities-test
#+begin_src elisp :results drawer

  ;; global
  (setq destination-buffer (find-file-noselect "munged-properties-copy.org"))

  (save-excursion
    (goto-char (point-min))
    ;; this will write a "corrected" version (no multiple priority drawers, no empty lines between headline and priority drawer)
    ;; to the above destination buffer
    (ndk/fix-priorities)
    ;; We then save the destination buffer to a file so we can eyeball it
    (with-current-buffer destination-buffer
      (save-buffer)))
#+end_src

#+RESULTS: fix-priorities-test
:results:
nil
:end:

Second code block:

#+name: fix-priorities-functions
#+begin_src elisp :results drawer
  (defun ndk/fix-priorities ()
    ;; this selects all level-2 headings in the current tree
    (org-map-entries #'ndk/munge "LEVEL=2" 'tree)
    ;; another possibility, maybe applicable to the OP's situation is
    ;; to select all top-level headlines
    ;(org-map-entries #'ndk/munge "LEVEL=1" 'file)
    ;; ... or perhaps all headlines
    ;(org-map-entries #'ndk/munge t 'file)
    
    )
    

  (defun ndk/delete-extra-property-drawers ()
    "Delete all the :PROPERTIES: drawers except the last one."

    (let (start end)
      ;; find the point before the first occurrence of the string `:PROPERTIES' and remember it
      ;; This calculation is probably "good enough", but it's making implicit assumptions about
      ;; the line where that first occurrence is found.
      (goto-char (point-min))
      (setq start (when (search-forward ":PROPERTIES:" nil t) (beginning-of-line) (point)))
      ;; find point at beginning of the last occurrence of the string `:PROPERTIES' and remember it
      ;; this is the one we'll keep
      (goto-char (point-max))
      (setq end  (search-backward ":PROPERTIES:" nil t))
      ;; check that we have a well-defined region and delete it
      (when (and start end)
        (delete-region start end))))
            
  (defun ndk/munge ()
    "Copy the current heading and its contents to a temp buffer and
  munge away. Currently the munging consists of deleting the extra
  properties drawers and deleting any empty lines between the
  heading (and its planning section, if applicable) and the
  remaining property drawer.  After that, append the contents of
  the temp buffer to the (globally defined) destination buffer."

    (let* ((heading (org-element-at-point))
           (beg (org-element-property :begin heading))
           (end (org-element-property :end heading))
           (s (progn (widen) (buffer-substring-no-properties beg end))))
      (with-temp-buffer
        (insert s)
        ;; delete extraneous property drawers
        (ndk/delete-extra-property-drawers)
        ;; delete empty lines
        (flush-lines "^[[:blank:]]*$" 1 (point))
        ;; append result to destination buffer
        (let ((s (buffer-substring-no-properties (point-min) (point-max))))
          (with-current-buffer destination-buffer
            (goto-char (point-max))
            (insert s))))))
#+end_src

#+RESULTS: fix-priorities-functions
:results:
ndk/munge
:end:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.