75

Given the size of a file in bytes, I want to format it with IEC (binary) prefixes to 3 significant figures with trailing zeros, e.g. 1883954 becomes 1.80M.

Floating-point arithmetic isn't supported in bash, so I used awk instead. The problem is I don't how to keep the trailing zeros. Current solution:

if [ $size -ge 1048576 ]
then
    size=$(awk 'BEGIN {printf "%.3g",'$size'/1048576}')M
elif [ $size -ge 1024 ]
then
    size=$(awk 'BEGIN {printf "%.3g",'$size'/1024}')K
fi

(The files aren't that big so I don't have to consider bigger units.)

Edit: There's another problem with this. See Adrian Frühwirth's comment below.

someguy
  • 7,144
  • 12
  • 43
  • 57

7 Answers7

147

Is there any reason you are not using

ls -lh

command ? If you are on a Linux system which has been released in the last few years, you have this functionality.

MelBurslan
  • 2,383
  • 4
  • 18
  • 26
  • 3
    Because it doesn't give it in the format I want. Like I said, to 3 significant figures with trailing zeros. The example I gave -- 1883954 -- would become 1.8M with ls, would it not? (It may differ with implementations but that's how it is with my system.) – someguy Apr 06 '13 at 18:38
  • 9
    Parsing the size from that is a *serious* mess. Don’t do that! – Evi1M4chine Dec 12 '13 at 00:16
  • This is the perfect answer. – xtheking Jan 09 '18 at 16:31
  • 1
    @xtheking: I understand it's the 'perfect answer' for a lot of people, but it doesn't actually answer my question. It also has its problems as Evi1M4achine mentioned. Admittedly, it's my fault for titling the question so broadly. I decided to leave it so that people can still find answers like this, though. – someguy Dec 25 '18 at 00:06
  • 1
    Per this answer (not OP question) `ls -lh ~/.bashrc | cut -d ' ' -f 5` >> `101k` – Victoria Stuart Feb 22 '22 at 20:21
95

GNU Coreutils contains an apparently rather unknown little tool called numfmt for numeric conversion, that does what you need:

$ numfmt --to=iec-i --suffix=B --format="%.3f" 4953205820
4.614GiB

I think that suits your needs well, and isn’t as large or hackish as the other answers.

If you want a more powerful solution, look at my other answer.

tedder42
  • 23,519
  • 13
  • 86
  • 102
