Shell scripts are irreplaceable!
2021-04-11
Introduction
One line of shell script is worth 10,000 lines of C code.
Shell scripts are one the most important but at the same time most
under appreciated aspect of Unix programming. After writing
substantial about of code to imitate and replace shell scripts with
other programming languages I have indeed realised that they are
irreplaceable. Most programming language tries to imitate programs
like cd
, mkdir
, ls
, etc but fails to identify it's greatest
strength, pipelines. The flow of data using pipelines feels much more
natural and efficient.
Now, I will try to present the difficulties I faced in replacing shell scripts with a 'proper' programming language.
Note: Here we are referring to POSIX Shell scripts as they are easier to wrap your head around.
Python
Even though Python is easy to use, fast to get things done and have batteries included philosophy it still fails to be a good replacements for shell scripts.
I decided to write a python program to install Arch Linux and
configure it to my liking (basically bootstrap it). At first I was
really happy with python modules like os
, pwd
and getpass
but as
time went on these abstraction proved more costly than useful.
Now here is a snippet that I used to create a Unix user account and it's equivalent version in shell script.
Note: Any of mistakes done in implementing these programs are unintentional and if you find any them please contact me.
Python version:
# add repodir = directory to clone all repo's def set_repodir(home): repodir = home + '/.local/src' os.mkdir(repodir) os.chown(repodir, user_info.pw_uid, pw_gid) # add user account def add_user(): try: home = '/home/' + user useradd = ['useradd', '-m', '-g', 'wheel', '-s', '/bin/bash', user] # make bash as default shell subprocess.run(useradd, capture_output=True, check=True) set_repodir(home) # if users exist, then modify user account except subprocess.CalledProcessError: usermod = ['usermod', '-a', '-G', 'wheel', user] subprocess.run(usermod, capture_output=True, check=True) os.mkdir(home) os.chown(home, user_info.pw_uid, pw_gid) set_repodir(home)
Shell script version (taken from LARBS):
adduserandpass() { \ useradd -m -g wheel -s /bin/bash "$name" >/dev/null 2>&1 || usermod -a -G wheel "$name" && mkdir -p /home/"$name" && chown "$name":wheel /home/"$name" repodir="/home/$name/.local/src"; mkdir -p "$repodir"; chown -R "$name":wheel "$(dirname "$repodir")" ;}
As we can see the python version is not only more lengthy but also
more error prone compared to shell script version. What will happen
when usermod
command will fail?
Now there comes some command which cannot be scripted without
pipelines and these require us to use a different subroutine called
getstatusoutput
to invoke shell, execute command and get status and
output back.
# add user's password def add_pass(password): chpass= 'echo ' + user + ':' + password + ' | chpasswd' output = subprocess.getstatusoutput(chpass) # exit if output status output is not zero if output[0] != 0: err_msg(output.chpass) exit(1)
Elisp (Emacs Lisp)
At first it seems confusing to use a editor's extension language to program your environment but due to extensible nature of LISP it can be even used to do so. Also I generally use Emacs as my interface to UNIX rather than a text editor.
I recently wrote a Elisp library called emount to make use of Emacs
completion to mount and umount USB drives and Android using Emacs! At
its core it was just using mount
and simple-mtpfs
to mount USB
drives and Android respectively.
Here is the function I used to mount USB devices with my own
ut-common-shell-command
which is just a wrapper around Emacs
call-process
command.
(defun ut-common-shell-command (command &rest args) "Run COMMAND with ARGS. Return the exit code and output in a list." (with-temp-buffer (list (apply 'call-process command nil (current-buffer) nil args) (buffer-string)))) (defun emount-mount-usb (usb-name mount-point) "Mount usb with USB-NAME at MOUNT-POINT." (interactive (list (emount--select-unmounted-usb (emount--get-drive-list emount--usb-drive)) (emount--select-mount-point))) (let* ((partition-cmd (concat "lsblk -no fstype " usb-name)) (partition-type (string-chop-newline (shell-command-to-string partition-cmd))) user-group result exit-code output) (if (string-equal partition-type "vfat") (progn (setq result (ut-common-shell-command "sudo" "mount" "-t" "vfat" "-o" "rw,umask=0000" usb-name mount-point)) (setq exit-code (nth 0 result)) (setq output (nth 1 result)) (if (= exit-code emount--success-code) (message "%s mounted successfully!" usb-name) (user-error output))) (setq result (ut-common-shell-command "sudo" "mount" usb-name mount-point)) (setq exit-code (nth 0 result)) (setq output (nth 1 result)) (if (not (eq exit-code emount--success-code)) (user-error output) ;; TODO: test groups output (setq user-group (nth 0 (split-string (shell-command-to-string "groups")))) (setq result (ut-common-shell-command "sudo" "chowm" (user-login-name) ":" user-group mount-point)) (setq exit-code (nth 0 result)) (setq output (nth 1 result)) (if (= exit-code emount--success-code) (message "%s mounted successfully!" usb-name) (user-error output))))))
Again you can see I am using (nth 0 result)
to get exit code and
printing error using (user-error output)
if exit code doesn't match
emount--success-code
.
Corresponding shell script version (taken from voidrice):
mountusb() { \ chosen="$(echo "$usbdrives" | dmenu -i -p "Mount which drive?")" || exit 1 chosen="$(echo "$chosen" | awk '{print $1}')" sudo -A mount "$chosen" 2>/dev/null && notify-send "USB mounting" "$chosen mounted." && exit 0 alreadymounted=$(lsblk -nrpo "name,type,mountpoint" | awk '$3!~/\/boot|\/home$|SWAP/&&length($3)>1{printf "-not ( -path *%s -prune ) ",$3}') getmount "/mnt /media /mount /home -maxdepth 5 -type d $alreadymounted" partitiontype="$(lsblk -no "fstype" "$chosen")" case "$partitiontype" in "vfat") sudo -A mount -t vfat "$chosen" "$mp" -o rw,umask=0000;; "exfat") sudo -A mount "$chosen" "$mp" -o uid="$(id -u)",gid="$(id -g)";; *) sudo -A mount "$chosen" "$mp"; user="$(whoami)"; ug="$(groups | awk '{print $1}')"; sudo -A chown "$user":"$ug" "$mp";; esac }
Conclusion
In the end I will let you decide which implementation suits you better but I still think shell literacy is important and it will help you to come up with a better solution. I would also recommend you check what Master Foo wants to say about shell scripting.