How to fix cursor position in Linux terminal using ANSI escape code? - terminal

I need to make it so that when I press a key, the cursor remains in its place (column) and just updates the symbol (doesn't move to the next column automatically). Is it possible to do this using ANSI escape code? I want to move the cursor only when I need it (for example, via printf("\e[1C");)
The terminal is in raw mode.

Put the terminal in non echo mode. Hence, your program controls what you want to display back on the screen.
Here is an example in shell which sets the terminal in raw and non echo mode, displays all the input chars at the same place (moving the cursor 1 step backward with "ESC[1D"), move the cursor 1 step forward if it is a space (with "ESC[1C") and exit if it is "Q":
#!/bin/sh
CSI="\e["
trap restore EXIT
restore() {
stty sane
}
# Terminal in RAW mode + non echo
stty cooked -echo
while true
do
read -n1 k >/dev/null 2>&1
case $k in
"") # Empty char (content of IFS= space, \n and TAB by default)
# move the cursor forward
echo -ne ${CSI}1C;;
Q) # quit the program
echo
exit 0;;
*) # Any other char is written in place
echo -ne $k${CSI}1D;;
esac
done
Doing the same in C is straightforward. Don't forget to call fflush(stdout) after the calls to printf() if they don't display \n

Related

How we can code Ctrl+D in a shell script?

