Write bootable image to USB device Recently, I've been making and refining installer and rescue images to boot from USB media, in the form of CD-ROM or hard disk images.  After a few occasions where I re-ran the command after removing the media (and ending up with a large file in /dev/disk/by-id/ instead of writing to a device), I wrote a small wrapper to check that the target is a block device.

Since then, it's grown features - notably to take input direct from network sources (e.g. a build server) to save making a local copy first.  In these cases, I want to be able to sudo write-image but still make the network connection as non-root user (partly as good practice, but also so that I can use my ~/.ssh/config and private keys in the normal way).

The usage() function ought to explain how it's intended to be used; if I need to explain further, then it's a sign that my documentation needs improving.

The reason that this needs Bash rather than POSIX shell is to use command substitution process substitution.  The use of a shell function in process substitution seems to be particularly non-portable, but it works for me with Bash 4.4.

#!/bin/bash

set -eu

die() { echo "$@" &gt;&amp;2; exit 1; }

usage() {
    cat &lt;&lt;END
usage: $0 [source] &lt;usb-device&gt;
The source can be
  * a filename
  * a URL
  * ssh:[user@]&lt;host&gt;:&lt;file&gt;  (must be passwordless)
  * '-' or absent (for piping from standard input)
END
}

as_real_user() {
    exec ${SUDO_USER+sudo -n -H -u "$SUDO_USER" }"$@"
}

case $# in
    1)
        case "$1" in
            --help|-h)
                usage
                exit 0
                ;;
        esac
        ;;
    2)
        case "$1" in
            ssh:*)
                file="${1#*:}"
                host="${file%%:*}"
                file="${file#*:}"
                exec &lt; &lt;(as_real_user ssh "$host" cat "$file")
                ;;
            *://*)
                exec &lt; &lt;(as_real_user wget -q -O - "$url")
                ;;
            -)
                # standard input is already connected
                ;;
            *)
                test -e "$1" || die "$1: not found"
                test -f "$1" || die "$1: not a plain file"
                test -r "$1" || die "$1: not readable"
                exec &lt; "$1"
                ;;
        esac
        shift
        ;;
    *)
        usage
        exit 1;
esac

test -b "$1" || die "$1: not a block device"
test -w "$1" || die "$1: not writable"
cut -d' ' -f1 /proc/mounts | grep -Fxq $(realpath "$1") &amp;&amp; die "$1: is mounted"
outfile="of=$1"

# Explanation
# * nocreat - just checking, given above tests of destination
# * fsync - don't return until image is fully written
# * dsync,bs,status - show update every 1 MB written
ddflags="conv=nocreat,fsync oflag=dsync bs=1M status=progress"

exec dd $ddflags $outfile
Quoting

Some recommended double-quotes were missed here:


cut -d' ' -f1 /proc/mounts | grep -Fxq $(realpath "$1") &amp;&amp; die "$1: is mounted"



Should be like this:

cut -d' ' -f1 /proc/mounts | grep -Fxq "$(realpath "$1")" &amp;&amp; die "$1: is mounted"




This will break for example if $outfile contains spaces:


exec dd $ddflags $outfile



You could make it safe by using an array:

ddargs=(conv=nocreat,fsync oflag=dsync bs=1M status=progress "of=$1")

exec dd "${ddargs[@]}"


Safety tip

set -eu is a nice safeguard, I also like to add -o pipefail:

set -euo pipefail


Cosmetic issues

There is an unnecessary trailing ; in exit 1;