docs: add bash progress bar article
This commit is contained in:
parent
d73dd9d0fa
commit
97b9cb24ad
14 changed files with 1160 additions and 0 deletions
18
content/articles/2024/bash_printf/files/script1_ping.sh
Normal file
18
content/articles/2024/bash_printf/files/script1_ping.sh
Normal file
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
PING_ITER=16
|
||||
|
||||
# shellcheck disable=2317
|
||||
command() {
|
||||
local -r host=${1?"first parameter must be an host"}
|
||||
local -r iteration=${2?"second parameter must be an iteration number"}
|
||||
local -r cmd=(ping -c "${iteration}" "${host}")
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
main() {
|
||||
command "aquilenet.fr" "$PING_ITER"
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
24
content/articles/2024/bash_printf/files/script2_getoutput.sh
Normal file
24
content/articles/2024/bash_printf/files/script2_getoutput.sh
Normal file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
PING_ITER=16
|
||||
|
||||
# shellcheck disable=2317
|
||||
command() {
|
||||
local -r host=${1?"first parameter must be an host"}
|
||||
local -r iteration=${2?"second parameter must be an iteration number"}
|
||||
local -r cmd=(ping -c "${iteration}" "${host}")
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
parse_output() {
|
||||
while read -r line; do
|
||||
printf "out: %s\n" "${line}"
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
command "aquilenet.fr" "$PING_ITER" > >(parse_output)
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
29
content/articles/2024/bash_printf/files/script3_rematch.sh
Normal file
29
content/articles/2024/bash_printf/files/script3_rematch.sh
Normal file
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env bash
|
||||
PING_ITER=16
|
||||
|
||||
# shellcheck disable=2317
|
||||
command() {
|
||||
local -r host=${1?"first parameter must be an host"}
|
||||
local -r iteration=${2?"second parameter must be an iteration number"}
|
||||
local -r cmd=(ping -c "${iteration}" "${host}")
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
parse_output() {
|
||||
while read -r line; do
|
||||
if [[ "$line" =~ icmp_seq=([[:digit:]]{1,}).*time=(.*) ]]; then
|
||||
printf "séquence: %s sur %s temps:%s\n" \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER" \
|
||||
"${BASH_REMATCH[2]}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
command "aquilenet.fr" "$PING_ITER" > >(parse_output)
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
42
content/articles/2024/bash_printf/files/script4_progress.sh
Normal file
42
content/articles/2024/bash_printf/files/script4_progress.sh
Normal file
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env bash
|
||||
# shellcheck disable=2317
|
||||
PING_ITER=16
|
||||
PROGRESS_BAR_CHAR=(█ ▒)
|
||||
|
||||
command() {
|
||||
local -r host=${1?"first parameter must be an host"}
|
||||
local -r iteration=${2?"second parameter must be an iteration number"}
|
||||
local -r cmd=(ping -c "${iteration}" "${host}")
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
draw_progressbar() {
|
||||
local -r progress=${1?"progress is mandatory"}
|
||||
local -r total=${2?"total is mandatory"}
|
||||
local progress_segment
|
||||
local todo_segment
|
||||
|
||||
printf -v progress_segment "%${progress}s" ""
|
||||
printf -v todo_segment "%$((total - progress))s" ""
|
||||
printf >&2 "%s%s\r" \
|
||||
"${progress_segment// /${PROGRESS_BAR_CHAR[0]}}" \
|
||||
"${todo_segment// /${PROGRESS_BAR_CHAR[1]}}"
|
||||
|
||||
}
|
||||
|
||||
parse_output() {
|
||||
while read -r line; do
|
||||
if [[ "$line" =~ icmp_seq=([[:digit:]]{1,}).*time=(.*) ]]; then
|
||||
draw_progressbar "${BASH_REMATCH[1]}" "$PING_ITER"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
command "aquilenet.fr" "$PING_ITER" > >(parse_output)
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
||||
# [...]
|
|
@ -0,0 +1,53 @@
|
|||
#!/usr/bin/env bash
|
||||
# shellcheck disable=2317
|
||||
PING_ITER=16
|
||||
PROGRESS_BAR_CHAR=(█ ▒)
|
||||
|
||||
command() {
|
||||
local -r host=${1?"first parameter must be an host"}
|
||||
local -r iteration=${2?"second parameter must be an iteration number"}
|
||||
local -r cmd=(ping -c "${iteration}" "${host}")
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
draw_progressbar() {
|
||||
local -r progress=${1?"progress is mandatory"}
|
||||
local -r total=${2?"Total elements is mandatory"}
|
||||
local -r info_segment=${3:""}
|
||||
local progress_segment
|
||||
local todo_segment
|
||||
|
||||
printf -v progress_segment "%${progress}s" ""
|
||||
printf -v todo_segment "%$((total - progress))s" ""
|
||||
printf >&2 "%s%s%s\r" \
|
||||
"${info_segment}" \
|
||||
"${progress_segment// /${PROGRESS_BAR_CHAR[0]}}" \
|
||||
"${todo_segment// /${PROGRESS_BAR_CHAR[1]}}"
|
||||
|
||||
}
|
||||
|
||||
parse_output() {
|
||||
while read -r line; do
|
||||
if [[ "$line" =~ icmp_seq=([[:digit:]]{1,}).*time=(.*) ]]; then
|
||||
draw_progressbar \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER" \
|
||||
"Ping in progress (time: ${BASH_REMATCH[2]}) "
|
||||
elif [[ "$line" =~ ^PING(.*\(.*\)).* ]]; then
|
||||
printf "Launch ping command to %s with %d iterations\n" \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER"
|
||||
elif [[ "$line" =~ .*packets\ transmitted.* ]]; then
|
||||
printf "%s\n" "$line"
|
||||
fi
|
||||
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
command "aquilenet.fr" "$PING_ITER" > >(parse_output)
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env bash
|
||||
# shellcheck disable=2317
|
||||
PING_ITER=16
|
||||
PROGRESS_BAR_CHAR=(█ ▒)
|
||||
|
||||
command() {
|
||||
local -r host=${1?"first parameter must be an host"}
|
||||
local -r iteration=${2?"second parameter must be an iteration number"}
|
||||
local -r cmd=(ping -c "${iteration}" "${host}")
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
draw_progressbar() {
|
||||
local -r progress=${1?"progress is mandatory"}
|
||||
local -r total=${2?"total elements is mandatory"}
|
||||
local -r info_segment=${3:""}
|
||||
local progress_segment
|
||||
local todo_segment
|
||||
|
||||
local -r bar_size=$((COLUMNS - ${#info_segment}))
|
||||
local -r progress_ratio=$((progress * 100 / total))
|
||||
local -r progress_segment_size=$((bar_size * progress_ratio / 100))
|
||||
local -r todo_segment_size=$((bar_size - progress_segment_size))
|
||||
|
||||
printf -v progress_segment "%${progress_segment_size}s" ""
|
||||
printf -v todo_segment "%${todo_segment_size}s" ""
|
||||
|
||||
printf >&2 "%s%s%s\r" \
|
||||
"${info_segment}" \
|
||||
"${progress_segment// /${PROGRESS_BAR_CHAR[0]}}" \
|
||||
"${todo_segment// /${PROGRESS_BAR_CHAR[1]}}"
|
||||
|
||||
}
|
||||
|
||||
parse_output() {
|
||||
while read -r line; do
|
||||
if [[ "$line" =~ icmp_seq=([[:digit:]]{1,}).*time=(.*) ]]; then
|
||||
draw_progressbar \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER" \
|
||||
"Ping in progress (time: ${BASH_REMATCH[2]}) "
|
||||
elif [[ "$line" =~ ^PING(.*\(.*\)).* ]]; then
|
||||
printf "Launch ping command to %s with %d iterations\n" \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER"
|
||||
elif [[ "$line" =~ .*packets\ transmitted.* ]]; then
|
||||
printf "%s\n" "$line"
|
||||
fi
|
||||
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
COLUMNS=$(tput cols)
|
||||
command "aquilenet.fr" "$PING_ITER" > >(parse_output)
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
69
content/articles/2024/bash_printf/files/script7_sigwinch.sh
Normal file
69
content/articles/2024/bash_printf/files/script7_sigwinch.sh
Normal file
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env bash
|
||||
# shellcheck disable=2317
|
||||
PING_ITER=64
|
||||
PROGRESS_BAR_CHAR=(█ ▒)
|
||||
|
||||
command() {
|
||||
local -r host=${1?"first parameter must be an host"}
|
||||
local -r iteration=${2?"second parameter must be an iteration number"}
|
||||
local -r cmd=(ping -c "${iteration}" "${host}")
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
trap change_column_size WINCH
|
||||
|
||||
draw_progressbar() {
|
||||
local -r progress=${1?"progress is mandatory"}
|
||||
local -r total=${2?"total elements is mandatory"}
|
||||
local -r info_segment=${3:""}
|
||||
local progress_segment
|
||||
local todo_segment
|
||||
|
||||
local -r bar_size=$((COLUMNS - ${#info_segment}))
|
||||
local -r progress_ratio=$((progress * 100 / total))
|
||||
local -r progress_segment_size=$((bar_size * progress_ratio / 100))
|
||||
local -r todo_segment_size=$((bar_size - progress_segment_size))
|
||||
|
||||
printf -v progress_segment "%${progress_segment_size}s" ""
|
||||
printf -v todo_segment "%${todo_segment_size}s" ""
|
||||
|
||||
printf >&2 "%s%s%s\r" \
|
||||
"${info_segment}" \
|
||||
"${progress_segment// /${PROGRESS_BAR_CHAR[0]}}" \
|
||||
"${todo_segment// /${PROGRESS_BAR_CHAR[1]}}"
|
||||
|
||||
}
|
||||
|
||||
parse_output() {
|
||||
trap change_column_size WINCH
|
||||
while read -r line; do
|
||||
if [[ "$line" =~ icmp_seq=([[:digit:]]{1,}).*time=(.*) ]]; then
|
||||
|
||||
draw_progressbar \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER" \
|
||||
"Ping in progress (time: ${BASH_REMATCH[2]}) "
|
||||
elif [[ "$line" =~ ^PING(.*\(.*\)).* ]]; then
|
||||
printf "Launch ping command to %s with %d iterations\n" \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER"
|
||||
elif [[ "$line" =~ .*packets\ transmitted.* ]]; then
|
||||
printf >&2 "\033[0K\r"
|
||||
printf "%s\n" "$line"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
change_column_size() {
|
||||
printf >&2 "%${COLUMNS}s" ""
|
||||
printf >&2 "\033[0K\r"
|
||||
COLUMNS=$(tput cols)
|
||||
}
|
||||
|
||||
main() {
|
||||
COLUMNS=$(tput cols)
|
||||
command "aquilenet.fr" "$PING_ITER" > >(parse_output)
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
97
content/articles/2024/bash_printf/files/script8_bonus.sh
Normal file
97
content/articles/2024/bash_printf/files/script8_bonus.sh
Normal file
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env bash
|
||||
# ...
|
||||
# shellcheck disable=2317,2059
|
||||
PING_ITER=16
|
||||
|
||||
# Progress bar dedicated variables
|
||||
# We can control how progress bar work
|
||||
PROGRESS_BAR_CHAR=(█ ▒)
|
||||
|
||||
# control which information is displayed"
|
||||
PROGRESS_BAR_DISPLAY_INFO=1
|
||||
PROGRESS_BAR_DISPLAY_COMPL=1
|
||||
|
||||
# Display template
|
||||
# We can control how informations is displayed
|
||||
PROGRESS_BAR_INFO_TEMPLATE=' %s [%2d/%2d] '
|
||||
PROGRESS_BAR_COMPL_TEMPLATE=' %2d%% '
|
||||
PROGRESS_BAR_TEMPLATE='\033[1m%s%s\033[0m%s%s\r'
|
||||
|
||||
command() {
|
||||
local -r host=${1?"first parameter must be an host"}
|
||||
local -r iteration=${2?"second parameter must be an iteration number"}
|
||||
local -r cmd=(ping -c "${iteration}" "${host}")
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
draw_progressbar() {
|
||||
# function parameters
|
||||
local -r progress=${1?"progress is mandatory"}
|
||||
local -r total=${2?"total elements is mandatory"}
|
||||
local -r info=${3:-"In progress"}
|
||||
|
||||
# function local variables
|
||||
local progress_segment
|
||||
local todo_segment
|
||||
local info_segment=""
|
||||
local compl_segment=""
|
||||
|
||||
if [[ ${PROGRESS_BAR_DISPLAY_INFO:-1} -eq 1 ]]; then
|
||||
printf -v info_segment "${PROGRESS_BAR_INFO_TEMPLATE}" \
|
||||
"$info" "$progress" "$total"
|
||||
fi
|
||||
|
||||
local -r progress_ratio=$((progress * 100 / total))
|
||||
if [[ ${PROGRESS_BAR_DISPLAY_COMPL:-0} -eq 1 ]]; then
|
||||
printf -v compl_segment "${PROGRESS_BAR_COMPL_TEMPLATE}" \
|
||||
"$progress_ratio"
|
||||
fi
|
||||
|
||||
# progress bar construction
|
||||
# calculate each element sizes, bar must fit in ou screen
|
||||
local -r bar_size=$((COLUMNS - ${#info_segment} - ${#compl_segment}))
|
||||
local -r progress_segment_size=$((bar_size * progress_ratio / 100))
|
||||
local -r todo_segment_size=$((bar_size - progress_segment_size))
|
||||
printf -v progress_segment "%${progress_segment_size}s" ""
|
||||
printf -v todo_segment "%${todo_segment_size}s" ""
|
||||
|
||||
printf >&2 "$PROGRESS_BAR_TEMPLATE" \
|
||||
"$info_segment" \
|
||||
"${progress_segment// /${PROGRESS_BAR_CHAR[0]}}" \
|
||||
"${todo_segment// /${PROGRESS_BAR_CHAR[1]}}" \
|
||||
"$compl_segment"
|
||||
}
|
||||
|
||||
parse_output() {
|
||||
trap change_column_size WINCH
|
||||
while read -r line; do
|
||||
if [[ "$line" =~ icmp_seq=([[:digit:]]{1,}).*time=(.*) ]]; then
|
||||
draw_progressbar \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER" \
|
||||
"Ping in progess"
|
||||
elif [[ "$line" =~ ^PING(.*\(.*\)).* ]]; then
|
||||
printf "Launch ping command to %s with %d iterations\n" \
|
||||
"${BASH_REMATCH[1]}" \
|
||||
"$PING_ITER"
|
||||
elif [[ "$line" =~ .*packets\ transmitted.* ]]; then
|
||||
printf >&2 "\033[0K\r"
|
||||
printf "%s\n" "$line"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
change_column_size() {
|
||||
printf >&2 "%${COLUMNS}s" ""
|
||||
printf >&2 "\033[0K\r"
|
||||
COLUMNS=$(tput cols)
|
||||
}
|
||||
|
||||
main() {
|
||||
COLUMNS=$(tput cols)
|
||||
command "aquilenet.fr" "$PING_ITER" > >(parse_output)
|
||||
}
|
||||
|
||||
main
|
||||
exit 0
|
150
content/articles/2024/bash_printf/images/barre_progression.svg
Normal file
150
content/articles/2024/bash_printf/images/barre_progression.svg
Normal file
|
@ -0,0 +1,150 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="150.35045mm"
|
||||
height="25.371393mm"
|
||||
viewBox="0 0 150.35045 25.371393"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<title
|
||||
id="title6">Découpage d'une barre de progression</title>
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
id="layer1"
|
||||
transform="translate(-29.578274,-38.464996)">
|
||||
<rect
|
||||
style="fill:#cccccc;stroke-width:0.196183"
|
||||
id="rect1"
|
||||
width="82.085648"
|
||||
height="5.4879713"
|
||||
x="86.700256"
|
||||
y="48.571499" />
|
||||
<rect
|
||||
style="fill:#333333;stroke-width:0.139933"
|
||||
id="rect2"
|
||||
width="45.89801"
|
||||
height="5.470767"
|
||||
x="86.700256"
|
||||
y="48.571499" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:200;font-size:3.175px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Ultra-Light';fill:#333333;stroke-width:0.3"
|
||||
x="31.546356"
|
||||
y="52.329395"
|
||||
id="text2"><tspan
|
||||
id="tspan2"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'FiraCode Nerd Font';-inkscape-font-specification:'FiraCode Nerd Font';stroke-width:0.3"
|
||||
x="31.546356"
|
||||
y="52.329395">Reticulating spline (12/42)</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:200;font-size:3.175px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Ultra-Light';fill:#333333;stroke-width:0.3"
|
||||
x="171.58591"
|
||||
y="52.329395"
|
||||
id="text3"><tspan
|
||||
id="tspan3"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'FiraCode Nerd Font';-inkscape-font-specification:'FiraCode Nerd Font';stroke-width:0.3"
|
||||
x="171.58591"
|
||||
y="52.329395">57%</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#ff2bd5;stroke-width:0.3;stroke-opacity:1"
|
||||
d="m 29.728274,54.571433 v 3.7407 H 87.1745 v -3.7407"
|
||||
id="path3" />
|
||||
<path
|
||||
style="fill:none;stroke:#20cc8a;stroke-width:0.3;stroke-opacity:1"
|
||||
d="m 86.878278,47.878793 v -3.7407 h 45.804582 v 3.7407"
|
||||
id="path4" />
|
||||
<path
|
||||
style="fill:none;stroke:#b183f5;stroke-width:0.3;stroke-opacity:1"
|
||||
d="m 132.38684,54.571433 v 3.7407 h 36.27987 v -3.7407"
|
||||
id="path5" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:200;font-size:3.175px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Ultra-Light';white-space:pre;inline-size:0;fill:#ff2bd5;fill-opacity:1;stroke:#808080;stroke-width:0.899999;stroke-dasharray:none"
|
||||
x="60.677799"
|
||||
y="154.16701"
|
||||
id="text23635"
|
||||
transform="matrix(1.8930273,0,0,1.8930273,-60.403974,-228.32521)"><tspan
|
||||
id="tspan23633"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'FiraCode Nerd Font Mono';-inkscape-font-specification:'FiraCode Nerd Font Mono Bold';fill:#ff2bd5;fill-opacity:1;stroke:none;stroke-width:0.9"
|
||||
x="60.677799"
|
||||
y="154.16701">1</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:200;font-size:3.175px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Ultra-Light';white-space:pre;inline-size:0;fill:#20cc8a;fill-opacity:1;stroke:#808080;stroke-width:0.899999;stroke-dasharray:none"
|
||||
x="60.677799"
|
||||
y="154.16701"
|
||||
id="text24389"
|
||||
transform="matrix(1.8930273,0,0,1.8930273,-6.4597365,-248.78285)"><tspan
|
||||
id="tspan24387"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'FiraCode Nerd Font Mono';-inkscape-font-specification:'FiraCode Nerd Font Mono Bold';fill:#20cc8a;fill-opacity:1;stroke:none;stroke-width:0.9"
|
||||
x="60.677799"
|
||||
y="154.16701">2</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-weight:200;font-size:3.175px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Ultra-Light';white-space:pre;inline-size:0;fill:#b183f5;fill-opacity:1;stroke:#808080;stroke-width:0.899999;stroke-dasharray:none"
|
||||
x="60.677799"
|
||||
y="154.16701"
|
||||
id="text25264"
|
||||
transform="matrix(1.8930273,0,0,1.8930273,34.889672,-228.11076)"><tspan
|
||||
id="tspan25262"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:'FiraCode Nerd Font Mono';-inkscape-font-specification:'FiraCode Nerd Font Mono Bold';fill:#b183f5;fill-opacity:1;stroke:none;stroke-width:0.9"
|
||||
x="60.677799"
|
||||
y="154.16701">3</tspan></text>
|
||||
<path
|
||||
style="fill:none;stroke:#398eff;stroke-width:0.3;stroke-opacity:1"
|
||||
d="m 168.65607,47.878793 v -3.7407 h 11.12266 v 3.7407"
|
||||
id="path6" />
|
||||
<path
|
||||
style="font-weight:bold;font-size:5.73938px;font-family:'FiraCode Nerd Font';-inkscape-font-specification:'FiraCode Nerd Font Bold';fill:#398eff;fill-opacity:0.992157;stroke-width:0.348625"
|
||||
d="m 189.69326,37.134481 v 0.983053 h 0.40912 v 0.697555 h -0.40912 v 0.856492 h -0.89181 l -0.006,-0.856492 h -1.68944 v -0.618087 l 1.13316,-2.70781 0.79174,0.291384 -0.96539,2.336958 h 0.73287 l 0.1089,-0.983053 z"
|
||||
id="text6"
|
||||
transform="scale(0.92263604,1.083851)"
|
||||
aria-label="4" />
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata6">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>Découpage d'une barre de progression</dc:title>
|
||||
<dc:date>2024.05.26</dc:date>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>ephase</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title>CC-BY-SA</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<dc:language>FR</dc:language>
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
After Width: | Height: | Size: 6.7 KiB |
BIN
content/articles/2024/bash_printf/images/responsive.png
Normal file
BIN
content/articles/2024/bash_printf/images/responsive.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
content/articles/2024/bash_printf/images/responsive_corruped.png
Normal file
BIN
content/articles/2024/bash_printf/images/responsive_corruped.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
content/articles/2024/bash_printf/images/video.gif
Normal file
BIN
content/articles/2024/bash_printf/images/video.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
618
content/articles/2024/bash_printf/index.md
Normal file
618
content/articles/2024/bash_printf/index.md
Normal file
|
@ -0,0 +1,618 @@
|
|||
Title: Bash avancé: barre de progression
|
||||
Category: sysadmin
|
||||
Tags: bash, script, printf, substitution
|
||||
Date: 2024-06-20 0:04
|
||||
Cover: assets/backgrounds/turris_ssh_cle.jpg
|
||||
|
||||
Après [les
|
||||
messages]({filename}../../2022/bash_gerer_les_messages_avance/index.md) et [les
|
||||
signaux]({filename}../../2022/bash_les_pieges/index.md), voici enfin un nouvel
|
||||
article dans la série **Bash avancé**. Avec presque deux ans de retard, il
|
||||
serait temps me direz-vous! Mais mieux vaut tard que jamais non?
|
||||
|
||||
Cette article me servira de prétexte pour utiliser massivement la commande
|
||||
interne `printf` et vous montrer quelques cas d'usages. Nous verrons aussi
|
||||
la *substitution de processus*, la *substitution de paramètre* et d'autres
|
||||
mécanismes offerts par *Bash*.
|
||||
|
||||
## Un peu de théorie
|
||||
|
||||
Il faut d'abord définir ce que j'entends par *barre de progression*: un élément
|
||||
affiché dans notre terminal représentant **la progression** d'une tâche exécutée
|
||||
par un script. Voici un exemple:
|
||||
|
||||
```
|
||||
Task 2/10 ████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 20%
|
||||
```
|
||||
|
||||
Cette barre se compose donc:
|
||||
|
||||
* d'une légende / titre;
|
||||
* de la barre de progression représentant graphiquement l'avancée de notre
|
||||
tâche;
|
||||
* de la progression en pourcentage.
|
||||
|
||||
### Découpage en segments
|
||||
|
||||
Afin de préparer notre implémentation, découpons notre barre de chargement en
|
||||
segments. Ce découpage nous permettra de faciliter le passage au code.
|
||||
|
||||

|
||||
|
||||
Voici la légende:
|
||||
|
||||
1. segment d'informations pour la légende;
|
||||
2. segment d'actions effectuées;
|
||||
3. segment d'actions en attentes;
|
||||
4. segment complémentaire qui peut être le pourcentage de progression, ou toute
|
||||
autre information annexe.
|
||||
|
||||
## Une commande comme base
|
||||
|
||||
Pour concevoir notre barre de progression il nous faut analyser la sortie d'une
|
||||
commande. Pour notre tutoriel, je vous propose d'utiliser la commande `ping`.
|
||||
|
||||
```
|
||||
ping -c4 aquilenet.fr
|
||||
PING aquilenet.fr (185.233.100.8) 56(84) bytes of data.
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=1 ttl=60 time=29.1 ms
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=2 ttl=60 time=56.3 ms
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=3 ttl=60 time=52.8 ms
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=4 ttl=60 time=29.1 ms
|
||||
|
||||
--- aquilenet.fr ping statistics ---
|
||||
4 packets transmitted, 4 received, 0% packet loss, time 3003ms
|
||||
rtt min/avg/max/mdev = 29.055/41.806/56.258/12.764 ms
|
||||
```
|
||||
|
||||
Ci-dessus, la commande a été lancée en indiquant le nombre de requêtes à envoyer
|
||||
via le paramètre `-c4` et chacune de ces requêtes affiche le numéro d'ordre
|
||||
(`icmp_seq=`).
|
||||
|
||||
Maintenant que nous savons où aller, il est temps de commencer à écrire notre
|
||||
script:
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script1_ping.sh !}
|
||||
```
|
||||
|
||||
[Télécharger le script]({attach}./files/script1_ping.sh)
|
||||
|
||||
Il se compose d'une fonction `command` qui nécessite deux paramètres : un hôte
|
||||
et un nombre d'itérations. Attardons-nous un instant sur deux points importants.
|
||||
|
||||
* Dans la mesure du possible il faut utiliser des variables locales dans les
|
||||
fonctions et en lecture seule pour s'arrurer que la variable ne sera pas
|
||||
modifiée. Exemple: `local -r mavariable` déclare la variable locale
|
||||
`mavariable` en lecture seule avec le paramètre `-r`.
|
||||
* La commande et ses arguments sont enregistrés dans un tableau. Avec cette
|
||||
méthodem plus besoin de gérer avec le gobbing et l'interprétation des
|
||||
paramètres lors de son appel.
|
||||
|
||||
Lors de son lancement, nous pouvons observer l'affichage des différentes
|
||||
requêtes:
|
||||
|
||||
```text
|
||||
./bash script1_ping.sh
|
||||
PING aquilenet.fr (185.233.100.8) 56(84) bytes of data.
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=1 ttl=60 time=29.1 ms
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=2 ttl=60 time=56.3 ms
|
||||
# [...]
|
||||
```
|
||||
|
||||
## Capturer les messages de sortie
|
||||
|
||||
Il faut maintenant capturer la sortie de la commande ping et en extraire
|
||||
l'information pertinente : **le numéro de séquence**. [Télécharger le script
|
||||
complet]({attach}./files/script2_getoutput.sh)
|
||||
|
||||
### Récupérer la sortie standard ...
|
||||
|
||||
Il est tout à fait possible de rediriger la sortie de la fonction `command` vers
|
||||
une autre fonction qui traitera le flux de données. C'est exactement le même
|
||||
principe utilisé par l'enchaînement de commande avec un *pipe* comme dans
|
||||
`dmesg | grep 'failure'`. Plusieurs techniques existent pour faire ceci en
|
||||
*Bash*, ici nous allons utiliser la **substitution de processus**.
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script2_getoutput.sh!lines=13-21 }
|
||||
```
|
||||
|
||||
Ici la sortie standard de notre fonction `command` est envoyée vers la fonction
|
||||
`parse_output`.
|
||||
|
||||
Notez que cette substitution peut aussi se faire dans l'autre sens avec la
|
||||
notation `<(commande)`, la sortie standard de la commande substituée sera
|
||||
envoyée vers l'entrée de la commande à gauche comme par exemple:
|
||||
|
||||
```bash
|
||||
# Affiche les éléments trouvés par la commande `ls` en les préfixants de
|
||||
# 'éléments: ', la sortie de notre substitution (ls) sera traitée par la boucle
|
||||
while read -r line; do
|
||||
printf "élément: %s\n" "$line"
|
||||
done < <(ls)
|
||||
```
|
||||
|
||||
Sous le capot, le flux entre la sortie standard de `command` et l'entrée de
|
||||
`parse_output` se fait grâce à un descripteur de fichier.
|
||||
|
||||
### ... et la traiter
|
||||
|
||||
Pour traiter le flux, `parse_output` place chaque ligne qui arrive dans la
|
||||
variable `$line` grâce à la bocle `while` couplée à la commande `read`.
|
||||
|
||||
Ensuite `$line` est affichée en y ajoutant `out:`. Voici ce que donne
|
||||
l'exécution de ce script:
|
||||
|
||||
```text
|
||||
bash script2_getoutput.sh
|
||||
out: PING aquilenet.fr (2a0c:e300::8) 56 data bytes
|
||||
out: 64 bytes from hestia.aquilenet.fr (2a0c:e300::8): icmp_seq=1 ttl=55 time=20.0 ms
|
||||
out: 64 bytes from hestia.aquilenet.fr (2a0c:e300::8): icmp_seq=2 ttl=55 time=19.8 ms
|
||||
[...]
|
||||
```
|
||||
|
||||
## Récupérer le numéro de séquence
|
||||
|
||||
Maintenant que le flux arrive dans `parse_output`, essayons de récupérer le
|
||||
numéro de séquence. Nous pourrons alors déterminer l'état d'avancement de notre
|
||||
commande. [Télécharger le script complet]({attach}./files/script3_rematch.sh)
|
||||
|
||||
Pour l'extraire du reste de la ligne, utilisons l'opérateur de comparaison `=~`
|
||||
qui permet de vérifier la présence d'un motif dans une chaîne de caractères via
|
||||
**une expression rationnelle** comme par exemple:
|
||||
|
||||
```bash
|
||||
if [[ "$nom" =~ ^(Y|y)orick$ ]]; then
|
||||
printf "oui c'est moi!\n"
|
||||
fi
|
||||
```
|
||||
|
||||
Il est possible de récupérer le motif capturé entre parenthèses ayant "matché"
|
||||
grâce à la variable `$BASH_REMATCH`, utilisée dans `parse_output`:
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script3_rematch.sh!lines=13-22 }
|
||||
```
|
||||
|
||||
En plus du numéro de séquence, le temps est aussi capturé afin d'illustrer le
|
||||
fonctionnement de la variable `$BASH_REMATCH` qui est en fait **un tableau**:
|
||||
|
||||
* l'indice `0` permet de récupérer entièrement la chaîne qui correspond au
|
||||
motif;
|
||||
* l'indice `1` au premier élément capturé, ici le numéro de séquence qui se
|
||||
trouve après `icmp_seq=`
|
||||
* et l'indice `2` à tout ce qui se trouve après `time=`.
|
||||
|
||||
Voici ce que donne l'exécution de ce script:
|
||||
|
||||
```text
|
||||
bash script3_rematch.sh
|
||||
séquence: 2 sur 16 temps:20.2 ms
|
||||
séquence: 3 sur 16 temps:20.5 ms
|
||||
séquence: 4 sur 16 temps:19.7 ms
|
||||
[...]
|
||||
```
|
||||
|
||||
Nous avons maintenant tout les éléments utiles pour la construction de notre
|
||||
barre de progression.
|
||||
|
||||
## Première barre de chargement
|
||||
|
||||
Il faut commencer doucement, par une barre de chargement aussi simple que
|
||||
possible, sans fioriture. Nous allons ajouté la fonction `draw_progressbar` qui
|
||||
se charge du dessin à l'écran. [Télécharger le script
|
||||
complet]({attach}./files/script4_progress.sh)
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=14-26 }
|
||||
```
|
||||
|
||||
`draw_progressbar` attend deux paramètres: le nombre total d'éléments et celui
|
||||
en cours. Cette fonction est appelée depuis `parse_output` et elle utilise
|
||||
`printf` pour dessiner les deux segments nécessaires (mais pas que...).
|
||||
|
||||
### `printf` vers une variable
|
||||
|
||||
Ne nous attardons pas pas sur les deux premières variables locales de notre
|
||||
fonction `draw_progressbar`. Les deux suivantes -- `progress_segment` et
|
||||
`todo_segment` sont par contre plus intéressantes.
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=42 17-21 42 }
|
||||
```
|
||||
|
||||
`printf -v progress_segment` permet **d'affecter le résultat de la commande
|
||||
`printf`** à la variable locale `progress_segment` au lieu de l'afficher sur la
|
||||
sortie standard.
|
||||
|
||||
#### `printf`: format, arguments, spécification
|
||||
|
||||
Le premier argument d'une commande `printf` est le *format*, les suivants
|
||||
sont les *paramètres*. Les arguments sont affichés par le format par
|
||||
l'intermédiaire de *spécifications*.
|
||||
|
||||
```bash
|
||||
number=2
|
||||
drink="water"
|
||||
printf "I have %d bottle of %s.\n" "$number" "$drink"
|
||||
```
|
||||
|
||||
Dans l'exemple suivant:
|
||||
|
||||
* `"I have %d bottle of %s.\n"` est le format,
|
||||
* `$number` et `$drink` sont les arguments
|
||||
* `%d` et `%s` sont des spécifications:
|
||||
* `%d` indique que le premier argument doit être affiché comme
|
||||
un nombre entier;
|
||||
* `%s` indique que le second argument doit être affiché comme une chaîne
|
||||
de caractère.
|
||||
|
||||
Il est possible de définit la taille **minimale d'une spécification**, les
|
||||
caractères manquants seront remplacés par des espaces, comme par exemple:
|
||||
|
||||
```text
|
||||
printf "bonjour %10s, bienvenue\n" "monsieur" "madame" "Linus Torvalds"
|
||||
bonjour monsieur, bienvenue
|
||||
bonjour madame, bienvenue
|
||||
bonjour Linus Torvalds, bienvenue
|
||||
```
|
||||
|
||||
Vous remarquerez que la taille de 10 caractères a bien été réservée pour les
|
||||
deux premiers éléments. Le dernier par contre contient plus de 10 caractères: il
|
||||
dépasse de cet espace.
|
||||
|
||||
Vous remarquez aussi une caractéristique particulière de `printf`. Une seule
|
||||
spécification est utilisée pour trois arguments, le format est donc répété trois
|
||||
fois.
|
||||
|
||||
#### Et dans notre script...
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=42 20 21 }
|
||||
```
|
||||
|
||||
Dans le script la taille des deux segments est définie:
|
||||
|
||||
* par **la variable** `progress` pour le *segment des actions effectuées*
|
||||
* par le résultat d'une soustraction pour le *segment des actions en attente*.
|
||||
|
||||
*Bash* réalise les expansions avant de passer dans la commande `printf`,
|
||||
pratique n'est ce pas!
|
||||
|
||||
Pour ces deux `printf` le formats a une spécification avec une taille minimale
|
||||
égale à la taille du segment que nous voulons obtenir (car les arguments sont
|
||||
vides). Nous obtenons **deux variables qui contiennent un nombre d'espace égal
|
||||
à la taille des segments représentés**, il ne reste plus qu'à assembler.
|
||||
|
||||
### On passe au dressage
|
||||
|
||||
La commande `printf` suivante sert à afficher effectivement la barre de
|
||||
progression. D'abord elle est redirigée sur `stderr`, cette sortie est utilisée
|
||||
pour les erreurs, mais aussi pour afficher les informations de diagnostic.
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=42 22-24 }
|
||||
```
|
||||
|
||||
Son format contient deux spécifications de type chaîne de caractère et **un
|
||||
retour chariot**. Ce dernier permet au curseur de revenir au début de la ligne
|
||||
une fois dessinée. Lors du prochain passage dans la fonction `draw_progressbar`:
|
||||
la barre sera alors réécrite, **pas besoin de gérer l'effacement de la ligne**.
|
||||
|
||||
Pour les deux arguments, nous utilisons *l'expansion de paramètre* afin de
|
||||
remplacer les espaces par les caractères choisis pour symboliser les deux
|
||||
segments de notre barre.
|
||||
|
||||
Comment fonctionne le remplacement de motif avec *l'expansion de paramètre*?
|
||||
Voici deux exemples:
|
||||
|
||||
```bash
|
||||
variable="voici une plante, jolie plante non?"
|
||||
|
||||
# remplacer plante par fleur dans `variable`
|
||||
# remplace la première occurrence trouvée
|
||||
echo "${variable/plante/fleur}"
|
||||
# affiche "voici une fleur, joli plante non?"
|
||||
|
||||
#remplace toutes les occurrences
|
||||
echo "${variable//plante/fleur}"
|
||||
# affiche "voici une fleur, joli fleur non?"
|
||||
```
|
||||
|
||||
Les caractères choisis pour remplacer les espaces sont affecté au tableau
|
||||
`PROGRESS_BAR_CHAR` initialisé en tout début de script.
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=4 }
|
||||
```
|
||||
|
||||
C'est la fonction `parse_output` qui fait appel à `draw_progressbar`
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=28-34 }
|
||||
```
|
||||
|
||||
### Exécution
|
||||
|
||||
Maintenant que vous avez compris comment le script est construit, il est temps
|
||||
de passer à l'exécution:
|
||||
|
||||
```
|
||||
bash script4_progress.sh
|
||||
█████▒▒▒▒▒▒▒▒▒▒▒
|
||||
```
|
||||
|
||||
Notre barre de progression fonctionne, mais elle ne prend qu'une petite part de
|
||||
l'écran (les 16 caractères qui correspondent aux nombres de requêtes totales).
|
||||
Mais surtout, **aucune information n'est affichée**.
|
||||
|
||||
## Ajouter des informations
|
||||
|
||||
Elle est bien belle notre barre de progression, mais là on ne sais pas du tout
|
||||
ce que fait le script. L'angle d'attaque: modifier les fonctions `parse_output`
|
||||
et `draw_progressbar` pour ajouter des informations sur la sortie standard.
|
||||
[Télécharger le script complet]({attach}./files/script5_informations.sh)
|
||||
|
||||
### La fonction `parse_output`
|
||||
|
||||
Elle contient 2 conditions de plus qui vérifient la ligne en cours envoyée par
|
||||
`command`.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script5_informations.sh!lines=30-46}
|
||||
```
|
||||
|
||||
#### Afficher des information en introduction ...
|
||||
|
||||
La première condition ajoutée récupère, dans la sortie de la commande `ping`,
|
||||
la ligne qui commence par `PING` comme dans l'exemple ci-dessous:
|
||||
|
||||
```
|
||||
PING aquilenet.fr (2a0c:e300::8) 56 data bytes
|
||||
```
|
||||
|
||||
De cette ligne est récupérée **le nom d'hôte** et **l'adresse IP associée** afin
|
||||
de l'afficher avant notre barre de progression.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script5_informations.sh!lines=37}
|
||||
```
|
||||
|
||||
L'information utile est capturée et réutilisée via la variable `BASH_REMATCH`.
|
||||
|
||||
#### ... une conclusion ...
|
||||
|
||||
La seconde condition ajoutée permet de récupérer la ligne qui contient le
|
||||
récapitulatif de ce qui a été fait par commande `ping` et de la renvoyer tel
|
||||
quelle sur la sortie standard.
|
||||
|
||||
#### ... et ajouter le segment d'information
|
||||
|
||||
Enfin un paramètre de plus est passé à la fonction `draw_progressbar`: une
|
||||
chaîne de caractère pour le *segment d'information* contenant la dernière mesure
|
||||
de temps retournée par `ping` (histoire d'utiliser `${BASH_REMATCH[2]}`)
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script5_informations.sh!lines=32-36}
|
||||
```
|
||||
|
||||
### la fonction `draw_progress`
|
||||
|
||||
Le troisième paramètre passé à cette fonction est affecté à la variable
|
||||
`info_segment`. Son contenu est ajouté à l'affichage avant la barre de
|
||||
chargement.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script5_informations.sh!lines=14-28}
|
||||
```
|
||||
|
||||
### lancement de notre script
|
||||
|
||||
Notre barre de progression est toujours là mais avec un peu de contexte.
|
||||
|
||||
```text
|
||||
bash script5_informations.sh
|
||||
Launch ping command to aquilenet.fr (2a0c:e300::8) with 16 iterations
|
||||
Ping in progress (time: 20.7 ms) ███████▒▒▒▒▒▒▒▒▒
|
||||
```
|
||||
|
||||
Et une fois terminé la barre de progression disparait et laisse place au
|
||||
récapitulatif:
|
||||
|
||||
```text
|
||||
bash script5_informations.sh
|
||||
Launch ping command to aquilenet.fr (2a0c:e300::8) with 16 iterations
|
||||
16 packets transmitted, 16 received, 0% packet loss, time 15019ms
|
||||
```
|
||||
|
||||
## Responsive design inside
|
||||
|
||||
Maintenant que l'affichage des informations est en place, attardons nous sur le
|
||||
côté *responsive design* en adaptant la taille de notre barre à la largeur de
|
||||
l'écran.[Télécharger le script complet]({attach}./files/script2_getoutput.sh)
|
||||
|
||||
La première question est bien évidement comment obtenir la largeur de la fenêtre
|
||||
de terminal? Lorque *Bash* est lancé en **mode interactif** cette largeur --
|
||||
exprimée en nombre de colonnes [^colonne] -- est disponible via la variable
|
||||
`$COLUMNS`. Mais dans le cadre d'un script, cette variable n'est pas disponible
|
||||
si vous n'utilisez pas *Bash* comme interpréteur de commande.
|
||||
|
||||
Il est possible d'utiliser la command interne `shopt` pour activer l'option
|
||||
`checkwinsize` mais elle pose deux problèmes:
|
||||
|
||||
1. Je n'ai pas réussi à activer l'option avec la commande
|
||||
`shopt -s checkwinsize` dans mes tests (mais un simple `shopt` rend les
|
||||
variables `COLUMNS` et `LINES` disponible);
|
||||
2. la récupération des ces deux variables ne se fait **qu'après exécution d'une
|
||||
commande externe**, or il y en a peu dans notre script (`ping`), ce qui
|
||||
posera problème un peu plus loin dans cet article.
|
||||
|
||||
[^colonne]: une colonne correspond à la largeur d'un caractère
|
||||
|
||||
la commande `tput`, qui permet de récupérer des informations sur le terminal,
|
||||
fait très bien le job comme vous pouvez le voir dans ce petit bout de code
|
||||
ajouté dans la fonction `main()`:
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script6_responsive.sh!lines=54-57}
|
||||
```
|
||||
|
||||
### le dessin de la barre de progression
|
||||
|
||||
Maintenant que nous avons le nombre de colonnes, passons au dessin de notre
|
||||
barre de progression. Voici la nouvelle version de `draw_progressbar`:
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script6_responsive.sh!lines=14-34}
|
||||
```
|
||||
|
||||
Afin de déterminer la place disponible pour la barre de chargement (`$bar_size`)
|
||||
-- qui sera la partie responsive de notre ligne -- il faut soustaire la taille
|
||||
de notre segment d'information (`$info_segment`) à la largeur (`COLUMNS`).
|
||||
|
||||
La progression doit maintenant s'exprimer par le ratio (`$progress_ratio`) entre
|
||||
les éléments traités (`progress`) et le nombre total d'éléments (`total`).
|
||||
|
||||
L'expansion arithmétique de ***Bash* ne prend pas en charge les nombres
|
||||
flottants**, alors ce ratio s'exprime en pourcentage qui sera arrondi à
|
||||
l'entier le plus proche.
|
||||
|
||||
Il suffit ensuite de calculer le nombre de colonnes occupées par le *segment
|
||||
d'actions effectuées* (`$progress_segment_size`) avec les deux valeurs obtenues
|
||||
précédemment (`$bar_size` et `$progress_ratio`).
|
||||
|
||||
Pour obtenir le nombre de colonnes occupées par le *segment d'actions en
|
||||
attentes*. il suffit de soustraire la taille du segment d'actions effectuées
|
||||
(`$progress_segment_size`) à la taille de la barre (`$bar_size`).
|
||||
|
||||
La suite est simple, dans les deux instructions `printf -v` qui permettent
|
||||
d'affecter le bon nombre d'espaces, il suffit d'utiliser `progress_segment_size`
|
||||
et `todo_segment_size` et le tour est joué.
|
||||
|
||||
### Exécution du script
|
||||
|
||||
Il est temps de passer à l'exécution de cette version de notre script:
|
||||
|
||||
```
|
||||
bash script6_responsive.sh
|
||||
Launch ping command to aquilenet.fr (2a0c:e300::8) with 16 iterations
|
||||
Ping in progress (time: 23.1 ms) █████████████████████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
```
|
||||
|
||||
La ligne prend maintenant **toute la place disponible**, la barre de progression
|
||||
ne fait plus juste 16 colonnes. Mais un problème subsiste sur cette version.
|
||||
|
||||
Changer la taille de la fenêtre de terminal ne change pas la taille de la barre.
|
||||
Si agrandir la fenêtre ne pose pas trop de problèmes -- la barre ne prend pas
|
||||
toute la place disponible -- la réduction de la taille mène à ceci:
|
||||
|
||||

|
||||
|
||||
L'explication est simple: la valeur de `$COLUMNS` n'est pas rafraichie! Il
|
||||
serait tout à fait possible de récupérer le nombre de colonnes au début de la
|
||||
fonction `draw_progress`, mais ce ne serait pas très optimisé pour une
|
||||
fonctionnalité utilisée rarement. **Une meilleure solution existe...** (mais
|
||||
elle n'est pas si simple).
|
||||
|
||||
## SIGWINCH à la rescousse
|
||||
|
||||
Figurez-vous qu'un signal existe pour signifier un changement de taille d'une
|
||||
fenêtre de terminal. Mais l'implémentation dans notre script est plus épineuse
|
||||
qu'il n'y parait et nécessite quelques changements sur des fonctions d'affichage
|
||||
**pour que notre code soit compatible avec le plus d'émulateurs de terminal**
|
||||
possibles. [Télécharger le script complet]({attach}./files/script7_sigwinch.sh)
|
||||
|
||||
D'abord ajoutons la fonction `change_column_size` qui se charge d'appliquer le
|
||||
changement du nombre de colonnes de la fenêtre de terminal.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script7_sigwinch.sh!lines=56-61}
|
||||
```
|
||||
|
||||
Cette fonction se charge:
|
||||
|
||||
1. de remplir la ligne courante sur `stderr` d'espace pour l'effacer;
|
||||
2. ensuite d'effacer les caractères de la ligne entière grâce à la séquence
|
||||
`\033[0K\r` passée à `printf`;
|
||||
3. enfin de récupérer le nouveau nombre de colonnes.
|
||||
|
||||
Elle sera exécutée dès le changement de géométrie de la fenêtre de terminal
|
||||
(c'est ici que `SIGWINCH` intervient). Cependant une petite subtilité se cache
|
||||
dans la gestion des signaux en *Bash*: ils ne sont pas transmis au *sub-shell*
|
||||
et notre fonction `parse_output` est justement exécuté comme tel via la
|
||||
substitution de processus.
|
||||
|
||||
Pour notre la capture de notre signal `SIGWICH`, il est alors préférable d'ajouter la commande `trap` au début de notre fonction `parse_output`. Si vous voulez plus d'information sir les signaux en Bash, je vous coneille la lecture de mon
|
||||
[précédent article]({filename}../../2022/bash_les_pieges/index.md).
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script7_sigwinch.sh!lines=37-40}
|
||||
```
|
||||
### Exécution du script
|
||||
|
||||
Notre barre de progression se redimensionne correctement, mais deux problèmes
|
||||
persistent.
|
||||
|
||||
1. Il faut attendre le rafraichissement de la barre de progression pour qu'elle
|
||||
prenne la bonne dimension une fois la fenêtre redimensionnée.
|
||||
2. La diminution de la taille de la fenêtre de terminal crée des lignes "vides"
|
||||
|
||||

|
||||
|
||||
Un programme avec une véritable TUI [^TUI] utilise une boucle et des buffers
|
||||
pour gérer spécifiquement le rafraichissement de l'affichage. Dans le cadre d'un
|
||||
script et de cet article, nous garderons ces deux bugs mineurs et nous en
|
||||
resterons là.
|
||||
|
||||
[^TUI]: Terminal User Interface -- interface utilisateur dans le terminal
|
||||
|
||||
## Bonus stage: un script plus aboutit
|
||||
|
||||
Maintenant que tous nous avons une barre de progression fonctionnelle, il est
|
||||
temps de l'améliorer pour la rendre réutilisable et paramétrable -- et de mettre
|
||||
en place le segment manquant. [Télécharger le script complet]({attach}./files/script8_bonus.sh)
|
||||
|
||||
D'abord il est possible d'ajouter des variables globales pour paramétrer les
|
||||
différents segments de la fonction `draw_progressbar`,
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script8_bonus.sh!lines=5-18}
|
||||
```
|
||||
|
||||
Les variables finissant par `_TEMPLATE` servent de *templates* aux segments
|
||||
d'information et complémentaires. Elles contiennent des définitions de
|
||||
**formats** pour `printf` qui sont utilisée dans la fonction d'affichage comme
|
||||
par exemple la mise en gras du segment d'information.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script8_bonus.sh!lines=28 2 40-49 2 59-63}
|
||||
```
|
||||
|
||||
Dans `draw_progressbar`, les affichages du segment *d'information* et celui
|
||||
*complémentaire* sont conditionnels. Ensuite les formats des différentes
|
||||
commandes `printf` sont aussi donnés par les variables globales définies plus
|
||||
haut.
|
||||
|
||||
Nous avons maintenant une fonction `draw_progressbar` et les variables associées
|
||||
nous permettant de personnaliser son apparence en fonction du contexte.
|
||||
|
||||
## en conclusion
|
||||
|
||||
Au final, voici ce que que donne notre barre de progression:
|
||||
|
||||

|
||||
|
||||
Nous avons abordé beaucoup de choses dans cet article un peu plus long que
|
||||
d'habitude. Principalement autour de la commande `printf` et ses nombreuses
|
||||
possibilités (mais nous n'avons pas tout vu...).
|
||||
|
||||
J'espère vous avoir montré que par rapport à la commande `echo` habituellement
|
||||
utilisées dans les script, `printf` est réellement bien plus **puissante** et
|
||||
**utile**.
|
||||
|
||||
Merci à [Heuzef][heuzef], Yishan et [RavenRamirez][alois] pour les nombreuse
|
||||
relectures, corrections et conseils.
|
||||
|
||||
[heuzef]:https://heuzef.com/
|
||||
[alois]:https://aloisbouny.fr/
|
Loading…
Add table
Add a link
Reference in a new issue