Skip to content

Commit ac2d0a2

Browse files
maxnikulinyantar92
authored andcommitted
org.el: Fix percent substitutions in `org-open-file'
* lisp/org.el (org--open-file-format-command): New function with better coverage of mailcap RFC 1524 syntax. Do not replace percent character in file name or link component, fix substitution of multiple regular expression groups matched in the link target. (org-open-file): Use `org--open-file-format-command' instead of inline code. * testing/lisp/test-org.el (org-test/org--open-file-format-command): Tests for `org--open-file-format-command'. The primary goal of moving code outside of `org-open-file' function is to make it testable. It should fix the following issues: - Maxim Nikulin. greedy substitution in org-open-file. Wed, 20 Jan 2021 23:08:35 +0700. https://list.orgmode.org/ru9ki4$t5e$1@ciao.gmane.io - Rodrigo Morales. Org mode links: Open a PDF file at a given page and highlight a given string. Tue, 02 Mar 2021 15:07:32 -0500. https://list.orgmode.org/87lfb5pbej.fsf@gmail.com
1 parent 7811fc5 commit ac2d0a2

File tree

2 files changed

+240
-21
lines changed

2 files changed

+240
-21
lines changed

lisp/org.el

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,16 @@ Possible values for the file identifier are:
12991299
\"evince -p %1 %s\")
13001300
to open [[file:document.pdf::5]] with evince at page 5.
13011301

1302+
Likely, you will need more entries: without page
1303+
number; with search pattern; with cross-reference
1304+
anchor; some combination of options. Consider simple
1305+
pattern here and a Lisp function to determine command
1306+
line arguments instead. Passing argument list to
1307+
`call-process' or `make-process' directly allows to
1308+
avoid treating some character in peculiar file names
1309+
as shell specialls causing executing part of file
1310+
name as a subcommand.
1311+
13021312
`directory' Matches a directory
13031313
`remote' Matches a remote file, accessible through tramp or efs.
13041314
Remote files most likely should be visited through Emacs
@@ -8015,6 +8025,74 @@ opened in Emacs."
80158025
(when add-auto-mode
80168026
(mapcar (lambda (x) (cons (car x) 'emacs)) auto-mode-alist))))
80178027

8028+
(defun org--open-file-format-command
8029+
(mailcap-command file link match-data)
8030+
"Format MAILCAP-COMMAND to launch viewer for the FILE.
8031+
8032+
MAILCAP-COMMAND may be an entry from the `org-file-apps' list or viewer
8033+
field from mailcap file loaded to `mailcap-mime-data'. See \"RFC
8034+
1524. A User Agent Configuration Mechanism For Multimedia Mail Format
8035+
Information\" (URL `https://www.rfc-editor.org/rfc/rfc1524.html') for
8036+
details, man page `mailcap(5)' for brief summary, and Info node
8037+
`(emacs-mime) mailcap' for specific related to Emacs. Only a part of
8038+
mailcap specification is supported.
8039+
8040+
The following substitutions are interpolated in the MAILCAP-COMMAND
8041+
string:
8042+
8043+
- \"%s\" to FILE name passed through
8044+
`convert-standard-filename', so it must be absolute path.
8045+
8046+
- \"%1\" to \"%9\" groups from MATCH-DATA found in the LINK string by
8047+
the regular expression in the key part of the `org-file-apps' entry.
8048+
(performed by caller). Not recommended, consider a lisp function
8049+
instead of a shell command. For example, the following link in an
8050+
Org file
8051+
8052+
<file:///usr/share/doc/bash/bashref.pdf::#Redirections::allocate a file>
8053+
8054+
may be handled by an `org-file-apps' entry like
8055+
8056+
(\"\\\\.pdf\\\\(?:\\\\.[gx]z\\\\|\\\\.bz2\\\\)?::\\\\(#[^:]+\\\\)::\\\\(.+\\\\)\\\\\\='\"
8057+
. \"okular --find %2 %s%1\")
8058+
8059+
Use backslash \"\\\" to quote percent \"%\" or any other character
8060+
including backslash itself.
8061+
8062+
In addition, each argument is passed through `shell-quote-argument',
8063+
so quotes around substitutions should not be used. For compliance
8064+
with mailcap files shipped e.g. in Debian GNU/Linux, single or double
8065+
quotes around substitutions are stripped. It deviates from mailcap
8066+
specification that requires file name to be safe for shell and for the
8067+
application."
8068+
(let ((spec (list (cons ?s (convert-standard-filename file))))
8069+
(ngroups (min 9 (- (/ (length match-data) 2) 1))))
8070+
(when (> ngroups 0)
8071+
(set-match-data match-data)
8072+
(dolist (i (number-sequence 1 ngroups))
8073+
(push (cons (+ ?0 i) (match-string-no-properties i link)) spec)))
8074+
(replace-regexp-in-string
8075+
(rx (or (and "\\" (or (group anything) string-end))
8076+
(and (optional (group (any "'\"")))
8077+
"%"
8078+
(or (group anything) string-end)
8079+
(optional (group (backref 2))))))
8080+
(lambda (fmt)
8081+
(let* ((backslash (match-string-no-properties 1 fmt))
8082+
(key (match-string 3 fmt))
8083+
(value (and key (alist-get (string-to-char key) spec))))
8084+
(cond
8085+
(backslash)
8086+
(value (let ((quot (match-string 2 fmt))
8087+
(subst (shell-quote-argument value)))
8088+
;; Remove quotes around the file name - we use
8089+
;; `shell-quote-argument'.
8090+
(if (match-string 4 fmt)
8091+
subst
8092+
(concat quot subst))))
8093+
(t (error "Invalid format `%s'" fmt)))))
8094+
mailcap-command nil 'literal)))
8095+
80188096
;;;###autoload
80198097
(defun org-open-file (path &optional in-emacs line search)
80208098
"Open the file at PATH.
@@ -8112,27 +8190,8 @@ If the file does not exist, throw an error."
81128190
(not org-open-non-existing-files))
81138191
(user-error "No such file: %s" file))
81148192
(cond
8115-
((and (stringp cmd) (not (string-match "^\\s-*$" cmd)))
8116-
;; Remove quotes around the file name - we'll use shell-quote-argument.
8117-
(while (string-match "['\"]%s['\"]" cmd)
8118-
(setq cmd (replace-match "%s" t t cmd)))
8119-
(setq cmd (replace-regexp-in-string
8120-
"%s"
8121-
(shell-quote-argument (convert-standard-filename file))
8122-
cmd
8123-
nil t))
8124-
8125-
;; Replace "%1", "%2" etc. in command with group matches from regex
8126-
(save-match-data
8127-
(let ((match-index 1)
8128-
(number-of-groups (- (/ (length link-match-data) 2) 1)))
8129-
(set-match-data link-match-data)
8130-
(while (<= match-index number-of-groups)
8131-
(let ((regex (concat "%" (number-to-string match-index)))
8132-
(replace-with (match-string match-index dlink)))
8133-
(while (string-match regex cmd)
8134-
(setq cmd (replace-match replace-with t t cmd))))
8135-
(setq match-index (+ match-index 1)))))
8193+
((org-string-nw-p cmd)
8194+
(setq cmd (org--open-file-format-command cmd file dlink link-match-data))
81368195

81378196
(save-window-excursion
81388197
(message "Running %s...done" cmd)

testing/lisp/test-org.el

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8421,6 +8421,166 @@ two
84218421
(call-interactively #'org-paste-subtree)
84228422
(buffer-string)))))
84238423

8424+
(ert-deftest test-org/org--open-file-format-command ()
8425+
"Test `org--open-file-format-command' helper for `org-open-file'."
8426+
(let ((system-type 'gnu/linux)) ; Fix behavior of `shell-quote-argument'.
8427+
;; No additional groups in `org-file-apps' key.
8428+
(let ((file "/file.pdf")
8429+
(pattern "\\.pdf\\'"))
8430+
(should
8431+
(equal "simple /file.pdf"
8432+
(and (string-match pattern file)
8433+
(org--open-file-format-command
8434+
"simple %s" file file (match-data)))))
8435+
(should
8436+
(equal "single-quotes /file.pdf"
8437+
(and (string-match pattern file)
8438+
(org--open-file-format-command
8439+
"single-quotes '%s'" file file (match-data)))))
8440+
(should
8441+
(equal "double-quotes /file.pdf"
8442+
(and (string-match pattern file)
8443+
(org--open-file-format-command
8444+
"double-quotes \"%s\"" file file (match-data)))))
8445+
(should
8446+
(equal "quotes 'mismatch \"/file.pdf'"
8447+
(and (string-match pattern file)
8448+
(org--open-file-format-command
8449+
"quotes 'mismatch \"%s'" file file (match-data)))))
8450+
(should
8451+
(equal "no subst"
8452+
(and (string-match pattern file)
8453+
(org--open-file-format-command
8454+
"no subst" file file (match-data)))))
8455+
(should
8456+
(equal "% literal percent 100% %s"
8457+
(and (string-match pattern file)
8458+
(org--open-file-format-command
8459+
"\\% literal percent 100\\% \\%s" file file (match-data)))))
8460+
(should
8461+
(equal "escape \"/file.pdf\" \\ more"
8462+
(and (string-match pattern file)
8463+
(org--open-file-format-command
8464+
;; Second quote is not escaped.
8465+
"escape \\\"%s\" \\\\ more" file file (match-data)))))
8466+
(should
8467+
(equal "/file.pdf file at start"
8468+
(and (string-match pattern file)
8469+
(org--open-file-format-command
8470+
"%s file at start" file file (match-data)))))
8471+
(should
8472+
(equal "backslash-newline\n/file.pdf"
8473+
(and (string-match pattern file)
8474+
(org--open-file-format-command
8475+
"backslash-newline\\\n%s" file file (match-data))))))
8476+
;; Anchors within target file.
8477+
(let ((file "/page-search.pdf")
8478+
(link "/page-search.pdf::10::some words")
8479+
(pattern "\\.pdf::\\([0-9]+\\)::\\(.*\\)\\'"))
8480+
(should
8481+
(equal "zathura --page 10 --find some\\ words /page-search.pdf"
8482+
(and (string-match pattern link)
8483+
(org--open-file-format-command
8484+
"zathura --page '%1' --find %2 \"%s\"" file link (match-data)))))
8485+
;; Unused %2.
8486+
(should
8487+
(equal "firefox file:///page-search.pdf\\#page=10"
8488+
(and (string-match pattern link)
8489+
(org--open-file-format-command
8490+
"firefox file://%s\\\\#page=%1" file link (match-data)))))
8491+
(should
8492+
(equal "adjucent-subst /page-search.pdfsome\\ words10some\\ words"
8493+
(and (string-match pattern link)
8494+
(org--open-file-format-command
8495+
"adjucent-subst %s%2'%1'%2" file link (match-data))))))
8496+
;; No more than 9 substitutions are supported.
8497+
(let ((file "/many.pdf")
8498+
(link "/many.pdf::one:2:3:4:5:6:7:8:9:a:b:c")
8499+
(pattern (concat "\\.pdf:"
8500+
(mapconcat (lambda (_) ":\\([^:]+\\)")
8501+
(number-sequence 1 12)
8502+
"")
8503+
"\\'")))
8504+
(should
8505+
(equal "overflow /many.pdf::one:2:3:4:5:6:7:8:9:one0:one1:one2"
8506+
(and (string-match pattern link)
8507+
(org--open-file-format-command
8508+
"overflow %s::%1:%2:%3:%4:%5:%6:%7:%8:%9:%10:%11:%12"
8509+
file link (match-data))))))
8510+
;; Percent character in link fields does not cause any problem.
8511+
(let ((file "/file-%2.pdf")
8512+
(link "/file-%2.pdf::anchor-%3::search %1")
8513+
(pattern "\\.pdf::\\([^:]+\\)::\\(.+\\)\\'"))
8514+
(should
8515+
(equal "percents --find search\\ \\%1 file:///file-\\%2.pdf\\#anchor-\\%3"
8516+
(and (string-match pattern link)
8517+
(org--open-file-format-command
8518+
"percents --find %2 file://%s\\\\#%1"
8519+
file link (match-data))))))
8520+
;; Errors.
8521+
(let ((file "/error.pdf")
8522+
(pattern "\\.pdf\\'"))
8523+
(let* ((err (should-error
8524+
(and (string-match pattern file)
8525+
(org--open-file-format-command
8526+
"trailing-percent %s %" file file (match-data)))
8527+
:type 'error))
8528+
(err-text (cadr err)))
8529+
(should-not (unless (and (stringp err-text)
8530+
(string-match-p "\\`Invalid format .*%" err-text))
8531+
err)))
8532+
(let* ((err (should-error
8533+
(and (string-match pattern file)
8534+
(org--open-file-format-command
8535+
"trailing-backslash %s \\" file file (match-data)))
8536+
:type 'error))
8537+
(err-text (cadr err)))
8538+
(should-not (unless (and (stringp err-text)
8539+
(string-match-p "\\`Invalid format .*\\\\" err-text))
8540+
err)))
8541+
(let* ((err (should-error
8542+
(and (string-match pattern file)
8543+
(org--open-file-format-command
8544+
"percent-newline %\n%s" file file (match-data)))
8545+
:type 'error))
8546+
(err-text (cadr err)))
8547+
(should-not (unless (and (stringp err-text)
8548+
(string-match-p "\\`Invalid format .*%\n" err-text))
8549+
err)))
8550+
;; Mailcap escape for "%" is "\%", not "%%".
8551+
(let* ((err (should-error
8552+
(and (string-match pattern file)
8553+
(org--open-file-format-command
8554+
"percent-percent %s%%" file file (match-data)))
8555+
:type 'error))
8556+
(err-text (cadr err)))
8557+
(should-not (unless (and (stringp err-text)
8558+
(string-match-p "\\`Invalid format .*%%" err-text))
8559+
err)))
8560+
;; Mailcap allows "%t" for MIME type, but Org has no such information.
8561+
(let* ((err (should-error
8562+
(and (string-match pattern file)
8563+
(org--open-file-format-command
8564+
"percent-t-unsupported --type '%t' %s" file file (match-data)))
8565+
:type 'error))
8566+
(err-text (cadr err)))
8567+
(should-not (unless (and (stringp err-text)
8568+
(string-match-p "\\`Invalid format .*%t" err-text))
8569+
err))))
8570+
;; Optional regular expression groups have no point in `org-file-apps' patterns.
8571+
(let* ((file "/error.pdf")
8572+
(link "/error.pdf::1")
8573+
(pattern "\\.pdf::\\([^:]+\\)\\(?:::\\(.+\\)\\)?\\'")
8574+
(err (should-error
8575+
(and (string-match pattern link)
8576+
(org--open-file-format-command
8577+
"no-such-match --search %2 %s" file link (match-data)))
8578+
:type 'error))
8579+
(err-text (cadr err)))
8580+
(should-not (unless (and (stringp err-text)
8581+
(string-match-p "\\`Invalid format.*%2" err-text))
8582+
err)))))
8583+
84248584
(provide 'test-org)
84258585

84268586
;;; test-org.el ends here

0 commit comments

Comments
 (0)