A progress bar for Bash scripts

When copying large files in Bash shell scripts, it would be nice to have a progress bar displayed. Unfortunately the cp command does not have a progress bar option.

The following script shows how we can implement a copy function in Bash that displays a progress bar with ETA (Estimated Time to Arrival). It simulates the cp command by piping the stdin and stdout of cat, dd and cat in series. The dd command is encapsulated in a while loop so that it can perform the copying in chunks of 1MB and also to print the progress bar. Note that the progress bar is printed on stderr as stdin and stdout are used to transfer the bytes.

copy() { # src dst [width]
    srcsize=$(stat -c %s $1) || return $?
    dstsize=0
    width=${3:-25}
    mega=$(( 1024 * 1024 ))
    start=$(date +%s)
    cat $1 | (
    while [[ dstsize -lt srcsize ]]
    do
        dd bs=512 count=2048 2>/dev/null || return $?
        (( dstsize += $mega ))
        [[ dstsize -gt srcsize ]] && dstsize=$srcsize

        # print truncated filename
        name=$(basename $1 | cut -b -20)
        printf "\r%-20s " $name 1>&2

        # print percentage
        percent=$(( 100 * $dstsize / $srcsize ))
        printf "%3d%% [" $percent 1>&2

        # print progress bar
        bar=$(( $width * $dstsize / $srcsize ))
        for i in $(seq 1 $bar); do printf "=" 1>&2; done
        for i in $(seq $bar $(( $width-1 ))); do printf " " 1>&2; done

        # print size of file copied
        if [[ $dstsize -le 1024 ]]; then
            printf -v size "%d" $dstsize;
        elif [[ $dstsize -le $mega ]]; then
            printf -v size "%d kB" $(( $dstsize / 1024  ));
        else
            printf -v size "%d MB" $(( $dstsize / $mega ));
        fi
        printf "] %7s" "$size" 1>&2

        # print estimated time of arrival
        elapsed=$(( $(date +%s) - $start ))
        remain=$(( $srcsize - $dstsize ))
        eta=$(( ($elapsed * $remain) / $dstsize + 1))
        if [[ $remain == 0 ]]; then eta=0; fi
        etamin=$(( $eta / 60 ))
        etasec=$(( $eta % 60 ))
        if [[ $eta > 0 ]]; then etastr="ETA"; else etastr="   "; fi
        printf "   %02d:%02d $etastr" $etamin $etasec 1>&2
    done
    echo 1>&2
    ) | cat >$2
}

The inspiration for this code came from studying the progress bar implemented at http://www.theiling.de/projects/bar.html. The major differences of the above code from bar are in the progress bar format and the use of the extra cat command at the beginning. The above code is also less generic but that’s fine with me since it is simpler and shorter.

To use the above code, simply cut and paste it into the top of your shell script. Then wherever you have cp srcfile dstfile replace it with copy srcfile dstfile to see the progress bar. Note that the copy function is not a drop-in replacement for cp since it handles neither options nor wildcards.

Advertisements
Posted in hacks. Tags: , . 12 Comments »

12 Responses to “A progress bar for Bash scripts”

  1. Abdul Says:

    It would be helpful if you can send a simple usage example or explanation.

    Thanks,
    Abdul

  2. Rob Says:

    Thanks, a good function.

    To use this, paste it into your ~/.bashrc profile file then open a new terminal and do copy file1 file2.

  3. Bogdan Says:

    This script is very nice.
    The possibility to copy directories would be great and adding it would make this an extremely usefull script.
    Thanks for sharing it!

  4. alejandro Says:

    v-cool script, it worked very nice.

    thanks

  5. Daniel Says:

    What about using rsync –progress ?

  6. Marcel Says:

    Thank you, that was the code I was looking for!

    If your are coping more than a couple of Mb on a fast media (HD to HD) above progress bar slows copying down. Use a bigger blocksize (32K) and copy more data per update. The code below updates the screen only 100 times.

    ps: you can use /dev/stdout/ :
    copy bigfile /dev/stdout 50 | wc

    copy() { # src dst [width]
    srcsize=$(stat -c %s $1) || return $?
    dstsize=0
    width=${3:-25}
    mega=$(( 1024 * 1024 ))
    count=$(( $srcsize / 32768 / 100 + 1 ))
    step=$(( $count * 32768 ))
    start=$(date +%s)
    cat $1 | (
    while [[ dstsize -lt srcsize ]]
    do
    dd bs=32768 count=$count 2>/dev/null || return $?
    (( dstsize += $step ))
    [[ dstsize -gt srcsize ]] && dstsize=$srcsize

    # print truncated filename
    # print truncated filename
    name=$(basename $1 | cut -b -20)
    printf “\r%-20s ” $name 1>&2

    # print percentage
    percent=$(( 100 * $dstsize / $srcsize ))
    printf “%3d%% [” $percent 1>&2

    # print progress bar
    bar=$(( $width * $dstsize / $srcsize ))
    for i in $(seq 1 $bar); do printf “=” 1>&2; done
    for i in $(seq $bar $(( $width-1 ))); do printf ” ” 1>&2; done

    # print size of file copied
    if [[ $dstsize -le 1024 ]]; then
    printf -v size “%d” $dstsize;
    elif [[ $dstsize -le $mega ]]; then
    printf -v size “%d kB” $(( $dstsize / 1024 ));
    else
    printf -v size “%d MB” $(( $dstsize / $mega ));
    fi
    printf “] %7s” “$size” 1>&2

    # print estimated time of arrival
    elapsed=$(( $(date +%s) – $start ))
    remain=$(( $srcsize – $dstsize ))
    eta=$(( ($elapsed * $remain) / $dstsize + 1))
    if [[ $remain == 0 ]]; then eta=0; fi
    etamin=$(( $eta / 60 ))
    etasec=$(( $eta % 60 ))
    if [[ $eta > 0 ]]; then etastr=”ETA”; else etastr=” “; fi
    printf ” %02d:%02d $etastr” $etamin $etasec 1>&2
    done
    echo 1>&2
    ) | cat >$2
    }

  7. Arkansas Says:

    Very nice indeed! It would be nicer it it supported the standard cp switches like -a -b -d -f -r etc.

  8. Corey Moorehouse Says:

    Very good site, where did you come up with the knowledge in this write-up? I’m pleased I found it though, ill be checking back soon to see what other articles you have.

  9. Paulo Espinosa Says:

    super cool, is very helpful thanks a lot!!

  10. ysangkok Says:

    i recommend pv (pipe viewer): http://www.ivarch.com/programs/pv.shtml

    standard posix tool.

  11. Paolo Supino Says:

    Great progerss bar :-)
    a smallish modification I did is set the width of the bar to the maximum possible based on the width of the terminal… The modifications I did are:
    +screen_width=$(tput cols)
    -width=${3:-25}
    +width=$((screen_width – 50))

    the 50 characters is the number I came up with after a little trial and error…


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: