4
\$\begingroup\$

Revised from: Bash scripts and udev rules to handle USB auto mounting / unmounting

Tested:
Uses USB insert/remove to control a headless Raspberry Pi 3 with Raspian Jessie Lite

Changes:

  • Implement more functions
  • Improve comments
  • Modify if-fi exit handling
  • Add optional flag for auto processing on insert/remove
  • Optional function for USB insert - copy file (future: read config/start streaming process)
  • Optional auto shutdown for USB removal (future: close process started by insert)

Goals:

  • Improve bash coding and learn by implementing suggestions
  • Improve my understanding of if-fi blocks and using inline commands (I'm not confident at all when it comes to streamlining code)

Current Code:
Uses udev rules to automount USB and create folder for device. Optionally can use an init constant to cause the automount/dismount process to auto start a process (in this case copy a config file) and shutdown pi on removal.

Work Flow:

uDev rules 
 usb-initloader.sh 
 Insert -> usb-automount.sh
 Remove -> usb-unloader.sh

udev rules

# /etc/udev/rules.d/85-usb-loader.rules
# ADD rule: 
# if USB inserted, 
# and flash drive loaded as sd#
# pass on dev name and device formatting type
# run short script to fork another processing script
# run script to initiate another script (first script must finish quickly)
# to mkdir and mount, process file
#
# reload rules on PI by: 
# sudo udevadm control --reload-rules
#
ACTION=="add", KERNEL=="sd*[0-9]", SUBSYSTEMS=="usb", RUN+="/home/pi/scripts/usb-initloader.sh ADD %k $env{ID_FS_TYPE}"
ACTION=="remove", KERNEL=="sd*[0-9]", SUBSYSTEMS=="usb", RUN+="/home/pi/scripts/usb-initloader.sh %k"

usb-initloader.sh

#!/bin/bash
#
# /home/pi/scripts/usb-initloader.sh
#
# this script uses udev rules 
# is initiated when usb device is inserted or removed
#
# ** DEVICE INSERTED - new USB device inserted **
# ---------------------------------------------
# should be called from a udev rule like:that passes 
# 1. "ADD", 
# 2. kernel device (%k)
# 3. filesystem type $env(ID_FS_TYPE)
#
# ACTION=="add", KERNEL=="sd*[0-9]", SUBSYSTEMS=="usb", RUN+="/home/pi/scripts/usb-initloader.sh ADD %k $env(ID_FS_TYPE)"
#
# Mounts usb device on /media/<dev>
# Logs changes to /var/log/syslog
# use tail /var/log/syslog to look at latest events in log
#
# ** DEVICE REMOVED - USB device removed **
# ---------------------------------------------
# on remove - we only need the kernel device (%k)
# should be called from a udev rule like:
#
# ACTION=="remove", KERNEL=="sd*[0-9]", SUBSYSTEMS=="usb", RUN+="/home/pi/scripts/usb-initloader.sh %k"
#
# CONFIGURATION
#
# Location of the three scripts (** MUST match your udev rules **)
SCRIPT_DIR=/home/pi/scripts
#
# Location of Log File
LOG_DIR=/home/pi/logs
LOG_FILE="${LOG_DIR}/usb-automount.log"
#
# Mount folder (sda1 will be added underneath this)
MOUNT_DIR=/media
#
# Optional parameter to:
# - auto start a program on ADD
# - auto end program and shutdown pi on REMOVE
#
AUTO_START_FINISH=1 # Set to 0 if false; 1 if true
#
# Call speciality script and leave this one (with trailing "&")
#
if [ "1ドル" == "ADD" ]; then
 DEVICE="2ドル" # USB device name (kernel passed from udev rule)
 DEVTYPE="3ドル" # USB device formatting type
 echo "==> Adding USB Device $DEVICE" >> "$LOG_FILE"
 ${SCRIPT_DIR}/usb-automount.sh "$LOG_FILE" "$MOUNT_DIR" "$DEVICE" "$DEVTYPE" "$AUTO_START_FINISH" >> "$LOG_FILE" 2>&1&
else
 DEVICE="1ドル" # USB device name (kernel passed from udev rule)
 echo "==> Unmounting USB Device $DEVICE" >> "$LOG_FILE"
 ${SCRIPT_DIR}/usb-unloader.sh "$LOG_FILE" "$MOUNT_DIR" "$DEVICE" "$AUTO_START_FINISH" >> "$LOG_FILE" 2>&1&
fi

usb-automount.sh

#!/bin/bash
#
# Script: /home/pi/scripts/usb-automount.sh
# make sure to chmod 0755 on this script
#
# USAGE: usb-automount.sh DEVICE FILESYSTEM
# LOG_FILE is the error/activity log file for shell (eg /home/pi/logs/usbloader.log)
# MOUNT_DIR is the full mount folder for device (/media/sda1)
# DEVICE is the actual device node at /dev/DEVICE (returned by udev rules %k parameter) (eg sda1)
# FILESYSTEM is the FileSystem type returned by rules (returned by udev rules %E{ID_FS_TYPE} or $env{ID_FS_TYPE} (eg vfat)
#
# In case the process of mounting takes too long for udev
# we call this script from /home/pi/scripts/usb-initloader.sh
# then fork out to speciality scripts
#
# Adapted for Raspberry Pi - Raspbian O/S
# from previous code found at:
# http://superuser.com/questions/53978/automatically-mount-external-drives-to-media-label-on-boot-without-a-user-logge
# and mount manager example at
# http://solvedforhome.com/?p=2806&v=3a52f3c22ed6
#
# Edited with many suggestions from @janos (https://codereview.stackexchange.com/users/12390/janos)
#
# Acknowledgement to:
# http://www.shellcheck.net/
# dostounix utility
LOG_FILE="1ドル"
MOUNT_DIR="2ドル"
DEVICE="3ドル" # USB device name (from kernel parameter passed from rule)
FILESYSTEM="4ドル"
AUTO_START="5ドル" # Do we want to auto-start a new process? 0 - No; 1 - Yes
# check for defined log file
if [ -z "$LOG_FILE" ]; then
 exit 1
fi
# Define all parameters needed to auto-start program
if [ "$AUTO_START" == "1" ]; then
 CONFIG_FILE="autostart-settings.cfg" # Check for this file on USB to initiate program with user-defined settings
 STARTUP_FOLDER="/home/pi/autostart" # Start up folder to copy settings file into
fi
# Functions:
# #########
#
# automount function to test/make/mount USB device 
#
automount() {
 dt=$(date '+%Y-%m-%d/ %H:%M:%S')
 echo "--- USB Auto Mount --- $dt"
 #check input parameters
 if [ -z "$MOUNT_DIR" ]; then
 echo "Missing Parameter: MOUNT_DIR"
 exit 1
 fi
 if [ -z "$DEVICE" ]; then
 echo "Missing Parameter: DEVICE"
 exit 1
 fi
 if [ -z "$FILESYSTEM" ]; then
 echo "Missing Parameter: FILESYSTEM"
 exit 1
 fi
 # Allow time for device to be added
 sleep 2
 # test for this device is already mounted
 device_mounted=$(grep "$DEVICE" /etc/mtab)
 if [ "$device_mounted" ]; then
 echo "Error: seems /dev/$DEVICE is already mounted"
 exit 1
 fi
 # test mountpoint - it shouldn't exist
 if [ -e "$MOUNT_DIR/$DEVICE" ]; then
 echo "Error: seems mountpoint $MOUNT_DIR/$DEVICE already exists"
 exit 1
 fi
 # make the mountpoint
 sudo mkdir "$MOUNT_DIR/$DEVICE"
 # make sure the pi user owns this folder
 sudo chown -R pi:pi "$MOUNT_DIR/$DEVICE"
 # mount the device base on USB file system
 case "$FILESYSTEM" in
 # most common file system for USB sticks
 vfat) sudo mount -t vfat -o utf8,uid=pi,gid=pi "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
 ;;
 # use locale setting for ntfs
 ntfs) sudo mount -t auto -o uid=pi,gid=pi,locale=en_US.UTF-8 "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
 ;;
 # ext2/3/4 do not like uid option
 ext*) sudo mount -t auto -o sync,noatime "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
 ;;
 esac
 device_mounted=$(grep "$DEVICE" /etc/mtab)
 if [ "$device_mounted" == "" ]; then
 echo "Error: Failed to Mount $MOUNT_DIR/$DEVICE"
 exit 1
 fi
 echo "SUCCESS: /dev/$DEVICE successfully mounted as $MOUNT_DIR/$DEVICE"
}
# Auto Start Funmction
autostart() {
 echo "--- USB Auto Start Program ---"
 look_for_cfg="$MOUNT_DIR/$DEVICE/$CONFIG_FILE"
 if [ -e "$look_for_cfg" ]; then
 echo "Copying Startup File to $STARTUP_FOLDER"
 cp -u -p "$look_for_cfg" "$STARTUP_FOLDER"
 fi
}
automount >> "$LOG_FILE" 2>&1
if [ "$AUTO_START" == "1" ]; then
 autostart >> "$LOG_FILE" 2>&1
