> What about modifying org-lint-search to skip spaces and also | when searching?

Done. What I ended up doing is extracting the code that is used to
search headlines from org-link-search. I then used that code to create
a new function. In order to avoid false positives, org-link-search
calls this function twice. On the first pass, the same regular text
search of headlines that was done before my patch is run. If that
first search returns nothing, a second search is done where we remove
"|"s from headlines before we check to see if a headline matches our
search query. Besides that, the behavior of org-link-search has not
changed.


Le dim. 10 mai 2026 à 04:32, Ihor Radchenko <[email protected]> a écrit :
>
> Earl Chase <[email protected]> writes:
>
> > When the value of link is t,the above two points apply. Additionally:
> > - Links with pipe chars can not be used for search strings even if the pipe
> > character is replaced with a space and fuzzy link searching is enabled.
>
> What about modifying org-lint-search to skip spaces and also | when searching?
>
> --
> Ihor Radchenko // yantar92,
> Org mode maintainer,
> Learn more about Org mode at <https://orgmode.org/>.
> Support Org development at <https://liberapay.com/org-mode>,
> or support my work at <https://liberapay.com/yantar92>
From 3be520ee78fe1085a45a45607b7158f7d0cbd95a Mon Sep 17 00:00:00 2001
From: ApollonDeParnasse <[email protected]>
Date: Wed, 6 May 2026 14:20:02 -0500
Subject: [PATCH] fix: Pipe char (|) in headings breaks clockreport

---
 lisp/ol.el                     |  49 +++++++-----
 lisp/org-clock.el              |  42 ++++++----
 testing/lisp/test-ol.el        |  66 ++++++++++++++++
 testing/lisp/test-org-clock.el | 139 ++++++++++++++++++++++++++++++---
 4 files changed, 251 insertions(+), 45 deletions(-)

