Subject: bug#65902: 29.0.92; emacsclient-mail.desktop fails due to complicated escaping

From: [email-masked]
Date: Fri, 22 Sep 2023 01:36:47 +0000 (UTC)

Eli Zaretskii <[email-masked]> writes:
>> From: Spencer Baugh <[email-masked]>
>> Cc: Spencer Baugh <[email-masked]>,  [email-masked],
>>    [email-masked]
>> Date: Thu, 14 Sep 2023 11:10:44 -0400
>> 
>> We could make a command-line-args-left equivalent for emacsclient,
>> called server-eval-args-left, which contains the FILE arguments passed
>> to emacsclient as strings.  This can be done without making any changes
>> to emacsclient.c or the server protocol.  Then the message-mailto
>> use case would look like this:
>> 
>> emacsclient --eval '(message-mailto (pop server-eval-args-left))' %u
>> 
>> This would match how message-mailto uses (pop command-line-args-left)
>> internally.
>> 
>> This would work for all the use-cases I described before; I'd be very
>> happy with this solution (actually, I'm starting to prefer it to
>> --apply).  And again, it doesn't change emacsclient.c or the server
>> protocol.
>
> This could perhaps be acceptable (although it's still rather kludgey,
> IMO), but are you sure you understand all the consequences?
> Currently, when emacsclient is invoked like this:
>
>   $ emacsclient --eval '(func args)' foo bar
>
> we send to the server the following commands:
>
>   -eval (func args)
>   -eval foo
>   -eval bar
>
> IOW, every command-line argument after --eval is treated as being
> implicitly preceded with --eval.
>
> With your proposal, how will the server know that some of "-eval foo"
> commands should cause foo to be added to server-eval-args-left instead
> of being evaluated as it does now?

As in the attached patch.

>From 1f0ff370a23eee1fdf740527efa75c0ddbe625bb Mon Sep 17 00:00:00 2001
From: Spencer Baugh <[email-masked]>
Date: Thu, 21 Sep 2023 21:35:50 -0400
Subject: [PATCH] Add server-eval-args-left to server.el

Passing arbitrary arguments to functions through emacsclient --eval
requires complicated escaping to avoid them being parsed as Lisp (as
seen in emacsclient-mail.desktop before this change).

This new variable server-eval-args-left allows access to the arguments
before they are parsed as Lisp.  By removing arguments from the
variable before they're parsed, a snippet of Lisp can consume
arguments, as in emacsclient-mail.desktop.

org-protocol might be able to use this as well, which might allow it
to drop its current advice on server-visit-files.

* etc/emacsclient-mail.desktop: Use server-eval-args-left. (bug#65902)
* lisp/server.el (server-eval-args-left): Add.
(server-process-filter, server-execute): Make -eval arguments
available through server-eval-args-left.
---
 etc/emacsclient-mail.desktop |  7 ++-----
 lisp/server.el               | 21 +++++++++++++--------
 2 files changed, 15 insertions(+), 13 deletions(-)

diff --git a/etc/emacsclient-mail.desktop b/etc/emacsclient-mail.desktop
index 0a2420ddead..5962fa1764c 100644
--- a/etc/emacsclient-mail.desktop
+++ b/etc/emacsclient-mail.desktop
@@ -1,10 +1,7 @@
 [Desktop Entry]
 Categories=Network;Email;
 Comment=GNU Emacs is an extensible, customizable text editor - and more
-# We want to pass the following commands to the shell wrapper:
-# u=$(echo "$1" | sed 's/[\"]/\\&/g'); exec emacsclient --alternate-editor= --display="$DISPLAY" --eval "(message-mailto \"$u\")"
-# Special chars '"', '$', and '\' must be escaped as '\\"', '\\$', and '\\\\'.
-Exec=sh -c "u=\\$(echo \\"\\$1\\" | sed 's/[\\\\\\"]/\\\\\\\\&/g'); exec emacsclient --alternate-editor= --display=\\"\\$DISPLAY\\" --eval \\"(message-mailto \\\\\\"\\$u\\\\\\")\\"" sh %u
+Exec=emacsclient --alternate-editor= --eval '(message-mailto (pop server-eval-args-left))' %u
 Icon=emacs
 Name=Emacs (Mail, Client)
 MimeType=x-scheme-handler/mailto;