fi

usb-unloader.sh

#!/bin/bash
# /home/pi/scripts/usb-unloader.sh
#
# Called from {SCRIPT_DIR}/usb-initloader.sh
#
# USAGE: usb-automount.sh DEVICE FILESYSTEM
# LOG_FILE is the error/activity log file for shell (eg /home/pi/logs/usbloader.log)
# MOUNT_DIR is the full mount folder for device (/media)
# DEVICE is the actual device node at /dev/DEVICE (returned by udev rules %k parameter) (eg sda1)
#
# UnMounts usb device on /media/<device>
# Logs changes to /var/log/syslog and local log folder
# use tail /var/log/syslog to look at latest events in log
#
# SUPPLIED PARAMETERS
#####################
LOG_FILE="1ドル"
MOUNT_DIR="2ドル"
DEVICE="3ドル" # USB device name (from kernel parameter passed from rule)
AUTO_END="4ドル" # Set to 0 if not wanting to shutdown pi, 1 otherwise
#
# check for defined log file
if [ -z "$LOG_FILE" ]; then
 exit 1
fi
#
# FUNCTIONS
###########
#
# autounload function to unmount USB device and remove mount folder
#
autounload() {
 dt=$(date '+%Y-%m-%d %H:%M:%S')
 echo "--- USB UnLoader --- $dt"
 if [ -z "$MOUNT_DIR" ]; then
 echo "Failed to supply Mount Dir parameter"
 exit 1
 fi
 if [ -z "$DEVICE" ]; then
 echo "Failed to supply DEVICE parameter"
 exit 1
 fi
 # Unmount device
 sudo umount "/dev/$DEVICE"
 # Wait for a second to make sure async umount has finished
 sleep 1
 # Remove folder after unmount
 sudo rmdir "$MOUNT_DIR/$DEVICE"
 # test that this device has disappeared from mounted devices
 device_mounted=$(grep "$DEVICE" /etc/mtab)
 if [ "$device_mounted" ]; then
 echo "/dev/$DEVICE failed to Un-Mount"
 exit 1
 fi
 echo "/dev/$DEVICE successfully Un-Mounted"
}
autounload >> "$LOG_FILE" 2>&1
# kill auto-start process and shutdown
if [[ "$AUTO_END" == "1" ]]; then
 sudo shutdown -H 0
