Table of Contents

Design is about pulling things apart.

Rich Hickey

With bash-completion we get a really nice boost for making shell commands from Emacs. Babashka tasks completions are for free, together with make, same as dynamic git, ssh and whatever.

Here is the screencast on youtube.

Below are sort of show notes.

1. Make sure you have completions in your shell (bash for me)

;  (setf shell-file-name "/bin/bash")

1.a for bb:



  _bb_complete() {
    BB_TASKS=$(bb tasks|bb -io '(->> *input* (drop 2) (map #(-> % (str/split #" ") first)))')
    BB_HELP=$(bb help|bb -io '(->> *input* (map #(->> % (re-find #"^  ([-a-z]+)") second)) (filter some?))')
    COMPREPLY=($(compgen -W "$BB_TASKS $BB_HELP" -- "${COMP_WORDS[$COMP_CWORD]}"))

complete -f -F _bb_complete bb

2. Setup bash-completion


(use-package bash-completion
  (setf bash-completion-use-separate-processes t)
  (defun bash-completion-capf-1 (bol)
    (bash-completion-dynamic-complete-nocomint (funcall bol) (point) t))
  (defun bash-completion-capf ()
    (bash-completion-capf-1 #'point-at-bol))
   (defun mm/add-bash-completion ()
     (add-hook 'completion-at-point-functions #'bash-completion-capf nil t))))


Afterwards, this should work for you: (I always try with git check...).

(read-shell-command "your command: ")

read-shell-command sets up a special keymap where tab now means completion-at-point.

read-from-minibuffer, 3thr arg

"foo: "
(let ((m (make-sparse-keymap)))
  (define-key m "f"
              (defun my-hi ()
                (insert "lol")))

Commands that use read-shell-command

  • async-shell-command, shell-command
  • dired-do-async-shell-command, dired-do-shell-command
  • compile

I use project.el to run async shell commands and compile in the project root.

Run in any dir?

Design is about splitting things apart.

  1. Select a current dir, up to you. My main paths would be
    • consult-dir,
    • project-switch-project,
    • consult-buffer + bookmarks
  2. Run async-shell-command or a friend of it.

More config

  (setq async-shell-command-buffer 'new-buffer)

  (defun path-slug (dir)
    "Returns the initials of `dir`s path,
with the last part appended fully


(path-slug \"/foo/bar/hello\")
=> \"f/b/hello\" "
    (let* ((path (replace-regexp-in-string "\\." "" dir))
           (path (split-string path "/" t))
           (path-s (mapconcat
                    (lambda (it)
                      (cl-subseq it 0 1))
                    (nbutlast (copy-sequence path) 1)
           (path-s (concat
                    (car (last path)))))

  (defun mm/put-command-in-async-buff-name (f &rest args)
    (let* ((path-s (if default-directory (path-slug default-directory) ""))
           (command (car args))
           (buffname (concat path-s " " command))
             "*async-shell-command %s*"
              (substring buffname 0 (min (length buffname) 50))))))
      (apply f args)))

  (advice-add 'shell-command :around #'mm/put-command-in-async-buff-name)

  (add-hook 'comint-mode-hook
            (defun mm/do-hack-dir-locals (&rest _)

  ;; it logs a warning when you hack a local
  ;; Making process-environment buffer-local while locally let-bound!
  ;; It is sort of want I want though
  (advice-add #'start-process-shell-command :before #'mm/do-hack-dir-locals)

  (advice-add 'compile :filter-args
              (defun mm/always-use-comint-for-compile (args) `(,(car args) t)))


(defun mm/with-current-window-buffer (f &rest args)
      (window-buffer (car (window-list)))
    (apply f args)))

(defun mm/shell-via-async-shell-command ()
  (let ((display-buffer-alist
         '((".*" display-buffer-same-window))))
    (async-shell-command shell-file-name)))

(advice-add #'mm/shell-via-async-shell-command :around #'mm/with-current-window-buffer)

(setf shell-kill-buffer-on-exit t)

 (defun mm/shell-dont-mess-with-scroll-conservatively ()
   (setq-local scroll-conservatively 0)))

I invoke mm/shell-via-async-shell-command with a stumpwm keybinding. This is the only reason I need mm/with-current-window-buffer irrc.

See also

Date: 2023-06-20 Tue 20:27

Email: Benjamin.Schwerdtner@gmail.com