Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

make bash subshells log to separate lines in stdout

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?

like image 896
glades Avatar asked Oct 15 '25 03:10

glades


2 Answers

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"
}
like image 179
John Bollinger Avatar answered Oct 18 '25 13:10

John Bollinger


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
like image 22
bruno Avatar answered Oct 18 '25 13:10

bruno