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

From: [email-masked]
Date: Wed, 13 Sep 2023 12:57:40 +0000 (UTC)

Jim Porter <[email-masked]> writes:

> On 9/12/2023 7:30 PM, [email-masked] wrote:
>> tags 65902 + patch
>> quit
>> This patch avoids the complicated scripting needed for
>> emacsclient-mail.desktop by adding a new flag to emacsclient, --funcall,
>> which mirrors emacs --funcall and allows emacsclient-mail.desktop to be
>> basically the same as emacs-mail.desktop.
>
> I think this is actually the same as the (very long) bug#57752, so
> thanks for working on this. (It was on my list of things to get to,
> but I just haven't had time.)
>
> Over there, we agreed that something like your patch is wanted, albeit
> with two caveats:
>
> 1. Since "--funcall" for the regular "emacs" binary doesn't pass
> arguments to the function, how about we call this option "--apply"
> instead?

Ah, for some reason I thought --funcall passed arguments.  Done in
attached patch.

> 2. It would be great if we could get "--apply" for the regular "emacs"
> binary too, so that both programs work the same way (at least in this
> regard).

Done.

Although the behavior is slightly different: emacs --apply calls the
function with subsequent FILE arguments, and emacsclient --apply calls
the function with all FILE arguments.  The "subsequent FILE arguments"
behavior is probably better, but I don't know a way to do that with
getopt (which emacsclient uses).

> Even better, if you could forward "--apply" from
> "emacsclient" to the alternate editor (which would be "emacs" 99% of
> the time) automatically. That works, in a roundabout way, for the
> Emacs daemon, but not if the alternate editor is "emacs".

This happens automatically anyway: When emacsclient starts the daemon,
it sends the --apply request.  I think that's exactly what you'd want.

>From 4881017055ea6831ee7fe2d722eb79856946d907 Mon Sep 17 00:00:00 2001
From: Spencer Baugh <[email-masked]>
Date: Tue, 12 Sep 2023 22:20:15 -0400
Subject: [PATCH] Add --apply argument to avoid escaping arguments

Passing arguments to functions through emacs --eval or emacsclient
--eval requires complicated escaping (as seen in
emacsclient-mail.desktop before this change).

The new --apply argument for both emacs and emacsclient passes command
line arguments as uninterpreted strings to the specified function.
This simplifies use cases where arbitrary input needs to be passed to
Emacs.

Note that there's a minor difference in behavior between emacs --apply
and emacsclient --apply: emacs --apply func calls func with only
command line arguments occuring after --apply, emacsclient --apply
func calls func with all command line arguments, even those which
occurred before --apply.  This is hard to avoid since emacsclient uses
getopt instead of fancier custom Lisp argument parsing in Emacs.

* etc/emacsclient-mail.desktop: Use --apply. (bug#65902)
* lib-src/emacsclient.c (longopts, decode_options, main): Add support
for --apply.
* lisp/server.el (server-apply-and-print): Add.
(server-process-filter): Add support for -apply and -applyargs
* lisp/startup.el (command-line-1): Add support for --apply.
* src/emacs.c (usage_message, standard_args): Add support for --apply.
---
 etc/emacsclient-mail.desktop |  9 +++------
 lib-src/emacsclient.c        | 36 ++++++++++++++++++++++++++++++++++--
 lisp/server.el               | 34 ++++++++++++++++++++++++++++++++++
 lisp/startup.el              | 18 +++++++++++++++---
 src/emacs.c                  |  3 +++
 5 files changed, 89 insertions(+), 11 deletions(-)

diff --git a/etc/emacsclient-mail.desktop b/etc/emacsclient-mail.desktop
index 0a2420ddead..750fcddacdc 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= --apply message-mailto -- %u
 Icon=emacs
 Name=Emacs (Mail, Client)
 MimeType=x-scheme-handler/mailto;
@@ -16,8 +13,8 @@ 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 --apply message-mailto -- %u
 
 [Desktop Action new-instance]
 Name=New Instance
-Exec=emacs -f message-mailto %u
+Exec=emacs --apply message-mailto -- %u
diff --git a/lib-src/emacsclient.c b/lib-src/emacsclient.c
index 698bf9b50ae..159c22d1ae9 100644
--- a/lib-src/emacsclient.c
+++ b/lib-src/emacsclient.c
@@ -116,6 +116,9 @@ #define DEFAULT_TIMEOUT (30)
 /* True means args are expressions to be evaluated.  --eval.  */
 static bool eval;
 
+/* The function to call.  Other arguments are passed as strings.  --apply.  */
+static char *apply;
+
 /* True means open a new frame.  --create-frame etc.  */
 static bool create_frame;
 
@@ -169,6 +172,7 @@ #define DEFAULT_TIMEOUT (30)
   { "quiet",	no_argument,	   NULL, 'q' },
   { "suppress-output", no_argument, NULL, 'u' },
   { "eval",	no_argument,	   NULL, 'e' },
+  { "apply",	required_argument, NULL, 'y' },
   { "help",	no_argument,	   NULL, 'H' },
   { "version",	no_argument,	   NULL, 'V' },
   { "tty",	no_argument,       NULL, 't' },
@@ -552,6 +556,10 @@ decode_options (int argc, char **argv)
 	  eval = true;
 	  break;
 
+	case 'y':
+	  apply = optarg;
+	  break;
+
 	case 'q':
 	  quiet = true;
 	  break;
@@ -690,6 +698,7 @@ print_help_and_exit (void)
 -F ALIST, --frame-parameters=ALIST\n\
 			Set the parameters of a new frame\n\
 -e, --eval    		Evaluate the FILE arguments as ELisp expressions\n\
+-y, --apply FUNC	Call ELisp FUNC, passing all FILE arguments as strings\n\
 -n, --no-wait		Don't wait for the server to return\n\
 -w, --timeout=SECONDS	Seconds to wait before timing out\n\
 -q, --quiet		Don't display messages on success\n\
@@ -1953,7 +1962,7 @@ main (int argc, char **argv)
   /* Process options.  */
   decode_options (argc, argv);
 
-  if (! (optind < argc || eval || create_frame))
+  if (! (optind < argc || eval || apply || create_frame))
     {
       message (true, ("%s: file name or argument required\n"
 		      "Try '%s --help' for more information\n"),
@@ -1961,6 +1970,14 @@ main (int argc, char **argv)
       exit (EXIT_FAILURE);
     }
 
+  if (eval && apply)
+    {
+      message (true, ("%s: can't pass both --eval and --apply\n"
+		      "Try '%s --help' for more information\n"),
+	       progname, progname);
+      exit (EXIT_FAILURE);
+    }
+
 #ifdef SOCKETS_IN_FILE_SYSTEM
   if (tty)
     {
@@ -2080,6 +2097,13 @@ main (int argc, char **argv)
               send_to_emacs (emacs_socket, " ");
               continue;
             }
+	  else if (apply)
+	    {
+              send_to_emacs (emacs_socket, "-applyarg ");
+              quote_argument (emacs_socket, argv[i]);
+              send_to_emacs (emacs_socket, " ");
+              continue;
+	    }
 
 	  char *p = argv[i];
 	  if (*p == '+')
@@ -2136,10 +2160,18 @@ main (int argc, char **argv)
       send_to_emacs (emacs_socket, " ");
     }
 
+  if (apply)
+    {
+      send_to_emacs (emacs_socket, "-apply ");
+      quote_argument (emacs_socket, apply);
+      send_to_emacs (emacs_socket, " ");
+    }
+
+
   send_to_emacs (emacs_socket, "\n");
 
   /* Wait for an answer. */
-  if (!eval && !tty && !nowait && !quiet && 0 <= process_grouping ())
+  if (!eval && !apply && !tty && !nowait && !quiet && 0 <= process_grouping ())
     {
       printf ("Waiting for Emacs...");
       skiplf = false;
diff --git a/lisp/server.el b/lisp/server.el
index c3325e5a24c..5981e90625d 100644
--- a/lisp/server.el
+++ b/lisp/server.el
@@ -873,6 +873,17 @@ server-eval-and-print
                        (point-min) (point-max))))
             (server-reply-print (server-quote-arg text) proc)))))))
 
