6

Personally I need run many shell command in emacs, I find it is not convenient to run a shell engine (eshell) or using it separately (bash), so is there a elisp function (local provide by emacs or any other third-part library) meet my need?

ccd
  • 259
  • 2
  • 11
  • 3
    `M-!` lets you run a shell command in the current directory. If you want something else, please give us more details. – choroba Apr 16 '19 at 09:14
  • If using this command I need to move to the directory and then type shell command through `M-!`, how to combine these to one step and not leaving current working buffer. – ccd Apr 16 '19 at 09:17
  • Type `cd dir && your command`. – choroba Apr 16 '19 at 09:28
  • The `cd dir` seems not smart as well as `find-file` function, is it possible let them work the same? – ccd Apr 16 '19 at 15:24
  • You can try running the `cd` elisp function instead, but it changes the current directory globally. – choroba Apr 16 '19 at 15:27
  • @choroba: No, Emacs command `cd` does not *"change the current directory globally"*. It simply changes the value of `default-directory` in the current buffer. The value of `default-directory` in other buffers is not affected, and its default value is not affected. `C-h f cd`. – Drew Apr 16 '19 at 15:49
  • @Drew: Ok, I just meant it doesn't change it back after running a command. – choroba Apr 16 '19 at 16:17
  • @choroba: That's a good clarification, which speaks to the OP request; thx. (`cd`'s purpose is not to invoke a command one-off, but to change the current directory for subsequent interactions.) – Drew Apr 16 '19 at 18:38

3 Answers3

10

From elisp you can set default-directory temporarily and then run your shell command:

(let ((default-directory "/tmp")) 
    (shell-command "ls"))
