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
1 Answer 1
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.
-
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\$dbmitch– dbmitch2016年07月08日 20:35:23 +00:00Commented Jul 8, 2016 at 20:35 -
\$\begingroup\$ Just read things over and Ooh - I really like your
fatal
suggestion \$\endgroup\$dbmitch– dbmitch2016年07月09日 03:07:21 +00:00Commented Jul 9, 2016 at 3:07 -
\$\begingroup\$ Great, glad I could help! \$\endgroup\$janos– janos2016年07月09日 05:50:06 +00:00Commented 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\$dbmitch– dbmitch2016年07月09日 16:03:50 +00:00Commented Jul 9, 2016 at 16:03