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:
- Write the same script in different languages (e.g. Bash and PowerShell)
- Write your script in one language and ensure that you can use it on all systems (e.g. by using PowerShell Core)
I've recently experimented with the second approach. But which portable language should one use? Well, I went down 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:
#!/bin/bash -eu 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:
#!/bin/bash -eu 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.