Select in loop don't ask for input - bash

I'm trying to ask a question in a loop (ask confirmation to treat each file in a directory, for instance).
This piece of code work perfectly :
PS3="Dummy question ? "
select CHOICE_MADE in "Ans1" 'Ans2' 'Ans3'; do
if [[ -n ${CHOICE_MADE} ]]; then
printf "Choice made : %s\n" "${CHOICE_MADE}"
break;
fi
done
I need to loop this question (Note I name the variable in loop, to avoid using default $REPLY variable) :
while read -r TOTO; do
PS3="Dummy question ? "
select CHOICE_MADE in Ans1 Ans2 Ans3; do
if [[ -n ${CHOICE_MADE} ]]; then
printf "Choice made : %s\n" "${CHOICE_MADE}"
break;
fi
done
done < <(echo Step1 Step2)
Select doesn't prompt anything.
I don't want to use whiptail (which masks script previous output), dialog (need to be installed on debian) or any gui.
I know I could use read to solve my problem, but I would like keeping select loop.
Thanks in advance for your help.

You have to note two things in order to make it works.
1) Bad use of `stdin`
First of all, passing < (echo Step1 Step2) to your while cycle, you are making use of stdin and that's why the prompt does not wait for any input at all (It already has it!).
You can get rid of it by using file descriptors:
exec 3< <(echo Step1 Step2) ### Create fd "3" and put output of echo in it
while read -r TOTO <&3; do
PS3="Dummy question ? "
select CHOICE_MADE in Ans1 Ans2 Ans3; do
if [[ -n ${CHOICE_MADE} ]]; then
printf "Choice made : %s\n" "${CHOICE_MADE}"
break
fi
done
done
exec 3>&- ### clean fd 3
I left unchanged the rest of code.
2) Bad use of `read`
Another thing you have to think of, is that read -r will read line by line what you give him, and a simple echo Step1 Step2 will produce only a single line, so you will cycle through just a single occurrence.
In order to get rid of this unwanted behaviour you will have to use other solutions.
One solution I can think of at the moment is heredoc:
exec 3<< EOF
Step1
Step2
EOF
while read -r TOTO <&3; do
PS3="Dummy question ? "
select CHOICE_MADE in Ans1 Ans2 Ans3; do
if [[ -n ${CHOICE_MADE} ]]; then
printf "Choice made: %s\n" "${CHOICE_MADE}"
break
fi
done
done
exec 3>&- ### clean fd 3
Note that using several heredoc is not advisable and best practices suggest to use actual files instead, because a script should contains logic, not data.

Related

How to "read" in bash without pressing enter

I've done a shell (bash) script that applies predefined rules to files dropped into the terminal.
It works quite well but because it uses 'read' it requires to press Enter once the files are dropped to the term window
This is part of the current code
while true ; do
echo "Drop file(s) here then press [ENTER]:"
echo "( x,q or exit,quit )"
read -p "> " read_file
while read dropped_file ;do
if [ -e ${dropped_file} ] ; then
...bunch of code here...
else
[[ "${dropped_file}" == *[xXqQ]* ]] && exit 1
fi
done <<< $(echo ${read_file} | tr " " "\n")
clear
done
I'd like to omit to press Enter each time I drop files and I was wondering if there is some wizardry to avoid to interact with the keyboard, except when I want to quit the script
Any help would be appreciated
Thanks in advance
EDIT
I've solved with this
while true ; do
read -sn1 read_file
while read splitted_filename; do
dropped_file="${dropped_file}${splitted_filename}"
done <<< $(echo ${read_file})
functionAction
[[ "${dropped_file}" == [xXqQ] ]] && exit 1
done
I think you can use the yes, printf, or the expect command and use the \n key, which should be able to perform what you are looking for.

Getting wrong value of passed argument to function in bash script