I need to code Ctrl+D in a shell script in order to exit and display the result of a code.
The goal it's instead of "bye" in my code I tried to find a solution on google and in other website to replace "bye" by a Ctrl+D code.
Can you help me please?
This is my code :
touch file.txt
while :
do
shopt -s nocasematch
read INPUT_STRING
case $INPUT_STRING in
bob)
echo "boy">>file.txt
;;
alicia)
echo "girl">>file.txt
;;
cookie)
echo "dog">>file.txt
;;
bye)
cat file.txt
break
echo " "
;;
*)
echo "unknown">>file.txt
;;
esac
done
CTRL+D is not a sequence which is sent to input but closes the input so after CTRL+D read will exit with a non null exit code and the variable INPUT_STRING will be empty. in your script read exit code is not checked. Depending on what you need, you can either check if INPUT_STRING is empty or check read exit code.
while read INPUT_STRING; do
...
done
# put the code after
cat file.txt ...
or
while :; do
...
read INPUT_STRING || break
...
done
# put the code after
cat file.txt ...
or
case $INPUT_STRING in
...
'') # to match empty INPUT_STRING (could be because of CTRL+D)
How about checking the return status of read before going into your case?
E.g.:
if [ $? -eq 1 ] ;
...
fi
I don't think you can change the behaviour of read to trap the ctrl-d in the result variable.
Caveat: This answer literally does exactly what is requested. However, keyboard typed terminal input must quote the ctrl-D and send it ( ctrl+v   ctrl+d   Enter ) so the script can "see" it since bash keyboard processing will "trap & grab" and process the ctrl-D without propagation otherwise.
Consequently bash preprocesses, with some predefined function, every keyboard control code:
( ^# ^A ... ^Z ^[ ^\ ^] ^^ ^_ ^? where ^ means hold ctrl then type the character)
so that the ^V predefined function "quotes" and thus escapes other ctrl'dkey functionality, so the key character code can be propagated.
aside curio:
ctrl# aka ctrlshift2 over the decades usefully creates a null character code
ctrl[ aka Esc also useful when Esc is missing or broken
Cut to the chase:
echo ^v^d>ctrl-D.txt
gedit your-script.sh ctrl-D.txt
Highlight the ctrl-D character code (it is the only) character in ctrl-D.txt and copy and paste it to replace the bye in your-script.sh.
Note bene: the embedded code is exactly 0x04 preceded by white space and postceded by a ).
The script will now terminate on receiving a ctrl-D, but here's the rub. If bash is the shell it will not receive it because bash will have already processed it. AFAIK there does not exist a unix / linux shell which does not behave like this so it is necessary to quote the ctrl-D so the shell does not grab it. (Ctrl-D was one of the early primitives "used to end text input or to exit a Unix shell".)
To quote the ctrl-D and pass it on through the shell to the script type ctrl+v   ctrl+d   Enter in bash. YMMV (your mileage may vary) and some other quoting method may be available in other shells.
The long answer:
To embed a control code in a shell script quote it using ctrl+v.
It is understood that the ^letter sequences in the coding examples are not to be typed literally with an ^, but as a ctrl and letter combination.
Type ctrl+v ctrl+d contiguously.
(for expediency hold the ctrl and type v then d)
Thus typing echo ctrl+vd
linuxuser#ubuntu:~$ echo ^v^d
get this:
____
|00|
|04|
but as a single "code revealed" character, not the 12 _'s |'s #'s used here to represent the result.
To embed a control code like ^D in a file such as bye use
echo ctrl+vd >bye
Thus:
linuxuser#ubuntu:~$ echo now you can see -^v^d- via cat -v >bye
linuxuser#ubuntu:~$ cat -v bye
now you can see -^D- via cat -v
reveals the ctrld character by the two characters, an ^ and a D.
For the curious:
Note that quoting the quote to quote a code has been useful.
Thus (hold) ctrl (type) vvvz (and then release ctrl) will create a ^V^Z sequence in bash.
A poignant point worth noting is that files may contain arbitrary bytes. Ergo control codes per se do not "mean anything". It is on exposure to interpretation, like bash scripts, that various sequences might have meaning as control codes. It is very useful in bash that ctrlv can "disable" bash control code interpretation by its quoting mechanism. Most particularly, quoting is useful within files themselves that are to be interpreted by bash as scripts.
Coda:
linuxuser#ubuntu:~$ echo -e "echo 'ctrl-D^v^j -^v^d-' >bye \ncat -v bye" >doit.sh
linuxuser#ubuntu:~$ ./doit.sh
prints
ctrl-D
-^D-
tested with
linuxuser#ubuntu:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.2 LTS
Release: 18.04
Codename: bionic

In bash print to line above terminal output

EDIT: Corrected process/thread terminology
My shell script has a foreground process that reads user input and a background process that prints messages. I would like to print these messages on the line above the input prompt rather than interrupting the input. Here's a canned example:
sleep 5 && echo -e "\nINFO: Helpful Status Update!" &
echo -n "> "
read input
When I execute it and type "input" a bunch of times, I get something like this:
> input input input inp
INFO: Helpful Status Update!
ut input
But I would like to see something like this:
INFO: Helpful Status Update!
> input input input input input
The solution need not be portable (I'm using bash on linux), though I would like to avoid ncurses if possible.
EDIT: According to #Nick, previous lines are inaccessible for historical reasons. However, my situation only requires modifying the current line. Here's a proof of concept:
# Make named pipe
mkfifo pipe
# Spawn background process
while true; do
sleep 2
echo -en "\033[1K\rINFO: Helpful Status Update!\n> `cat pipe`"
done &
# Start foreground user input
echo -n "> "
pid=-1
collected=""
IFS=""
while true; do
read -n 1 c
collected="$collected$c"
# Named pipes block writes, so must do background process
echo -n "$collected" >> pipe &
# Kill last loop's (potentially) still-blocking pipe write
if kill -0 $pid &> /dev/null; then
kill $pid &> /dev/null
fi
pid=$!
done
This produces mostly the correct behavior, but lacks CLI niceties like backspace and arrow navigation. These could be hacked in, but I'm still having trouble believing that a standard approach hasn't already been developed.
The original ANSI codes still work in bash terminal on Linux (and MacOS), so you can use \033[F where \033 is the ESCape character. You can generate this in bash terminal by control-V followed by the ESCape character. You should see ^[ appear. Then type [F. If you test the following script:
echo "original line 1"
echo "^[[Fupdated line 1"
echo "line 2"
echo "line 3"
You should see output:
updated line 1
line 2
line 3
EDIT:
I forgot to add that using this in your script will cause the cursor to return to the beginning of the line, so further input will overwrite what you have typed already. You could use control-R on the keyboard to cause bash to re-type the current line and return the cursor to the end of the line.

Populating a variable from a script that needs to write to the terminal

How can I populate a variable passed to bash script, which is called from another bash script? I want to know about options and best practices.
Let me elaborate why the many questions answered by either setting a exit status or echoing are not sufficient:
exit n: n is restricted to 0 <= n <= 255
echo foo doesn't allow me to echo any relevant information in that script, alternate screen buffer also won't help with this.
I have figured out one possible solution:
#outer.sh
source inner.sh
populate result
echo "Evaluated: $result"
#inner.sh
function populate {
local __popvar=$1
eval $__popvar="'RETURN VALUE'"
}
I dislike this solution for three reasons:
The need to source the inner script, polluting global scope with helper functions.
The need to eval, especially because of the confusing multi-quotes.
Verbosity. I first need to source, and then call one function, whereas I'd much rather like to bash inner.sh result.
Further information on the inner script
The inner script is supposed to write to the alternate screen buffer. On this buffer, the user is able to select an option from an array (selection via arrow keys or ijkl style, confirmation with space or enter). This option should be returned from the script somehow. Returning the index is not an option, as the number of elements in the array can exceed 256. Code:
#!/usr/bin/bash
prompt=$1; shift
options=( "$#" )
c_opts=${#options[#]}
selected=0
# switch to alternate screen and trap the kill signal to switch back
tput smcup
trap ctrl_c INT
function ctrl_c {
tput rmcup
exit 1
}
function print_opts {
for (( i = 0; i < $c_opts; i++ )); do
if [[ i -eq $selected ]]; then
echo -e "\t\e[7m ${options[i]} \e[0m"
else
echo -e "\t ${options[i]} "
fi
done
}
function reset_term {
for (( i = 0; i < $c_opts; i++ )); do
tput cuu1 # move cursor up 1 line
tput el # delete current line
done
}
function reprint_opts {
reset_term
print_opts
}
echo $prompt
print_opts
while read -sN1 key; do
read -sN1 -t 0.0001 k1
read -sN1 -t 0.0001 k2
read -sN1 -t 0.0001 k3
key+="${k1}${k2}${k3}"
# colemak layout
case "$key" in
n|u|$'\e[A'|$'\e0A'|$'\e[D'|$'\e0D') # up or left
((selected > 0)) && ((selected--));;
e|i|$'\e[B'|$'\e0B'|$'\e[C'|$'\e0C') # down or right
((selected < $c_opts-1)) && ((selected++));;
$'\e[1~'|$'\e0H'|$'\e[H') # home key
selected=0;;
$'\e[4~'|$'\e0F'|$'\e[F') # end key
((selected = $c_opts-1));;
' '|$'\x0a') # enter or space
tput rmcup && echo ${options[$selected]} && exit 0;;
q|$'\e') # q or escape
tput rmcup && exit 0;;
esac
reprint_opts
done
Based on #JohnKugelman's comment, the script should be called as follows:
prompt="Your options are:"
options=(
"Option A"
"Option B"
"Option C"
"Option D"
)
result=$( exec 3>&1; bash select-menu.sh "$prompt" "${options[#]}" 2>&1 1>&3; exec 3>&- )
echo $result
This does seem an appealing solution, but it does not fix the problem. The Selection menu that is to be printed on the alternate screen buffer is not printed. Input however works correctly and the selection is stored in result.
To get a sense of the desired behavior you can replace the last two lines in the calling script like so:
bash select-menu.sh "$prompt" "${options[#]}"
Don't rule out result=$(inner.sh) just yet. If you want to display interactive prompts or dialogs in the script, do those on stderr, and have it write only the answer to stdout. Then you can have your cake and eat it too: interactive prompts and the result saved to a variable.
For example, dialog does exactly this if you use --output-fd 1 to tell it to write its answer to stdout. It uses curses to draw a dialog to the alternate screen but does it all on stderr.
$ value=$(dialog --keep-tite --output-fd 1 --inputbox title 10 40)
<dialog box shown>
<type "hello">
hello
(via Ask Ubuntu: How to get dialog box input directed to a variable?)
The script you posted can be made to do the same thing. It currently writes to stdout. Put exec 3>&1 1>&2 at the top so it'll write to stderr instead, and change the final echo ${options[$selected]} to echo ${options[$selected]} >&3 to write the answer to stdout. That'd get rid of the need for the caller to juggle file descriptors.
That said, prompts are not very UNIX-y. Consider eschewing interactivity entirely in favor of command-line arguments, configuration files, or environment variables. These options are better for power users who know how to use your script and want to automate it themselves.
My main purpose here is to commit a latest-stable-config from a selection of my last backups, which in my opinion needs the human judgement of when to consider a backup as appropriately stable.
The way I'd personally handle it is by writing a script with a couple of modes. Let's call it backups. backups --list would display a list of backups. You pick one and then call backups --commit <id> which would commit the named config. backups with no arguments would display usage for the unfamiliar user.
$ backups
Usage: backups --list
or: backups --commit <id>
Manages a selection of backups. Use --list to list available backups
and --commit to commit the latest stable config.
$ backups --list
4ac6 10 minutes ago
18f2 1 day ago
3019 7 days ago
$ backups --commit 4ac6

