Configure shell to always print prompt on new line, like zsh - bash

If a command's output does not end with a \n, the next prompt appears, awkwardly, immediately afterwards:
$ echo -n hai
hai$
I just noticed a colleague whose shell (zsh, for what it's worth) is configured to print a % (with background and foreground colours inverted for emphasis) followed by a \n in such cases:
$ echo -n hai
hai%
$
I'd like to do the same. I use Bash. Is this possible? If so, what would I add to my ~/.bashrc?
UPDATE
I've spent several hours gaining an understanding of how gniourf_gniourf's solution works. I'll share my findings here, in case they are of use to others.
ESC[6n is the control sequence introducer for accessing the cursor position (http://en.wikipedia.org/wiki/ANSI_escape_code).
\e is not a valid representation of ESC when using echo on OS X (https://superuser.com/q/33914/176942). \033 can be used instead.
IFS is Bash's internal field separator (http://tldp.org/LDP/abs/html/internalvariables.html#IFSREF).
read -sdR looks like shorthand for read -s -d -R, but in fact the "R" is not a flag, it's the value of the -d (delimiter) option. I decided to write read -s -d R instead to avoid confusion.
The double-parentheses construct, (( ... )), permits arithmetic expansion and evaluation (http://tldp.org/LDP/abs/html/dblparens.html).
Here's the relevant snippet from my .bashrc:
set_prompt() {
# CSI 6n reports the cursor position as ESC[n;mR, where n is the row
# and m is the column. Issue this control sequence and silently read
# the resulting report until reaching the "R". By setting IFS to ";"
# in conjunction with read's -a flag, fields are placed in an array.
local curpos
echo -en '\033[6n'
IFS=';' read -s -d R -a curpos
curpos[0]="${curpos[0]:2}" # strip leading ESC[
(( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
# set PS1...
}
export PROMPT_COMMAND=set_prompt
Note: The curpos[0]="${curpos[0]:2}" line is unnecessary. I included it so this code could be used in a context where the row is also relevant.

A little trick using PROMPT_COMMAND:
The value of the variable PROMPT_COMMAND is examined just before Bash prints each primary prompt. If PROMPT_COMMAND is set and has a non-null value, then the value is executed just as if it had been typed on the command line.
Hence, if you put this in your .bashrc:
_my_prompt_command() {
local curpos
echo -en "\E[6n"
IFS=";" read -sdR -a curpos
((curpos[1]!=1)) && echo -e '\E[1m\E[41m\E[33m%\E[0m'
}
PROMPT_COMMAND=_my_prompt_command
you'll be quite good. Feel free to use other fancy colors in the echo "%" part. You can even put the content of that in a variable so that you can modify it on the fly.
The trick: obtain the column of the cursor (with echo -en "\E[6n" followed by the read command) before printing the prompt and if it's not 1, print a % and a newline.
Pros:
pure bash (no external commands),
no subshells,
leaves your PS1 all nice and clean: if you want to change your PS1 sometimes (I do this when I work in deeply nested directory — I don't like having prompts that run on several miles), this will still work.
As tripleee comments, you could use stty instead of echoing a hard-coded control sequence. But that uses an external command and is not pure bash anymore. Adapt to your needs.
Regarding your problem with the ugly character codes that get randomly printed: this might be because there's still some stuff in the tty buffer. There might be several fixes:
Turn off and then on the echo of the terminal, using stty.
set_prompt() {
local curpos
stty -echo
echo -en '\033[6n'
IFS=';' read -d R -a curpos
stty echo
(( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
}
PROMPT_COMMAND=set_prompt
the main difference is that the echo/read combo has been wrapped with stty -echo/stty echo that respectively disables and enables echoing on terminal (that's why the -s option to read is now useless). In this case you won't get the cursor position correctly and this might lead to strange error messages, or the % not being output at all.
Explicitly clear the tty buffer:
set_prompt() {
local curpos
while read -t 0; do :; done
echo -en '\033[6n'
IFS=';' read -s -d R -a curpos
(( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
}
PROMPT_COMMAND=set_prompt
Just give up if the tty buffer can't be cleaned:
set_prompt() {
local curpos
if ! read -t 0; then
echo -en '\033[6n'
IFS=';' read -s -d R -a curpos
(( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
# else
# here there was still stuff in the tty buffer, so I couldn't query the cursor position
fi
}
PROMPT_COMMAND=set_prompt
As a side note: instead of reading in an array curpos, you can directly obtain the position of the cursor in variables, say, curx and cury as so:
IFS='[;' read -d R _ curx cury
If you only need the y-position cury:
IFS='[;' read -d R _ _ cury

Thanks to Gilles on unix.stackexchange:
You can make the bash display its prompt on the next line if the previous command left the cursor somewhere other than the last margin. Put this in your .bashrc (variation by GetFree of a proposal by Dennis Williamson)
From the two linked answers I distilled this solution:
PS1='\[\e[7m%\e[m\]$(printf "%$((COLUMNS-1))s")\r$ '
Explanation:
\[\e[7m%\e[m\] -- reverse video percent sign
printf "%$((COLUMNS-1))s" -- COLUMNS-1 spaces. The COLUMNS variable stores the width of your terminal if the checkwinsize options is set. Since the printf is within a $() sub-shell, instead of printing to the screen its output will be added to PS1
\r a carriage return character
So, basically, it's a % sign, a long sequence of spaces, followed by a return key. This works, but to be honest I don't understand why this has the desired effect. Specifically, why does it look like it adds a line break only when it's needed, otherwise no extra line break? Why are the spaces necessary there?

if you do echo $PS1 you will see de current code of your prompt like this:
\[\e]0;\u#\h: \w\a\]${debian_chroot:+($debian_chroot)}\u#\h:\w\$
now prepend it with a \n like this:
PS1="\n\[\e]0;\u#\h: \w\a\]${debian_chroot:+($debian_chroot)}\u#\h:\w\$"
now your prompt will always begin on a new line.

Related

Assign and/or manipulate incoming variables (string) from external program in bash

I have an external program which hands me a bunch of information via stdin ($1) to my script.
I get a line like the following:
session number="2018/06/20-234",data name="XTRDF_SLSLWX3_FSLO",data group="Testing",status="Error",data type="0"
Now I want to use this line split into single variables.
I thought about two ways until now:
INPUT='session number="2018/06/20-234",data name="XTRDF_SLSLWX3_FSLO",data group="Testing",status="Error",data type="0"'
echo "$INPUT" | tr ',' '\n' | tr ' ' '_' > vars.tmp
set vars.tmp
This will do the job until I have a data_name variable with a space in it, my trim command will automatically change it to _ and my assigned variable is no longer correct in upcoming checks.
So I thought about loading the input into a array and do some pattern substitution on the array to delete everything until and including the = and do some variable assignments afterwards
INPUT='session number="2018/06/20-234",data name="XTRDF_SLSLWX3_FSLO",data group="Testing",status="Error",data type="0"'
IFS=',' read -r -a array <<< "$INPUT"
array=("${array[#]/#*=/}")
session_number="${array[0]}"
data_name="${array[1]}"
....
But now I have a strange behaviour cutting the input if there is a = somewhere in the data name or data group and I have no idea if this is the way to do it. I'm pretty sure there should be no = in the data name or data group field compared to a space but you never know...
How could I do this?
Simple Case: No Commas Within Strings
If you don't need to worry about commas or literal quotes inside the quoted data, the following handles the case you asked about (stray =s within the data) sanely:
#!/usr/bin/env bash
case $BASH_VERSION in ''|[123].*) echo "ERROR: Requires bash 4.0 or newer" >&2; exit 1;; esac
input='session number="2018/06/20-234",data name="XTRDF_SLSLWX3_FSLO",data group="Testing",status="Error",data type="0"'
declare -A data=( )
IFS=, read -r -a pieces <<<"$input"
for piece in "${pieces[#]}"; do
key=${piece%%=*} # delete everything past the *first* "=", ignoring later ones
value=${piece#*=} # delete everything before the *first* "=", ignoring later ones
value=${value#'"'} # remove leading quote
value=${value%'"'} # remove trailing quote
data[$key]=$value
done
declare -p data
...results in (whitespace added for readability, otherwise literal output):
declare -A data=(
["data type"]="0"
[status]="Error"
["data group"]="Testing"
["data name"]="XTRDF_SLSLWX3_FSLO"
["session number"]="2018/06/20-234"
)
Handling Commas Inside Quotes
Now, let's say you do need to worry about commas inside your quotes! Consider the following input:
input='session number="123",error="Unknown, please try again"'
Now, if we try to split on commas without considering their position, we'll have error="Unknown and have please try again as a stray value.
To solve this, we can use GNU awk with the FPAT feature.
#!/usr/bin/env bash
case $BASH_VERSION in ''|[123].*) echo "ERROR: Requires bash 4.0 or newer" >&2; exit 1;; esac
input='session number="123",error="Unknown, please try again"'
# Why do so many awk people try to write one-liners? Isn't this more readable?
awk_script='
BEGIN {
FPAT = "[^=,]+=(([^,]+)|(\"[^\"]+\"))"
}
{
printf("%s\0", NF)
for (i = 1; i <= NF; i++) {
printf("%s\0", $i)
}
}
'
while :; do
IFS= read -r -d '' num_fields || break
declare -A data=( )
for ((i=0; i<num_fields; i++)); do
IFS= read -r -d '' piece || break
key=${piece%%=*}
value=${piece#*=}
value=${value#'"'}
value=${value%'"'}
data[$key]=$value
done
declare -p data # maybe invoke a callback here, before going on to the next line
done < <(gawk "$awk_script" <<<"$input")
...whereafter output is properly:
declare -A data=(["session number"]="123" [error]="Unknown, please try again" )

Bash read function returns error code when using new line delimiter

I have a script that I am returning multiple values from, each on a new line. To capture those values as bash variables I am using the read builtin (as recommended here).
The problem is that when I use the new line character as the delimiter for read, I seem to always get a non-zero exit code. This is playing havoc with the rest of my scripts, which check the result of the operation.
Here is a cut-down version of what I am doing:
$ read -d '\n' a b c < <(echo -e "1\n2\n3"); echo $?; echo $a $b $c
1
1 2 3
Notice the exit status of 1.
I don't want to rewrite my script (the echo command above) to use a different delimiter (as it makes sense to use new lines in other places of the code).
How do I get read to play nice and return a zero exit status when it successfully reads 3 values?
Update
Hmmm, it seems that I may be using the "delimiter" wrongly. From the man page:
-d *delim*
The first character of delim is used to terminate the input line,
rather than newline.
Therefore, one way I could achieve the desired result is to do this:
read -d '#' a b c < <(echo -e "1\n2\n3\n## END ##"); echo $?; echo $a $b $c
Perhaps there's a nicer way though?
The "problem" here is that read returns non-zero when it reaches EOF which happens when the delimiter isn't at the end of the input.
So adding a newline to the end of your input will make it work the way you expect (and fix the argument to -d as indicated in gniourf_gniourf's comment).
What's happening in your example is that read is scanning for \ and hitting EOF before finding it. Then the input line is being split on \n (because of IFS) and assigned to $a, $b and $c. Then read is returning non-zero.
Using -d for this is fine but \n is the default delimiter so you aren't changing anything if you do that and if you had gotten the delimiter correct (-d $'\n') in the first place you would have seen your example not work at all (though it would have returned 0 from read). (See http://ideone.com/MWvgu7)
A common idiom when using read (mostly with non-standard values for -d is to test for read's return value and whether the variable assigned to has a value. read -d '' line || [ "$line" ] for example. Which works even when read fails on the last "line" of input because of a missing terminator at the end.
So to get your example working you want to either use multiple read calls the way chepner indicated or (if you really want a single call) then you want (See http://ideone.com/xTL8Yn):
IFS=$'\n' read -d '' a b c < <(printf '1 1\n2 2\n3 3')
echo $?
printf '[%s]\n' "$a" "$b" "$c"
And adding \0 to the end of the input stream (e.g. printf '1 1\n2 2\n3 3\0') or putting || [ "$a" ] at the end will avoid the failure return from the read call.
The setting of IFS for read is to prevent the shell from word-splitting on spaces and breaking up my input incorrectly. -d '' is read on \0.
-d is the wrong thing to use here. What you really want is three separate calls to read:
{ read a; read b; read c; } < <(echo $'1\n2\n3\n')
Be sure that the input ends with a newline so that the final read has an exit status of 0.
If you don't know how many lines are in the input ahead of time, you need to read the values into an array. In bash 4, that takes just a single call to readarray:
readarray -t arr < <(echo $'1\n2\n3\n')
Prior to bash 4, you need to use a loop:
while read value; do
arr+=("$value")
done < <(echo $'1\n2\n3\n')
read always reads a single line of input; the -d option changes read's idea of what terminates a line. An example:
$ while read -d'#' value; do
> echo "$value"
> done << EOF
> a#b#c#
> EOF
a
b
c

How do I detect input from the user while a bash script is in execution? [duplicate]

I have a shell script that essentially says something like
while true; do
read -r input
if ["$input" = "a"]; then
echo "hello world"
fi
done
That is all well, and good, but I just realized having to hit ENTER presents a serious problem in this situation. What I need is for the script to respond when a key is pressed, without having to hit enter.
Is there a way to achieve this functionality within a shell script?
read -rsn1
Expect only one letter (and don't wait for submitting) and be silent (don't write that letter back).
so the final working snippet is the following:
#!/bin/bash
while true; do
read -rsn1 input
if [ "$input" = "a" ]; then
echo "hello world"
fi
done
Another way of doing it, in a non blocking way(not sure if its what you want). You can use stty to set the min read time to 0.(bit dangerous if stty sane is not used after)
stty -icanon time 0 min 0
Then just run your loop like normal. No need for -r.
while true; do
read input
if ["$input" = "a"]; then
echo "hello world"
fi
done
IMPORTANT!
After you have finished with non blocking you must remember to set stty back to normal using
stty sane
If you dont you will not be able to see anything on the terminal and it will appear to hang.
You will probably want to inlcude a trap for ctrl-C as if the script is quit before you revert stty back to normal you will not be able to see anything you type and it will appear the terminal has frozen.
trap control_c SIGINT
control_c()
{
stty sane
}
P.S Also you may want to put a sleep statement in your script so you dont use up all your CPU as this will just continuously run as fast as it can.
sleep 0.1
P.S.S It appears that the hanging issue was only when i had used -echo as i used to so is probably not needed. Im going to leave it in the answer though as it is still good to reset stty to its default to avoid future problems.
You can use -echo if you dont want what you have typed to appear on screen.
You can use this getkey function:
getkey() {
old_tty_settings=$(stty -g) # Save old settings.
stty -icanon
Keypress=$(head -c1)
stty "$old_tty_settings" # Restore old settings.
}
It temporarily turns off "canonical mode" in the terminal settings
(stty -icanon) then returns the input of "head" (a shell built-in) with the -c1 option which is returning ONE byte of standard input. If you don't include the "stty -icanon" then the script echoes the letter of the key pressed and then waits for RETURN (not what we want). Both "head" and "stty" are shell built-in commands. It is important to save and restore the old terminal settings after the key-press is received.
Then getkey() can be used in combination with a "case / esac" statement for interactive one-key selection from a list of entries:
example:
case $Keypress in
[Rr]*) Command response for "r" key ;;
[Ww]*) Command response for "w" key ;;
[Qq]*) Quit or escape command ;;
esac
This getkey()/case-esac combination can be used to make many shell scripts interactive. I hope this helps.
How to read a single key press into variable c, and print it out. This prints out the key you pressed instantly, withOUT you having to press Enter first:
read -n1 c && printf "%s" "$c"
Or, with a little more "prettifying" in the output print:
read -n1 c && printf "\nYou Pressed: %s\n" "$c"
Example output of the latter command:
$ read -n1 c && printf "\nYou Pressed: %s\n" "$c"
M
You Pressed: M
To suppress your initial keypress from being echoed to the screen, add the -s option as well, which says from the read --help menu:
-s do not echo input coming from a terminal
Here is the final command:
read -sn1 c && printf "You Pressed: %s\n" "$c"
And a demo:
$ read -sn1 c && printf "You Pressed: %s\n" "$c"
You Pressed: p
You can also optionally separate the -sn1 argument into two arguments (-s -n1) for clarity.
References:
I learned about read -n1 from #pacholik here.
See also:
read_keypress.sh in my eRCaGuy_hello_world repo.
I use this bash cmd in C and C++ system calls to read keys here:
[my answer] Capture characters from standard input without waiting for enter to be pressed
[my answer] Read Key pressings in C ex. Arrow keys, Enter key
I have a way to do this in my project: https://sourceforge.net/p/playshell/code/ci/master/tree/source/keys.sh
It reads a single key everytime key_readonce is called. For special keys, a special parsing loop would run to also be able to parse them.
This is the crucial part of it:
if read -rn 1 -d '' "${T[#]}" "${S[#]}" K; then
KEY[0]=$K
if [[ $K == $'\e' ]]; then
if [[ BASH_VERSINFO -ge 4 ]]; then
T=(-t 0.05)
else
T=(-t 1)
fi
if read -rn 1 -d '' "${T[#]}" "${S[#]}" K; then
case "$K" in
\[)
KEY[1]=$K
local -i I=2
while
read -rn 1 -d '' "${T[#]}" "${S[#]}" "KEY[$I]" && \
[[ ${KEY[I]} != [[:upper:]~] ]]
do
(( ++I ))
done
;;
O)
KEY[1]=$K
read -rn 1 -d '' "${T[#]}" 'KEY[2]'
;;
[[:print:]]|$'\t'|$'\e')
KEY[1]=$K
;;
*)
__V1=$K
;;
esac
fi
fi
utils_implode KEY __V0

shell script respond to keypress

I have a shell script that essentially says something like
while true; do
read -r input
if ["$input" = "a"]; then
echo "hello world"
fi
done
That is all well, and good, but I just realized having to hit ENTER presents a serious problem in this situation. What I need is for the script to respond when a key is pressed, without having to hit enter.
Is there a way to achieve this functionality within a shell script?
read -rsn1
Expect only one letter (and don't wait for submitting) and be silent (don't write that letter back).
so the final working snippet is the following:
#!/bin/bash
while true; do
read -rsn1 input
if [ "$input" = "a" ]; then
echo "hello world"
fi
done
Another way of doing it, in a non blocking way(not sure if its what you want). You can use stty to set the min read time to 0.(bit dangerous if stty sane is not used after)
stty -icanon time 0 min 0
Then just run your loop like normal. No need for -r.
while true; do
read input
if ["$input" = "a"]; then
echo "hello world"
fi
done
IMPORTANT!
After you have finished with non blocking you must remember to set stty back to normal using
stty sane
If you dont you will not be able to see anything on the terminal and it will appear to hang.
You will probably want to inlcude a trap for ctrl-C as if the script is quit before you revert stty back to normal you will not be able to see anything you type and it will appear the terminal has frozen.
trap control_c SIGINT
control_c()
{
stty sane
}
P.S Also you may want to put a sleep statement in your script so you dont use up all your CPU as this will just continuously run as fast as it can.
sleep 0.1
P.S.S It appears that the hanging issue was only when i had used -echo as i used to so is probably not needed. Im going to leave it in the answer though as it is still good to reset stty to its default to avoid future problems.
You can use -echo if you dont want what you have typed to appear on screen.
You can use this getkey function:
getkey() {
old_tty_settings=$(stty -g) # Save old settings.
stty -icanon
Keypress=$(head -c1)
stty "$old_tty_settings" # Restore old settings.
}
It temporarily turns off "canonical mode" in the terminal settings
(stty -icanon) then returns the input of "head" (a shell built-in) with the -c1 option which is returning ONE byte of standard input. If you don't include the "stty -icanon" then the script echoes the letter of the key pressed and then waits for RETURN (not what we want). Both "head" and "stty" are shell built-in commands. It is important to save and restore the old terminal settings after the key-press is received.
Then getkey() can be used in combination with a "case / esac" statement for interactive one-key selection from a list of entries:
example:
case $Keypress in
[Rr]*) Command response for "r" key ;;
[Ww]*) Command response for "w" key ;;
[Qq]*) Quit or escape command ;;
esac
This getkey()/case-esac combination can be used to make many shell scripts interactive. I hope this helps.
How to read a single key press into variable c, and print it out. This prints out the key you pressed instantly, withOUT you having to press Enter first:
read -n1 c && printf "%s" "$c"
Or, with a little more "prettifying" in the output print:
read -n1 c && printf "\nYou Pressed: %s\n" "$c"
Example output of the latter command:
$ read -n1 c && printf "\nYou Pressed: %s\n" "$c"
M
You Pressed: M
To suppress your initial keypress from being echoed to the screen, add the -s option as well, which says from the read --help menu:
-s do not echo input coming from a terminal
Here is the final command:
read -sn1 c && printf "You Pressed: %s\n" "$c"
And a demo:
$ read -sn1 c && printf "You Pressed: %s\n" "$c"
You Pressed: p
You can also optionally separate the -sn1 argument into two arguments (-s -n1) for clarity.
References:
I learned about read -n1 from #pacholik here.
See also:
read_keypress.sh in my eRCaGuy_hello_world repo.
I use this bash cmd in C and C++ system calls to read keys here:
[my answer] Capture characters from standard input without waiting for enter to be pressed
[my answer] Read Key pressings in C ex. Arrow keys, Enter key
I have a way to do this in my project: https://sourceforge.net/p/playshell/code/ci/master/tree/source/keys.sh
It reads a single key everytime key_readonce is called. For special keys, a special parsing loop would run to also be able to parse them.
This is the crucial part of it:
if read -rn 1 -d '' "${T[#]}" "${S[#]}" K; then
KEY[0]=$K
if [[ $K == $'\e' ]]; then
if [[ BASH_VERSINFO -ge 4 ]]; then
T=(-t 0.05)
else
T=(-t 1)
fi
if read -rn 1 -d '' "${T[#]}" "${S[#]}" K; then
case "$K" in
\[)
KEY[1]=$K
local -i I=2
while
read -rn 1 -d '' "${T[#]}" "${S[#]}" "KEY[$I]" && \
[[ ${KEY[I]} != [[:upper:]~] ]]
do
(( ++I ))
done
;;
O)
KEY[1]=$K
read -rn 1 -d '' "${T[#]}" 'KEY[2]'
;;
[[:print:]]|$'\t'|$'\e')
KEY[1]=$K
;;
*)
__V1=$K
;;
esac
fi
fi
utils_implode KEY __V0

newline character in POSIX shell

I'm parsing output of avahi-browse tool and my script should be POSIX compatible.
I'm doing it next way:
local _dnssd=`avahi-browse -apt`
if [ -z "$_dnssd" ]; then
echo "No info"
else
IFS='
' # it's new line character in IFS
for _row in $_dnssd
do
local _tmpIFP="$IFS"
IFS=";"
case "$_row" in
...
esac
IFS="$_tmpIFS"
done
fi
I really don't like line with newline assignment to IFS. Is it possible to replace it in better way?
I tried some suggestions from stackoverflow, but it doesn't work:
IFS=$(echo -e '\n')
avahi-browse output:
+;br0;IPv4;switch4B66E4;_http._tcp;local
+;br0;IPv4;switch4B66E4;_csco-sb._tcp;local
Add a space after \n in the IFS variable, then remove that space again:
IFS="$(printf '\n ')" && IFS="${IFS% }"
#IFS="$(printf '\n ')" && IFS="${IFS%?}"
printf '%s' "$IFS" | od -A n -c
It's better to use a while loop than trying to iterate over a string that contains the entire output.
avahi-browse -apt | while IFS=";" read field1 field2 ...; do
case ... in
...
esac
done
Note you should need one name per field for the read command. The ... is just a placeholder, not valid shell syntax for a variable number of fields.
This simply does nothing if the program produces no output. If you really need to detect that case, try
avahi-browse -apt | {
read line || { echo "No info"; exit; }
while : ; do
IFS=";" read field1 field2 ... <<EOF
$line
EOF
case ... in
...
esac
read line || break
done
}
In both cases, any variables set in the right-hand side of the pipe are local to that shell. If you need to set variables for later use, you'll need to make some further adjustments.
If one can rely* that IFS has its default value (space, tab, new line) then one can simply strip the first two characters (space and tab) and the new line character will remain:
IFS=${IFS#??}
*You can rely on it if IFS has not been modified by the script before and if it is a POSIX shell (as the topic implies):
The shell shall set IFS to <space><tab><newline> when it is invoked.
See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_05_03

Resources