2

How do I take a string giving a shell command line such as:

"program arg1 arg2 \"long argument with spaces\" arg\\\"3"

and turn it into a list of unquoted arguments like:

("program" "arg1" "arg2" "long argument with spaces" "arg\"3")

I.e. almost the reverse of shell-quote-argument. shell-unquote-argument doesn't do the trick.

I realize this sounds like an XY problem. I'd like to offer a minibuffer prompt for the user to give command line arguments to an external program, then run that program via call-process. I could prompt for the command line arguments one at a time until the user gives a blank, but it seems like better UI to let them type all the arguments at once as they would at a real shell prompt. I could also error out on fancy characters and only let them type alphanumerics, dashes and spaces, but that seems limiting. As a third alternative, I could use the call-process variant that gives the command line to a shell instead using it directly as the argv, but I'd like to avoid giving the user the full power of the shell (pipes, redirects, etc.) as that would probably confuse more than help.

3 Answers 3

5

Does this do what you want?

(split-string-and-unquote "program arg1 arg2 \"long argument with spaces\" \"arg\"3")

There seemed to be an error in your string with an unmatched \" at the end that caused this function to not work. I am not sure if it makes sense to have the single " in there or not. If it does, you might have to write your own parser.

8
  • You're right, I made a mistake with the nested quoting. Fixed.
    – Lassi
    Nov 6, 2018 at 21:44
  • split-string-and-unquote is a great find, thanks for this. However, it only seems to handle double quotes, not single quotes 'foo bar' and backslashes foo\ bar.
    – Lassi
    Nov 6, 2018 at 21:46
  • @Lassi: Yes, split-string-and-unquote isn't parsing shell syntax. You give it an elisp string, and it returns a list of elisp strings. Which is fine, so long as the user understands the syntax to use. To pass the results to a shell command you would map shell-quote-argument over the list.
    – phils
    Nov 6, 2018 at 22:18
  • n.b. If you want to guarantee that the string is interpreted exactly as the user's shell (whatever that may be!) interprets it, you would presumably need to talk to a shell process?
    – phils
    Nov 6, 2018 at 22:29
  • Oh, noting that you said you would be using call-process, you obviously wouldn't need shell-quote-argument, as you're not going via a shell at all.
    – phils
    Nov 6, 2018 at 22:32
2

Since there doesn't appear to be a function for the job, I wrote one:

(defun shell-command-line-to-argument-list (command-line)
  (let (args arg inquote)
    (with-temp-buffer
      (insert command-line)
      (goto-char (point-min))
      (while (not (eobp))
        (cond ((looking-at "\\s-+")
               (cond ((not inquote)
                      (when arg (push arg args))
                      (setq arg nil))
                     (t (setq arg (concat arg (match-string 0))))))
              ((looking-at "['\"]")
               (let ((ch (match-string 0)))
                 (cond ((not inquote)
                        (setq inquote ch)
                        (setq arg (or arg "")))
                       ((equal inquote ch)
                        (setq inquote nil))
                       (t (setq arg (concat arg ch))))))
              ((or (looking-at "\\\\\\(.?\\)") (looking-at "\\(.\\)"))
               (setq arg (concat (or arg "") (match-string 1)))))
        (goto-char (match-end 0)))
      (when inquote (error "Missing closing quote"))
      (when arg (push arg args))
      (nreverse args))))

(shell-command-line-to-argument-list "")
nil

(shell-command-line-to-argument-list "    ")
nil

(shell-command-line-to-argument-list "  'foo bar'baz  '' \"abcd efg\" \\ ghij\\ kl")
("foo barbaz" "" "abcd efg" " ghij kl")

(shell-command-line-to-argument-list " foo\\")
("foo")  ;; This is arguably a bug, but not a serious one.

(shell-command-line-to-argument-list " \"foo\\\" ")
;; Error: Missing closing quote

I'd like to contribute this to Emacs or to a relevant library in MELPA instead of keeping it around in my unrelated Emacs package. Any thoughts on what would be a good place for it?

2
  • 1
    M-x report-emacs-bug to suggest adding it to Emacs.
    – phils
    Nov 7, 2018 at 10:43
  • split-string-shell-command is now available since version 28.1 :)
    – Adam Oudad
    Nov 13, 2022 at 8:55
1

Since version 28.1, you can use split-string-shell-command which does just that.

(split-string-shell-command "program arg1 arg2 \"long argument with spaces\" arg\\\"3")
> ("program" "arg1" "arg2" "long argument with spaces" "arg\"3")

Your Answer

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

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