I am writing bash script given below (Please ignore the capital letters variable names, this is just my test file):
#!/bin/bash
create_nodes_directories(){
HOSTS=(192.168.110.165 192.168.110.166 192.168.110.167)
accounts=('accountnum11' 'accountnum12' 'accountnum13')
for i in "${!HOSTS[#]}"; do
read -r curhost _ < <(hostname -I)
printf 'Enter the key pair for the %s node\n' "${accounts[i]}"
printf "Enter public key\n"
read -r EOS_PUB_KEY
printf "Enter private key\n"
read -r EOS_PRIV_KEY
PRODUCER=${accounts[i]}
args=()
args+=("$curhost")
for j in "${!HOSTS[#]}"; do
if [[ "$i" != "$j" ]]; then
args+=("${HOSTS[$j]}")
else
continue;
fi
done
#echo 'Array before test:'"${args[*]}"
create_genesis_start_file "$EOS_PUB_KEY" "$EOS_PRIV_KEY" "${HOSTS[$i]}" "$PRODUCER" args
create_start_file "$EOS_PUB_KEY" "$EOS_PRIV_KEY" "${HOSTS[$i]}" "$PRODUCER" args
done
}
create_genesis_start_file(){
EOS_PUB_KEY=$1
EOS_PRIV_KEY=$2
CURRENTHOST=$3
PRODUCER=$4
peerags="$5[#]"
peers=("${!peerags}")
echo 'Genesis Currenthost is:'"$CURRENTHOST"
#echo "${peers[*]}"
VAR=""
length=${#peers[#]}
last=$((length - 1))
for i in "${!peers[#]}" ; do
if [[ "$i" == "$last" ]]; then
VAR+="--p2p-peer-address ${peers[$i]}:8888 \\"
else
VAR+=$"--p2p-peer-address ${peers[$i]}:8888 \\"$'\n\t'
fi
done
}
create_start_file(){
EOS_PUB_KEY=$1
EOS_PRIV_KEY=$2
CURRENTHOST=$3
PRODUCER=$4
peerags="$5[#]"
peers=("${!peerags}")
echo 'Start file Currenthost is:'"$CURRENTHOST"
#echo "${peers[*]}"
}
create_nodes_directories
For every iteration of the first for loop, I am displaying the third argument $CURRENTHOST which is passed to functions create_genesis_start_file and create_start_file.
For first iteration, output is:
Genesis Currenthost is:192.168.110.165
Start file Currenthost is:192.168.110.167
Second iteration:
Genesis Currenthost is:192.168.110.166
Start file Currenthost is:192.168.110.167
Third iteration,
Genesis Currenthost is:192.168.110.167
Start file Currenthost is:192.168.110.167
Genesis Currenthost is as expected and Start file Currenthost should be same with it. I am not getting why the Start file Currenthost is always set as 192.168.110.167.
If I remove the below code from create_genesis_start_file it is working fine:
VAR=""
length=${#peers[#]}
last=$((length - 1))
for i in "${!peers[#]}" ; do
if [[ "$i" == "$last" ]]; then
VAR+="--p2p-peer-address ${peers[$i]}:8888 \\"
else
VAR+=$"--p2p-peer-address ${peers[$i]}:8888 \\"$'\n\t'
fi
done
I am not getting the exact problem why the variable value is getting changed? Please help.
The "$5[#]" looks odd to me. You can't use a scalar $5 as if it were an array.
It seems that you want to pass a whole array as parameter. Since bash does not have a native way to do this, I suggest that on the calling side, you pass "${args[#]}" as parameter, and inside your function, you do a
shift 4
peers=( "$#" )
Another possibility, which however violates the idea of encapsulation, is to treet peers as a global variable, which is accessible to all functions. With this approach, you would on the caller side collect the information already in the variable peers instead of args.
From a programming style, global variables (accross function boundaries) are usually disliked for good reasons, but in my personal opinion, if you just do simple shell scripting, I would find it an acceptable solution.

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

Parsing .csv file in bash, not reading final line

I'm trying to parse a csv file I made with Google Spreadsheet. It's very simple for testing purposes, and is basically:
1,2
3,4
5,6
The problem is that the csv doesn't end in a newline character so when I cat the file in BASH, I get
MacBook-Pro:Desktop kkSlider$ cat test.csv
1,2
3,4
5,6MacBook-Pro:Desktop kkSlider$
I just want to read line by line in a BASH script using a while loop that every guide suggests, and my script looks like this:
while IFS=',' read -r last first
do
echo "$last $first"
done < test.csv
The output is:
MacBook-Pro:Desktop kkSlider$ ./test.sh
1 2
3 4
Any ideas on how I could have it read that last line and echo it?
Thanks in advance.
You can force the input to your loop to end with a newline thus:
#!/bin/bash
(cat test.csv ; echo) | while IFS=',' read -r last first
do
echo "$last $first"
done
Unfortunately, this may result in an empty line at the end of your output if the input already has a newline at the end. You can fix that with a little addition:
!/bin/bash
(cat test.csv ; echo) | while IFS=',' read -r last first
do
if [[ $last != "" ]] ; then
echo "$last $first"
fi
done
Another method relies on the fact that the values are being placed into the variables by the read but they're just not being output because of the while statement:
#!/bin/bash
while IFS=',' read -r last first
do
echo "$last $first"
done <test.csv
if [[ $last != "" ]] ; then
echo "$last $first"
fi
That one works without creating another subshell to modify the input to the while statement.
Of course, I'm assuming here that you want to do more inside the loop that just output the values with a space rather than a comma. If that's all you wanted to do, there are other tools better suited than a bash read loop, such as:
tr "," " " <test.csv
cat file |sed -e '${/^$/!s/$/\n/;}'| while IFS=',' read -r last first; do echo "$last $first"; done
If the last (unterminated) line needs to be processed differently from the rest, #paxdiablo's version with the extra if statement is the way to go; but if it's going to be handled like all the others, it's cleaner to process it in the main loop.
You can roll the "if there was an unterminated last line" into the main loop condition like this:
while IFS=',' read -r last first || [ -n "$last" ]
do
echo "$last $first"
done < test.csv

Resources