Evi1M4chine
  • 6,992
  • 1
  • 24
  • 18
  • @someguy: The `numfmt` of `coreutils-8.22` tells me, that the changes you made are an in an invalid format. Hence there is no result for `1048575`. Using my original format, it works, and the result is `1.0MiB`. So I rolled your changes back. If you have a version that accepts your format, feel free to change it back again, and mention the version. – Evi1M4chine Feb 17 '14 at 00:59
  • @someguy: All I get with your format, is `numfmt: invalid format „%3g“, the directive must be in the form %['][-][N]f` (Manually translated from German by me.) It seems numfmt` does not support what you want, even though it should. :/ Maybe give the developers a hint at http://www.gnu.org/software/coreutils/. – Evi1M4chine Feb 17 '14 at 01:11
  • 1
    Sorry, I thought it followed the printf format. Could you change it so that the result is printed to three significant figures with trailing zeroes? Otherwise I can't accept it as the answer. – someguy Feb 23 '14 at 11:32
  • 2
    You can specify the precision since version 8.24. `numfmt --to=iec-i --suffix=B --format="%.3f" 4953205820` outputs 4.614GiB – pixelbeat Sep 05 '15 at 00:56
  • Nicely done. This is how to use if with file-path: `size=$(echo $(numfmt --to=iec-i --suffix=B --format="%.3f" $(stat --format=%s hosts.txt)) | sed -e "s/i//g");` (removing that weird `i`...). I'm using it in my [project](https://github.com/eladkarako/hosts.eladkarako.com). –  Jan 08 '16 at 02:40
  • 2
    @project: If you don't want the 'i' in the prefix, instead of the `--to=iec-i` option, use `--to=iec`. – someguy Oct 30 '17 at 12:39
  • 1
    is there a way to place a space between the number and the unit. e.g 5 KB instead of 5KB – Tim May 09 '19 at 02:31
  • 1
    @Tim: It doesn't look like there is, unfortunately. You could pipe the output to sed: `... | sed -r 's/([A-Z])/ \1/'`. – someguy Jul 27 '19 at 18:51
  • Similar to the @user257319 approach, combining this with @user208145's approach is easy: `numfmt --to=iec --format="%.0f" $(stat -c %s somefile.ext)`. This gives you 24K, 10M etc, without any extra `i` or `B` suffixes. – Per Lundberg Aug 02 '23 at 13:34
16
ls -lah /path/to/your/file | awk -F " " {'print $5'}
David J Merritt
  • 161
  • 1
  • 2
5

Instead of using ls and awk to get the file size, use stat -c %s filename.ext. It outputs only the number, with nothing else (at least on version 8.21). I can't use numfmt because it's an older version which doesn't appear to use the printf syntax with decimal precision. I instead use the script below. I use the last line to test if the script is being sourced. If it's not, I can call it directly on the command line.

#!/bin/bash

function getFriendlyFileSize() {
    OUT='/dev/null'
    [ "$#" == 0 ] && echo 'No number given' && return 1
    [ ! $(echo $1 | egrep -i '\-?[0-9]+') ] && echo 'Garbage data' && return 1

    if [ "$1" == '' -o "$1" -lt 0 ] 2>$OUT
    then
            echo '0 B'
            return 1
    else
            FSIZE=$1
    fi

    [ "$2" == '' ] && DECPTS=1 || DECPTS=$2

    KB=1024
    MB=1048576
    GB=1073741824
    TB=1099511627776
    PB=1125899906842624
    EB=1152921504606846976
    LM=9223372036854775807 # bash comparison limit = 2^63-1 (signed int?)

    [ "$FSIZE" -le 0 ] 2>$OUT && echo "0 B" && return
    [ "$FSIZE" -lt $KB ] 2>$OUT && echo "$FSIZE B" && return
    [ "$FSIZE" -lt $MB ] 2>$OUT && echo "$(echo "scale=$DECPTS;$FSIZE/$KB"|bc) KB" && return
    [ "$FSIZE" -lt $GB ] 2>$OUT && echo "$(echo "scale=$DECPTS;$FSIZE/$MB"|bc) MB" && return
    [ "$FSIZE" -lt $TB ] 2>$OUT && echo "$(echo "scale=$DECPTS;$FSIZE/$GB"|bc) GB" && return
    [ "$FSIZE" -lt $PB ] 2>$OUT && echo "$(echo "scale=$DECPTS;$FSIZE/$TB"|bc) TB" && return
    [ "$FSIZE" -lt $EB ] 2>$OUT && echo "$(echo "scale=$DECPTS;$FSIZE/$PB"|bc) PB" && return
    [ "$FSIZE" -le $LM ] 2>$OUT && echo "$(echo "scale=$DECPTS;$FSIZE/$EB"|bc) EB" && return
    [ "$?" -ne '0' ] 2>$OUT && echo "Bad input" && return 1
}

[[ $_ == $0 ]] && getFriendlyFileSize $1 $2
user208145
  • 369
  • 4
  • 13
3

I know that it's a little late. But may someone find it useful.

The answer is, simply, to use %.2f instead of %.3g in your script. (src)


Test:

#!/bin/bash

size=1883954

if [ $size -ge 1048576 ]
then
    size=$(awk 'BEGIN {printf "%.2f",'$size'/1048576}')M
elif [ $size -ge 1024 ]
then
    size=$(awk 'BEGIN {printf "%.2f",'$size'/1024}')K
fi

echo $size

The Output:

1.80M
Community
  • 1
  • 1
Nour-eddin
  • 31
  • 1
2

If you don't mind using bc then the following will help do floating point operations. scale can changed as per your needs depending on many digits you want to print.

size=1883954

if [ $size -ge 1048576 ]
then
    size=$(echo "scale=2;$size/1048576"| bc)M
elif [ $size -ge 1024 ]
then
    size=$(echo "scale=2;$size/1024" | bc)K
fi

echo $size
P.P
  • 117,907
  • 20
  • 175
  • 238
0

If you happen to have Qalculate! installed (which is awesome by the way), there’s an easy trick:

human_readable="$( qalc -t set "precision $precision" "${in_bytes}B" )"

Example:

$ qalc -t -set "precision 3" 5264334820B
5.26 GB

It’s a very very powerful tool to have in shell scripting, as it can even simplify formulas, solve for unknowns, and many many more things.

$ qalc -t "e^(i*x)=-1"
x = 3.1415927

If you want a simpler, less heavy-weight solution, look at my other answer.

Evi1M4chine
  • 6,992
  • 1
  • 24
  • 18