tricky

process bar

with dot .

reference:

while true; do
  (( i++ == 0 )) && printf $(c sY)%-6s$(c) 'waiting ...' || printf $(c sY)%s$(c) '.'
  sleep 1
done
waiting bar with dot

another:

# main function designed for quickly copying to another program
progressBar() {
  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

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

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

Main () {
  tput civis                              # Turn off cursor
  for ((i=0; i<=100; i++)); do
    CurrLevel=$(progressBar "$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 "$@"
progress bar with ▎▌ ▊ ▉

another solution:

BAR='##############################'
FILL='------------------------------'
totalLines=100
barLen=30
count=0

while [ ${count} -lt ${totalLines} ]; 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}%)"
  sleep .1
done
progress bar with [###----]

while :; do
  for s in / - \\ \|
    do printf "\r$s"
    sleep .1
  done
done
progress bar with |\|/

spinner

[!TIP|label:check more:]

Braille Patterns

[!NOTE|label:references:]

Braille 8 dot Cell Numbering

+---+---+
| 1 | 4 |
+---+---+
| 2 | 5 |
+---+---+
| 3 | 6 |
+---+---+
| 7 | 8 |
+---+---+

4-dots

UNICODE
ICON
HTML ENCODING
COMMENTS

28C4

&#x28C4;

378

28C6

&#x28C6;

2378

2847

&#x2847;

1237

280F

&#x280F;

1234

280B

&#x280B;

124

2839

&#x2839;

1456

28B8

&#x28B8;

4568

28F0

&#x28F0;

5678

28E0

&#x28E0;

678

local spinner=( '⣄' '⣆' '⡇' '⠏' '⠋' '⠹' '⢸' '⣰' '⣠' )
local spinner=(
  "$(c Rs)⣄$(c)"    # red
  "$(c Ys)⣆$(c)"    # yellow
  "$(c Gs)⡇$(c)"    # green
  "$(c Bs)⠏$(c)"    # blue
  "$(c Ms)⠋$(c)"    # magenta
  "$(c Ys)⠹$(c)"    # yellow
  "$(c Gs)⢸$(c)"    # green
  "$(c Bs)⣰$(c)"    # blue
  "$(c Ms)⣠$(c)"    # magenta
)

7-dots

UNICODE
ICON
HTML ENCODING
COMMENTS

28FE

&#x28FE;

2345678

28FD

&#x28FD;

1345678

28FB

&#x28FB;

1245678

28BF

&#x28BF;

1234568

287F

&#x287F;

1234567

28DF

&#x28DF;

1234578

28EF

&#x28EF;

1234678

28F7

&#x28F7;

1235678

local spinner=( '⣾' '⣽' '⣻' '⢿' '⡿' '⣟' '⣯' '⣷' '⣿' )
local spinner=(
  "$(c Rs)⣾$(c)"     # red
  "$(c Ys)⣽$(c)"     # yellow
  "$(c Gs)⣻$(c)"     # green
  "$(c Cs)⢿$(c)"     # cyan
  "$(c Rs)⡿$(c)"     # red
  "$(c Ys)⣟$(c)"     # yellow
  "$(c Gs)⣯$(c)"     # green
  "$(c Cs)⣷$(c)"     # cyan
)

1-dot

local spinner=( '⠁' '⠂' '⠄' '⡀' '⢀' '⠠' '⠐' '⠈' )
local spinner=(
  "$(c Ys)⠁$(c)"     # yellow
  "$(c Gs)⠂$(c)"     # green
  "$(c Cs)⠄$(c)"     # cyan
  "$(c Ms)⡀$(c)"     # magenta
  "$(c Ys)⢀$(c)"     # yellow
  "$(c Gs)⠠$(c)"     # green
  "$(c Cs)⠐$(c)"     # cyan
  "$(c Ms)⠈$(c)"     # magenta
)

others

local spinner=( '∙∙∙∙∙' '●∙∙∙∙' '∙●∙∙∙' '∙∙●∙∙' '∙∙∙●∙' '∙∙∙∙●' )
local spinner=(
  "∙∙∙∙∙"
  "$(c Ys)●$(c)∙∙∙∙"     # yellow
  "∙$(c Gs)●$(c)∙∙∙"     # green
  "∙∙$(c Cs)●$(c)∙∙"     # cyan
  "∙∙∙$(c Bs)●$(c)∙"     # blue
  "∙∙∙∙$(c Ms)●$(c)"     # magenta
)
local spinner=(
  "$(c Rs)∙∙∙∙∙$(c)"     # red
  "$(c Ys)●∙∙∙∙$(c)"     # yellow
  "$(c Gs)∙●∙∙∙$(c)"     # green
  "$(c Cs)∙∙●∙∙$(c)"     # cyan
  "$(c Bs)∙∙∙●∙$(c)"     # blue
  "$(c Ms)∙∙∙∙●$(c)"     # magenta
)

bash script

with stdout

[!TIP|label:use case:]

#!/usr/bin/env bash

# credit: https://github.com/ppo/bash-colors
# shellcheck disable=SC2015,SC2059
c() { [ $# == 0 ] && printf "\e[0m" || printf "$1" | sed 's/\(.\)/\1;/g;s/\([SDIUFNHT]\)/2\1/g;s/\([KRGYBMCW]\)/3\1/g;s/\([krgybmcw]\)/4\1/g;y/SDIUFNHTsdiufnhtKRGYBMCWkrgybmcw/12345789123457890123456701234567/;s/^\(.*\);$/\\e[\1m/g'; }

# capture ctrl-c to exit the sub-process
# return the sub-process stdout ( to external variable )
function withSpinner() {
  local msg="$1"; shift
  local __resultvar="$1"; shift
  local spinner=(
    "$(c Rs)⣾$(c)"
    "$(c Ys)⣽$(c)"
    "$(c Gs)⣻$(c)"
    "$(c Cs)⢿$(c)"
    "$(c Rs)⡿$(c)"
    "$(c Ys)⣟$(c)"
    "$(c Gs)⣯$(c)"
    "$(c Cs)⣷$(c)"
  )
  local frame=0
  local output
  local cmdPid
  local pgid=''
  local interrupted=0

  # define the cursor recovery function
  restoreCursor() { printf "\033[?25h" >&2; }

  # make sure that any exit restores the cursor
  trap 'restoreCursor' EXIT

  # hide cursor
  printf "\033[?25l" >&2
  printf "%s " "$msg" >&2

  set -m
  trap 'interrupted=1; [ -n "$pgid" ] && kill -TERM -- -$pgid 2>/dev/null' INT

  # use file descriptor to capture output
  local tmpout
  tmpout=$(mktemp)
  exec 3<> "${tmpout}"

  # shellcheck disable=SC2031,SC2030
  output="$(
    {
      # execute command and redirect output to file descriptor 3
      "$@" >&3 2>/dev/null &
      cmdPid=$!
      pgid=$(ps -o pgid= "$cmdPid" | tr -d ' ')

      # update the spinner while the command is running
      while kill -0 "$cmdPid" 2>/dev/null && (( interrupted == 0 )); do
        printf "\r\033[K%s %b" "${msg}" "${spinner[frame]}" >&2
        ((frame = (frame + 1) % ${#spinner[@]}))
        sleep 0.08
      done

      wait "$cmdPid" 2>/dev/null
      # show the captured content
      cat "${tmpout}"
    }
  )"

  # clean the temporary file
  exec 3>&-
  rm -f "${tmpout}"

  # \r : beginning of line
  # \033[K : clear current position to end of line
  # shellcheck disable=SC2031
  if (( interrupted )); then
    printf "\r\033[K\033[31m✗\033[0m Interrupted!\033[K\n" >&2
    [ -n "${pgid}" ] && kill -TERM -- -"${pgid}" 2>/dev/null
  else
    # or using `printf "\r" >&2` directly without sub-progress status output
    printf "\r\033[K\033[32m✓\033[0m Done!\033[K\n" >&2
  fi

  # assign the result to an external variable
  printf -v "$__resultvar" "%s" "$output"
}

function main() {
  # shellcheck disable=SC2155
  local tmpfile=$(mktemp)
  trap 'rm -f "${tmpfile}"' EXIT

  local response
  withSpinner "Loading..." response \
    curl -s https://<API> ...

  # check curl output
  echo "${response}"
}

main "$@"

# vim:tabstop=2:softtabstop=2:shiftwidth=2:expandtab:filetype=sh:

with exitcode

#!/usr/bin/env bash

# credit: https://github.com/ppo/bash-colors
# shellcheck disable=SC2015,SC2059
c() { [ $# == 0 ] && printf "\e[0m" || printf "$1" | sed 's/\(.\)/\1;/g;s/\([SDIUFNHT]\)/2\1/g;s/\([KRGYBMCW]\)/3\1/g;s/\([krgybmcw]\)/4\1/g;y/SDIUFNHTsdiufnhtKRGYBMCWkrgybmcw/12345789123457890123456701234567/;s/^\(.*\);$/\\e[\1m/g'; }

# capture ctrl-c to exit the sub-process
function withSpinner() {
  local msg="$1"; shift
  local __resultvar="$1"; shift
  local spinner=(
    "$(c Rs)⣄$(c)"
    "$(c Ys)⣆$(c)"
    "$(c Gs)⡇$(c)"
    "$(c Bs)⠏$(c)"
    "$(c Ms)⠋$(c)"
    "$(c Ys)⠹$(c)"
    "$(c Gs)⢸$(c)"
    "$(c Bs)⣰$(c)"
    "$(c Ms)⣠$(c)"
  )
  local frame=0
  local output
  local cmdPid
  local pgid=""
  local interrupted=0

  # explicit recovery cursor
  function restoreCursor() { printf "\033[?25h" >&2; }

  # ensure that any exit restores the cursor.
  trap 'restoreCursor' EXIT

  # hide cursor
  printf "\033[?25l" >&2
  printf "%s " "${msg}" >&2

  set -m
  trap 'interrupted=1; [ -n "${pgid}" ] && kill -TERM -- -${pgid} 2>/dev/null' INT

  # shellcheck disable=SC2034,SC2030
  output="$(
    {
      "$@" 2>/dev/null &
      cmdPid=$!
      pgid=$(ps -o pgid= ${cmdPid} | tr -d ' ')
      echo "${pgid}" > "${tmpfile}"

      while kill -0 "$cmdPid" 2>/dev/null && (( interrupted == 0 )); do
        printf "\r\033[K%s %b" "${msg}" "${spinner[frame]}" >&2
        ((frame = (frame + 1) % ${#spinner[@]}))
        sleep 0.1
      done

      wait "${cmdPid}" 2>/dev/null
    }
  )"

  # \r : beginning of line
  # \033[K : clear current position to end of line
  if (( interrupted )); then
    printf "\r\033[K\033[31m✗\033[0m Interrupted!\033[K\n" >&2
    # shellcheck disable=SC2031
    [ -n "${pgid}" ] && kill -TERM -- -"${pgid}" 2>/dev/null
  else
    printf "\r\033[K\033[32m✓\033[0m Done!\033[K\n" >&2
  fi

  # a separate recovery cursor is no longer required because the exit trap is handled
}

# main function
function main() {
  tmpfile=$(mktemp)
  trap 'rm -f "${tmpfile}"' EXIT

  withSpinner "Loading..." result sleep 5
  echo "Exit code: $?"
}

main "$@"

# vim:tabstop=2:softtabstop=2:shiftwidth=2:expandtab:filetype=sh:

save & restore screen

tput

  • clear

    $ tput smcup
  • restore

    $ tput rmcup

echo

  • save

    $ echo -e '\033[?47h'
  • restore

    $ echo -e '\033[?47l'

terminfo escape sequences

$ infocmp
  ...
  colors#256, cols#80, it#8, lines#24, pairs#32767,
  bel=^G, blink=\E[5m, bold=\E[1m, cbt=\E[Z, civis=\E[?25l,
  clear=\E[H\E[2J, cnorm=\E[?12l\E[?25h, cr=^M,
  ...

tput

reset terminal

[!NOTE]

$ reset
# or
$ stty sane

clear screen

$ tput home

# or
$ tput cup %py %px
# or
$ tput cup %py %px >/dev/null

show term

$ tput color

show terminal width

$ tput cols

$ export GREP_COLORS="sl=0;33;49:ms=1;34;49"
$ find /etc/ -type f | head | grep --color=always '^\|[^/]*$'
customized color output

Operation not permitted

[!NOTE|label:references:]

$ sudo lsattr /etc/resolv.conf
----i-------------- /etc/resolv.conf
$ sudo rm -rf /etc/resolv.conf
rm: cannot remove '/etc/resolv.conf': Operation not permitted

# solution
$ sudo chattr -i /etc/resolv.conf
$ sudo lsattr /etc/resolv.conf
------------------- /etc/resolv.conf
$ sudo mv /etc/resolv.conf{,.bak}

# revert back
$ sudo chattr +i /etc/resolv.conf
$ sudo lsattr /etc/resolv.conf
----i-------------- /etc/resolv.conf

array

differences in bash parameter calls

[!NOTE🏷️thinking:] I got a issue with/without eval commands like:

local fdOpt="--type f --hidden --follow --unrestricted --ignore-file $HOME/.fdignore"
local ignores=(
  '*.pem' '*.p12'
  '*.png' '*.jpg' '*.jpeg' '*.gif' '*.svg'
  '*.zip' '*.tar' '*.gz' '*.bz2' '*.xz' '*.7z' '*.rar'
  'Music' '.target_book' '_book' 'OneDrive*'
)
while read -r pattern; do fdOpt+=" --exclude '${pattern}'"; done <<< "$(printf '%s\n' "${ignores[@]}")"
fdOpt+=' --exec-batch ls -t'
  • the --exclude options are not passed correctly when using :

fd . ${fdOpt} | fzf ${foption} --bind="enter:become(${VIM} {+})"
  • but it works when using eval :

eval "fd . ${fdOpt}" | fzf ${foption} --bind="enter:become(${VIM} {+})"

[!TIP|label:tips:]

since fdOpt is a single string (containing multiple arguments), Bash treats it as one single argument when passed to fd. This leads to the following issues:

--exclude '*.png' is treated as one single argument, rather than two separate ones: --exclude and '*.png';

As a result, fd cannot correctly interpret the glob pattern and treats it as a literal string; Therefore, --exclude '*.png' does not actually exclude anything.

recommend using arrays to store multiple arguments and then pass them to the command.

# array
local -a fdArgs=(--type f --hidden --follow --unrestricted --ignore-file "${HOME}/.fdignore")
local ignores=(
  '*.pem' '*.p12'
  '*.png' '*.jpg' '*.jpeg' '*.gif' '*.svg'
  '*.zip' '*.tar' '*.gz' '*.bz2' '*.xz' '*.7z' '*.rar'
  'Music' '.target_book' '_book' 'OneDrive*'
)

for pattern in "${ignores[@]}"; do fdArgs+=(--exclude "${pattern}"); done
fdArgs+=(--exec-batch ls -t)

#      array call
#    +------------+
fd . "${fdArgs[@]}" | fzf ${foption} --bind="enter:become(${VIM} {+})"

details:

FORM
WORKS?
REASON

fd . ${fdOpt}

❌ No

${fdOpt} is a single string; arguments are not properly split

eval "fd . ${fdOpt}"

✅ Yes

Bash re-splits the command string before execution, but it’s risky

fd . "${fdArgs[@]}"

✅✅ Yes (Recommended)

Uses an argument array — most recommended, safe, and clean

METHOD
ARGUMENT PARSING
SAFETY
WILDCARD EXPANSION
RECOMMENDED USE CASE

$cmd

❌ Incorrect, treated as a single command

❌ Low

❌ No

Avoid using

eval "$cmd"

✅ Correctly splits arguments

⚠️ Low

✅ Yes

Quick testing or executing ad-hoc command strings

"${cmd[@]}"

✅ Correct and safe argument passing

✅ High

❌ No (no expansion)

Recommended for building command argument lists programmatically

$ ls
bar.bak  bar.txt  demo.sh  foo.log  foo.txt

$ bash demo.sh
→ Running: echo Listing *.txt with excludes: --exclude '*.log' --exclude '*.bak'
Listing bar.txt foo.txt with excludes: --exclude '*.log' --exclude '*.bak'
#       +-------------+
#     *.txt got expanded

→ Running with eval: echo Listing *.txt with excludes: --exclude '*.log' --exclude '*.bak'
Listing bar.txt foo.txt with excludes: --exclude *.log --exclude *.bak
#       +-------------+
#     *.txt got expanded

→ Running with array: echo Listing *.txt with excludes: --exclude *.log --exclude *.bak
Listing *.txt with excludes: --exclude *.log --exclude *.bak
$ cat -c demo.sh
#!/usr/bin/env bash

set -euo pipefail

function plainString() {
  local cmd="echo Listing *.txt with excludes: --exclude '*.log' --exclude '*.bak'"
  echo "→ Running: $cmd"
  $cmd
}

function evalString() {
  local cmd="echo Listing *.txt with excludes: --exclude '*.log' --exclude '*.bak'"
  echo "→ Running with eval: $cmd"
  eval "$cmd"
}

function arrayCall() {
  local -a cmd=("echo" "Listing" "*.txt" "with" "excludes:" "--exclude" "*.log" "--exclude" "*.bak")
  echo "→ Running with array: ${cmd[*]}"
  "${cmd[@]}"
}

plainString
echo ''
evalString
echo ''
arrayCall

# vim:tabstop=2:softtabstop=2:shiftwidth=2:expandtab:filetype=sh:

wildcard expansion

METHOD
WILDCARD EXPANDED?
EXPLANATION

eval "echo *.txt"

✅ Yes

Shell expands the wildcard during evaluation

eval "echo '*.txt'"

❌ No

'*.txt' is a quoted string literal, not subject to expansion

"${arr[@]}"

❌ No

Arguments are passed as literal strings, no globbing applied

Last updated

Was this helpful?