Df -h results in a loop

Hello everyone,

I am doing a check of the disk space using df -h, I want to combine the result in break line; but the result after while/done is empty:

# df -h
Filesystem      Size  Used Avail Use% Mounted on
rootfs           20G   14G  4.6G  75% /
/dev/root        20G   14G  4.6G  75% /
devtmpfs        993M  4.0K  993M   1% /dev
none            199M  2.7M  196M   2% /run
none            5.0M     0  5.0M   0% /run/lock
none            994M   72K  994M   1% /run/shm
/dev/sda2       894G  840G  9.2G  99% /home

disk.sh

useSpPer=''

df -h | awk 'NR>2 { print $1 " " $5 }' | while read output;
do

#echo $output
res=$(echo $output | awk '{ print $2}')

useSpPer=${useSpPer}'-'$res

echo 'use '$useSpPer
echo 'res '$res

done
echo 'final '$useSpPer

final prints empty result

# sh disk.sh 
use -75%
res 75%
use -75%-1%
res 1%
use -75%-1%-2%
res 2%
use -75%-1%-2%-0%
res 0%
use -75%-1%-2%-0%-1%
res 1%
use -75%-1%-2%-0%-1%-99%
res 99%
final

I am expecting a final result like in the last 'use':

use -75%-1%-2%-0%-1%-99%

to be

final 75%-1%-2%-0%-1%-99%

Thanks in advance

Hi,
the effect you see is caused by the way you feed your loop. The pipe creates a subshell which has its own copies of the variables, the ones in the parent shell are not affected by actions in the subshell.
Process substitution is one way to fix this problem:

useSpPer=''

while read output; do
   res=$(echo $output | awk '{ print $2}')
   useSpPer=${useSpPer}'-'$res
   echo 'use '$useSpPer
   echo 'res '$res
done < <(df -h | awk 'NR>2 { print $1 " " $5 }')
echo 'final '$useSpPer
1 Like

That behaviour is due to the loop being executed in a subshell, and the variable's contents is not given back to the parent shell.

Try this pure shell:

df -h | { read REST; while read FS SZ AV US PC MT REST; do useSpPer=${useSpPer}'-'$PC;  echo 'use '$useSpPer; echo 'res '$PC;  done; echo final $useSpPer; }
use -28%
res 28%
use -28%-28%
res 28%
use -28%-28%-42%
res 42%
use -28%-28%-42%-3%
res 3%
final -28%-28%-42%-3%
1 Like

When you pipe something to a block, the block runs in a sub-shell.
Variables in the block are not copied back to the main shell.

A while loop is such a block.
In RudiC example the { } is the block.
In cero example the pipe is avoided; the <( ) needs bash or zsh.

1 Like

Thank you guys for your effort, but I found a little mistake in cero code:

This is the mistake

str.sh: line 8: syntax error near unexpected token `<'
str.sh: line 8: `done < <(df -h | awk 'NR>2 { print $1 " " $5 }')'

Only Bash, Zsh and Ksh93 supports process substitution , which is what the <(...) means. Since you are invoking it with sh , judging by this # sh disk.sh , I suggest you use the command grouping

Here's an example using your original post #1

df -h | awk 'NR>2 { print $1 " " $5 }' |
{
    while read output
    do
        res=$(echo $output | awk '{ print $2}')
        useSpPer=${useSpPer}'-'$res

        echo 'use '$useSpPer
        echo 'res '$res
    done
    echo 'final '$useSpPer
}
1 Like

To explain how this works, this doesn't avoid the subshell problem, as much as put the entire relevant program inside the subshell so nothing loses context.

1 Like

I did not get this part of code

read REST; while read FS SZ AV US PC MT REST

The read REST; takes care of disregarding the header (first line or unwanted information):

Filesystem      Size  Used Avail Use% Mounted on

while read FS SZ AV US PC MT REST takes care of dividing the disk information for the mount point.

so these ones are FS (filesystem) SZ (size) AV (avalaible) US (used) PC (????) MT (mounted)

From the heading you used in post #1 in this thread:

Filesystem      Size  Used Avail Use% Mounted on

It looks to me like the reads should be changed from:

 read REST; while read FS SZ AV US PC MT REST

to:

 read REST; while read FS SZ US AV PC REST

where $US will expand to the data in the "Used" field and $PC will expand to the data in the "Use%" (i.e., used percent) field. Note that the standards don't specify the output format used in df -h output (although -h is a common option; it is an extension not required in the standards); on my system the heading used with it is:

Filesystem          Size   Used  Avail Capacity  iused     ifree %iused  Mounted on

where "Capacity" corresponds to the "Use%" heading you listed and in either case "$REST" will expand to the contents of the remaining fields (whether that is just the "Mount on" (directory on which the filesystem is mounted) as on your system, or the "iused" (number of i-nodes used), "ifree" (number of i-nodes free), "%iused" (percentage of i-nodes used), and "Mounted on" (directory on which the filesystem is mounted) fields produced by OS X df -h . If anyone else is trying to use code similar to yours, they need to adapt the arguments passed to read to assign needed fields to variables with names corresponding to the output format produced by df -h , df , or df -P (the last one being the only one with the output format specified by the standards) on their system for fields that they care about.

But, of course, since the code RudiC suggested doesn't use $US or $AV the order in which those are specified in the read command doesn't really matter in the way the script functions; the only thing that is crucial is the number of fields specified before PC (to get the percentage data needed by your script) and having at least one variable listed after PC so the remaining data on each line is not mixed in with the desired data when $PC is expanded.

1 Like

So these letters (FS SZ US AV PC REST) are standardised !!!

In the script:

df -h | {
	read REST
	while read FS SZ AV US PC MT REST
	do	useSpPer=${useSpPer}'-'$PC
		echo 'use '$useSpPer
		echo 'res '$PC
	done
	echo final $useSpPer
}

there is nothing standardized about the chosen variable names RudiC used. The only thing that matters is that the position in the list of values printed by the command df -h corresponds to the variable names used later in the script. In this specific script with the output produced by dh -h on your system, the following script would produce identical output:

df -h | {
	read junk
	while read junk junk junk junk needed junk
	do	useSpPer=${useSpPer}'-'$needed
		echo 'use '$useSpPer
		echo 'res '$needed
	done
	echo final $useSpPer
}

If there is a possibility that useSpPer might be an exported variable inherited into the environment of these scripts, it would be safer to define an initial value before the loop as in:

df -h | {
	read junk
	useSpPer=''
	while read junk junk junk junk needed junk
	do	useSpPer=${useSpPer}'-'$needed
		echo 'use '$useSpPer
		echo 'res '$needed
	done
	echo final $useSpPer
}
1 Like

I used a predefined variable before the loop, thanks everybody, I will try to develop my code of check disk space by sending an email to the admin, if any difficulties, I will get back to you

You may find additional inspiration in the many many threads in these forums dealing with df and mailing its results.

1 Like