How to add a progress bar to a shell script?

后端 未结 30 2960
情歌与酒
情歌与酒 2020-11-22 05:48

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 b

30条回答
  •  面向向阳花
    2020-11-22 06:40

    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
    

提交回复
热议问题