diff --git a/lisp/ol.el b/lisp/ol.el
index 73645fb97..10b492db5 100644
--- a/lisp/ol.el
+++ b/lisp/ol.el
@@ -1733,6 +1733,29 @@ Optional argument ARG is passed to `org-open-file' when S is a
                  s (substring s (1- (org-element-end link)))))
     (link (org-link-open link arg))))
 
+(cl-defun org-link--search-headlines (words &optional remove-pipes)
+  "Search headlines in Org mode buffers.
+WORDS is a list of strings.  When the value of
+REMOVE-PIPES is t, pipe chars are removed from
+headlines before we test for equality.  Ignore
+COMMENT keyword, TODO keywords, priority cookies,
+statistics cookies and tags."
+  (let ((title-re
+	 (format "%s.*\\(?:%s[ \t]\\)?.*%s"
+		 org-outline-regexp-bol
+		 org-comment-string
+		 (regexp-opt words)))
+        (case-fold-search t))
+    (goto-char (point-min))
+    (catch :found
+      (while (re-search-forward title-re nil t)
+        (when-let* ((heading-content (org-link--normalize-string
+		                      (org-get-heading t t t t)))
+                    (heading-parts (split-string (if remove-pipes (string-replace "|" " " heading-content) heading-content)))
+                    (match-found (equal words heading-parts)))
+	  (throw :found t)))
+      nil)))
+
 (defun org-link-search (s &optional avoid-pos stealth new-heading-container)
   "Search for a search string S in the accessible part of the buffer.
 
@@ -1832,25 +1855,15 @@ respects buffer narrowing."
 		     (forward-line 0)
 		     (throw :name-match t))))
 	       nil))))
-     ;; Regular text search.  Prefer headlines in Org mode buffers.
-     ;; Ignore COMMENT keyword, TODO keywords, priority cookies,
-     ;; statistics cookies and tags.
+     ;; Regular text search of headlines in Org mode buffers
      ((and (derived-mode-p 'org-mode)
-	   (let ((title-re
-		  (format "%s.*\\(?:%s[ \t]\\)?.*%s"
-			  org-outline-regexp-bol
-			  org-comment-string
-			  (mapconcat #'regexp-quote words ".+"))))
-	     (goto-char (point-min))
-	     (catch :found
-	       (while (re-search-forward title-re nil t)
-		 (when (equal (mapcar #'upcase words)
-                              (mapcar #'upcase
-			              (split-string
-			               (org-link--normalize-string
-				        (org-get-heading t t t t)))))
-		   (throw :found t)))
-	       nil)))
+	   (org-link--search-headlines words))
+      (forward-line 0)
+      (setq type 'dedicated))
+     ;; Second attempt of regular text search of headlines in Org mode buffers
+     ;; This time we remove pipes from headlines
+     ((and (derived-mode-p 'org-mode)
+	   (org-link--search-headlines words 't))
       (forward-line 0)
       (setq type 'dedicated))
      ;; Offer to create non-existent headline depending on
diff --git a/lisp/org-clock.el b/lisp/org-clock.el
index 53d326e58..e6604aabd 100644
--- a/lisp/org-clock.el
+++ b/lisp/org-clock.el
@@ -62,6 +62,7 @@
 (defvar org-state)
 (defvar org-link-bracket-re)
 
+
 (defgroup org-clock nil
   "Options concerning clocking working time in Org mode."
   :tag "Org Clock"
@@ -3120,6 +3121,31 @@ a number of clock tables."
         (setq start next))
       (end-of-line 0))))
 
+
+(defun org-clock--create-clean-headline (headline)
+  "Clean HEADLINE for a clocktable.
+Prune statistics cookies.  Replace links with their description,
+or a plain link if there is none."
+  (thread-last headline (substring-no-properties) (replace-regexp-in-string
+	                                           "\\[[0-9]*\\(?:%\\|/[0-9]*\\)\\]" "")
+               (org-link-display-format)
+               (string-replace "|" " ")
+               (org-trim)))
+
+(defun org-clock--create-link-for-headline (headline)
+  "Convert HEADLINE into a link for a clocktable.
+Prune statistics cookies.  Replace links with their description,
+or a plain link if there is none."
+  (let* ((file-name (buffer-file-name))
+         (headline-contains-pipe (numberp (string-match-p (regexp-quote "|") headline)))
+         (description (org-clock--create-clean-headline headline))
+         (link (cond
+                ((and file-name (not headline-contains-pipe)) (format "file:%s::%s" file-name (org-link-heading-search-string headline)))
+                ((and file-name headline-contains-pipe) (format "file:%s::%s" file-name (string-replace "|" " " (org-link-heading-search-string headline))))
+                ((and (not file-name) headline-contains-pipe) (string-replace "|" " " (org-link-heading-search-string headline)))
+                ((not headline-contains-pipe) (org-link-heading-search-string headline)))))
+    (org-link-make-string link description)))
+
 (defun org-clock-get-table-data (file params)
   "Get the clocktable data for file FILE, with parameters PARAMS.
 FILE is only for identification - this function assumes that
@@ -3206,20 +3232,8 @@ PROPERTIES: The list properties specified in the `:properties' parameter
 	      (when (<= level maxlevel)
 		(let* ((headline (org-get-heading t t t t))
 		       (hdl
-			(if (not link) headline
-			  (let ((search
-				 (org-link-heading-search-string headline)))
-			    (org-link-make-string
-			     (if (not (buffer-file-name)) search
-			       (format "file:%s::%s" (buffer-file-name) search))
-			     ;; Prune statistics cookies.  Replace
-			     ;; links with their description, or
-			     ;; a plain link if there is none.
-			     (org-trim
-			      (org-link-display-format
-			       (replace-regexp-in-string
-				"\\[[0-9]*\\(?:%\\|/[0-9]*\\)\\]" ""
-				headline)))))))
+			(if (not link) (org-clock--create-clean-headline headline)
+			  (org-clock--create-link-for-headline headline)))
 		       (tgs (and tags (org-get-tags)))
 		       (tsp
 			(and timestamp
diff --git a/testing/lisp/test-ol.el b/testing/lisp/test-ol.el
index 371bf1e0f..457e86f5a 100644
--- a/testing/lisp/test-ol.el
+++ b/testing/lisp/test-ol.el
@@ -886,5 +886,71 @@ API in `org-link-parameters'.  Used in test
       (org-insert-link nil nil "altered description"))
     (should (equal (buffer-string) "[[file:file.org][altered description]]"))))
 
+(defun test-regular-org-link-heading-search-string (heading)
+  "Helper function for `test-org-link-search'.
+Inserts HEADING into an org
+buffer and then returns the heading value
+as a org-link search string."
+  (org-test-with-temp-text heading
+    (org-link-heading-search-string)))
+
+(defun test-org-link-heading-search-string-remove-pipes (heading)
+  "Helper function for `test-org-link-search'.
+Inserts HEADING into an org
+buffer and then returns the heading value
+as a org-link search string.
+Replaces pipe chars with spaces
+in order to simulate the way
+org-clock removes spaces from
+headings when it creates links
+for a clocktable."
+  (org-test-with-temp-text heading
+    (string-replace "|" " " (org-link-heading-search-string))))
+
+(cl-defun test-org-link-search (search-string-creator)
+  "Returns a closure that can be used to test `org-link-search'.
+SEARCH-STRING-CREATOR should be a function
+that returns an org-link search string."
+  (lambda (buffer-text headline-to-find)
+    (let ((org-link-search-must-match-exact-headline nil)
+          (org-todo-regexp "TODO"))
+      (org-test-with-temp-text buffer-text
+        (org-link-search (funcall search-string-creator headline-to-find))
+        (should (string-equal (buffer-substring-no-properties (point) (line-end-position)) headline-to-find))))))
+
+(defalias 'test-org-link-search-basic (test-org-link-search #'test-regular-org-link-heading-search-string))
+
+(defalias 'test-org-link-search-replace-pipe-chars (test-org-link-search #'test-org-link-heading-search-string-remove-pipes))
+
+(ert-deftest test-org-link/search-first-pass ()
+  "First test for `org-link-search'.
+Confirm that we can find an exact match for
+a given heading search string."
+  (test-org-link-search-basic "* Head1\n* Head2\n* Head3\n* [[Head2]]" "* Head2")
+  (test-org-link-search-basic "* Test 1 2 3\n** Test 1 2\n* [[*Test 1 2]]" "* [[*Test 1 2]]")
+  (let ((first-line
+	 "*** TODO [#A] [/]  Test [1/2] [33%] 1 \t  2 [%] :work:urgent: "))
+    (test-org-link-search-basic
+     (concat "* Foo Bar\n** [[*Test 1 2]]\n" first-line) first-line)
+    (test-org-link-search-basic
+     (concat "* Foo Bar\n** [[*Test 1 2]]\n" first-line) "** [[*Test 1 2]]")))
+
+(ert-deftest test-org-link/search-second-pass ()
+  "Second test for `org-link-search'.
+Confirm that we can find a match for
+a heading when the heading search string does
+not contain pipe chars even though
+the original heading does."
+  (test-org-link-search-replace-pipe-chars "* Head1\n* Head2\n* | Head3\n* [[Head2]]" "* | Head3")
+  (test-org-link-search-replace-pipe-chars "* Test 1 2 3\n** Test 1 | 2 |\n* [[*Test 1 2]]" "** Test 1 | 2 |")
+  (test-org-link-search-replace-pipe-chars "* DONE task \n* WAITING another task \n* [[file:/home/binarin/test.org::*A|B][A|B]]"  "* [[file:/home/binarin/test.org::*A|B][A|B]]")
+  (let ((first-line
+	 "*** TODO [#A] [/]  Test [1/2] [33%] 1 |  2 [%] :work:urgent: "))
+    (test-org-link-search-replace-pipe-chars
+     (concat "* Foo Bar\n** [[*Test 1 |2]]\n" first-line) first-line)
+    (test-org-link-search-replace-pipe-chars
+     (concat "* Foo Bar\n** [[*Test 1 2 |]]\n" first-line) "** [[*Test 1 2 |]]")))
+
+
 (provide 'test-ol)
 ;;; test-ol.el ends here
diff --git a/testing/lisp/test-org-clock.el b/testing/lisp/test-org-clock.el
index 4d5cb055e..bde81b52a 100644
--- a/testing/lisp/test-org-clock.el
+++ b/testing/lisp/test-org-clock.el
@@ -861,10 +861,59 @@ CLOCK: [2016-12-28 Wed 13:09]--[2016-12-28 Wed 15:09] =>  2:00"
   CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (test-org-clock-clocktable-contents ":maxlevel 1 :lang foo")))))
 