Korn Shell - How can I make “Press any key to continue”

I am writing korn shell script. I need to put funtionality called "Press any key to continue" to execute rest of the script.How can I achieve this?
Thanks in advance
You can use read command:
read -n1 -r -p "Press space to continue..." key
if [ "$key" = '' ]; then
# Space pressed, do something
# echo [$key] is empty when SPACE is pressed # uncomment to trace
else
# Anything else pressed, do whatever else.
# echo [$key] not empty
fi
This is very simple script.
#!/usr/bin/ksh
echo "First Method"
read -s -n 1 -p "Press any key to continue..."
# insert echo here for cleaner output
echo
echo "Second Method"
echo "Press any key to continue..."
read -s -n 1 any_key
echo "Now exiting"
exit 0
Information about the read command:
To get input from the keyboard, you use the read command. The read command takes input from the keyboard and assigns it to a variable. Here is an example:
echo -n "Enter some text > "
read text
echo "You entered: $text"
read is the command for input (usually form terminal). There is (nearly) no way around the <Enter>, its the way to tell read to start processing the input.
If you want to start processing after whatever keystroke, you have to dive deep into terminal settings via stty, changing to raw mode and .....
And it is likely your terminal is in foul mood when you are coming out of that.
The effort to avoid side effects is usually not worth the effect.
Think about, you changed the terminal mode, waiting for a keystroke, do you allow Ctrl-C, backspace, ESC, ... to be keystroke or do they need special handling? You changed the terminal mode, you are respnsible now. How to set the terminal to a sane state if the process has been killed , ....
If you are up for a big challenge, go for it :-)
OK, I understand why my post was deleted. Here I go again.
What I did is to create a function 'readOne' and call it when needed.
readOne () {
tput smso
echo "Press any key to return \c"
tput rmso
oldstty=`stty -g`
stty -icanon -echo min 1 time 0
dd bs=1 count=1 >/dev/null 2>&1
stty "$oldstty"
echo
}
Here is the explanation from the author :
The tty driver controls how input lines that you type are delivered to programs. Normally a tty driver will wait until a complete line is available. It also handles stuff like backspace so that program doesn'y need to. The stty command lets you change the way the tty driver works. "stty -a" will display all of the settings. You should do that to see what's available.
"stty -g" displays all of the settings too. But it's encoded and you can't understand the output. But you can save the output and feed it back into the stty command. So:
oldtty='stty -g'
stty $oldstty
will save and restore the original settings of the tty driver.
stty -icanon -echo min 1 time 0
is setting some options in the tty driver. -icanon turns off all special character processing. Now a backspace will be passed to the program rather than being processed. And a carriage return won't terminate a line. So now min and time control when a read has finished. "min 1" says we need at least one character. "time 0" means that we won't wait a while before completing a read. So each read from the program may return after just one character.
the -echo just turns off echo. I would not have done that.
dd is a program that is prepared to read data that is not organized into lines. This dd will read one block (count=1) of data. And that block will be one character in length (size=1). So the dd will read one character and return to the script.
The final echo moves the cursor to the next line.

Is it possible to make changes to a line written to STDOUT in shell?

Is it possible to make changes to a line written to STDOUT in shell, similar to the way many programs such as scp do?
The point would be to allow me to essentially have a ticker, or a monitor of some sort, without it scrolling all over the screen.
You can manipulate the terminal with control characters and ANSI escape codes. For example \b returns the cursor one position back, and \r returns it to the beginning of the line. This can be used to make a simple ticker:
for i in $(seq 10)
do
echo -en "Progress... $i\r" # -e is needed to interpret escape codes
sleep 1
done
echo -e "\nDone."
With ANSI escape codes you can do even more, like clear part of the screen, jump to any position you want, and change the output color.
You can overwrite the last printed line by printing the \r character.
For instance this:
for i in `seq 1 10`; do
echo -n $i;
sleep 1;
echo -n -e "\r" ;
done
Will print 1 then update it with 2 and so on until 10.
You can do modify the output of stdout using another program in a pipeline. When you run the program you use | to pipe the input into the next program. The next program can do whatever it wants with the output. A general purpose program for modifying the output of a program is sed, or you could write something yourself that modifies the data from the previous program.
A shell program would be something like:
while read line; do
# do something with $line and output the results
done
so you can just:
the_original_program | the_above_program

Resources