4

How would I return (output) the path to any USB flash memory stick(s) connected to the local computer using bash (Ubuntu and Linux Mint)?

Background:

I'm providing users with an automated backup script. (The actual backup software is already installed on their computer.)

The user's job is to plug in a USB flash memory stick and enter one command at the terminal (without any parameters, options or any other variable information).

I need a bash script that can find the path to the USB flash memory stick. If more than one such path is found, I will probably just abort and pop up a message to contact me. Rather than make a complicated script, it is easier for me to just tell them to make sure only one memory stick is plugged into the computer at the time they wish to perform a backup.

MountainX
  • 17,948
  • You need something that will run as soon as the USB stick is plugged in, or that the user can run? You might want to look at the udevadm tool... it can be used to monitor the system for udev events, and dumps a bunch of info that you could parse to figure out where the device was just plugged in, and what type of device it is. – rainbowgoblin Mar 12 '14 at 04:14
  • @rainbowgoblin - it doesn't have to run automatically and it doesn't need to be triggered by the USB stick being plugged in. I only need to find the path when I start my script from the terminal. – MountainX Mar 12 '14 at 04:19
  • @MountainX is the script being run from the USB drive? Or is it coming from elsewhere and it needs to mount the drive? – phemmer Mar 13 '14 at 03:50
  • @Patrick - The script is not being run from the USB drive. The script will reside on the main internal drive. Thanks – MountainX Mar 14 '14 at 04:57

4 Answers4

5

After plugging in a USB device you can tell what was installed by simply looking at this path:

$ ls -l /dev/disk/by-id/usb*

Example

$ ls -l /dev/disk/by-id/usb*
lrwxrwxrwx. 1 root root  9 Mar 12 01:01 /dev/disk/by-id/usb-JMTek_USBDrive-0:0 -> ../../sdb
lrwxrwxrwx. 1 root root 10 Mar 12 01:01 /dev/disk/by-id/usb-JMTek_USBDrive-0:0-part1 -> ../../sdb1

With the above information your script could simply look at those entries using something like readlink:

$ readlink -f /dev/disk/by-id/usb-JMTek_USBDrive-0:0*
/dev/sdb
/dev/sdb1

And then using the mount command walk backwards to find out what directory the device was automounted under:

$ mount | grep '/dev/sdb\b'
$ mount | grep '/dev/sdb1\b'
/dev/sdb1 on /run/media/saml/HOLA type vfat (rw,nosuid,nodev,relatime,uid=1000,gid=1000,fmask=0022,dmask=0077,codepage=437,iocharset=ascii,shortname=mixed,showexec,utf8,flush,errors=remount-ro,uhelper=udisks2)

This could be expanded to a one liner like this:

$ readlink -f /dev/disk/by-id/usb-JMTek_USBDrive-0:0* | \
    while read dev;do mount | grep "$dev\b" | awk '{print $3}';done
/run/media/saml/HOLA

Getting a device's ID

You can parse this out like so from the output from /dev/disk/by-id/usb*, like so:

$ ls /dev/disk/by-id/usb* | sed 's/.*usb-\(.*\)-[0-9]:.*/\1/'
JMTek_USBDrive
JMTek_USBDrive

Incidentally this information is a concatenation of the USB's manufacturer + product descriptions.

$ usb-devices
...
T:  Bus=02 Lev=02 Prnt=02 Port=01 Cnt=02 Dev#= 10 Spd=12  MxCh= 0
D:  Ver= 1.10 Cls=00(>ifc ) Sub=00 Prot=00 MxPS= 8 #Cfgs=  1
P:  Vendor=058f ProdID=9380 Rev=01.00
S:  Manufacturer=JMTek
S:  Product=USBDrive
C:  #Ifs= 1 Cfg#= 1 Atr=80 MxPwr=100mA
I:  If#= 0 Alt= 0 #EPs= 2 Cls=08(stor.) Sub=06 Prot=50 Driver=usb-storage
...

You can also access it this way once you've established which device (/dev/sd*) the USB device is using, through UDEV:

$ udevadm info --query=all --name=sdb | grep -E "MODEL=|VENDOR=|ID_SERIAL"
E: ID_MODEL=USBDrive
E: ID_SERIAL=JMTek_USBDrive-0:0
E: ID_VENDOR=JMTek
slm
  • 369,824
  • thanks (not only for this answer, but for all your answers!) Unfortunately, I couldn't quite get this to work. (For example, I would need to automate the picking up of the device name, i.e., the JMTek_USBDrive in your example.) I'm going to try again later today when I have more time. I am also going to try this solution: http://unix.stackexchange.com/a/60335/15010 – MountainX Mar 12 '14 at 13:25
  • Thanks for updating with the additional info. I'm still getting the error I was getting previously: readlink: extra operand \/dev/disk/by-id/usb-JMTek_USBDrive-0:0-part1'Upon that error it fails to provide this info:/dev/sdb1`. – MountainX Mar 13 '14 at 01:46
  • 1
    @MountainX - it looks like an issue with readlink on Ubuntu, it can only take 1 argument, so the you need to give it each path that matches the /dev/disk/by-id/usb* wildcard individually, vs. giving multiples to readlink. Try that and see if it gets rid of the extra operand msg. – slm Mar 13 '14 at 02:18
  • I guess my bash skills are too poor to get this sorted out on Ubuntu... no luck yet. – MountainX Mar 14 '14 at 03:02
2

You can write a script to go through /etc/mtab and look at mounted devices, then use udevadm to check whether they're USB devices. /etc/mtab includes both the name of the device in /dev and its mount point. So you could do something like:

IFS=$'\n'
for mtabline in `cat /etc/mtab`; do 
  device=`echo $mtabline | cut -f 1 -d ' '`
  udevline=`udevadm info -q path -n $device 2>&1 |grep usb` 
  if [ $? == 0 ] ; then
    devpath=`echo $mtabline | cut -f 2 -d ' '`
    echo "devpath: $devpath"
  fi
done

(You need to set IFS in your script so that mtab is read line by line, rather than "word" by "word").

  • Why don't you just use while read line; do ...;done < /etc/mtab? That way you don't need to fiddle with $IFS. – terdon Mar 12 '14 at 10:23
  • Ubuntu isn't giving me a proper filesystem path; instead, udevadm is returning something like /devices/pci0000:00/0000:00:1a.0/usb1/1-1/1-1.2/1-1.2:1.0/host11/target11:0:0/11:0:0:0/block/sdd/sdd1. I plan to try again later today when I have more time to test. I am also going to try this solution: http://unix.stackexchange.com/a/60335/15010 – MountainX Mar 12 '14 at 13:23
  • @MountainX Yes... the udevadm line was just to filter for USB devices. If it's a USB device, you'll see "usb" somewhere in the output, so grep will return that line and the value of $? (the return value of the last command) will be zero. You actually get the path from the devpath=... line (i.e., it's also extracted from the mtabline variable). Not very elegant, but it works for me. – rainbowgoblin Mar 13 '14 at 22:49
  • OK, it works for me too. So far yours is the only answer that has worked for me. But I'm still trying to understand and potentially tweak the other approaches before giving up on them. – MountainX Mar 14 '14 at 02:47
  • Glad it works. There are lots of ways to do this, I guess. Incidentally, I think my solution would be nicer if I'd used awk instead of cut (you could get both the device's location in /dev and mount point out of mtab at once, plus awk is just a classier way to do things). But I'm lousy at awk. – rainbowgoblin Mar 14 '14 at 05:32
  • @rainbowgoblin - I'm no good at awk either ;-) – MountainX Mar 14 '14 at 22:30
0

On my desktop with no usb flash devices present:

% lsblk -o tran,name,mountpoint
> TRAN   NAME   MOUNTPOINT
> sata   sda
>        ├─sda1 /esp
>        ├─sda2 /
>        ├─sda3 /mnt/sda3
>        ├─sda4
>        └─sda5
> sata   sdb
> sata   sdc
> sata   sr0

On my laptop with two:

% lsblk -o tran,name,mountpoint
> TRAN   NAME   MOUNTPOINT
> usb    sda
>        ├─sda1 /esp
>        └─sda2 /
> usb    sdb
>        └─sdb1 /home
> sata   sr0

That should be all you need.

So some others also told me that some debian family systems do not yet support the transport option. In that case, try this:

    ( set -- $(lsblk -ndo name,rm)
        while [ $# -ge 2 ] ; do {
            [ $2 -eq 1 ] && \
                udevadm info --query=all --name "/dev/$1"
        shift 2 
     } ; done )   

The above command first queries lsblk for a list of current (parent-only - so only sda and not sda1) block devices by name and a column for the removable flag. The output looks like:

sda 0
sdb 1
sdc 1

Only the devices flagged with 1 are removable.

So we set our ( subshell's ) positional parameters to the split content which makes two parameters per entry. while we have at least 2 positional parameters we[ test ] $2 for the 1 removable flag, && and if present we query udevadm the system's device manager for all information it has on our first positional parameter, or /dev/$1. Next shift our first 2 positional parameters away and start over with the next two.

udevadm provides a lot of info on devices you might be interested in, but if you also really just want to get straight to the point, you could add the following |pipe to udevadm's stdout:

    udevadm info --name "/dev/$1" |\
        grep -q ID_BUS=usb && echo "/dev/$1"

This much would provide you a list of only parent /dev/$DEV removable usb block devices currently present in the system in the format:

/dev/$DEV /dev/$ALSODEV

If at this point you were interested only in mount points of mounted partitions of the above provided parent links you could roll it all together with findmnt like this:

( set -- $(lsblk -ndo name,rm)
    while [ $# -ge 2 ] ; do {
        [ $2 -eq 1 ] &&
            udevadm info --query=all --name "/dev/$1" |\
            grep -q ID_BUS=usb &&
                 printf 'findmnt %s -no TARGET ;' "/dev/$1" /dev/"$1"[0-9]
        shift 2 ; } ; done ) |\ 
    . /dev/stdin

This should provide you a list like:

/removable/usb/device/mount/point/1
/ditto/2
/also/3
mikeserv
  • 58,310
  • You might want to add further details or an example to this, so that's it more obvious. – slm Mar 12 '14 at 15:38
  • Yeah, I guess I figured lsblk would be obvious enough, but if you insist. – mikeserv Mar 12 '14 at 16:11
  • Thanks, we like the A's to be more detailed than on most of the other SE sites if you haven't noticed 8-) – slm Mar 12 '14 at 16:35
  • 1
    Me too. Just got lazy. – mikeserv Mar 12 '14 at 16:55
  • I get lsblk: unknown column: tran,name,mountpoint on Ubuntu 12.04 – MountainX Mar 13 '14 at 00:56
  • Added some more that will likely resolve that problem. – mikeserv Mar 13 '14 at 04:34
  • I appreciate your step-by-step explanations and I think I followed all/most of that. But I think there is a missing { somewhere. Running it results in ./b.sh: line 4: syntax error near unexpected token \do' ./b.sh: line 4: ` while { [ $# -ge 2 ] ; do {'` – MountainX Mar 14 '14 at 02:59
  • Definitely missing. Sorry, man. I was really tired. – mikeserv Mar 14 '14 at 03:45
  • Actually, there was one extra, I think. – mikeserv Mar 14 '14 at 03:46
  • @mikeserv - I'm still getting a syntax error. I thought I could figure it out, but I guess not. Running in debug mode, I get `+ udevadm info --name /dev/sr0
    • grep -q ID_BUS=usb

    missing option`

    – MountainX Mar 14 '14 at 22:31
  • I guess my shell auto-completes udev queries or something... darn convenience. I think you need the "query=all" parameter I inserted above. – mikeserv Mar 15 '14 at 00:01
0

Here's the approach I'm using. It comes from:

https://unix.stackexchange.com/a/119782/15010
https://unix.stackexchange.com/a/60335/15010

export USBKEYS=($(
    for blk in $(lsblk -ndo name) ; do {
        udevadm info --query=all --name "/dev/$blk" |\
        grep -q ID_BUS=usb &&
            printf 'findmnt %s -no TARGET ;'\
                "/dev/$blk" /dev/"$blk"[0-9]
        } ; done 2>&- |. /dev/stdin 
))
echo "$USBKEYS"
export STICK
case ${#USBKEYS[@]} in
    0 ) echo "No USB stick found."; exit 0 ;;
    1 ) STICK=$USBKEYS; echo "STICK=$USBKEYS" ;;
    * ) NOTE: the code for this case is not included in the interest of brevity.
esac

This is also working for me. It mostly comes from https://unix.stackexchange.com/a/119260/15010

#!/bin/bash

while read mtabline
do
  device=`echo $mtabline | awk '{print $1}'`
  udevline=`udevadm info -q path -n $device 2>&1 |grep usb` 
  if [ $? == 0 ] ; then
    echo "$device"
  fi
done < /etc/mtab
MountainX
  • 17,948