I want to find out what device my file is on so that I can use it in a script. I can get this far:
$ df . Filesystem 512-blocks Used Available Capacity Mounted on /dev/disk0s2 498438976 294369520 203557456 60% /
but this output feels too clumsy; is there a better way than parsing this to get the first ‘word’ of the second line?
What I really need is something like this so I can pipe it to the next command:
$ somecommand . /dev/disk0s2
How can I achieve this, preferably without resorting to string hacking the ‘df’ output?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
You can do it with the shell alone (works in bash, dash, ksh, zsh):
df . | (read a; read a b; echo "$a")
Or if output is not needed (result will be kept in $a) and your shell supports process substitution (like bash, zsh):
{ read; read a b;}< <(df .)
And here are some comparisons with the other solutions’ speed:
# pure shell solution 1
bash-4.2$ time for i in $(seq 500); do df . | (read a; read a b; echo "$a"); done > /dev/null
1.899
(dash) $ time -f '%e' dash -c 'for i in $(seq 500); do df . | (read a; read a b; echo "$a"); done > /dev/null'
1.05
(ksh) $ time for i in $(seq 500); do df . | (read a; read a b; echo "$a"); done > /dev/null
0m1.16s real 0m0.02s user 0m0.12s system
(zsh) manatwork% time (for i in $(seq 500); do df . | (read a; read a b; echo "$a"); done > /dev/null)
1.51s
# pure shell solution 2
bash-4.2$ time for i in $(seq 500); do { read; read a b;}< <(df .); done
1.192
(zsh) manatwork% time (for i in $(seq 500); do { read; read a b;}< <(df .); done)
3.51s
# other solutions
bash-4.2$ time for i in $(seq 500); do df . | tail -1 | cut -f 1 -d " "; done > /dev/null
1.405
bash-4.2$ time for i in $(seq 500); do df . | sed '2!d' | awk '{print $1}'; done > /dev/null
5.407
bash-4.2$ time for i in $(seq 500); do df . | sed -n '2{s/ .*$//;p}'; done > /dev/null
1.767
bash-4.2$ time for i in $(seq 500); do df . | sed '2!d' | awk '{print $1}'; done > /dev/null
3.334
bash-4.2$ time for i in $(seq 500); do df . | gawk 'NR==2{print $1}'; done > /dev/null
3.013
bash-4.2$ time for i in $(seq 500); do df . | mawk 'NR==2{print $1}'; done > /dev/null
1.747
bash-4.2$ time for i in $(seq 500); do df . | perl -nae 'print$F[0]if$.==2'; done > /dev/null
2.752
(Not compared with the stat solution as it not works here.)
Method 2
It’s the usual way on UNIX to concatenate the powers of simple programs that to just a little. Hence don’t worry to pipe the output of df through some filter.
df /path/to/file | sed -n '2{s/ .*$//;p}'
-n suppresses printing lines automatically, 2{} executes the enclosed commands on second line, s/ .*$// discards everything from the first space, p prints what’s left. Adding q after the p in cases when one parses longer input and just wants the second (or n-th) line could speed it up a bit too.
Method 3
You can use simple one-line with sed, awk as
df . | sed '2!d' | awk '{print $1}'
In sed, specifying 2d mean delete the 2nd line. Adding a ! negate this, so it just deletes all other lines, and prints the 2nd line. The awk command then displays the first column value.
Output:
/dev/disk0s2
Method 4
Parsing the output of df is the best you can do portably. Pass -P to df to avoid it formatting the output in a weird way (you’re probably safe everywhere since you’re grabbing the first field, but you do need -P to grab the mount point as it may be relegated to a subsequent line if preceding columns are too wide).
device_name=$(df -P . | awk 'NR==2 {print $1}')
Note that some systems allow device names to contain whitespace (IIRC that tends to happen on OSX). There’s no portable or convenient way to handle this case.
I don’t think there’s a better way to do this under Linux. stat can give you the device number (stat -c %t .), but if you want a device entry under /dev, you have to extract it from /proc, which df is better at doing.
Method 5
If you’re using linux, you can do it with findmnt (part of util-linux package) “without resorting to string hacking”:
findmnt -no source -T /path/to/file
/dev/sda1
When using the option
-T, --target path
if the path is not a mountpoint file or directory, findmnt checks path elements in reverse order to get the mountpoint. The other two options suppress the header line: -n, --noheading and select the column(s) to be listed: -o, --output
df from coreutils has a similar option --output= to print only certain fields, like source e.g.:
df --output=source /path/to/file
Filesystem /dev/sda1
there’s no option to remove the header though. If that is a problem you’ll have to do some minimal string hacking e.g. pipe it to sed 1d
Method 6
Note that none of the above solutions will work, if you are using ecryptfs.
However, the solution is simple: Just re-call the commands with the output as an argument, i.e.:
findmnt -no source -T "$(findmnt -no source -T /path/to/file)"df "$(df . | awk 'NR==2{print $1}')" | awk 'NR==2{print $1}'
Unfortunately df . | sed '2!d' | awk '{print $1}' will not work at all.
$ df "$(df . | sed '2!d' | awk '{print $1}')" | sed '2!d' | awk '{print $1}'
bash: !d': event not found
First, bash will try to interpret ! inside double-quotes as a history expansion (doesn’t matter that inside command substitution it’s inside single quotes), then escaping it …
$ df "$(df . | sed '2!d' | awk '{print $1}')" | sed '2!d' | awk '{print $1}'
sed: -e expression #1, char 2: unknown command: `'
… will make sed complain about it (because is inside single quotes inside the command substitution, so it’s literal and not replaced with simply !)
You could still manually do it, though.
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0