Scan ~/bin and turn the scripts into Emacs commands

I want to automate little things on my computer so that I don’t have to look up command lines or stitch together different applications. Many of these things make sense to turn into shell scripts. That way, I can call them from other programs and assign keyboard shortcuts to them. Still, I spend most of my computer time in Emacs, and I don’t want to think about whether I’ve defined a command in Emacs Lisp or in a shell script. Besides, I like the way Helm lets me type parts of commands in order to select and call them.

Emacs Lisp allows you to define a macro that results in Emacs Lisp code. In this case, I want to define interactive functions so I can call them with M-x. In case I decide to call them from Emacs Lisp, such as (my/shell/rotate-screen "left"), I want to be able to pass arguments. I’m also using dash.el to provide functions like -filter and -not, although I could rewrite this to just use the standard Emacs Lisp functions.

Here’s the code that scans a given directory for executable files and creates interactive functions, and some code that calls it for my ~/bin directory.

(defmacro my/convert-shell-scripts-to-interactive-commands (directory)
  "Make the shell scripts in DIRECTORY available as interactive commands."
  (cons 'progn
          (-map
           (lambda (filename)
             (let ((function-name (intern (concat "my/shell/" (file-name-nondirectory filename)))))
               `(defun ,function-name (&rest args)
                  (interactive)
                  (apply 'call-process ,filename nil nil nil args))))
           (-filter (-not #'file-directory-p)
                    (-filter #'file-executable-p (directory-files directory t))))))

(my/convert-shell-scripts-to-interactive-commands "~/bin")

Let’s see how that goes!

  • Phil

    From the title I assumed this was going to be about automatically using `shell-command-on-region‘ with the REPLACE argument (ala C-u M-| ), in order to seamlessly use your external programs as if they were elisp commands.

    • Phil, you could do something like that as well, and it wouldn’t be too different. Many of my scripts turn out to be filters I use in just that way.

  • Meddix Boh

    Thank you very much for your insights, as usual.
    Just a minor point: -not is part of dash-functional

    • Yes, I’ve found that I had to add that as a require to my config. =) Thanks!

  • Rather a neat idea. Thanks!

  • I love this idea! Boosting the habit of doing all stuff from within emacs and not having to switch to other tools. I would love to see evolving that idea, e.g., also using this kind of shell to emacs lisp conversions for ~/bin/filters with shell-command-on-region. Thanks a lot for sharing it!