@@ -16,7 +13,7 @@ Actions=new-window;new-instance;
 
 [Desktop Action new-window]
 Name=New Window
-Exec=sh -c "u=\\$(echo \\"\\$1\\" | sed 's/[\\\\\\"]/\\\\\\\\&/g'); exec emacsclient --alternate-editor= --create-frame --eval \\"(message-mailto \\\\\\"\\$u\\\\\\")\\"" sh %u
+Exec=emacsclient --alternate-editor= --create-frame --eval '(message-mailto (pop server-eval-args-left))' %u
 
 [Desktop Action new-instance]
 Name=New Instance
diff --git a/lisp/server.el b/lisp/server.el
index c3325e5a24c..6eff6ebe140 100644
--- a/lisp/server.el
+++ b/lisp/server.el
@@ -1165,7 +1165,7 @@ server-process-filter
     (when prev
       (setq string (concat prev string))
       (process-put proc 'previous-string nil)))
-  (condition-case err
+  (condition-case-unless-debug err
       (progn
 	(server-add-client proc)
 	;; Send our pid
@@ -1189,6 +1189,7 @@ server-process-filter
 		parent-id  ; Window ID for XEmbed
 		dontkill   ; t if client should not be killed.
 		commands
+		evalexprs
 		dir
 		use-current-frame
 		frame-parameters  ;parameters for newly created frame
@@ -1319,8 +1320,7 @@ server-process-filter
                  (let ((expr (pop args-left)))
                    (if coding-system
                        (setq expr (decode-coding-string expr coding-system)))
-                   (push (lambda () (server-eval-and-print expr proc))
-                         commands)
+                   (push expr evalexprs)
                    (setq filepos nil)))
 
                 ;; -env NAME=VALUE:  An environment variable.
@@ -1345,7 +1345,7 @@ server-process-filter
 	    ;; arguments, use an existing frame.
 	    (and nowait
 		 (not (eq tty-name 'window-system))
-		 (or files commands)
+		 (or files commands evalexprs)
 		 (setq use-current-frame t))
 
 	    (setq frame
@@ -1394,7 +1394,7 @@ server-process-filter
                  (let ((default-directory
                          (if (and dir (file-directory-p dir))
                              dir default-directory)))
-                   (server-execute proc files nowait commands
+                   (server-execute proc files nowait commands evalexprs
                                    dontkill frame tty-name)))))
 
             (when (or frame files)
@@ -1404,22 +1404,27 @@ server-process-filter
     ;; condition-case
     (t (server-return-error proc err))))
 
-(defun server-execute (proc files nowait commands dontkill frame tty-name)
+(defvar server-eval-args-left)
+
+(defun server-execute (proc files nowait commands evalexprs dontkill frame tty-name)
   ;; This is run from timers and process-filters, i.e. "asynchronously".
   ;; But w.r.t the user, this is not really asynchronous since the timer
   ;; is run after 0s and the process-filter is run in response to the
   ;; user running `emacsclient'.  So it is OK to override the
-  ;; inhibit-quit flag, which is good since `commands' (as well as
+  ;; inhibit-quit flag, which is good since `evalexprs' (as well as
   ;; find-file-noselect via the major-mode) can run arbitrary code,
   ;; including code that needs to wait.
   (with-local-quit
     (condition-case err
         (let ((buffers (server-visit-files files proc nowait)))
           (mapc 'funcall (nreverse commands))
+          (let ((server-eval-args-left (nreverse evalexprs)))
+            (while server-eval-args-left
+              (server-eval-and-print (pop server-eval-args-left) proc)))
 	  ;; If we were told only to open a new client, obey
 	  ;; `initial-buffer-choice' if it specifies a file
           ;; or a function.
-          (unless (or files commands)
+          (unless (or files commands evalexprs)
             (let ((buf
                    (cond ((stringp initial-buffer-choice)
 			  (find-file-noselect initial-buffer-choice))
-- 
2.41.0