How to add a progress bar to a shell script?

Viewed 456391

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?

42 Answers

You can implement this by overwriting a line. Use \r to go back to the beginning of the line without writing \n to the terminal.

Write \n when you're done to advance the line.

Use echo -ne to:

  1. not print \n and
  2. to recognize escape sequences like \r.

Here's a demo:

echo -ne '#####                     (33%)\r'
sleep 1
echo -ne '#############             (66%)\r'
sleep 1
echo -ne '#######################   (100%)\r'
echo -ne '\n'

In a comment below, puk mentions this "fails" if you start with a long line and then want to write a short line: In this case, you'll need to overwrite the length of the long line (e.g., with spaces).

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 ("Monitor Process Progress on Unix").

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

Preview

progress-bar.sh in action

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

APT style progress bar (Does not break normal output)

enter image description here

EDIT: For an updated version check my github page

I was not satisfied with the responses on this question. What I was personally looking for was a fancy progress bar as is seen by APT.

I had a look at the C source code for APT and decided to write my own equivalent for bash.

This progress bar will stay nicely at the bottom of the terminal and will not interfere with any output sent to the terminal.

Please do note that the bar is currently fixed at 100 characters wide. If you want scale it to the size of the terminal, this is fairly easy to accomplish as well (The updated version on my github page handles this well).

I will post my script here. Usage example:

source ./progress_bar.sh
echo "This is some output"
setup_scroll_area
sleep 1
echo "This is some output 2"
draw_progress_bar 10
sleep 1
echo "This is some output 3"
draw_progress_bar 50
sleep 1
echo "This is some output 4"
draw_progress_bar 90
sleep 1
echo "This is some output 5"
destroy_scroll_area

The script (I strongly recommend the version on my github instead):

#!/bin/bash

# This code was inspired by the open source C code of the APT progress bar
# http://bazaar.launchpad.net/~ubuntu-branches/ubuntu/trusty/apt/trusty/view/head:/apt-pkg/install-progress.cc#L233

#
# Usage:
# Source this script
# setup_scroll_area
# draw_progress_bar 10
# draw_progress_bar 90
# destroy_scroll_area
#


CODE_SAVE_CURSOR="\033[s"
CODE_RESTORE_CURSOR="\033[u"
CODE_CURSOR_IN_SCROLL_AREA="\033[1A"
COLOR_FG="\e[30m"
COLOR_BG="\e[42m"
RESTORE_FG="\e[39m"
RESTORE_BG="\e[49m"

function setup_scroll_area() {
    lines=$(tput lines)
    let lines=$lines-1
    # Scroll down a bit to avoid visual glitch when the screen area shrinks by one row
    echo -en "\n"

    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"
    # Set scroll region (this will place the cursor in the top left)
    echo -en "\033[0;${lines}r"

    # Restore cursor but ensure its inside the scrolling area
    echo -en "$CODE_RESTORE_CURSOR"
    echo -en "$CODE_CURSOR_IN_SCROLL_AREA"

    # Start empty progress bar
    draw_progress_bar 0
}

function destroy_scroll_area() {
    lines=$(tput lines)
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"
    # Set scroll region (this will place the cursor in the top left)
    echo -en "\033[0;${lines}r"

    # Restore cursor but ensure its inside the scrolling area
    echo -en "$CODE_RESTORE_CURSOR"
    echo -en "$CODE_CURSOR_IN_SCROLL_AREA"

    # We are done so clear the scroll bar
    clear_progress_bar

    # Scroll down a bit to avoid visual glitch when the screen area grows by one row
    echo -en "\n\n"
}

function draw_progress_bar() {
    percentage=$1
    lines=$(tput lines)
    let lines=$lines
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"

    # Move cursor position to last row
    echo -en "\033[${lines};0f"

    # Clear progress bar
    tput el

    # Draw progress bar
    print_bar_text $percentage

    # Restore cursor position
    echo -en "$CODE_RESTORE_CURSOR"
}

