I made a mount.sh file inspired by balena-storage. It works when I login to the container via the balena.io dashboard where I'm deploying (could be the same elsewhere) and run the script manually. It hangs with unpopulated variables when the container runs the script when starting (a script that runs the script). I think it’s a permissions issue or script running script thing. I'm not sure how to proceed in reading USB device variables.
mount.sh:
# Automatically mount a USB drive by specified volume name.
# Note: make sure to have USB_VOLUME_NAME set in env vars.
# Thanks: https://github.com/balena-io-playground/balena-storage
echo "Checking for USB_VOLUME_NAME..."
echo "A"
if [[ -z $USB_VOLUME_NAME ]]; then
echo "Make sure to set environment variable USB_VOLUME_NAME in order to find a connected USB drive by that label and connect to it. Exiting..." >> /usr/src/app/mount.log
exit 1
fi
echo "B"
# Get device by label env var set in balena.io dashboard device env vars
USB_DEVICE=$(blkid -L $USB_VOLUME_NAME)
if [[ -z $USB_DEVICE ]]; then
echo "Invalid USB_DEVICE name: $USB_DEVICE" >> /usr/src/app/mount.log
exit 1
fi
echo $USB_DEVICE
echo "C"
# Get extra device info
ID_FS_TYPE=${ID_FS_TYPE:=$(/bin/udevadm info -n $USB_DEVICE | /usr/bin/awk -F "=" '/ID_FS_TYPE/{ print $2 }')}
ID_FS_UUID_ENC=${ID_FS_UUID_ENC:=$(/bin/udevadm info -n $USB_DEVICE | /usr/bin/awk -F "=" '/ID_FS_UUID_ENC/{ print $2 }')}
ID_FS_LABEL_ENC=${ID_FS_LABEL_ENC:=$(/bin/udevadm info -n $USB_DEVICE | /usr/bin/awk -F "=" '/ID_FS_LABEL_ENC/{ print $2 }')}
MOUNT_POINT=/mnt/$USB_VOLUME_NAME
echo $ID_FS_TYPE
echo $ID_FS_UUID_ENC
echo $ID_FS_LABEL_ENC
echo $MOUNT_POINT
echo "D"
# Bail if file system is not supported by the kernel
if ! /bin/grep -qw $ID_FS_TYPE /proc/filesystems; then
echo "File system not supported: $ID_FS_TYPE" >> /usr/src/app/mount.log
exit 1
fi
echo "E"
# Mount device
if /bin/findmnt -rno SOURCE,TARGET $USB_DEVICE >/dev/null; then
echo "Device $USB_DEVICE is already mounted!" >> /usr/src/mount.log
else
echo "Mounting - Source: $USB_DEVICE - Destination: $MOUNT_POINT" >> /usr/src/app/mount.log
/bin/mkdir -p $MOUNT_POINT
/bin/mount -t $ID_FS_TYPE -o rw $USB_DEVICE $MOUNT_POINT
fi
echo "F"
When the container runs the script, it gets stuck after "D", with ID_FS_TYPE, ID_FS_UUID_ENC and ID_FS_LABEL_ENC being empty (a good reason to hang).
output:
Checking for USB_VOLUME_NAME...
A
B
/dev/sda1
C
/mnt/MYDRIVE
D
My dockerfile.template:
FROM balenalib/%%BALENA_MACHINE_NAME%%-node
# Enable udev for detection of dynamically plugged devices
ENV UDEV=on
COPY udev/usb.rules /etc/udev/rules.d/usb.rules
# Install dependencies
RUN install_packages util-linux
WORKDIR /usr/src/app
# Move scripts used for mounting USB
COPY scripts scripts
RUN chmod +x scripts/*
# server.js will run when container starts up on the device
CMD ["/bin/bash", "/usr/src/app/scripts/start.sh"]
start.sh:
echo "Mounting USB drive..."
cd /usr/src/app/scripts
/bin/bash mount.sh
# It won't get this far while the script above hangs.
echo "Starting server..."
cd /usr/src/app
/usr/local/bin/yarn run serve
I can confirm that everything works when running from within the container manually:
cd /usr/src/app/scripts
/bin/bash mount.sh
Output:
Checking for USB_VOLUME_NAME...
A
B
/dev/sda1
C
vfat
BE23-31BA
MYDRIVE
/mnt/MYDRIVE
D
E
F
(and the drive mounted)
How would I resolve the empty variables?
Always quote every shell variable you use. (Unless you're absolutely sure of what you're doing, and what you expect to happen if the variable value is empty or includes spaces.)
Without quoting, when you
/bin/grep -qw $ID_FS_TYPE /proc/filesystems
and $ID_FS_TYPE is empty, that word just gets omitted from the command line, so you get
/bin/grep -qw /proc/filesystems
which uses /proc/filesystems as a regexp, and tries to grep over its stdin; this leads to the apparent hang you see.
If instead you quote it:
/bin/grep -qw "$ID_FS_TYPE" /proc/filesystems
it will get an empty string as the regexp parameter and a filename as the input parameter, which will trivially succeed (but not hang).
For similar reasons, I'd expect you'd get a shell syntax error if $USB_VOLUME_NAME is unset, and the whole script will act oddly if that variable name has a space in it.
I'm trying to add a conditional to my .zshrc file that will initialize some config stuff that I only want to happen if I'm in Windows Subsystem for Linux. I tried this but no luck:
if [ "$('cmd.exe /c "systeminfo" | grep "^OS Name"')" =~ "Windows" ]; then
echo "windows baby!"
fi
Which gives:
no such file or directory: cmd.exe /c "systeminfo" | grep "^OS Name"
...but that command works if I type it directly in the shell.
Any ideas?
Using uname -r does the trick
According to https://github.com/microsoft/WSL/issues/423#issuecomment-608236476, if you use
uname -r | sed -n 's/.*\( *Microsoft *\).*/\1/ip'
You'll get output as "Microsoft" in case it is WSL. Otherwise, you should get no output.
So you can use something like
if [ $(uname -r | sed -n 's/.*\( *Microsoft *\).*/\1/ip') ];
then
echo "This is Windows WSL baby!"
else
echo "Not Windows"
fi
I want to run a shell script when a specific file or directory changes.
How can I easily do that?
You may try entr tool to run arbitrary commands when files change. Example for files:
$ ls -d * | entr sh -c 'make && make test'
or:
$ ls *.css *.html | entr reload-browser Firefox
or print Changed! when file file.txt is saved:
$ echo file.txt | entr echo Changed!
For directories use -d, but you've to use it in the loop, e.g.:
while true; do find path/ | entr -d echo Changed; done
or:
while true; do ls path/* | entr -pd echo Changed; done
I use this script to run a build script on changes in a directory tree:
#!/bin/bash -eu
DIRECTORY_TO_OBSERVE="js" # might want to change this
function block_for_change {
inotifywait --recursive \
--event modify,move,create,delete \
$DIRECTORY_TO_OBSERVE
}
BUILD_SCRIPT=build.sh # might want to change this too
function build {
bash $BUILD_SCRIPT
}
build
while block_for_change; do
build
done
Uses inotify-tools. Check inotifywait man page for how to customize what triggers the build.
Use inotify-tools.
The linked Github page has a number of examples; here is one of them.
#!/bin/sh
cwd=$(pwd)
inotifywait -mr \
--timefmt '%d/%m/%y %H:%M' --format '%T %w %f' \
-e close_write /tmp/test |
while read -r date time dir file; do
changed_abs=${dir}${file}
changed_rel=${changed_abs#"$cwd"/}
rsync --progress --relative -vrae 'ssh -p 22' "$changed_rel" \
usernam#example.com:/backup/root/dir && \
echo "At ${time} on ${date}, file $changed_abs was backed up via rsync" >&2
done
How about this script? Uses the 'stat' command to get the access time of a file and runs a command whenever there is a change in the access time (whenever file is accessed).
#!/bin/bash
while true
do
ATIME=`stat -c %Z /path/to/the/file.txt`
if [[ "$ATIME" != "$LTIME" ]]
then
echo "RUN COMMNAD"
LTIME=$ATIME
fi
sleep 5
done
Check out the kernel filesystem monitor daemon
http://freshmeat.net/projects/kfsmd/
Here's a how-to:
http://www.linux.com/archive/feature/124903
As mentioned, inotify-tools is probably the best idea. However, if you're programming for fun, you can try and earn hacker XPs by judicious application of tail -f .
Just for debugging purposes, when I write a shell script and want it to run on save, I use this:
#!/bin/bash
file="$1" # Name of file
command="${*:2}" # Command to run on change (takes rest of line)
t1="$(ls --full-time $file | awk '{ print $7 }')" # Get latest save time
while true
do
t2="$(ls --full-time $file | awk '{ print $7 }')" # Compare to new save time
if [ "$t1" != "$t2" ];then t1="$t2"; $command; fi # If different, run command
sleep 0.5
done
Run it as
run_on_save.sh myfile.sh ./myfile.sh arg1 arg2 arg3
Edit: Above tested on Ubuntu 12.04, for Mac OS, change the ls lines to:
"$(ls -lT $file | awk '{ print $8 }')"
Add the following to ~/.bashrc:
function react() {
if [ -z "$1" -o -z "$2" ]; then
echo "Usage: react <[./]file-to-watch> <[./]action> <to> <take>"
elif ! [ -r "$1" ]; then
echo "Can't react to $1, permission denied"
else
TARGET="$1"; shift
ACTION="$#"
while sleep 1; do
ATIME=$(stat -c %Z "$TARGET")
if [[ "$ATIME" != "${LTIME:-}" ]]; then
LTIME=$ATIME
$ACTION
fi
done
fi
}
Quick solution for fish shell users who wanna track a single file:
while true
set old_hash $hash
set hash (md5sum file_to_watch)
if [ $hash != $old_hash ]
command_to_execute
end
sleep 1
end
replace md5sum with md5 if on macos.
Here's another option: http://fileschanged.sourceforge.net/
See especially "example 4", which "monitors a directory and archives any new or changed files".
inotifywait can satisfy you.
Here is a common sample for it:
inotifywait -m /path -e create -e moved_to -e close_write | # -m is --monitor, -e is --event
while read path action file; do
if [[ "$file" =~ .*rst$ ]]; then # if suffix is '.rst'
echo ${path}${file} ': '${action} # execute your command
echo 'make html'
make html
fi
done
Suppose you want to run rake test every time you modify any ruby file ("*.rb") in app/ and test/ directories.
Just get the most recent modified time of the watched files and check every second if that time has changed.
Script code
t_ref=0; while true; do t_curr=$(find app/ test/ -type f -name "*.rb" -printf "%T+\n" | sort -r | head -n1); if [ $t_ref != $t_curr ]; then t_ref=$t_curr; rake test; fi; sleep 1; done
Benefits
You can run any command or script when the file changes.
It works between any filesystem and virtual machines (shared folders on VirtualBox using Vagrant); so you can use a text editor on your Macbook and run the tests on Ubuntu (virtual box), for example.
Warning
The -printf option works well on Ubuntu, but do not work in MacOS.
I have a script I am trying to work out to scan my LAN and send me notification if there is a new MAC address that does not appear in my master list. I believe my variables may be messed up. This is what I have:
#!/bin/bash
LIST=$HOME/maclist.log
MASTERFILE=$HOME/master
FILEDIFF="$(diff $LIST $MASTERFILE)"
# backup the maclist first
if [ -f $LIST ]; then
cp $LIST maclist_`date +%Y%m%H%M`.log.bk
else
touch $LIST
fi
# this will scan the network and extract the IP and MAC address
nmap -n -sP 192.168.122.0/24 | awk '/^Nmap scan/{IP=$5};/^MAC/{print IP,$3};{next}' > $LIST
# this will use a diff command to compare the maclist created above and master list of known good devices on the LAN
if [ $FILEDIFF ] 2> /dev/null; then
echo
echo "---- All is well on `date` ----" >> macscan.log
echo
else
# echo -e "\nWARNING!!" | `mutt -e 'my_hdr From:user#email.com' -s "WARNIG!! NEW DEVICE ON THE LAN" -i maclist.log user#email.com`
echo "emailing you"
fi
When I execute this when the maclist.log does not exist I get this response:
diff: /root/maclist.log: No such file or directory
If I execute it again with the maclist.log file existing the file gets renamed from the cp line without any issue.
The line
FILEDIFF="$(diff $LIST $MASTERFILE)"
executes the diff when it is run (not when you use $FILELIST later). At that time the list file hasn't been created.
The easiest fix is just to put the diff command in full where $FILELIST is currently used.
A bunch of system variables are missing in Cygwin when using ssh. Some that I noticed include:
A proper PATH including the Visual Studio paths.
VS80COMNTOOLS
TEMP
TMP
PROCESSOR_ARCHITECTURE
PROCESSOR_IDENTIFIER
PROCESSOR_LEVEL
PROCESSOR_REVISION
FP_NO_HOST_CHECK
PSMODULEPATH
This impacts trying to compile source, such as ruby, via ssh. These variables exist when using rdesktop.
How do I get these variables to exist in Cygwin when using ssh?
I found a post describing how to work around this: http://smithii.com/node/44
Here is the bit 'o bash from that page:
if [ "$SSH_TTY" ]; then
pushd . >/dev/null
for __dir in \
/proc/registry/HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session\ Manager/Environment \
/proc/registry/HKEY_CURRENT_USER/Environment
do
cd "$__dir"
for __var in $(ls -1 | tr '[a-z]' '[A-Z]')
do
test -z "${!__var}" && export $__var="`cat $__var`" >/dev/null 2>&1
done
done
unset __dir
unset __var
popd >/dev/null
fi
edited: Moved the tr so it is only done once. It was painfully slow otherwise.
Looking at the 1 answer here, >=BASH-4.4 will throw warnings about:
/etc/profile: warning: command substitution: ignored null byte in input
To fix this, simply change the 'test' line from:
test -z "${!__var}" && export $__var="`cat $__var`" >/dev/null 2>&1
to:
test -z "${!__var}" && export $__var="`cat $__var | tr -d '\0'`" >/dev/null 2>&1