fi
asked Jul 8, 2016 at 3:03
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

Introduce a helper function: is_mounted

As I suggested in the previous review, I recommend to replace this code:

device_mounted=$(grep "$DEVICE" /etc/mtab)
if [ "$device_mounted" ]; then
 echo "Error: seems /dev/$DEVICE is already mounted"
 exit 1
fi
# ...
device_mounted=$(grep "$DEVICE" /etc/mtab)
if [ "$device_mounted" == "" ]; then
 echo "Error: Failed to Mount $MOUNT_DIR/$DEVICE"
 exit 1
fi

With this:

if is_mounted "$DEVICE"; then
 echo "Error: seems /dev/$DEVICE is already mounted"
 exit 1
fi
# ...
if ! is_mounted "$DEVICE"; then
 echo "Error: Failed to Mount $MOUNT_DIR/$DEVICE"
 exit 1
fi

Where the implementation of is_mounted:

is_mounted() {
 grep -q "1ドル" /etc/mtab
}

It's shorter and actually quite intuitive.

Introduce a helper function: fatal

Another repeating pattern I see is the check-then-exit combos, like this:

if some_requirement_fails; then
 echo "Error: Failed some_requirement"
 exit 1
fi

You could create a helper function to make repeated usages slightly easier:

fatal() {
 echo "Error: $*"
 exit 1
}
if some_requirement_fails; then
 fatal "Failed some_requirement"
fi

Actually, this form opens up the possibility for a more compact syntax:

some_requirement || fatal "Failed some_requirement"

With this and the earlier suggestions, automount could be written like this:

