How to add a progress bar to a shell script?

2018-12-31 14:30发布

When scripting in bash or any other shell in *NIX, while running a command that will take more than a few seconds, a progress bar is needed.

For example, copying a big file, opening a big tar file.

What ways do you recommend to add progress bars to shell scripts?

标签: bash shell zsh
30条回答
公子世无双
2楼-- · 2018-12-31 15:00

I did a pure shell version for an embedded system taking advantage of:

  • /usr/bin/dd's SIGUSR1 signal handling feature.

    Basically, if you send a 'kill SIGUSR1 $(pid_of_running_dd_process)', it'll output a summary of throughput speed and amount transferred.

  • backgrounding dd and then querying it regularly for updates, and generating hash ticks like old-school ftp clients used to.

  • Using /dev/stdout as the destination for non-stdout friendly programs like scp

The end result allows you to take any file transfer operation and get progress update that looks like old-school FTP 'hash' output where you'd just get a hash mark for every X bytes.

This is hardly production quality code, but you get the idea. I think it's cute.

For what it's worth, the actual byte-count might not be reflected correctly in the number of hashes - you may have one more or less depending on rounding issues. Don't use this as part of a test script, it's just eye-candy. And, yes, I'm aware this is terribly inefficient - it's a shell script and I make no apologies for it.

Examples with wget, scp and tftp provided at the end. It should work with anything that has emits data. Make sure to use /dev/stdout for programs that aren't stdout friendly.

#!/bin/sh
#
# Copyright (C) Nathan Ramella (nar+progress-script@remix.net) 2010 
# LGPLv2 license
# If you use this, send me an email to say thanks and let me know what your product
# is so I can tell all my friends I'm a big man on the internet!

progress_filter() {

        local START=$(date +"%s")
        local SIZE=1
        local DURATION=1
        local BLKSZ=51200
        local TMPFILE=/tmp/tmpfile
        local PROGRESS=/tmp/tftp.progress
        local BYTES_LAST_CYCLE=0
        local BYTES_THIS_CYCLE=0

        rm -f ${PROGRESS}

        dd bs=$BLKSZ of=${TMPFILE} 2>&1 \
                | grep --line-buffered -E '[[:digit:]]* bytes' \
                | awk '{ print $1 }' >> ${PROGRESS} &

        # Loop while the 'dd' exists. It would be 'more better' if we
        # actually looked for the specific child ID of the running 
        # process by identifying which child process it was. If someone
        # else is running dd, it will mess things up.

        # My PID handling is dumb, it assumes you only have one running dd on
        # the system, this should be fixed to just get the PID of the child
        # process from the shell.

        while [ $(pidof dd) -gt 1 ]; do

                # PROTIP: You can sleep partial seconds (at least on linux)
                sleep .5    

                # Force dd to update us on it's progress (which gets
                # redirected to $PROGRESS file.
                # 
                # dumb pid handling again
                pkill -USR1 dd

                local BYTES_THIS_CYCLE=$(tail -1 $PROGRESS)
                local XFER_BLKS=$(((BYTES_THIS_CYCLE-BYTES_LAST_CYCLE)/BLKSZ))

                # Don't print anything unless we've got 1 block or more.
                # This allows for stdin/stderr interactions to occur
                # without printing a hash erroneously.

                # Also makes it possible for you to background 'scp',
                # but still use the /dev/stdout trick _even_ if scp
                # (inevitably) asks for a password. 
                #
                # Fancy!

                if [ $XFER_BLKS -gt 0 ]; then
                        printf "#%0.s" $(seq 0 $XFER_BLKS)
                        BYTES_LAST_CYCLE=$BYTES_THIS_CYCLE
                fi
        done

        local SIZE=$(stat -c"%s" $TMPFILE)
        local NOW=$(date +"%s")

        if [ $NOW -eq 0 ]; then
                NOW=1
        fi

        local DURATION=$(($NOW-$START))
        local BYTES_PER_SECOND=$(( SIZE / DURATION ))
        local KBPS=$((SIZE/DURATION/1024))
        local MD5=$(md5sum $TMPFILE | awk '{ print $1 }')

        # This function prints out ugly stuff suitable for eval() 
        # rather than a pretty string. This makes it a bit more 
        # flexible if you have a custom format (or dare I say, locale?)

        printf "\nDURATION=%d\nBYTES=%d\nKBPS=%f\nMD5=%s\n" \
            $DURATION \
            $SIZE \
            $KBPS \
            $MD5
}

Examples:

echo "wget"
wget -q -O /dev/stdout http://www.blah.com/somefile.zip | progress_filter

echo "tftp"
tftp -l /dev/stdout -g -r something/firmware.bin 192.168.1.1 | progress_filter

echo "scp"
scp user@192.168.1.1:~/myfile.tar /dev/stdout | progress_filter
查看更多
后来的你喜欢了谁
3楼-- · 2018-12-31 15:01

This is only applicable using gnome zenity. Zenity provides a great native interface to bash scripts: https://help.gnome.org/users/zenity/stable/

From Zenity Progress Bar Example:

#!/bin/sh
(
echo "10" ; sleep 1
echo "# Updating mail logs" ; sleep 1
echo "20" ; sleep 1
echo "# Resetting cron jobs" ; sleep 1
echo "50" ; sleep 1
echo "This line will just be ignored" ; sleep 1
echo "75" ; sleep 1
echo "# Rebooting system" ; sleep 1
echo "100" ; sleep 1
) |
zenity --progress \
  --title="Update System Logs" \
  --text="Scanning mail logs..." \
  --percentage=0

if [ "$?" = -1 ] ; then
        zenity --error \
          --text="Update canceled."
fi
查看更多
怪性笑人.
4楼-- · 2018-12-31 15:02

A simpler method that works on my system using the pipeview ( pv ) utility.

srcdir=$1
outfile=$2


tar -Ocf - $srcdir | pv -i 1 -w 50 -berps `du -bs $srcdir | awk '{print $1}'` | 7za a -si $outfile
查看更多
素衣白纱
5楼-- · 2018-12-31 15:02

I would also like to contribute my own progress bar

It achieves sub-character precision by using Half unicode blocks

enter image description here

Code is included

查看更多
临风纵饮
6楼-- · 2018-12-31 15:04

Some posts have showed how to display the command's progress. In order to calculate it, you'll need to see how much you've progressed. On BSD systems some commands, such as dd(1), accept a SIGINFO signal, and will report their progress. On Linux systems some commands will respond similarly to SIGUSR1. If this facility is available, you can pipe your input through dd to monitor the number of bytes processed.

Alternatively, you can use lsof to obtain the offset of the file's read pointer, and thereby calculate the progress. I've written a command, named pmonitor, that displays the progress of processing a specified process or file. With it you can do things, such as the following.

$ pmonitor -c gzip
/home/dds/data/mysql-2015-04-01.sql.gz 58.06%

An earlier version of Linux and FreeBSD shell scripts appears on my blog.

查看更多
只若初见
7楼-- · 2018-12-31 15:05

use the linux command pv:

http://linux.die.net/man/1/pv

it doesn't know the size if it's in the middle of the stream, but it gives a speed and total and from there you can figure out how long it should take and get feedback so you know it hasn't hung.

查看更多
登录 后发表回答