function clear_progress_bar() {
    lines=$(tput lines)
    let lines=$lines
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"

    # Move cursor position to last row
    echo -en "\033[${lines};0f"

    # clear progress bar
    tput el

    # Restore cursor position
    echo -en "$CODE_RESTORE_CURSOR"
}

function print_bar_text() {
    local percentage=$1

    # Prepare progress bar
    let remainder=100-$percentage
    progress_bar=$(echo -ne "["; echo -en "${COLOR_FG}${COLOR_BG}"; printf_new "#" $percentage; echo -en "${RESTORE_FG}${RESTORE_BG}"; printf_new "." $remainder; echo -ne "]");

    # Print progress bar
    if [ $1 -gt 99 ]
    then
        echo -ne "${progress_bar}"
    else
        echo -ne "${progress_bar}"
    fi
}

printf_new() {
    str=$1
    num=$2
    v=$(printf "%-${num}s" "$str")
    echo -ne "${v// /$str}"
}

Here is how it might look

Uploading a file

[##################################################] 100% (137921 / 137921 bytes)

Waiting for a job to complete

[#########################                         ] 50% (15 / 30 seconds)

Simple function that implements it

You can just copy-paste it to your script. It does not require anything else to work.

PROGRESS_BAR_WIDTH=50  # progress bar length in characters

draw_progress_bar() {
  # Arguments: current value, max value, unit of measurement (optional)
  local __value=$1
  local __max=$2
  local __unit=${3:-""}  # if unit is not supplied, do not display it

  # Calculate percentage
  if (( $__max < 1 )); then __max=1; fi  # anti zero division protection
  local __percentage=$(( 100 - ($__max*100 - $__value*100) / $__max ))

  # Rescale the bar according to the progress bar width
  local __num_bar=$(( $__percentage * $PROGRESS_BAR_WIDTH / 100 ))

  # Draw progress bar
  printf "["
  for b in $(seq 1 $__num_bar); do printf "#"; done
  for s in $(seq 1 $(( $PROGRESS_BAR_WIDTH - $__num_bar ))); do printf " "; done
  printf "] $__percentage%% ($__value / $__max $__unit)\r"
}

Usage example

Here, we upload a file and redraw the progress bar at each iteration. It does not matter what job is actually performed as long as we can get 2 values: max value and current value.

In the example below the max value is file_size and the current value is supplied by some function and is called uploaded_bytes.

# Uploading a file
file_size=137921

while true; do
  # Get current value of uploaded bytes
  uploaded_bytes=$(some_function_that_reports_progress)

  # Draw a progress bar
  draw_progress_bar $uploaded_bytes $file_size "bytes"

  # Check if we reached 100%
  if [ $uploaded_bytes == $file_size ]; then break; fi
  sleep 1  # Wait before redrawing
done
# Go to the newline at the end of upload
printf "\n"

Hires (floating point) progress bar

Preamble

Sorry for this not so short answer. In this answer I will use integer to render floating point, UTF-8 fonts for rendering progress bar more finely, and parallelise another task (sha1sum) in order to follow his progression, all of this with minimal resource footprint using pure and no forks.

For impatiens: Please test code (copy/paste in a new terminal window) at Now do it! (in the middle), with

  • either: Last animated demo (near end of this.),
  • either Practical sample (at end).

All demos here use read -t <float seconds> && break instead of sleep. So all loop could be nicely stopped by hitting Return key.

Introduction

Yet Another Bash Progress Bar...

As there is already a lot of answer here, I want to add some hints about performances and precision.

1. Avoid forks!

Because a progress bar are intented to run while other process are working, this must be a nice process...

So avoid using forks when not needed. Sample: instead of

mysmiley=$(printf '%b' \\U1F60E)

Use

printf -v mysmiley '%b' \\U1F60E

Explanation: When you run var=$(command), you initiate a new process to execute command and send his output to variable $var once terminated. This is very resource expensive. Please compare:

TIMEFORMAT="%R"
time for ((i=2500;i--;)){ mysmiley=$(printf '%b' \\U1F60E);}
2.292
time for ((i=2500;i--;)){ printf -v mysmiley '%b' \\U1F60E;}
0.017
bc -l <<<'2.292/.017'
134.82352941176470588235

On my host, same work of assigning $mysmiley (just 2500 time), seem ~135x slower / more expensive by using fork than by using built-in printf -v.

Then

echo $mysmiley 

So your function have to not print (or output) anything. Your function have to attribute his answer to a variable.

2. Use integer as pseudo floating point

Here is a very small and quick function to compute percents from integers, with integer and answer a pseudo floating point number:

percent(){
    local p=00$(($1*100000/$2))
    printf -v "$3" %.2f ${p::-3}.${p: -3}
}

Usage:

# percent <integer to compare> <reference integer> <variable name>
percent 33333 50000 testvar
printf '%8s%%\n' "$testvar"
   66.67%

3. Hires console graphic using UTF-8: ▏ ▎ ▍ ▌ ▋ ▊ ▉ █

To render this characters using bash, you could:

printf -v chars '\\U258%X ' {15..8}
printf "$chars\\n"
▏ ▎ ▍ ▌ ▋ ▊ ▉ █ 

Then we have to use 8x string with as graphic width.

Now do it!

This function is named percentBar because it render a bar from argument submited in percents (floating):

percentBar ()  { 
    local prct totlen=$((8*$2)) lastchar barstring blankstring;
    printf -v prct %.2f "$1"
    ((prct=10#${prct/.}*totlen/10000, prct%8)) &&
        printf -v lastchar '\\U258%X' $(( 16 - prct%8 )) ||
            lastchar=''
    printf -v barstring '%*s' $((prct/8)) ''
    printf -v barstring '%b' "${barstring// /\\U2588}$lastchar"
    printf -v blankstring '%*s' $(((totlen-prct)/8)) ''
    printf -v "$3" '%s%s' "$barstring" "$blankstring"
}

Usage:

# percentBar <float percent> <int string width> <variable name>
percentBar 42.42 $COLUMNS bar1
echo "$bar1"
█████████████████████████████████▉                                              

To show little differences:

percentBar 42.24 $COLUMNS bar2
printf "%s\n" "$bar1" "$bar2"
█████████████████████████████████▉                                              
█████████████████████████████████▊                                              

With colors

As rendered variable is a fixed widht string, using color is easy:

percentBar 72.1 24 bar
printf 'Show this: \e[44;33;1m%s\e[0m at %s%%\n' "$bar" 72.1

Bar with color

Little animation:

for i in {0..10000..33} 10000;do i=0$i
    printf -v p %0.2f ${i::-2}.${i: -2}
    percentBar $p $((COLUMNS-9)) bar
    printf '\r|%s|%6.2f%%' "$bar" $p
    read -srt .002 _ && break    # console sleep avoiding fork
done

|███████████████████████████████████████████████████████████████████████|100.00%

clear; for i in {0..10000..33} 10000;do i=0$i
     printf -v p %0.2f ${i::-2}.${i: -2}
     percentBar $p $((COLUMNS-7)) bar
     printf '\r\e[47;30m%s\e[0m%6.2f%%' "$bar" $p
     read -srt .002 _ && break
done

PercentBar animation

Last animated demo

Another demo showing different sizes and colored output:

printf '\n\n\n\n\n\n\n\n\e[8A\e7'&&for i in {0..9999..99} 10000;do 
    o=1 i=0$i;printf -v p %0.2f ${i::-2}.${i: -2}
    for l in 1 2 3 5 8 13 20 40 $((COLUMNS-7));do
        percentBar $p $l bar$((o++));done
    [ "$p" = "100.00" ] && read -rst .8 _;printf \\e8
    printf '%s\e[48;5;23;38;5;41m%s\e[0m%6.2f%%%b' 'In 1 char width: ' \
        "$bar1" $p ,\\n 'with 2 chars: ' "$bar2" $p ,\\n 'or 3 chars: ' \
        "$bar3" $p ,\\n 'in 5 characters: ' "$bar4" $p ,\\n 'in 8 chars: ' \
        "$bar5" $p .\\n 'There are 13 chars: ' "$bar6" $p ,\\n '20 chars: '\
        "$bar7" $p ,\\n 'then 40 chars' "$bar8" $p \
        ', or full width:\n' '' "$bar9" $p ''
    ((10#$i)) || read -st .5 _; read -st .1 _ && break
done

Could produce something like this:

Last animation percentBar animation

Practical GNU/Linux sample: sha1sum with progress bar

Under linux, you could find a lot of usefull infos under /proc pseudo filesystem, so using previoulsy defined functions percentBar and percent, here is sha1progress:

percent(){ local p=00$(($1*100000/$2));printf -v "$3" %.2f ${p::-3}.${p: -3};}
sha1Progress() { 
    local -i totsize crtpos cols=$(tput cols) sha1in sha1pid
    local sha1res percent prctbar
    exec {sha1in}< <(exec sha1sum -b - <"$1")
    sha1pid=$!
    read -r totsize < <(stat -Lc %s "$1")
    while ! read -ru $sha1in -t .025 sha1res _; do
        read -r _ crtpos < /proc/$sha1pid/fdinfo/0
        percent $crtpos $totsize percent
        percentBar $percent $((cols-8)) prctbar
        printf '\r\e[44;38;5;25m%s\e[0m%6.2f%%' "$prctbar" $percent;

    done
    printf "\r%s  %s\e[K\n" $sha1res "$1"
}

Of course, 25 ms timeout mean approx 40 refresh per second. This could look overkill, but work fine on my host, and anyway, this can be tunned.

sha1Progress sample

Explanation:

  • exec {sha1in}< create a new file descriptor for the output of
  • <( ... ) forked task run in background
  • sha1sum -b - <"$1" ensuring input came from STDIN (fd/0)
  • while ! read -ru $sha1in -t .025 sha1res _ While no input read from subtask, in 25 ms...
  • /proc/$sha1pid/fdinfo/0 kernel variable showing information about file descriptor 0 (STDIN) of task $sha1pid

I needed a progress bar for iterating over the lines in a csv file. Was able to adapt cprn's code into something useful for me:

BAR='##############################'
FILL='------------------------------'
totalLines=$(wc -l $file | awk '{print $1}')  # num. lines in file
barLen=30

# --- iterate over lines in csv file ---
count=0
while IFS=, read -r _ col1 col2 col3; do
    # update progress bar
    count=$(($count + 1))
    percent=$((($count * 100 / $totalLines * 100) / 100))
    i=$(($percent * $barLen / 100))
    echo -ne "\r[${BAR:0:$i}${FILL:$i:barLen}] $count/$totalLines ($percent%)"

    # other stuff
    (...)
done <$file

Looks like this:

[##----------------------------] 17128/218210 (7%)

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.

I needed a progress bar that would fit in popup bubble message (notify-send) to represent TV volume level. Recently I've been writing a music player in python and the TV picture is turned off most of the time.

Sample output from terminal

test_progress_bar3.gif


Bash script

#!/bin/bash

# Show a progress bar at step number $1 (from 0 to 100)


function is_int() { test "$@" -eq "$@" 2> /dev/null; } 

# Parameter 1 must be integer
if ! is_int "$1" ; then
   echo "Not an integer: ${1}"
   exit 1
fi

# Parameter 1 must be >= 0 and <= 100
if [ "$1" -ge 0 ] && [ "$1" -le 100 ]  2>/dev/null
then
    :
else
    echo bad volume: ${1}
    exit 1
fi

# Main function designed for quickly copying to another program 
Main () {

    Bar=""                      # Progress Bar / Volume level
    Len=25                      # Length of Progress Bar / Volume level
    Div=4                       # Divisor into Volume for # of blocks
    Fill="▒"                    # Fill up to $Len
    Arr=( "▉" "▎" "▌" "▊" )     # UTF-8 left blocks: 7/8, 1/4, 1/2, 3/4

    FullBlock=$((${1} / Div))   # Number of full blocks
    PartBlock=$((${1} % Div))   # Size of partial block (array index)

    while [[ $FullBlock -gt 0 ]]; do
        Bar="$Bar${Arr[0]}"     # Add 1 full block into Progress Bar
        (( FullBlock-- ))       # Decrement full blocks counter
    done

    # If remainder zero no partial block, else append character from array
    if [[ $PartBlock -gt 0 ]]; then
        Bar="$Bar${Arr[$PartBlock]}"
    fi

    while [[ "${#Bar}" -lt "$Len" ]]; do
        Bar="$Bar$Fill"         # Pad Progress Bar with fill character
    done

    echo Volume: "$1 $Bar"
    exit 0                      # Remove this line when copying into program
} # Main

Main "$@"

Test bash script

Use this script to test the progress bar in the terminal.

#!/bin/bash

# test_progress_bar3

Main () {

    tput civis                              # Turn off cursor
    for ((i=0; i<=100; i++)); do
        CurrLevel=$(./progress_bar3 "$i")   # Generate progress bar 0 to 100
        echo -ne "$CurrLevel"\\r            # Reprint overtop same line
        sleep .04
    done
    echo -e \\n                             # Advance line to keep last progress
    echo "$0 Done"
    tput cnorm                              # Turn cursor back on
} # Main

Main "$@"

TL;DR

This section details how notify-send is used to quickly spam popup bubble messages to the desktop. This is required because volume level can change many times a second and the default bubble message behavior is for a message to stay on the desktop for many seconds.

Sample popup bubble message

tvpowered.gif

Popup bubble message bash code

From the script above the main function was copied to a new functioned called VolumeBar in an existing bash script called tvpowered. The exit 0 command in the copied main function was removed.

Here's how to call it and let Ubuntu's notify-send command know we will be spamming popup bubble message:

VolumeBar $CurrVolume
# Ask Ubuntu: https://askubuntu.com/a/871207/307523
notify-send --urgency=critical "tvpowered" \
    -h string:x-canonical-private-synchronous:volume \
    --icon=/usr/share/icons/gnome/48x48/devices/audio-speakers.png \
    "Volume: $CurrVolume $Bar"

This is the new line which tells notify-send to immediately replace last popup bubble:

-h string:x-canonical-private-synchronous:volume \

volume groups the popup bubble messages together and new messages in this group immediately replaces the previous. You can use anything instead of volume.

It may be achieved in a pretty simple way:

  • iterate from 0 to 100 with for loop
  • sleep every step for 25ms (0.25 second)
  • append to the $bar variable another = sign to make the progress bar wider
  • echo progress bar and percentage (\r cleans line and returns to the beginning of the line; -ne makes echo doesn't add newline at the end and parses \r special character)
function progress {
    bar=''
    for (( x=0; x <= 100; x++ )); do
        sleep 0.25
        bar="${bar}="
        echo -ne "$bar ${x}%\r"
    done
    echo -e "\n"
}
$ progress
> ========== 10% # here: after 2.5 seconds
$ progress
> ============================== 30% # here: after 7.5 seconds

COLORED PROGRESS BAR

function progress {
    bar=''
    for (( x=0; x <= 100; x++ )); do
        sleep 0.05
        bar="${bar} "

        echo -ne "\r"
        echo -ne "\e[43m$bar\e[0m"

        local left="$(( 100 - $x ))"
        printf " %${left}s"
        echo -n "${x}%"
    done
    echo -e "\n"
}

To make a progress bar colorful, you can use formatting escape sequence - here the progress bar is yellow: \e[43m, then we reset custom settings with \e[0m, otherwise it would affect further input even when the progress bar is done.

custom progress bar

Using suggestions listed above, I decided to implement my own progress bar.

#!/usr/bin/env bash

main() {
  for (( i = 0; i <= 100; i=$i + 1)); do
    progress_bar "$i"
    sleep 0.1;
  done
  progress_bar "done"
  exit 0
}

progress_bar() {
  if [ "$1" == "done" ]; then
    spinner="X"
    percent_done="100"
    progress_message="Done!"
    new_line="\n"
  else
    spinner='/-\|'
    percent_done="${1:-0}"
    progress_message="$percent_done %"
  fi

  percent_none="$(( 100 - $percent_done ))"
  [ "$percent_done" -gt 0 ] && local done_bar="$(printf '#%.0s' $(seq -s ' ' 1 $percent_done))"
  [ "$percent_none" -gt 0 ] && local none_bar="$(printf '~%.0s' $(seq -s ' ' 1 $percent_none))"

  # print the progress bar to the screen
  printf "\r Progress: [%s%s] %s %s${new_line}" \
    "$done_bar" \
    "$none_bar" \
    "${spinner:x++%${#spinner}:1}" \
    "$progress_message"
}

main "$@"

Flexible version with randomized colors, a string to manipulate and date.

function spinner() {
  local PID="$1"
  local str="${2:-Processing!}"
  local delay="0.1"
  # tput civis  # hide cursor
  while ( kill -0 $PID 2>/dev/null )
    do
      printf "\e[38;5;$((RANDOM%257))m%s\r\e[0m" "[$(date '+%d/%m/%Y %H:%M:%S')][      $str      ]"; sleep "$delay"
      printf "\e[38;5;$((RANDOM%257))m%s\r\e[0m" "[$(date '+%d/%m/%Y %H:%M:%S')][      $str      ]"; sleep "$delay"
      printf "\e[38;5;$((RANDOM%257))m%s\r\e[0m" "[$(date '+%d/%m/%Y %H:%M:%S')][      $str      ]"; sleep "$delay"
  done
  printf "\e[38;5;$((RANDOM%257))m%s\r\e[0m" "[$(date '+%d/%m/%Y %H:%M:%S')][ ✅  ✅  ✅   Done!   ✅  ✅  ✅ ]"; sleep "$delay"
  # tput cnorm  # restore cursor

  return 0
}

Usage:

# your long running proccess pushed to the background
sleep 20 &

# spinner capture-previous-proccess-id string
spinner $! 'Working!'

output example:

[04/06/2020 03:22:24][      Seeding!      ]
#!/bin/bash
tot=$(wc -c /proc/$$/fd/255 | awk '/ /{print $1}')
now() {
echo $(( 100* ($(awk '/^pos:/{print $2}' < /proc/$$/fdinfo/255)-166) / (tot-166) )) "%"
}
now;
now;
now;
now;
now;
now;
now;
now;
now;

output:

0 %
12 %
25 %
37 %
50 %
62 %
75 %
87 %
100 %

note: if instead of 255 you put 1 you will monitor the standard in...with 2 the standard out (but you must modify the source to set "tot" to the projected output file size)

There are many different answers about this topic, but when calculating percentage for text file operation, using current length / total size way, for example showing percentage of ver_big_file.json progress, and I recommend using awk for this purpose, like below code:

awk '
    function bar(x){s="";i=0;while (i++ < x) s=s "#";return s}
    BEGIN{
        ("ls -l " ARGV[1]) | getline total;
        split(total,array);
        total=array[5];
    }
    {
        cur+=length($0)+1;
        percent=int(cur / total * 100);
        printf "LINE %s:%s %s%%\r", NR, bar(percent*.8), percent 
    }
    END {print}' very_big_file.json | grep "keyword" | ...

This way it's very precise, stream based, but it's only suitable for text files.

Related