I want to write a bash script that establishes an ssh connection to multiple hosts to execute a command. Because this can be very slow I want to do it in parallel using subshells and log the outputs in an overwriting manner to a designated line in my stdout. How do I get the logging part right? This is an example what ChatGPT came up with but it is not reliable as lines can switch places:
#!/bin/bash
# Total number of lines/tasks
NUM_TASKS=5
# Function to simulate work
run_task() {
local line=$1
local name=$2
for i in {1..10}; do
# Move cursor to the task's line
printf "\033[%d;0H" "$line"
# Clear line and print update
printf "\033[2KTask %s - step %d\n" "$name" "$i"
sleep 0.5
done
# Final message
printf "\033[%d;0H\033[2KTask %s - DONE\n" "$line" "$name"
}
# Clear screen
clear
# Launch all tasks in parallel
for ((i=0; i<NUM_TASKS; i++)); do
run_task $((i + 1)) "T$i" &
done
# Wait for all background jobs to finish
wait
# Move cursor below the last task line
printf "\033[%d;0HAll tasks completed.\n" $((NUM_TASKS + 2))
I need each function to only log to the respective assigned line. How can I do this?
it is not reliable as lines can switch places
This is at least because the tasks each use two separate commands to produce their interim output: one to position the cursor and a separate one to replace the contents of the current line. But you have multiple tasks all doing this, so it is possible for task A to position the cursor and then for task B to reposition it before task A emits its output.
I think you will find that that does not happen if the tasks each limit themselves to a single printf
command for each update they print:
# Function to simulate work
run_task() {
local line=$1
local name=$2
for i in {1..10}; do
# Move cursor to the task's line, clear it, and print the new status:
printf "\033[%d;0H\033[2KTask %s - step %d\n" "$line" "$name" "$i"
sleep 0.5
done
# Final message
printf "\033[%d;0H\033[2KTask %s - DONE\n" "$line" "$name"
}
About your comment That's a good idea. But how would I do that?
, I propose you that, just doing few changes from your script.
The calling script :
#!/bin/bash
# Total number of lines/tasks
NUM_TASKS=5
for ((i=0; i<NUM_TASKS; i++)); do
run_task $((i + 1)) "T$i" &
done
# Wait for all background jobs to finish
wait
# Move cursor below the last task line
printf "\033[%d;0HAll tasks completed.\n" $((NUM_TASKS + 2))
with that run_task
script, from your function, does the ssh
command (here replaced by the do_ssh
script) and write each read line on the right position
#!/bin/bash
line=$1
name=$2
#do_ssh simulate ssh exec
do_ssh 2>&1 | while IFS= read -r l; do
# Move cursor to the task's line
printf "\033[%d;0H" "$line"
# Clear line and print update
printf "\033[2KTask %s - %s\n" "$name" "$l"
sleep 0.5
done
# Final message
printf "\033[%d;0H\033[2KTask %s - DONE\n" "$line" "$name"
(as said in John Bollinger answer replace the two separated printf
by one only one)
and that do_ssh
script to simulate a ssh command producing outputs both on stdout
and stderr
:
#!/bin/bash
for i in {1..10}; do
# write on stdout
echo bla bla 1 from $$
# wait 1 or 2 seconds
sleep `expr 1 + $RANDOM % 2`
# write on stderr
echo bla bla 2 from $$ 1>&2
# wait 1 or 2 seconds
sleep `expr 1 + $RANDOM % 2`
done
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With