+(ert-deftest test-org-clock/clocktable/remove-pipe-chars ()
+  "Confirm pipe chars are removed from headings before they are added to the Clock Table."
+  (should
+   (string-match-p "| Foo   Bar +| 26:00   |"
+		   (org-test-with-temp-text
+		    "* Foo | Bar
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+		    (test-org-clock-clocktable-contents ":block untilnow :indent nil"))))
+  (should
+   (string-match-p "| Foo   Bar   Baz | 26:00   |"
+		   (org-test-with-temp-text
+		    "* Foo | Bar | Baz
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+		    (test-org-clock-clocktable-contents ":block untilnow :indent nil")))))
+
+(ert-deftest test-org-clock/clocktable/remove-links ()
+  "Confirm links are replaced with their description or a plain link before they are added to the Clock Table."
+    (should
+     (string-match-p
+      "| Foo https://example\\.com | 26:00 +|"
+      (org-test-with-temp-text
+       "* Foo [[https://example.com]]
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+       (test-org-clock-clocktable-contents ":lang en"))))
+    (should
+     (string-match-p
+      "| Foo A link to a site | 26:00 +|"
+      (org-test-with-temp-text
+       "* Foo [[https://example.com][A link to a site]]
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+       (test-org-clock-clocktable-contents ":lang en"))))
+    ;; works even with pipe characters in links
+    (should
+     (string-match-p
+      "| Foo file:foo\\.org::\\*Heading with   inside | 26:00 +|"
+      (org-test-with-temp-text
+       "* Foo [[file:foo.org::*Heading with | inside]]
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+       (test-org-clock-clocktable-contents ":lang en"))))
+    (should
+     (string-match-p
+      "| Bar  A   B +| 26:00 +|"
+      (org-test-with-temp-text
+       "* Bar  [[file:/home/binarin/test.org][A | B]]
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+       (test-org-clock-clocktable-contents ":lang en")))))
+
+
 (ert-deftest test-org-clock/clocktable/link ()
   "Test \":link\" parameter in Clock table."
   ;; If there is no file attached to the document, link directly to
   ;; the headline.
+  
   (should
    (string-match-p "| +\\[\\[\\*Foo]\\[Foo]] +| 26:00 +|"
 		   (org-test-with-temp-text
@@ -876,13 +925,13 @@ CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
    (string-match-p
     "| \\[\\[file:filename::\\*Foo]\\[Foo]] +| 26:00 +|"
     (org-test-with-temp-text
-	(org-test-with-temp-text-in-file
-	    "* Foo
+        (org-test-with-temp-text-in-file
+            "* Foo
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
-	  (let ((file (buffer-file-name)))
+          (let ((file (buffer-file-name)))
 	    (replace-regexp-in-string
 	     (regexp-quote file) "filename"
-	     (test-org-clock-clocktable-contents ":link t :lang en"))))
+             p	     (test-org-clock-clocktable-contents ":link t :lang en"))))
       (org-table-align)
       (buffer-substring-no-properties (point-min) (point-max)))))
   ;; Ignore TODO keyword, priority cookie, COMMENT and tags in
@@ -891,28 +940,28 @@ CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
    (string-match-p
     "| \\[\\[\\*Foo]\\[Foo]] +| 26:00 +|"
     (org-test-with-temp-text
-	"* TODO Foo
+        "* TODO Foo
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (test-org-clock-clocktable-contents ":link t :lang en"))))
   (should
    (string-match-p
     "| \\[\\[\\*Foo]\\[Foo]] +| 26:00 +|"
     (org-test-with-temp-text
-	"* [#A] Foo
+        "* [#A] Foo
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (test-org-clock-clocktable-contents ":link t :lang en"))))
   (should
    (string-match-p
     "| \\[\\[\\*Foo]\\[Foo]] +| 26:00 +|"
     (org-test-with-temp-text
-	"* COMMENT Foo
+        "* COMMENT Foo
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (test-org-clock-clocktable-contents ":link t"))))
   (should
    (string-match-p
     "| \\[\\[\\*Foo]\\[Foo]] +| 26:00 +|"
     (org-test-with-temp-text
-	"* Foo :tag:
+        "* Foo :tag:
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (test-org-clock-clocktable-contents ":link t :lang en"))))
   ;; Remove statistics cookie from headline description.
@@ -920,14 +969,14 @@ CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
    (string-match-p
     "| \\[\\[\\*Foo]\\[Foo]] +| 26:00 +|"
     (org-test-with-temp-text
-	"* Foo [50%]
+        "* Foo [50%]
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (test-org-clock-clocktable-contents ":link t :lang en"))))
   (should
    (string-match-p
     "| \\[\\[\\*Foo]\\[Foo]] +| 26:00 +|"
     (org-test-with-temp-text
-	"* Foo [1/2]
+        "* Foo [1/2]
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (test-org-clock-clocktable-contents ":link t :lang en"))))
   ;; Replace links with their description, or turn them into plain
