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

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 "$@"

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

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

spinner
[!TIP|label:check more:]
Braille Patterns
[!NOTE|label:references:]
+---+---+ | 1 | 4 | +---+---+ | 2 | 5 | +---+---+ | 3 | 6 | +---+---+ | 7 | 8 | +---+---+
4-dots
28C4
⣄
⣄
378
28C6
⣆
⣆
2378
2847
⡇
⡇
1237
280F
⠏
⠏
1234
280B
⠋
⠋
124
2839
⠹
⠹
1456
28B8
⢸
⢸
4568
28F0
⣰
⣰
5678
28E0
⣠
⣠
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
28FE
⣾
⣾
2345678
28FD
⣽
⣽
1345678
28FB
⣻
⣻
1245678
28BF
⢿
⢿
1234568
287F
⡿
⡿
1234567
28DF
⣟
⣟
1234578
28EF
⣯
⣯
1234678
28F7
⣷
⣷
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
[!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:
#!/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
tput
clear
$ tput smcup
restore
$ tput rmcup
echo
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
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 '^\|[^/]*$'

Operation not permitted
Operation not permitted
[!NOTE|label:references:]
mac equivalent
/bin/ls -lO
$ /bin/ls -lO total 288 -rwxr-xr-x@ 1 marslo staff compressed,dataless 1116834851 Feb 21 17:00 Ubuntu2204-221101.AppxBundle
$ 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 tofd
. 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:
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
$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
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?