bash prinf \r multiple line show only one line - bash

I have code :
printf '\r%s' 'first line here' 'second line here' 'thrid line heere'
sleep 1
printf '\r%s' 'iam new' 'new again' 'and new'
but the script show only one line
i want show three line, each line will update after one second, without new line again
only three line and update all three line
thanks

Assuming the objective is to print 3 lines, wait a bit, and then overwrite the same 3 lines, then you'll need to incorporate some additional code that provides for 'up and down' movement of the cursor.
While you can hardcode some of this based on a specific terminal, an easier (most of the time) approach is to use something like tput to manage cursor movement.
In this case you'll need the ability to not only 'go up' with the cursor but also overwrite/clear a previous set of output in the case where the new output is shorter in length; we'll also need to replace the current \r with \n.
The general approach:
EraseToEOL=$(tput el) # grab code for clearing from cursor to end of line; useful for erasing text from a previous longer print
tput sc # save the point we want to return to
printf "%s\n" 'first line here' 'second line here' 'third line here'
sleep 1
tput rc # return to our save point (tput sc), ie, the place where the cursor was located just before the 1st printf
printf "%s${EraseToEOL}\n" 'i am new' 'new again' 'and new'
The results:
FWIW, here's what it looks like if we remove ${EraseToEOL} from the 2nd printf:

