3

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?

antonyh
  • 145
  • 4
    Parsing the df output is not difficult: df . | tail -1 | cut -f 1 -d " " But maybe there are better solutions. – jofel Feb 22 '13 at 12:35
  • Nice, this works. I'm running it on 10'000s of files, so I will have to see what the performance is like VS just one pipe – antonyh Feb 22 '13 at 13:15
  • 2
    ... the other way is to use stat, which gives a device field, but you'll have to translate that back. May be much faster though, especially if df is taking forever to get usage over, e.g., NFS. – derobert Feb 22 '13 at 13:37
  • 2
    stat -f "%Sdf" . seems quicker - it decreased the time from 1.8s to 1.7 over 500 iterations. I have no network concerns, but this is a top tip, thanks. – antonyh Feb 22 '13 at 14:45

6 Answers6

4

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.

peterph
  • 30,838
  • This didn't work for me: sed: 1: "2{s/ .*$//;p}": extra characters at the end of p command – antonyh Feb 22 '13 at 13:14
  • 1
    Unix seds are picky on missing ;s. Try to add one after the p command. If still not works, specify what kind of sed are you using. – manatwork Feb 22 '13 at 13:25
  • BSD sed, as found on OSX 10.7 - df . | sed -n '2{s/ .*$//;p;}' - this works – antonyh Feb 22 '13 at 13:28
  • This seems to be faster than tail | cut and sed|awk. – jofel Feb 22 '13 at 13:34
  • Quite right, here's my timings;
    time for i in {1..500}; do df . | sed -n '2{s/ .*$//;p;}'; done  
    real 0m1.587s  
    user 0m0.472s  
    sys 0m1.180s  
    
    time for i in {1..500}; do df . | sed '2!d' | awk '{print $1}'; done  
    real 0m2.484s  
    user 0m0.722s  
    sys 0m1.833s  
    
    time for i in {1..500}; do df . | tail -1 | awk '{print $1}'; done  
    real 0m2.267s  
    user 0m0.672s  
    sys 0m1.701s  
    
    

    I just need to work out how to do this one more way then I'll be happy. awk?

    – antonyh Feb 22 '13 at 14:49
  • @jofel it should be - only one pipe (i.e. process spawning) + sed has quite small footprint even when compared to awk. – peterph Feb 22 '13 at 15:57
  • It is the usual UNIX way if there is no single command to do it. As @derobert commented you can just use stat -f "%Sdf". – Matteo Feb 22 '13 at 16:38
4

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.)

manatwork
  • 31,277
  • All good; I love that there's so many ways to do this, and it's interesting to see how the time changes between awk, mawk, and gawk. What a great reference for this, thanks for taking the time to benchmark them too! – antonyh Feb 23 '13 at 22:08
1

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
mtk
  • 27,530
  • 35
  • 94
  • 130
1

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.

  • On OSX the -P option forces 512-byte blocks and has nothing to do with formatting. stat -f %Sdr . gives me the device name without the /dev/ prefix so it's close but I don't want to make assumptions that the device isn't in a sub-directory. – antonyh Feb 23 '13 at 22:03
  • @antonyh -P is for POSIX compatibility. This includes both using 512-byte blocks and not splitting lines. The output of stat -f %Sdr is relative to /dev, you can safely use that on *BSD/OSX. – Gilles 'SO- stop being evil' Feb 23 '13 at 23:36
  • my apologies; this isn't mentioned in the man page, only the 512-byteness of the output for -P is in the documentation. – antonyh Feb 24 '13 at 23:42
0

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

don_crissti
  • 82,805
0

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.