Florian Winkelbauer

Shared Scripts on Windows and Linux

Sometimes you end up in a situation where you would like to use the same script on different operating systems (e.g. Linux and Windows). Now you have to choose between two possibilities:

I’ve recently experimented with the second approach. But which portable language should one use? Well, I went done the crazy road and opened up my good old friend Emacs. Let’s start with two Bash examples:

A script to run git pull in several directories:

set -euo pipefail

repositories=(~/.emacs.d/ ~/Projects/*/)

for repo in ${repositories[*]}
do
    echo "Pulling '$repo'"
    pushd "$repo" > /dev/null
    git pull
    popd > /dev/null
done

And a simple rsync backup script:

set -euo pipefail

dest=/some/backup/location

rsync="rsync -avh --delete"

$rsync ~/Documents "$dest"
$rsync ~/Projects "$dest"

Now let’s rewrite these scripts in Lisp. Here’s the git script (which relies on the magit package):

(defun fw/git-pull-all ()
  "Runs git pull for every repository found in `magit-list-repos'"
  (interactive)
  (require 'magit)
  (if magit-repository-directories
      (dolist (path (magit-list-repos))
        (let ((default-directory path))
          (let ((exit-code (fw/exec-app "git" (list "pull" path))))
            (unless (eq exit-code 0)
              (user-error "Error while pulling \"%s\"" path)))))))

And the rsync script:

(defun fw/rsync (&rest args)
  "Calls the rsync binary"
  (fw/exec-app-validated "rsync" args '(0)))

(defun fw/rsync-backup ()
  "Creates my rsync backup"
  (interactive)
  (let ((destination "/some/backup/location")
        (directories '("~/Documents"
                       "~/Projects")))
    (dolist (directory directories)
      (fw/rsync "-avh --delete" directory destination))))

The above snippets rely on these helper functions:

(defun fw/exec-app (application args)
  "Runs an `application' with a list of `args' using
`shell-command' and returns the exit code"
  (let ((shell-command-dont-erase-buffer t)
        (full-cmd (string-join (add-to-list 'args application) " ")))
    (shell-command full-cmd (concat "*exec " application "*"))))

(defun fw/exec-app-validated (application args valid-exit-codes)
  "Runs an `application' with a list of `args' using
`shell-command' and throws a `user-error' if the exit code is not
a member of the `valid-exit-codes' list"
  (let ((exit-code (fw/exec-app application args)))
    (unless (member exit-code valid-exit-codes)
      (user-error "Exit code of \"%s\" was %s" application exit-code))))

It was fun to come up with the above solutions, but for the most part I’ll rewrite my scripts depending on which operating system I’m on. While Emacs has become a big part of my daily Computer work, I’m still not convinced that this tool should be utilized everywhere.