tl;dr :
printf "One Line\nSecond line\nThird line"
sleep 1
printf "\033[1F\033[1Fnew 1\nnew 2\nnew 3"
Not sure what you are trying to do exactly.
But I am under the impression that you want to update three different lines.
Eg, you want your output to go from
first line here
second line here
third line heere (sic)
to
iam new
new again
and new
If so, you can't do that with \n, \r...
As Paul 's explained, those are inherited from the days when there was no screen but printers. And not modern printers: some electronically controlled good old typing machines.
So sending 'A' to the machine, types 'A'.
Etc. That the ascii code.
And some special codes meant special behaviour.
Such as '\r' which return to the beginning of the line (as it is possible with a typing machine, as you know if you've ever seen one). Or '\n', going to next line (using the right handle). Or '\07' ringing the bell. Etc.
But you cannot go back 1 line. Well, not with basic control char.
Now, depending on what terminal (the application emulating a physical terminal such as VT100, which itself is a device emulating a paperless printer. The "size of train is derived from size of roman horse ass" joke, is not entirely a myth) you are using, you may use special control sequences.
The most likely to work is ESC [ 1 F one.
So, in bash
printf "One Line\nSecond line\nThird line"
sleep 1
printf "\033[1F\033[1Fnew 1\nnew 2\nnew 3"
You may need to print some spaces to erase the remaining of the former line (or use some other control sequence to do so. You could check vt100 control sequences to find many others)
For example
printf "\n\n" # just to "create" the 2 extra lines
n=1
while true
do
printf "\033[1F\033[1F"
printf "n = $n\n"
printf "n² = $((n*n))\n"
printf "1000000/n = $((1000000/n)) " # <- note the trailing spaces because this line may become shorter and shorter
((n++))
sleep 1
done
(\033 is ESCape character. So each \033[1F sequence take you to the beginning of the previous line)
It is important tho to understand that you are assuming, doing so, that the terminal you are using understand those sequence. \n and \r are supposed to work with any device that understands ascii control char. VT100 Escape sequence are only working with devices that were designed to be compatible with those sequences. Which is the case of most terminal, but not all. A notable exception is jupyter notebook for example (And probably windows command terminal, but I don't have one to check).
So, if you need a clean solution for such things, you need to find a library such as bashsimplecurses.
That will adapt to any terminal.

Carriage returns (\r) ONLY reset to the beginning of the line.
Newline characters (\n) "roll the paper" a line vertically.
Imagine controlling a physical print head.
The result is that you can use \r for dynamic output effects like a countdown.
printf "\n" # get a clean line to start so you don't print over something
for c in {10..1}; do printf "\rNew lines in: %2.2s" $c; sleep 1; done # countdown in place
printf "\r%20.20s\r" "" # blank the line and reset to overwrite cleanly
printf "New line %2.2s!\n" {1..10}; # include \n's to move to next line, NOT overwrite
This counts down in place, doesn't leave the hanging 0 from the 10 (that's what the %2.2s if for), and writes the first of the lines that stay over where the countdown was without leaving hanging characters there (that's the %20.20s).
edit
I think I understand the OP a little better today.
The main point was to use printf '%s\n' instead of printf '\r%s'.
Others have done an excellent job of demonstrating how to move the cursor back up the screen to a previous point, but I wanted to throw one more perspective.
IF it's ok for this stuff to be the ONLY thing on the screen, there's a simple solution without quite so much explicit vertical cursor management -
clear # blank the screen, putting the cursor at the top
printf '%s\n' 'first line here' 'second line here' 'third line here'
sleep 1
clear # blank the screen and put the cursor at the top again
printf '%s\n' 'I am new' 'new again' 'and new'
Sometimes simple is easier to understand, use, and remember.
The other solutions here are a lot more flexible, though.
I recommend learning those.

Related

How does a terminal "ASCII animation" work?

I am calling it an ASCII animation for lack of a better word. What I am referring to is for example a loading bar, let's say like in pacman (arch package manager) ,that starts like this...
[ ]
and turns over time to this...
[#### ]
From my understanding of stdout I can't seem to wrap my head around this seemingly simple feature.I expect...
[ ]
[# ]
[### ]
...
What I'm not understanding is how is it able to print on top of stdout ,(if it's even doing that).
We sometimes think of terminals as just showing text, but they're actually more like browsers, rendering their own little markup in the form of control characters and ANSI terminal escape codes.
Simple, single-line animations are typically done using the Carriage Return control character. By writing a carriage return, the cursor returns to the left-most margin, and you can therefore write over the line again as many times as you'd like.
You'd obviously use a loop but here's an example written out for clarity:
{
printf '[## ]'
sleep 1
printf '\r[### ]'
sleep 1
printf '\r[#### ]'
}
For more advanced animations, you can e.g. arbitrarily position the cursor by writing special ANSI escape sequences as text. The tput tool is helpful for this in shell scripts, and tput cup 4 50 will output an ANSI sequence to move the cursor to line 4 column 50. This is equivalent to printf '\x1B[4;50H' and just writes a snippet of magic text to the terminal.
Here's this functionality used for a starfield animation (ctrl-c to exit):
while sleep 0.1
do
tput cup $((RANDOM%LINES)) $((RANDOM%COLUMNS))
printf "*"
done
Even tools like top and nano show what they show by carefully writing text and control characters to create color, lines, refreshing lists, etc.

zsh truncates previous stdout when prompt width is same as COLUMNS

I have this simple zshrc which displays time in prommpt and resets it every 1 second
below is simplified version of my zshrc
repeat_string(){
# this works fine
printf "-%.0s" $(seq 1 $(( $COLUMNS - 1)))
# this doesn't works fine
# printf "-%.0s" $(seq 1 $COLUMNS)
}
TMOUT=1
TRAPALRM() {
PROMPT="$(repeat_string)
$(date)
hello >>>"
zle reset-prompt
}
I have simple function here repeat_string which I'm calling in my prompt string. purpose of this function is to display seperator (-) which has length equal to column width. It works fine when I pass repeat count which is not equal to $COLUMN. but if I pass $COLUMN, it behaves weird and truncates previous prompts and stdout also. Here is asciicinema link. https://asciinema.org/a/9FhIvtLD0XTnctEUXSRyZ9IrC
Use following script to quickly reproduce issue
mkdir /tmp/zshdebug
cat <<'EOF' > /tmp/zshdebug/.zshrc
repeat_string(){
# this works fine
printf "-%.0s" $(seq 1 $(( $COLUMNS - 1)))
# this doesn't works fine
# printf "-%.0s" $(seq 1 $COLUMNS)
}
TMOUT=1
TRAPALRM() {
PROMPT="$(repeat_string)
$(date)
hello >>>"
zle reset-prompt
}
EOF
ZDOTDIR=/tmp/zshdebug zsh
zsh version: zsh 5.2 (x86_64-apple-darwin16.0)
Found solution here https://www.zsh.org/mla/users/2011/msg00052.html
This is a problem with terminals and terminfo or termcap descriptions.
Some terminals silently discard a newline when one is output at the
right margin, others don't, and still others discard it only when it
is output at the right margin of the very last line of the screen.
Some terminals cause the cursor to wrap onto the next line right
away when the right margin is reached, others leave the cursor on
top of the last character that was output until at least one more
character appears.
Terminal description databases aren't very good at differentiating
these cases. ZLE decides in this case that the terminal both wraps
the line and does not discard the newline, so it believes it needs
to move up one line before redrawing the prompt. Your terminal
wraps at the margin but does discard the newline, so ZLE's internal
idea of what the screen looks like does not match reality.
You can get around this by replacing $'\n' with $'%1(l.\n.)' which
means to emit the newline only if ZLE believes that at least one
character has been output since the last (implicit) newline. If
the line is exactly $COLUMNS wide, ZLE will think that the terminal
has wrapped and that zero characters have been output since, so it
won't fool itself by emitting a newline that the terminal discards.
On a different terminal, though, you might need to go back to $'\n'
unconditionally.

Customize argument input routine for shell script

I have written a shell script for automating some tasks that I run from the terminal as -
v#ubuntu:$ ./automate.sh from:a1 to:a2 msg:'edited'
How can I (if at all) customize the script so as to enter each argument in a custom format on a separate line and execute it by pressing some other key to execute the shell script? So, I would do -
v#ubuntu:$ ./automate.sh
from : a1
to : a2
msg : 'next change'
... and then hit say Ctrl+Enter or F5 to execute this particular script?
NOTE : I know there is a hacky work around by simply typing ./automate.sh \ and hitting Enter after the trailing backslash to get a new line, but I was hoping to find a more elegant way to do this from within the script itself.
Also, I've purposely changed each argument to include whitespaces and the msg argument to include a string with spaces. So if anyone can point me in the right direction as to how to accomplish that as an added bonus, I'll be really grateful :)
If you know the number of arguments it is easy. Basics first.
#!/bin/bash
if [ $# == 0 ]
then
read v1 # gets onto new line. reads the whole line until ENTER
read v2 # same
read v3 # same
fi
# Parse $v1, $v2, $v3 as needed and run your script
echo ""
echo "Got |$v1|, |$v2|, |$v3|"
When you type automate.sh and hit enter the script is started, having received no arguments. With no arguments ($# == 0) the first read is executed, which prints a new line, waits, and gets the line typed in (once enter is hit) into $v1. The control goes back to the script, the next read gets the next typed line ... after the last one it drops out of if-else and continues. Parse your variables and run the script.
Session:
> automate.sh Enter
typed line Enter
more items Enter
yet more Enter
Got |typed line| |more items| |yet more|
>
You don't need Control-Enter or F5, it continues after 3 (three) lines.
This also allows you to provide both behaviors. Add an else, which will be executed if there are some arguments. You can then use the script by either supplying arguments on the first line (invocation you have so far), or in this new way.
If you need an unspecified number of arguments this approach will need more work.
Read words in input line into variables
If read is followed by variable names, like read v1 v2, then it reads each word into a variable, and the last variable gets everything that may have remained on the line. So replace read lines with
read k1 p1 s1
read k2 p2 s2
read k3 p3 s3
Now $k1 contains the first word (from), and $k2 and $k3 have the first words on their lines; then $p1 (etc) have the second word (:), and $s1 (etc) have everything else to the end of their lines (a1, a2, 'next change'). So you don't need those single quotes. All this is simple to modify if you want the script to print something on each line before input.
Based on the clarification in the comment, it is indeed desirable to not have to enter the whole strings, as one might think. This is "simple to modify"
read -p 'from :' s1
read -p 'to :' s2
read -p 'msg :' s3
Now the user only needs to enter the part after :, captured in $s variables. All else is the same.
See, for example: The section on user input in the Bash Guide; Their Advanced Guide (special variables); For a far more involved user interaction, this post. And, of course, man read.
It will be hard to bind Control-Enter in bash. If you are ok to change it for Control-D then everything might look like:
#!/usr/local/bin/bash
read -p 'From: ' from
read -p 'To: ' to
read -p 'Msg: ' msg
read keystroke
if [ "$keystroke" == "^D" ]; then
echo "$from $to $msg"
# do something else
fi

How to remove two lines from terminal output

Given that two lines have been printed out in the terminal, is it possible to delete both of them so they may be replaced with two new lines?
I know you can use \r to replace 1 line (well, to move the cursor to the start of the line), but is there any way of doing this for the line above?
As an example, I'm running a program for computing the eigenfunctions of the Schrodinger equation and I want to keep an eye on how my variables are changing as it's being run, so I'd like an output like:
Param 1: xxxxxxx
Param 2: xxxxxxx
So I'd have the two parameters on two lines so they can be easily read and they'd be updated on each iteration of the program's matching function.
The cuu1 terminal capability allows you to go up a line. Pass it to tput in order to read the character sequence from the terminfo/termcap database, and then echo it twice.
echo -e '123\nabc\n'"$(tput cuu1)$(tput cuu1)"'*\n*'
You could also use $(tput cuu 2) instead of $(tput cuu1)$(tput cuu1)
-- Aesthir

Block Comments in a Shell Script

Is there a simple way to comment out a block of code in a shell script?
In bash:
#!/bin/bash
echo before comment
: <<'END'
bla bla
blurfl
END
echo after comment
The ' and ' around the END delimiter are important, otherwise things inside the block like for example $(command) will be parsed and executed.
For an explanation, see this and this question.
There is no block comment on shell script.
Using vi (yes, vi) you can easily comment from line n to m
<ESC>
:10,100s/^/#/
(that reads, from line 10 to 100 substitute line start (^) with a # sign.)
and un comment with
<ESC>
:10,100s/^#//
(that reads, from line 10 to 100 substitute line start (^) followed by # with noting //.)
vi is almost universal anywhere where there is /bin/sh.
Use : ' to open and ' to close.
For example:
: '
This is a
very neat comment
in bash
'
This is from Vegas's example found here
You can use:
if [ 1 -eq 0 ]; then
echo "The code that you want commented out goes here."
echo "This echo statement will not be called."
fi
The following should work for sh,bash, ksh and zsh.
The blocks of code to be commented can be put inside BEGINCOMMENT and ENDCOMMENT:
[ -z $BASH ] || shopt -s expand_aliases
alias BEGINCOMMENT="if [ ]; then"
alias ENDCOMMENT="fi"
BEGINCOMMENT
echo "This line appears in a commented block"
echo "And this one too!"
ENDCOMMENT
echo "This is outside the commented block"
Executing the above code would result in:
This is outside the commented block
In order to uncomment the code blocks thus commented, say
alias BEGINCOMMENT="if : ; then"
instead of
alias BEGINCOMMENT="if [ ]; then"
in the example above.
if you can dodge the single quotes:
__='
blah blah comment.
'
In Vim:
go to first line of block you want to comment
shift-V (enter visual mode), up down highlight lines in block
execute the following on selection :s/^/#/
the command will look like this:
:'<,'>s/^/#
hit enter
e.g.
shift-V
jjj
:s/^/#
<enter>
You could use Vi/Vim's Visual Block mode which is designed for stuff like this:
Ctrl-V
Highlight first element in rows you want commented
Shift-i
#
esc
Uncomment would be:
Ctrl-V
Highlight #'s
d
l
This is vi's interactive way of doing this sort of thing rather than counting or reading line numbers.
Lastly, in Gvim you use ctrl-q to get into Visual Block mode rather than ctrl-v (because that's the shortcut for paste).
In all honesty, why so much overengineering...
I consider it really a bad practice to write active code for generating passive code.
My solution: most editors have block select mode. Just use it to add # to all lines you want to comment out.
What's the big deal...
Notepad example:
To create: Alt - mousedrag down, press #.
To delete: Alt-mousedrag down, shift-right arrow, delete.
A variation on the here-doc trick in the accepted answer by sunny256 is to use the Perl keywords for comments. If your comments are actually some sort of documentation, you can then start using the Perl syntax inside the commented block, which allows you to print it out nicely formatted, convert it to a man-page, etc.
As far as the shell is concerned, you only need to replace 'END' with '=cut'.
echo "before comment"
: <<'=cut'
=pod
=head1 NAME
podtest.sh - Example shell script with embedded POD documentation
etc.
=cut
echo "after comment"
(Found on "Embedding documentation in shell script")
You can put the code to comment inside a function. A good thing about this is you can "uncomment" by calling the function just after the definition.
Unless you plan to "uncomment" by calling the function, the text inside the function does not have to be syntactically correct.
ignored() {
echo this is comment
echo another line of comment
}
Many GUI editors will allow you to select a block of text, and press "{" to automatically put braces around the selected block of code.
Let's combine the best of all of these ideas and suggestions.
alias _CommentBegin_=": <<'_CommentEnd_'"
as has been said, the single quote is very important, in that without them
$(commandName) and ${varName} would get evaluated.
You would use it as:
_CommentBegin_
echo "bash code"
or
none code can be in here
_CommentEnd_
The alias makes the usage more obvious and better looking.
I like a single line open and close:
if [ ]; then ##
...
...
fi; ##
The '##' helps me easily find the start and end to the block comment. I can stick a number after the '##' if I've got a bunch of them. To turn off the comment, I just stick a '1' in the '[ ]'. I also avoid some issues I've had with single-quotes in the commented block.
Another mode is:
If your editor HAS NO BLOCK comment option,
Open a second instance of the editor (for example File=>New File...)
From THE PREVIOUS file you are working on, select ONLY THE PART YOU WANT COMMENT
Copy and paste it in the window of the new temporary file...
Open the Edit menu, select REPLACE and input as string to be replaced '\n'
input as replace string: '\n#'
press the button 'replace ALL'
DONE
it WORKS with ANY editor
In vscode ctrl+K+C (ctrl+K+U to uncomment).

Resources