Why \r required when using tput el1 - bash

While creating a countdown timer in bash, I came up with the following code:
for ((n=15; n > 0; n--)); do
printf "Reload | $n" && sleep 1
done
This works fine, it keeps adding the printf to the same line, as expected.
So I opened the documentation on tput and found:
tput el1
Clear to beginning of line
And tried it out:
for ((n=15; n > 0; n--)); do
tput el1; printf "Reload | $n" && sleep 1
done
However, this adds a tab on each iteration, so the output becomes:
Reload | 15
Reload | 14
Reload | 13
Those outputs are on the same line, so the 'clear' works but for some reason is the cursor not restored to the first column.
I've managed to fix it by adding a carriage return (\r) behind the printf:
for ((n=15; n > 0; n--)); do
tput el1; printf "Reload | $n\r" && sleep 1
done
I've read quite some docs, but can't grasp why the \r is needed here. Please point me to the right documentation/duplicate about this matter.

Compare el1
tput el1
Clear to beginning of line
and clear
tput clear
clear screen and home cursor
Note that clear explicitly states that it moves the cursor; el1 does not. It only erases whatever was on the current line between the start of the line and the current cursor position, leaving the cursor where it is for the following text.
The carriage return, on the other hand, is typically interpreted as moving the cursor to the beginning of the current line without advancing to the next line. The corresponding terminal capability would be cr.
A more robust solution would be to move the cursor first, then clear to the end of the line, and finally output your next bit of text. This handles the case where a new bit of text is shorter than the previous, which will be an issue when you switch from double-digit to single-digit numbers.
for ((n=15; n>0; n--)); do
tput cr el; printf "Reload | %2d" "$n"; sleep 1
done
(The %2d is to ensure the single-digit values don't "jump" one space to the left.)

Related

Can I print until the end of the line?

Consider this small example:
printf "Loading data..."; \
sleep 5; \
echo -e "\rThis is my cool data point."
This brings, of course, Loading data..., and after 5 seconds, that will be overwritten by This is my cool data point..
But what if the text printed out after the \r is shorter as the first line?
printf "Loading data..."; \
sleep 5; \
echo -e "\rNo data."
...brings No data.data... after the waiting time.
Do I have to keep track of the longest possible line and print "\rNo data. " or is there any "magic character" that fills the line until its end in a normal terminal?
You can delete to end of line with tput el. So you can do:
eol=$(tput el)
printf "Loading data..."
sleep 5
printf "\rNo data.${eol}\n"
It's not really a "magic character" so much as a "magic sequence", and the actual values that are used may vary with the terminal. tput will (should) do the right thing and give you a reasonably portable method. Attempting to determine precisely which sequence to use is a futile effort.
Another solution would be:
#!/bin/bash
msg1='Loading data...'
msg2='No data.'
printf '%s' "$msg1"
sleep 5
printf '\r%*s\n' $(( -(${#msg1} > ${#msg2} ? ${#msg1} : ${#msg2}) )) "$msg2"
This appends trailing spaces to the line if the length of msg1 is greater than that of msg2.

What happens in detail in this bash script?

In according to learn something about bash scripting I found this interesting bash script.
I am curious how this works in detail but I am a beginner in bash scripting.
I understand, that a semicolon seperates several commands and that there are some predefined variables used like $LINES and $RANDOM and something is being piped later on, but thats it.
And why is it only executable when pasted in one line?
INFO:
I am using Git Bash on Windows 10
Maybe someone with experience can do a walk through? Additional explanation is desirable.
echo -e "\e[1;40m" ; clear ; while :; do echo $LINES $COLUMNS $(( $RANDOM % $COLUMNS)) $(( $RANDOM % 72 )) ;sleep 0.05; done|awk '{ letters="ABCDEFGHIJKLMNOPQRSTUVWXYZムキノフレアイ√メリ0123456789()₿₹₵иηøρгţчςợгµαϤδχϞϗγπϥ";c=$4; letter=substr(letters,c,1);a[$3]=0;for (x in a) {o=a[x];a[x]=a[x]+1; printf "\033[%s;%sH\033[2;32m%s",o,x,letter; printf "\033[%s;%sH\033[1;37m%s\033[0;0H",a[x],x,letter;if (a[x] >= $1) { a[x]=0; } }}'
Very interesting script. Let me break down the one-liner for clarity adding some comments with a little modifications:
#!/bin/bash
shopt -s checkwinsize # bash updates the values of LINES and COLUMNS
echo -e "\e[1;40m"
clear
while :; do
echo "$LINES" "$COLUMNS" "$(( $RANDOM % $COLUMNS))" "$(( $RANDOM % 72 ))"
sleep 0.05
done | awk '
{letters="ABCDEFGHIJKLMNOPQRSTUVWXYZムキノフレアイ√メリ0123456789()₿₹₵иηøρгţчςợгµαϤδχϞϗγπϥ"
c=$4
letter=substr(letters, c, 1) # pick a character from letters in random
a[$3]=0 # a[x] holds a row position of column x
# pick a column in random to put a new character
for (x in a) { # loop over the columns which has characters on it
o=a[x] # original row position of column x
a[x]=a[x]+1 # increment the row position of column x
# to express the character falls down
printf "\033[%s;%sH\033[2;32m%s", o, x, letter
printf "\033[%s;%sH\033[1;37m%s\033[0;0H", a[x], x, letter
if (a[x] >= $1) { # if the row position of column x reaches the bottom line
a[x]=0 # then reset the row position
}
}
}'
The escape sequence, the point of the animation, will require additional explanation:
echo -e "\e[1;40m" set the character bold, and the background black
printf "\033[%s;%sH", o, x move cursor to row "o", column "x"
printf "\033[2;32m%s", letter put "letter" with dim green color
printf "\033[%s;%sH", a[x], x move cursor down to row "a[x]", column "x"
printf "\033[1;37m%s", letter put "letter" with white color
printf "\033[0;0H" move cursor to the top-left corner

How to display text at the bottom of the terminal window, and make it stay there, in Bash?

I am using a while loop to perform an ffmpeg operation that makes a bunch of files, and I want an indicator at the bottom of the screen that says what file I am on, that stays at the bottom of the screen, while ffmpeg is giving output. Sort of similar to how in the apt package manager, there is a progress bar that is always at the bottom, while it gives output information above it. I don't need a progress bar, just a string of text containing the file number to always be at the bottom.
A very simplified version of my code:
# Initialize file number
file_number=1
while IFS=, read -r starttime name endtime; do
# ffmpeg command
ffmpeg -ss $starttime_seconds -i "$1" -t $duration -codec libopus "$safename" < /dev/null
# Display progress, this is what I want at the bottom of the screen
echo -en "\r--- FILE $file_number ---"
file_number=$((file_number+1))
done < "$2"
With tput. Replace in your code
echo -en "\r--- FILE $file_number ---"
with
print_status
and put this before your code:
LINES=$(tput lines)
set_window ()
{
# Create a virtual window that is two lines smaller at the bottom.
tput csr 0 $(($LINES-2))
}
print_status ()
{
# Move cursor to last line in your screen
tput cup $LINES 0;
echo -n "--- FILE $file_number ---"
# Move cursor to home position, back in virtual window
tput cup 0 0
}
set_window
See: man tput and man 5 terminfo

Creating a progress bar for BASH script exporting system log files

Essentially for a set number of systems logs pulled and exported I need to indicate the scripts progress by printing a character "#". This should eventually create a progress bar with a width of 60. Something like what's presented below: ############################################# ,additionally I need the characters to build from left to right indicating the progression of the script.
The Question/Problem that this code was based off of goes as follows: "Use a separate invocation of wevtutil el to get the count of the number of logs and scale this to,say, a width of 60."
SYSNAM=$(hostname)
LOGDIR=${1:-/tmp/${SYSNAM}_logs}
i=0
LOGCOUNT=$( wevtutil el | wc -l )
x=$(( LOGCOUNT/60 ))
wevtutil el | while read ALOG
do
ALOG="${ALOG%$'\r'}"
printf "${ALOG}:\r"
SAFNAM="${ALOG// /_}"
SAFNAM="${SAFNAM//\//-}"
wevtutil epl "$ALOG" "${SYSNAM}_${SAFNAM}.evtx"
done
I've attempted methods such as using echo -ne "#", and printf "#%0.s" however the issue that I encounter is that the "#" characters gets printed with each instance of the name of the log file being retrieved; also the pattern is printed vertically rather than horizontally.
LOGCOUNT=$( wevtutil el | wc -l )
x=$(( LOGCOUNT/60 ))
echo -ne "["
for i in {1..60}
do
if [[ $(( x*i )) != $LOGCOUNT ]]
then
echo -ne "#"
#printf '#%0.s'
fi
done
echo "]"
printf "\n"
echo "Transfer Complete."
echo "Total Log Files Transferred: $LOGCOUNT"
I tried previously integrating this code into the first block but no luck. But something tells me that I don't need to establish a whole new loop, I keep thinking that the first block of code only needs a few lines of modification. Anyhow sorry for the lengthy explanation, please let me know if anything additional is needed for assistance--Thank you.
For the sake of this answer I'm going to assume the desired output is a 2-liner that looks something like:
$ statbar
file: /bin/cygdbusmenu-qt5-2.dll
[######## ]
The following may not work for everyone as it comes down to individual terminal attributes and how they can(not) be manipulated by tput (ie, ymmv) ...
For my sample script I'm going to loop through the contents of /bin, printing the name of each file as I process it, while updating the status bar with a new '#' after each 20 files:
there are 719 files under my /bin so there should be 35 #'s in my status bar (I add an extra # at the end once processing has completed)
we'll use a few tput commands to handle cursor/line movement, plus erasing previous output from a line
for printing the status bar I've pre-calculated the number of #'s and then use 2 variables ... $barspace for spaces, $barhash for #'s; for each 20 files I strip a space off $barspace and add a single # to $barhash; by (re)printing these 2x variables every 20x files I get the appearance of a moving status bar
Putting this all together:
$ cat statbar
clear # make sure we have plenty of room to display our status bar;
# if we're at the bottom of the console/window and we cause the
# windows to 'scroll up' then 'tput sc/rc' will not work
tput sc # save pointer/reference to current terminal line
erase=$(tput el) # save control code for 'erase (rest of) line'
# init some variables; get a count of the number of files so we can pre-calculate the total length of our status bar
modcount=20
filecount=$(find /bin -type f | wc -l)
# generate a string of filecount/20+1 spaces (35+1 for my particular /bin)
barspace=
for ((i=1; i<=(filecount/modcount+1); i++))
do
barspace="${barspace} "
done
barhash= # start with no #'s for this variable
filecount=0 # we'll re-use this variable to keep track of # of files processed so need to reset
while read -r filename
do
filecount=$((filecount+1))
tput rc # return cursor to previously saved terminal line (tput sc)
# print filename (1st line of output); if shorter than previous filename we need to erase rest of line
printf "file: ${filename}${erase}\n"
# print our status bar (2nd line of output) on the first and every ${modcount} pass through loop;
if [ ${filecount} -eq 1 ]
then
printf "[${barhash}${barspace}]\n"
elif [[ $((filecount % ${modcount} )) -eq 0 ]]
then
# for every ${modcount}th file we ...
barspace=${barspace:1:100000} # strip a space from barspace
barhash="${barhash}#" # add a '#' to barhash
printf "[${barhash}${barspace}]\n" # print our new status bar
fi
done < <(find /bin -type f | sort -V)
# finish up the status bar (should only be 1 space left to 'convert' to a '#')
tput rc
printf "file: -- DONE --\n"
if [ ${#barspace} -gt 0 ]
then
barspace=${barspace:1:100000}
barhash="${barhash}#"
fi
printf "[${barhash}${barspace}]\n"
NOTE: While testing I had to periodically reset my terminal in order for the tput commands to function properly, eg:
$ reset
$ statbar
I couldn't get the above to work on any of the (internet) fiddle sites (basically having problems getting tput to work with the web-based 'terminals').
Here's a gif displaying the behavior ...
NOTES:
the script does print every filename to stdout but since this script isn't actually doing anything with the files in question a) the printfs occur quite rapidly and b) the video/gif only captures a (relatively) few fleeting images ("Duh, Mark!" ?)
the last printf "file: -- DONE --\n" was added after I created the gif, and I'm being lazy by not generating and uploading a new gif

Color escape codes in pretty printed columns

I have a tab-delimited text file which I send to column to "pretty print" a table.
Original file:
1<TAB>blablablabla<TAB>aaaa bbb ccc
2<TAB>blabla<TAB>xxxxxx
34<TAB>okokokok<TAB>zzz yyy
Using column -s$'\t' -t <original file>, I get
1 blablablabla aaaa bbb xxx
2 blabla xxxxxx
34 okokokok zzz yyy
as desired. Now I want to add colors to the columns. I tried to add the escape codes around each tab-delimited field in the original file. column successfully prints in color, but the columns are no longer aligned. Instead, it just prints the TAB separators verbatim.
The question is: how can I get the columns aligned, but also with unique colors?
I've thought of two ways to achieve this:
Adjust the column parameters to make the alignment work with color codes
Redirect the output of column to another file, and do a search+replace on the first two whitespace-delimited fields (the first two columns are guaranteed to not contain spaces; the third column most likely will contain spaces, but no TAB characters)
Problem is, I'm not sure how to do either of those two...
For reference, here is what I'm passing to column:
Note that the fields are indeed separated by TAB characters. I've confirmed this with od.
edit:
There doesn't seem to be an issue with the colorization. I already have the file shown above with the color codes working. The issue is column won't align once I send it input with escape codes. I am thinking of passing the fields without color codes to column, then copying the exact number of spaces column output between each field, and using that in a pretty print scheme.
I wrote a bash version of column (similar to the one from util-linux) which works with color codes:
#!/bin/bash
which sed >> /dev/null || exit 1
version=1.0b
editor="Norman Geist"
last="04 Jul 2016"
# NOTE: Brilliant pipeable tool to format input text into a table by
# NOTE: an configurable seperation string, similar to column
# NOTE: from util-linux, but we are smart enough to ignore
# NOTE: ANSI escape codes in our column width computation
# NOTE: means we handle colors properly ;-)
# BUG : none
addspace=1
seperator=$(echo -e " ")
columnW=()
columnT=()
while getopts "s:hp:v" opt; do
case $opt in
s ) seperator=$OPTARG;;
p ) addspace=$OPTARG;;
v ) echo "Version $version last edited by $editor ($last)"; exit 0;;
h ) echo "column2 [-s seperator] [-p padding] [-v]"; exit 0;;
* ) echo "Unknow comandline switch \"$opt\""; exit 1
esac
done
shift $(($OPTIND-1))
if [ ${#seperator} -lt 1 ]; then
echo "Error) Please enter valid seperation string!"
exit 1
fi
if [ ${#addspace} -lt 1 ]; then
echo "Error) Please enter number of addional padding spaces!"
exit 1
fi
#args: string
function trimANSI()
{
TRIM=$1
TRIM=$(sed 's/\x1b\[[0-9;]*m//g' <<< $TRIM); #trim color codes
TRIM=$(sed 's/\x1b(B//g' <<< $TRIM); #trim sgr0 directive
echo $TRIM
}
#args: len
function pad()
{
for ((i=0; i<$1; i++))
do
echo -n " "
done
}
#read and measure cols
while read ROW
do
while IFS=$seperator read -ra COLS; do
ITEMC=0
for ITEM in "${COLS[#]}"; do
SITEM=$(trimANSI "$ITEM"); #quotes matter O_o
[ ${#columnW[$ITEMC]} -gt 0 ] || columnW[$ITEMC]=0
[ ${columnW[$ITEMC]} -lt ${#SITEM} ] && columnW[$ITEMC]=${#SITEM}
((ITEMC++))
done
columnT[${#columnT[#]}]="$ROW"
done <<< "$ROW"
done
#print formatted output
for ROW in "${columnT[#]}"
do
while IFS=$seperator read -ra COLS; do
ITEMC=0
for ITEM in "${COLS[#]}"; do
WIDTH=$(( ${columnW[$ITEMC]} + $addspace ))
SITEM=$(trimANSI "$ITEM"); #quotes matter O_o
PAD=$(($WIDTH-${#SITEM}))
if [ $ITEMC -ne 0 ]; then
pad $PAD
fi
echo -n "$ITEM"
if [ $ITEMC -eq 0 ]; then
pad $PAD
fi
((ITEMC++))
done
done <<< "$ROW"
echo ""
done
Example usage:
bold=$(tput bold)
normal=$(tput sgr0)
green=$(tput setaf 2)
column2 -s § << END
${bold}First Name§Last Name§City${normal}
${green}John§Wick${normal}§New York
${green}Max§Pattern${normal}§Denver
END
Output example:
I would use awk for the colorization (sed can be used as well):
awk '{printf "\033[1;32m%s\t\033[00m\033[1;33m%s\t\033[00m\033[1;34m%s\033[00m\n", $1, $2, $3;}' a.txt
and pipe it to column for the alignment:
... | column -s$'\t' -t
Output:
A solution using printf to format the ouput as well :
while IFS=$'\t' read -r c1 c2 c3; do
tput setaf 1; printf '%-10s' "$c1"
tput setaf 2; printf '%-30s' "$c2"
tput setaf 3; printf '%-30s' "$c3"
tput sgr0; echo
done < file
In my case, I wanted to selectively colorise values in a column depending on its value. Let's say I want okokokok to be green and blabla to be red.
I can do it such way (the idea is to colorise values of columns after columnisation):
GREEN_SED='\\033[0;32m'
RED_SED='\\033[0;31m'
NC_SED='\\033[0m' # No Color
column -s$'\t' -t <original file> | echo -e "$(sed -e "s/okokokok/${GREEN_SED}okokokok${NC_SED}/g" -e "s/blabla/${RED_SED}blabla${NC_SED}/g")"
Alternatively, with a variable:
DATA=$(column -s$'\t' -t <original file>)
GREEN_SED='\\033[0;32m'
RED_SED='\\033[0;31m'
NC_SED='\\033[0m' # No Color
echo -e "$(sed -e "s/okokokok/${GREEN_SED}okokokok${NC_SED}/g" -e "s/blabla/${RED_SED}blabla${NC_SED}/g" <<< "$DATA")"
Take a note of that additional backslash in values of color definitions. It is made for sed to not interpret an origingal backsash.
This is a result:
2021 Updated BASH Answer
TL;DR
I really liked #NORMAN GEIST's answer but was way too slow for what i needed... So i coded my own version of his script, this time written in Perl (stdin looping and formatting) + Bash (only for presentation/help).
You can find the full code here with an explanation on how to use it.
It is comprehensive of:
A Bash column-like command interface (same parameters like -t, -s, -o)
Exaustive help with column_ansi --help or column_ansi -h
Option to horizontally center.
The actual "core" code can broken down to only the Perl part.
Background and differences
I needed to format a very long awk-generated colored output (more than 300 lines) into a nice table.
I first thought of using column, but as i discovered it didn't take into consideration ANSI characters, since the output would come out not aligned.
After searching a bit on Google i found #NORMAN GEIST's interesting answer on SO which dynamically calculated the width of every single column in the output after removing the ANSI characters and THEN it built the table.
It was all good, but it was taking way too long to load (as someone pointed in the comments)...
So i tried to convert #NORMAN GEIST's column2 from bash to perl and my god if there was a change!
After trying out this version in my production script the time used to display data dropped from 30s to <1s!!
Enjoy!

Resources