automount() {
 dt=$(date '+%Y-%m-%d/ %H:%M:%S')
 echo "--- USB Auto Mount --- $dt"
 # check input parameters
 [ "$MOUNT_DIR" ] || fatal "Missing Parameter: MOUNT_DIR"
 [ "$DEVICE" ] || fatal "Missing Parameter: DEVICE"
 [ "$FILESYSTEM" ] || fatal "Missing Parameter: FILESYSTEM"
 # Allow time for device to be added
 sleep 2
 is_mounted "$DEVICE" && fatal "seems /dev/$DEVICE is already mounted"
 # test mountpoint - it shouldn't exist
 [ -e "$MOUNT_DIR/$DEVICE" ] && fatal "seems mountpoint $MOUNT_DIR/$DEVICE already exists"
 # make the mountpoint
 sudo mkdir "$MOUNT_DIR/$DEVICE"
 # make sure the pi user owns this folder
 sudo chown -R pi:pi "$MOUNT_DIR/$DEVICE"
 # mount the device base on USB file system
 case "$FILESYSTEM" in
 # most common file system for USB sticks
 vfat) sudo mount -t vfat -o utf8,uid=pi,gid=pi "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
 ;;
 # use locale setting for ntfs
 ntfs) sudo mount -t auto -o uid=pi,gid=pi,locale=en_US.UTF-8 "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
 ;;
 # ext2/3/4 do not like uid option
 ext*) sudo mount -t auto -o sync,noatime "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
 ;;
 esac
 is_mounted "$DEVICE" || fatal "Failed to Mount $MOUNT_DIR/$DEVICE"
 echo "SUCCESS: /dev/$DEVICE successfully mounted as $MOUNT_DIR/$DEVICE"
}

Notice that the calls to fatal can be chained using || or &&, depending on whether the requirement checked should be true or false, respectively.

If you're not comfortable yet with chaining commands with || and &&, you can stick to the if-fi syntax, there's nothing wrong with that.

Explanation about exit codes, grep -q, if, && and ||

You mentioned in comment that the grep -q part is not exactly easy to understand, so here's a bit more explanation, I hope it helps.

grep exits with exit code 0 if there was a match, and some non-zero exit code if there was no match. For example:

$ echo hello | grep e
hello
$ echo $?
0
$ echo hello | grep x
$ echo $?
1

The $? variable stores the exit code of the last command.

We can build conditions using the exit codes of commands, for example:

$ if echo hello | grep e; then echo success; else echo failure; fi
hello
success
$ if echo hello | grep x; then echo success; else echo failure; fi
failure

Notice that in case of success, the matched pattern is printed. Of course. That's why we normally use grep, to find matching lines. If we don't care about the matching line, if we just want to know if there was a matching line or not, then we can suppress the output using the -q flag. Rerunning the above using the -q flag:

$ if echo hello | grep -q e; then echo success; else echo failure; fi
success
$ if echo hello | grep -q x; then echo success; else echo failure; fi
failure

Notice the difference from earlier: no more "hello" line, the matched pattern was not printed.

Lastly, the same example using && and || instead of if statement:

$ echo hello | grep -q e && echo success || echo failure
success
$ echo hello | grep -q x && echo success || echo failure
failure

Slightly more compact, but equivalent solution. But this is by no means a preferred syntax. It's "ok" to use this syntax when the condition is simple and easy to understand. It's not well-suited and can get very confusing with more complex conditions, and then it's not recommended.

answered Jul 8, 2016 at 20:10
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Thanks again - and especially clarifying The $? variable stores the exit code of the last command. - and that grep returns 0 on success. Both of those simple statements help a lot in understanding the code. There's so much to learn and massaging code found on Internet often works but doesn't help with understanding. This site has been amazing. I can share my knowledge of PC dbs and vb coding - and learn from others on the Linux side. Soaking it all in! \$\endgroup\$ Commented Jul 8, 2016 at 20:35
  • \$\begingroup\$ Just read things over and Ooh - I really like your fatal suggestion \$\endgroup\$ Commented Jul 9, 2016 at 3:07
  • \$\begingroup\$ Great, glad I could help! \$\endgroup\$ Commented Jul 9, 2016 at 5:50
  • \$\begingroup\$ Just a quick update - I had NO problems whatever implementing your suggestions and the code looks so much more streamlined than before. I'm going to try and emulate that style in my other programs coming up. \$\endgroup\$ Commented Jul 9, 2016 at 16:03

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.