Writing an equivalent of Python's input function in bash - bash

I am trying to write a simple equivalent of Python's input function in Bash.
An example in a Python script would be something like this:
s = input('Please input something: ')
I imagine the call to a Bash equivalent would look like this:
s=$(input "Please input something: ")
This is the implementation that I wrote:
function input {
# $1 is the prompt passed as an argument; in my above example,
# it would be 'Please input something: '
printf "%s" "$1"
read in
echo "${in}"
return 0
}
With the call s=$(input "Please enter something: "), the prompt never prints; instead, the Bash script simply waits for user input without ever displaying the text. Pressing Enter, that is, giving no input, sets s to the prompt itself. What seems to be happening is s captures the output from the line printf "%s" "$1", then reads input which it also echoed back. I have also tried explicitly directing the prompt to stdout with printf "%s" "$1" >&1, but to no avail.
How can I print the prompt first, then capture input and echo it back to the calling function?

You're in luck. It already exists in the form of read -p.
read -p 'Please input something: ' s
To be safe, it's a good idea to use IFS= and -r as well. IFS= makes sure leading and trailing whitespace are retained. -r preserves backslashes so \n isn't interpreted as a newline, or \\ turned into \.
IFS= read -rp 'Please input something: ' s
As to why your function doesn't work, it's because the prompt is being printed to stdout, which is captured. Print to stderr to get it to show up.
input() {
printf "%s" "$1" >&2
read in
printf '%s\n' "$in"
}
A careful scripter will use printf '%s\n' in place of echo. If the user types -n or -e you want those printed, not interpreted as options.

Related

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

Bash Script: want to call a function that reads input

I'm writing a script that calls on a function that reads for input in multiple lines. I want to pass parameters into the read, but I don't know if I can or how to.
aka how to get enter-grades to take my values as input instead of waiting for input at the prompts?
Inside my bash script
...
login="studentName"
echo "enter score:"
read score
echo "comments:"
read comments
enter-grades $hw #---> calls another function (dont know definition)
#
# i want to pass parameters into enter-grades for each read
echo "$login" #---> input to enter-grade's first read
echo "$score $comments" #---> input to enter-grade's second read
echo "." #---> input to enter-grade's third read
outside my bash script
#calling enter-grades
> enter-grades hw2
Entering grades for assignment hw2.
Reading previous scores for hw2...
Done.
Enter grades one at a time. End with a login of '.'
Login: [READS INPUT HERE]
Grade and comments: [READS INPUT HERE]
Login: [READS INPUT HERE]
Assuming that enter-grades does not read directly from the terminal, just supply the information on that program's standard input:
login="studentName"
read -p "enter score: " score
read -p "comments: " comments
then, group your echo commands together, and pass all that into the program:
{
echo "$login"
echo "$score $comments"
echo "."
} | enter-grades "$hw"
or, succinctly
printf "%s\n" "$login" "$score $comments" "." | enter-grades "$hw"
Quote all your variables.
Or, with a here-doc
enter-grades "$hw" <<END
$login
$score $comments
.
END

Passing multiple arguments in bash (without getopt)

I have a bash script I am working on where I need to pass two arguments into a function. When I run the script everything works fine, except when it parses through the file I pass to it, the result I always get is Association (this is written in the text file). Can you advise what I would need to do so that it parses the correct info, which should be what I would type in as uid? I wrote in the input 123456789 because that's in the text file, but again I am getting Association as the result instead.
echo "Please enter the UID: ";
read uid
echo "Please enter server file: ";
read server
uidAssoc(){
arg1=$1
arg2=$2
for arg1 in $(cat ~/jlog/"$2"); do awk 'match($i, $2){ print substr($i, RSTART, RLENGTH)} ' ~/jlog/"$2";done;}
uidAssoc "$uid" "$server"
exit
The closing brace of the function requires whitespace in front of it (reference). Formatting your code more readably will help.
uidAssoc(){
arg1=$1
arg2=$2
for arg1 in $(cat ~/jlog/"$2"); do
awk 'match($i, $2){ print substr($i, RSTART, RLENGTH)} ' ~/jlog/"$2"
done
}
Some questions for you:
Why do you assign arg1 and arg2 but never use them?
What is $i in your awk script? (since i is unset, that will eventually evaluate to the whole line)
Are you aware that the $2 inside single quotes is different from the $2 outside of single quotes?

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

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

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.

Resources