Categories
Bash

safer Bash scripts

TL;DR

You should write the following at the beginning of every script:

set -euo pipefail
shopt -s lastpipe

Long version

Bash is a common used script language, if it comes to write some shell scripts for Unix. To make your scripts safer for common mistakes you should set some options of Bash in your script to prevent unexpected behavior and to enable some stricter rules.

set -e

If you set this option, Bash will immediately stop execution of your script if an error occurs.

The script:

#!/bin/bash

unknowncommand
echo "my second line"

will output with an return code of 0:

./test.sh: line 3: unknowncommand: command not found
my second output

If you add set -e at the beginning:

#!/bin/bash

set -e

unknowncommand
echo "my second line"

you will get the following output with an return code of 127.

./test.sh: line 5: unknowncommand: command not found

So you can trust that if a command like mkdir or cd for example will fail, your script will stop.

If you want to allow a command to fail, you have to add || true, for example:

#!/bin/bash

set -e

unknowncommand || true
echo "my second line"

set -u

This option will prevent you from using undefined variables.

The script:

#!/bin/bash

echo "Hello $NAME"

will output:

Hello

Bash handle undefined variables as empty variables per default. If you add set -u at the beginning of your script:

#!/bin/bash

set -u

echo "Hello $NAME"

you will get the following output:

./test.sh: line 5: NAME: unbound variable

If you are not sure, if the variable exists, you can use the syntax for default values:

#!/bin/bash

set -u

echo "Hello ${NAME:-World}"

set -o pipefail

If you are using pipes, only the last command will define the result of the whole pipe. So if you run this script:

#!/bin/bash

set -eu

unknowncommand | echo "other command"
echo "after command"

you will get the following output:

./test.sh: line 5: unknowncommand: command not found
other command
after command

If you add set -o pipefail at the beginning of your script:

#!/bin/bash

set -euo pipefail

unknowncommand | echo "other command"
echo "after command"

the script will stop after the failed pipe and you get the output:

./test.sh: line 5: unknowncommand: command not found

shopt -s lastpipe

If you run a script in a pipe and want to set variables, this won’t work, because your command is run in a sub shell and then you cannot change variables.

The following script:

#!/bin/bash

set -euo pipefail

VAR=Hello
echo "some" | { while read -r file ; do VAR=World; done }
echo $VAR

would output:

Hello

If you add shopt -s lastpipe at the beginning of your script:

#!/bin/bash

set -euo pipefail
shopt -s lastpipe

VAR=Hello
echo "some" | { while read -r file ; do VAR=World; done }
echo $VAR

the script would output:

World
Categories
Linux

run custom script on USB connect in Linux

You can run custom scripts after you have connected a USB device to a Linux system. So you can then perform custom actions also only if a specific device was plugged in.

In modern Linux systems the udevd daemon is used to handle the devices. You can define rules for specific actions the udev should do.

First you have to gather some informations about your target device. You can use the command udevadm monitor to monitor udev events. Some useful commands are also:

CommandUsage
udevadm monitor -pview all events with full properties
udevadm monitor -p -s blockview all events with full properties of block subsystem related events

If your device is already connected, you can also query the attributes of an existing device using (for example disk /dev/sdb): udevadm info -a -n /dev/sdb

Here you should extract some unique attributes of your device.

To run your custom script or command you have to create such a rules file, for example /etc/udev/rules.d/10-myusbstick.rules, defining the match rules for your extracted attributes and the wanted action.

SUBSYSTEM=="block", ACTION=="add", ENV{DEVTYPE}=="partition", ATTRS{idProduct}=="441a", ATTRS{idVendor}=="0815", ATTRS{serial}=="123697755110", ENV{UDISKS_AUTO}="0", ENV{UDISKS_IGNORE}="1", RUN += "/usr/local/sbin/customscript.sh"

You should check that for every event the attributes are matching, the script will be started. So if your rule is too wide, you will call your script multiple times. You can use udevadm monitor to monitor the events and you also can use a simple debug script to get a feeling about the handling:

#!/bin/bash
echo "got event in pid: $$" >> /tmp/udevdebug.log
env >> /tmp/udevdebug-$$.log

The script will add an entry to the file /tmp/udevdebug.log with the own PID and will output the full environment, which contains all relevant variables from udevd, to an logfile.