How to add a progress bar to a shell script?

后端 未结 30 2274
情歌与酒
情歌与酒 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:29

    To indicate progress of activity, try the following commands:

    while true; do sleep 0.25 && echo -ne "\r\\" && sleep 0.25 && echo -ne "\r|" && sleep 0.25 && echo -ne "\r/" && sleep 0.25 && echo -ne "\r-"; done;
    

    OR

    while true; do sleep 0.25 && echo -ne "\rActivity: \\" && sleep 0.25 && echo -ne "\rActivity: |" && sleep 0.25 && echo -ne "\rActivity: /" && sleep 0.25 && echo -ne "\rActivity: -"; done;
    

    OR

    while true; do sleep 0.25 && echo -ne "\r" && sleep 0.25 && echo -ne "\r>" && sleep 0.25 && echo -ne "\r>>" && sleep 0.25 && echo -ne "\r>>>"; sleep 0.25 && echo -ne "\r>>>>"; done;
    

    OR

    while true; do sleep .25 && echo -ne "\r:Active:" && sleep .25 && echo -ne "\r:aCtive:" && sleep .25 && echo -ne "\r:acTive:" && sleep .25 && echo -ne "\r:actIve:" && sleep .25 && echo -ne "\r:actiVe:" && sleep .25 && echo -ne "\r:activE:"; done;
    

    One can use flags/variables inside the while loop to check and display the value/extent of progress.

    0 讨论(0)
  • 2020-11-22 06:30

    Haven't seen anything similar and all custom functions here seem to focus on rendering alone so... my very simple POSIX compliant solution below with step by step explanations because this question isn't trivial.

    TL;DR

    Rendering the progress bar is very easy. Estimating how much of it should render is a different matter. This is how to render (animate) the progress bar - you can copy&paste this example to a file and run it:

    #!/bin/sh
    
    BAR='####################'   # this is full bar, e.g. 20 chars
    
    for i in {1..20}; do
        echo -ne "\r${BAR:0:$i}" # print $i chars of $BAR from 0 position
        sleep .1                 # wait 100ms between "frames"
    done
    
    • {1..20} - values from 1 to 20
    • echo -n - print without new line at the end
    • echo -e - interpret special characters while printing
    • "\r" - carriage return, a special char to return to the beginning of the line

    You can make it render any content at any speed so this method is very universal, e.g. often used for visualization of "hacking" in silly movies, no kidding.

    Full answer

    The meat of the problem is how to determine the $i value, i.e. how much of the progress bar to display. In the above example I just let it increment in for loop to illustrate the principle but a real life application would use an infinite loop and calculate the $i variable on each iteration. To make said calculation it needs the following ingredients:

    1. how much work there is to be done
    2. how much work has been done so far

    In case of cp it needs the size of a source file and the size of the target file:

    #!/bin/sh
    
    $src=/path/to/source/file
    $tgt=/path/to/target/file
    
    cp "$src" "$tgt" &                     # the & forks the `cp` process so the rest
                                           # of the code runs without waiting (async)
    
    BAR='####################'
    
    src_size=$(stat -c%s "$src")           # how much there is to do
    
    while true; do
        tgt_size=$(stat -c%s "$tgt")       # how much has been done so far
        i=$(( $tgt_size * 20 / $src_size ))
        echo -ne "\r${BAR:0:$i}"
        if [ $tgt_size == $src_size ]; then
            echo ""                        # add a new line at the end
            break;                         # break the loop
        fi
        sleep .1
    done
    
    • stat - check file stats
    • -c - return formatted value
    • %s - total size

    In case of operations like file unpacking, calculating the source size is slightly more difficult but still as easy as getting the size of an uncompressed file:

    #!/bin/sh
    src_size=$(gzip -l "$src" | tail -n1 | tr -s ' ' | cut -d' ' -f3)
    
    • gzip -l - display info about zip archive
    • tail -n1 - work with 1 line from the bottom
    • tr -s ' ' - translate multiple spaces to one (squeeze them)
    • cut -d' ' -f3 - cut 3rd space-delimited column

    Here's the meat of the problem, though. This solution is less and less general. All calculations of the actual progress are tightly bound to the domain you're trying to visualize, is it a single file operation, a timer countdown, a rising number of files in a directory, operation on multiple files, etc., therefore, it can't be reused. The only reusable part is progress bar rendering. To reuse it you need to abstract it and save in a file (e.g. /usr/lib/progress_bar.sh), then define functions that calculate input values specific to your domain. This is how a generalized code could look like (I also made the $BAR dynamic because people were asking for it, the rest should be clear by now):

    #!/bin/sh
    
    BAR_length=50
    BAR_character='#'
    BAR=$(printf "%${BAR_length}s" | tr ' ' $BAR_character)
    
    work_todo=$(get_work_todo)             # how much there is to do
    
    while true; do
        work_done=$(get_work_done)         # how much has been done so far
        i=$(( $work_done * $BAR_length / $work_todo ))
        echo -ne "\r${BAR:0:$i}"
        if [ $work_done == $work_todo ]; then
            echo ""
            break;
        fi
        sleep .1
    done
    
    • printf - a builtin for printing stuff in a given format
    • printf '%50s' - print nothing, pad it with 50 spaces
    • tr ' ' '#' - translate every space to hash sign

    And this is how you'd use it:

    #!/bin/sh
    
    src=/path/to/source/file
    tgt=/path/to/target/file
    
    function get_work_todo() {
        echo $(stat -c%s "$src")
    }
    
    function get_work_done() {
        [ -e "$tgt" ] &&                   # if target file exists
            echo $(stat -c%s "$tgt") ||    # echo its size, else
            echo 0                         # echo zero
    }
    
    cp "$src" "$tgt" &                     # copy in the background
    
    source /usr/lib/progress_bar.sh        # execute the progress bar
    

    Obviously it can be wrapped in a function, rewritten to work with piped streams, rewritten to other language, whatever's your poison.

    0 讨论(0)
  • 2020-11-22 06:31

    Most unix commands will not give you the sort of direct feedback from which you can do this. Some will give you output on stdout or stderr that you can use.

    For something like tar you could use the -v switch and pipe the output to a program that updates a small animation for each line it reads. As tar writes out a list of files it's unravelled the program can update the animation. To do a percent complete you would have to know the number of files and count the lines.

    cp doesn't give this sort of output as far as I know. To monitor the progress of cp you would have to monitor the source and destination files and watch the size of the destination. You could write a small c program using the stat (2) system call to get the file size. This would read the size of the source then poll the destination file and update a % complete bar based on the size of the file written to date.

    0 讨论(0)
  • 2020-11-22 06:32

    I was looking for something more sexy than the selected answer, so did my own script.

    Preview

    Source

    I put it on github progress-bar.sh

    progress-bar() {
      local duration=${1}
    
    
        already_done() { for ((done=0; done<$elapsed; done++)); do printf "▇"; done }
        remaining() { for ((remain=$elapsed; remain<$duration; remain++)); do printf " "; done }
        percentage() { printf "| %s%%" $(( (($elapsed)*100)/($duration)*100/100 )); }
        clean_line() { printf "\r"; }
    
      for (( elapsed=1; elapsed<=$duration; elapsed++ )); do
          already_done; remaining; percentage
          sleep 1
          clean_line
      done
      clean_line
    }
    

    Usage

     progress-bar 100
    
    0 讨论(0)
  • 2020-11-22 06:32

    First of all bar is not the only one pipe progress meter. The other (maybe even more known) is pv (pipe viewer).

    Secondly bar and pv can be used for example like this:

    $ bar file1 | wc -l 
    $ pv file1 | wc -l
    

    or even:

    $ tail -n 100 file1 | bar | wc -l
    $ tail -n 100 file1 | pv | wc -l
    

    one useful trick if you want to make use of bar and pv in commands that are working with files given in arguments, like e.g. copy file1 file2, is to use process substitution:

    $ copy <(bar file1) file2
    $ copy <(pv file1) file2
    

    Process substitution is a bash magic thing that creates temporary fifo pipe files /dev/fd/ and connect stdout from runned process (inside parenthesis) through this pipe and copy sees it just like an ordinary file (with one exception, it can only read it forwards).

    Update:

    bar command itself allows also for copying. After man bar:

    bar --in-file /dev/rmt/1cbn --out-file \
         tape-restore.tar --size 2.4g --buffer-size 64k
    

    But process substitution is in my opinion more generic way to do it. An it uses cp program itself.

    0 讨论(0)
  • 2020-11-22 06:32

    https://github.com/extensionsapp/progre.sh

    Create 40 percent progress: progreSh 40

    0 讨论(0)
提交回复
热议问题