Passing Emacs variables to minibuffer shell commands
Asked Answered
C

6

9

I can run a shell command quickly by hitting M-!. One thing I'd like to do is perform shell quick operations on the current file. An example would be checking the file out through perforce:

M-! p4 edit buffer-file-name RET

(Yes there are perforce integrations, but I'm more interested in the minishell/variable problem rather than a specific workflow)

Of course, the buffer-file-name variable is not evaluated before the command is sent to the shell.

Is there an easy on-the-fly way to do this? Or will I have to roll a custom elisp function?

Cyrenaica answered 12/4, 2012 at 10:40 Comment(2)
perforce should integrate with Emacs see: emacswiki.org/emacs/PerforceSCMPhiphenomenon
I'll edit the question to make it clear that I mean this as an example only. I'm interested in how to easy evaluate a variable before passing it to the minibuffer shell.Cyrenaica
M
5

Indeed using C-u M-: is almost right. I'm not so sure about using shell-quote-argument in eval-to-shell-argument since it only works on strings making it impossible to use eval-to-shell-argument to insert a number or a symbol. You could try something like:

(defun sm-minibuffer-insert-val (exp)
  (interactive
   (list (let ((enable-recursive-minibuffers t))
           (read-from-minibuffer "Insert: "
                                 nil read-expression-map t
                                 'read-expression-history))))
    (let ((val (with-selected-window (minibuffer-selected-window)
                 (eval exp)))
          (standard-output (current-buffer)))
      (prin1 val)))

and then bind this function in your minibuffer with (define-key minibuffer-local-map [?\M-:] 'sm-minibuffer-insert-val). Of course, if the only thing you ever want to insert is the buffer-file-name, then your execute-shell-command-on-buffer is simpler.

Mascarenas answered 12/4, 2012 at 14:11 Comment(2)
Can you go into more detail? I'm sorry but I just don't follow what your function is supposed to do. My inexperience, not your code.Cyrenaica
Aha, so the function I was looking for in my edited answer is minibuffer-selected-window! Thanks for the lesson.Fein
A
11

It seems current Emacs has something built-in to achieve the desired result, after M-! (shell-command) press <down>, you will get the file name you are currently visiting on the prompt. Now you can edit it to add the command you want to run on it.

In dired-mode it will give you the file your cursor is currently on.

Alexandros answered 3/4, 2013 at 10:1 Comment(0)
M
5

Indeed using C-u M-: is almost right. I'm not so sure about using shell-quote-argument in eval-to-shell-argument since it only works on strings making it impossible to use eval-to-shell-argument to insert a number or a symbol. You could try something like:

(defun sm-minibuffer-insert-val (exp)
  (interactive
   (list (let ((enable-recursive-minibuffers t))
           (read-from-minibuffer "Insert: "
                                 nil read-expression-map t
                                 'read-expression-history))))
    (let ((val (with-selected-window (minibuffer-selected-window)
                 (eval exp)))
          (standard-output (current-buffer)))
      (prin1 val)))

and then bind this function in your minibuffer with (define-key minibuffer-local-map [?\M-:] 'sm-minibuffer-insert-val). Of course, if the only thing you ever want to insert is the buffer-file-name, then your execute-shell-command-on-buffer is simpler.

Mascarenas answered 12/4, 2012 at 14:11 Comment(2)
Can you go into more detail? I'm sorry but I just don't follow what your function is supposed to do. My inexperience, not your code.Cyrenaica
Aha, so the function I was looking for in my edited answer is minibuffer-selected-window! Thanks for the lesson.Fein
C
3

I did roll my own elisp function, and it looks like this:

(defun execute-shell-command-on-buffer (shell-command-text)
    (interactive "MShell command:")
    (shell-command (format shell-command-text (shell-quote-argument buffer-file-name)))
    )

https://gist.github.com/2367513

I bound it to M-", so now my example can be completed with:

M-"p4 edit %sRET

I won't accept this as the answer, because I did ask for solutions that don't require a function.

Cyrenaica answered 12/4, 2012 at 14:7 Comment(0)
F
2

You can use C-u M-: (eval-expression with a universal prefix argument) to evaluate any Lisp expression and insert its value at point in the current buffer (including minibuffers, as long as you have enable-recursive-minibuffers set to a non-nil value).

In your example: C-u M-: buffer-file-name RET.

Note that the result of the expression is printed in Lisp form: that is, quoted in such a way that a subsequent call to read would construct an equal Lisp value. For strings, this means enclosing in double quotes, which will probably be interpreted as you expect by the inferior shell. However, you may run into problems with strings that contain special characters, which need different escaping by Elisp and the shell.

The more correct way uses shell-quote-argument, as in phils' solution. Here's a quick defun that reads a Lisp expression and inserts its value at point as a properly quoted shell word:

(defun eval-to-shell-argument (form)
  (interactive "XEval: ")
  (insert (shell-quote-argument form)))

The read-and-evaluate step happens automatically by using an "X" as the argument to interactive.

Edited to add: As @tenpn notes, the above solution doesn't work for inserting buffer-local variables like buffer-file-name in a minibuffer like the one M-! pops up (more precisely, it inserts the buffer-local value of the minibuffer, which is unlikely to be useful). Here is a revised version which seems to work. If the minibuffer is active, it makes the buffer of the previously-selected window temporarily active while reading and evaluating an expression.

Final edit: From @Stefan's answer I see that I should have used (minibuffer-selected-window) to find the previously-selected window. I've also added a (format "%s" ..) to allow inserting non-string values, while still quoting special characters in strings. Here's the final version:

(defun eval-to-shell-argument ()
  (interactive)
  (let* ((buffer
          (if (minibufferp)
              (window-buffer (minibuffer-selected-window))
            (current-buffer)))
         (result
          (with-current-buffer buffer
            (eval-minibuffer "Eval: "))))
    (insert (shell-quote-argument (format "%s" result)))))
Fein answered 12/4, 2012 at 12:37 Comment(4)
If I do M-! p4 edit C-u M-: buffer-file-name RET RET, buffer-file-name evaluates to nil. I guess this is because it's taking the file name of the minibuffer (which logically doesn't exist), not the currently open main buffer?Cyrenaica
And I don't quite follow what your function is supposed to do - excuse my emacs newbness. Can you go into more detail?Cyrenaica
@tenpn: Hmm, you're absolutely right about using M-: in the minibuffer -- I should have tested it more thoroughly. I'll see if I can find a better solution.Fein
@tenpn: I've posted a revised solution (although your own is clearly a simpler way if you usually want to just use buffer-file-name). Let me know if you'd like more detail on it.Fein
L
1

You can't do that with M-!, but you can evaluate arbitrary elisp from the minibuffer, so writing a function isn't strictly necessary:

M-: (shell-command (format "p4 edit %s" (shell-quote-argument buffer-file-name))) RET

In this case however, I think eshell is what you want to use:

M-x eshell-command RET p4 edit (eval buffer-file-name) RET

Edit: Except unfortunately that doesn't work, as the *eshell cmd* buffer is selected when that is evaluated. One solution would be:

M-x eshell-command RET p4 edit (eval buffer-file-name (other-buffer nil t)) RET

(Not quite as elegant, sorry.)

Logographic answered 12/4, 2012 at 12:32 Comment(2)
Your first suggestion works, but the second doesn't. (eval buffer-file-name) seems to evaluate to nothing.Cyrenaica
Curses; I didn't test that properly. The *eshell cmd* buffer is selected at that point in time.Logographic
A
1

Everyone seems to be rolling their own version, so here's mine -- it will substitue the current filename or marked dired-files or current dired file wherever a % is in the shell command. It follows the same conventions as M-! so I bind it to that.

(defun my-shell-command (command &optional output-buffer error-buffer)
  "Run a shell command with the current file (or marked dired files).
In the shell command, the file(s) will be substituted wherever a '%' is."
  (interactive (list (read-from-minibuffer "Shell command: "
                                           nil nil nil 'shell-command-history)
                     current-prefix-arg
                     shell-command-default-error-buffer))
  (cond ((buffer-file-name)
         (setq command (replace-regexp-in-string "%" (buffer-file-name) command nil t)))
        ((and (equal major-mode 'dired-mode) (save-excursion (dired-move-to-filename)))
         (setq command (replace-regexp-in-string "%" (mapconcat 'identity (dired-get-marked-files) " ") command nil t))))
  (shell-command command output-buffer error-buffer))
Ashore answered 12/4, 2012 at 16:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.