+(defun server-apply-and-print (func args proc)
+  "Call FUNC on ARGS and send the result back to client PROC."
+  (let ((v (with-local-quit (eval (apply (intern func) args) t))))
+    (when proc
+      (with-temp-buffer
+        (let ((standard-output (current-buffer)))
+          (pp v)
+          (let ((text (buffer-substring-no-properties
+                       (point-min) (point-max))))
+            (server-reply-print (server-quote-arg text) proc)))))))
+
 (defconst server-msg-size 1024
   "Maximum size of a message sent to a client.")
 
@@ -1196,6 +1207,7 @@ server-process-filter
 		tty-type   ; string.
 		files
 		filepos
+		applyargs
 		args-left)
 	    ;; Remove this line from STRING.
 	    (setq string (substring string (match-end 0)))
@@ -1323,6 +1335,28 @@ server-process-filter
                          commands)
                    (setq filepos nil)))
 
+                ;; -apply FUNC:  Call a function on arguments.
+                ("-apply"
+                 (if use-current-frame
+                     (setq use-current-frame 'always))
+                 (let ((func (pop args-left)))
+                   (if coding-system
+                       (setq func (decode-coding-string func coding-system)))
+                   (push (lambda () (server-apply-and-print func applyargs proc))
+                         commands)
+                   (setq applyargs nil)
+                   (setq filepos nil)))
+
+                ;; -applyarg ARG:  Add an argument for later -apply.
+                ("-applyarg"
+                 (if use-current-frame
+                     (setq use-current-frame 'always))
+                 (let ((arg (pop args-left)))
+                   (if coding-system
+                       (setq arg (decode-coding-string arg coding-system)))
+                   (push arg applyargs)
+                   (setq filepos nil)))
+
                 ;; -env NAME=VALUE:  An environment variable.
                 ("-env"
                  (let ((var (pop args-left)))
diff --git a/lisp/startup.el b/lisp/startup.el
index 7f601668369..bb8da76bdf1 100644
--- a/lisp/startup.el
+++ b/lisp/startup.el
@@ -2531,10 +2531,11 @@ command-line-1
                ;; straight away upon any --directory/-L option.
                splice
                just-files ;; t if this follows the magic -- option.
+               applysym applyargs ;; function and arguments for --apply
                ;; This includes our standard options' long versions
                ;; and long versions of what's on command-switch-alist.
                (longopts
-                (append '("--funcall" "--load" "--insert" "--kill"
+                (append '("--funcall" "--apply" "--load" "--insert" "--kill"
                           "--dump-file" "--seccomp"
                           "--directory" "--eval" "--execute" "--no-splash"
                           "--find-file" "--visit" "--file" "--no-desktop")
@@ -2632,6 +2633,11 @@ command-line-1
                          (command-execute tem)
                        (funcall tem)))
 
+                    ((member argi '("-y" "-apply"))
+                     (setq inhibit-startup-screen t)
+                     ;; Subsequent file args will be accumulated into applyargs.
+                     (setq applysym (intern (or argval (pop command-line-args-left)))))
+
                     ((member argi '("-eval" "-execute"))
                      (setq inhibit-startup-screen t)
                      (let* ((str-expr (or argval (pop command-line-args-left)))
@@ -2763,13 +2769,19 @@ command-line-1
                          ;; screen for -nw?
                          (unless initial-window-system
                            (setq inhibit-startup-screen t))
-                         (funcall process-file-arg orig-argi)))))
+                         (if applysym
+                             (push orig-argi applyargs)
+                           (funcall process-file-arg orig-argi))))))
 
               ;; In unusual circumstances, the execution of Lisp code due
               ;; to command-line options can cause the last visible frame
               ;; to be deleted.  In this case, kill emacs to avoid an
               ;; abort later.
-              (unless (frame-live-p (selected-frame)) (kill-emacs nil)))))))
+              (unless (frame-live-p (selected-frame)) (kill-emacs nil))))
+
+          ;; Call the function specified with --apply, if any.
+          (when applysym
+            (apply applysym (nreverse applyargs))))))
 
     (when (eq initial-buffer-choice t)
       ;; When `initial-buffer-choice' equals t make sure that *scratch*
diff --git a/src/emacs.c b/src/emacs.c
index 80a013b68df..8de40936250 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -323,6 +323,8 @@ #define MAIN_PROGRAM
 --file FILE             visit FILE\n\
 --find-file FILE        visit FILE\n\
 --funcall, -f FUNC      call Emacs Lisp function FUNC with no arguments\n\
+--apply, -y FUNC        call Emacs Lisp function FUNC, passing\n\
+                          subsequent positional FILE arguments as strings\n\
 --insert FILE           insert contents of FILE into current buffer\n\
 --kill                  exit without asking for confirmation\n\
 --load, -l FILE         load Emacs Lisp FILE using the load function\n\
@@ -2648,6 +2650,7 @@ main (int argc, char **argv)
      option.  In any case, this is entirely an internal option.  */
   { "-scriptload", NULL, 0, 1 },
   { "-f", "--funcall", 0, 1 },
+  { "-y", "--apply", 0, 1 },
   { "-funcall", 0, 0, 1 },
   { "-eval", "--eval", 0, 1 },
   { "-execute", "--execute", 0, 1 },
-- 
2.41.0