@@ -936,16 +985,80 @@ CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
    (string-match-p
     "| \\[\\[\\*Foo \\\\\\[\\\\\\[https://orgmode\\.org\\\\]\\\\\\[Org mode\\\\]\\\\]]\\[Foo Org mode]] +| 26:00 +|"
     (org-test-with-temp-text
-	"* Foo [[https://orgmode.org][Org mode]]
+        "* Foo [[https://orgmode.org][Org mode]]
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (test-org-clock-clocktable-contents ":link t :lang en"))))
   (should
    (string-match-p
     "| \\[\\[\\*Foo \\\\\\[\\\\\\[https://orgmode\\.org\\\\]\\\\]]\\[Foo https://orgmode\\.org]] +| 26:00 +|"
     (org-test-with-temp-text
-	"* Foo [[https://orgmode.org]]
+        "* Foo [[https://orgmode.org]]
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
-      (test-org-clock-clocktable-contents ":link t :lang en")))))
+      (test-org-clock-clocktable-contents ":link t :lang en"))))
+  ;; remove pipe characters before creating lnks
+  (should
+   (string-match-p
+    "| \\[\\[\\*Foo   Bar]\\[Foo   Bar]]    | 26:00   |"
+    (org-test-with-temp-text
+        "* Foo | Bar
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+      (test-org-clock-clocktable-contents ":link t :lang en"))))
+  ;; Works even when the heading has a link.
+  (should
+   (string-match-p
+    "| \\[\\[\\*Foo <file:foo\\.org::\\*Heading with   inside>]\\[Foo <file:foo\\.org::\\*Heading with\\.\\.\\.]] | 26:00   |"
+    (org-test-with-temp-text
+        "*  Foo <file:foo.org::*Heading with | inside>
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+      (test-org-clock-clocktable-contents ":link t :lang en"))))
+  (should
+   (string-match-p
+    "| \\[\\[\\*\\\\\\[\\\\\\[file:/home/binarin/test\\.org::\\*A   B\\\\]\\\\\\[A   B\\\\]\\\\]]\\[A   B]] +| 26:00   |"
+    (org-test-with-temp-text
+        "*  [[file:/home/binarin/test.org::*A | B][A | B]]
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+      (test-org-clock-clocktable-contents ":link t :lang en"))))
+  ;; Works in files as as well.
+  (should
+   (string-match-p
+    "| \\[\\[file:filename::\\*Foo   Bar]\\[Foo   Bar]]    | 26:00   |"
+    (org-test-with-temp-text
+        (org-test-with-temp-text-in-file
+	    "* Foo | Bar
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+	  (let ((file (buffer-file-name)))
+	    (replace-regexp-in-string
+	     (regexp-quote file) "filename"
+	     (test-org-clock-clocktable-contents ":link t :lang en"))))
+      (org-table-align)
+      (buffer-substring-no-properties (point-min) (point-max)))))
+  (should
+   (string-match-p
+    "| \\[\\[file:filename::\\*Foo <file:foo\\.org::\\*Heading with   inside>]\\[Foo <file:foo\\.org::\\*Heading with\\.\\.\\.]] | 26:00   |"
+    (org-test-with-temp-text
+        (org-test-with-temp-text-in-file
+	    "*  Foo <file:foo.org::*Heading with | inside>
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+	  (let ((file (buffer-file-name)))
+	    (replace-regexp-in-string
+	     (regexp-quote file) "filename"
+	     (test-org-clock-clocktable-contents ":link t :lang en"))))
+      (org-table-align)
+      (buffer-substring-no-properties (point-min) (point-max)))))
+  (should
+   (string-match-p
+    "| \\[\\[file:filename::\\*\\\\\\[\\\\\\[file:/home/binarin/test\\.org::\\*A   B\\\\]\\\\\\[A   B\\\\]\\\\]]\\[A   B]]  +| 26:00   |"
+    (org-test-with-temp-text
+        (org-test-with-temp-text-in-file
+	    "*  [[file:/home/binarin/test.org::*A | B][A | B]]
+CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
+	  (let ((file (buffer-file-name)))
+	    (replace-regexp-in-string
+	     (regexp-quote file) "filename"
+	     (test-org-clock-clocktable-contents ":link t :lang en"))))
+      (org-table-align)
+      (buffer-substring-no-properties (point-min) (point-max))))))
+
 
 (ert-deftest test-org-clock/clocktable/compact ()
   "Test \":compact\" parameter in Clock table."
-- 
2.54.0

Reply via email to