amitp
  • 2,451
  • 12
  • 23
  • Hard coding the file path and shell command might not as my expect, sorry. – ccd Apr 17 '19 at 00:53
  • 4
    @yuanlai This is not your use-case, yes. But, maybe this answer is helpful for other users that stumble across this question. It *is* the Elisp answer to your question. The string `"/tmp"` is just an example. The general principle of locally binding `default-directory` is of importance. If you study [the code from my answer](https://emacs.stackexchange.com/a/48970/2370) you will see that I also used this technique. – Tobias Apr 17 '19 at 01:13
  • simple and useful. thanks – RoyM Jul 19 '20 at 13:00
3

I execute all of my python code through the shell within Emacs. Here is how I do it.

Although it's not clear what specifically you're trying to call, I suspect that if you understand how this code works, you will be able to adapt it to your needs. Use the built-in Emacs help (C-h f and C-h v) to learn specifically what various parts of the code do.

Create a shell in which to send a command to

This function is basically a convenience wrapper around what @amitp has already posted. It first checks if a shell process currently exists, creating one if none does. It then sends the provided command argument to the shell and executes it, along with some repositioning of the cursor. (Essentially it pastes the command into the shell and presses <RET>.)

    (defun my-sh-send-command (command)
      "Send COMMAND to current shell process.

    Creates new shell process if none exists.

    See URL `https://stackoverflow.com/a/7053298/5065796'"
      (let ((proc (get-process "shell"))
            pbuf)
        (unless proc
          (let ((currbuff (current-buffer)))
            (shell)
            (switch-to-buffer currbuff)
            (setq proc (get-process "shell"))
            ))
        (setq pbuff (process-buffer proc))
        (setq command-and-go (concat command "\n"))
        (with-current-buffer pbuff
          (goto-char (process-mark proc))
          (insert command-and-go)
          (move-marker (process-mark proc) (point))
          )
        (process-send-string proc command-and-go)))

Send a shell command using the currently visited file

I use python, so this function concatenates the current buffer name with a "python " prefix and sends it to the shell. Just change the "python " part in the (concat ...) statement to whatever you need.

    (defun my-buffer-file-to-shell ()
      "Send current buffer file to shell as python call."
      (interactive)
      (my-sh-send-command (concat "python " (buffer-file-name))))

In this case, the path is absolute and comes from buffer-file-name. If you need to execute the code in a different directory, you will need to modify the default-directory, as @amitp states. Note that default-directory is buffer local. You can read more about setting default-directory programmatically here.

Bind everything to a convenient key

I have a global shell command which runs the main project file and is bound to F7. If I want to run the currently visited file with python, I press S-f7.

    (global-set-key (kbd "<S-f7>") 'my-buffer-file-to-shell)
Lorem Ipsum
  • 4,327
  • 2
  • 14
  • 35
3

The following Elisp code advises shell-command so that you can input a default-directory for the shell-command if you call it with numeric prefix argument.

If the prefix argument is negative the original shell-command code will be called without prefix argument. You get the output of the shell command in *Shell Command Output* buffer in that case.

If the prefix argument is positive the original shell-command gets also that prefix argument and the output is send to the current buffer.

If shell-command is called non-interactively it works like expected if the first argument is a string. Otherwise the second argument should be the default-directory to be used for the shell command and the original arguments of shell-command should start at the third third argument of the advised version.

(defun ad-read-default-directory-args (interactive-spec)
  "Read default directory and apply INTERACTIVE-SPEC.
Return a list (DIRECTORY PREFIX-ARG RESULT-OF-INTERACTIVE-SPEC).
The default directory is only read with numeric `current-prefix-arg'."
  (let ((default-directory (or
                (and
                 (numberp current-prefix-arg)
                 (expand-file-name (read-directory-name "Default directory: " nil nil t)))
                default-directory))
    (current-prefix-arg (and (numberp current-prefix-arg)
                 (null (< current-prefix-arg 0))
                 current-prefix-arg)))
    (append
     (list
      current-prefix-arg ;; never a string
      default-directory)
     (advice-eval-interactive-spec interactive-spec))))

(defun ad-read-default-directory-&-call (fun &rest args)
  "Run FUN with ARGS with `default-directory' set to DIRECTORY and PREFIX-ARG.
Only change `default-directory' if the prefix arg is numeric.
Positive prefix args are passed to FUN negative are not."
  (interactive #'ad-read-default-directory-args)
  (if (stringp (car args))
      (apply fun args)
    (let ((default-directory (or (cl-second args) default-directory))
      (current-prefix-arg (cl-first args)))
      (apply fun (nthcdr 2 args)))))

(advice-add 'shell-command :around #'ad-read-default-directory-&-call)
Tobias
  • 32,569
  • 1
  • 34
  • 75
  • When run `ad-read-default-directory-&-call`, it report `entered--Lisp error: (wrong-type-argument sequencep ad-read-default-directory-args) call-interactively(ad-read-default-directory-&-call record nil)` error. – ccd Apr 17 '19 at 00:44
  • @yuanlai That command is not meant to be run by a user. It is exclusively the advice for `shell-command`. Please use `shell-command` only, maybe with a numerical prefix. – Tobias Apr 17 '19 at 00:46
  • Something weird, the `shell-command` only execute the shell command and not let me to select the file path. – ccd Apr 17 '19 at 00:51
  • @yuanlai Try a numeric prefix argument to select the `default-directory` for `shell-command`. The key sequence is e.g.: `C-- C-1 M-!` Literally: Hold the control key down and press the "minus" key and the "1" key. Afterwards hold the Alt key down and press `:`. The actual number is irrelevant you can also input `-9` if that is easier for you. Please read the answer carefully to understand what the difference between a positive and a negative prefix argument is. – Tobias Apr 17 '19 at 00:54
  • when I run `C-1 M-!` and then type `ls` shell command, the result paste on my current working buffer. – ccd Apr 17 '19 at 00:59
  • @yuanlai Yes, that is expected since the positive prefix argument is forwarded to the original version of `shell-command` (see the [doc for `shell-command`](https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell.html)). Negative prefix arguments are not forwarded to the original `shell-command` and the result is only shown in the minibuffer and not pasted in the current buffer. – Tobias Apr 17 '19 at 01:02