Pseudo-input for Bash's "read" variable - bash

I have a script listening for a user input like that.
read -p "Run? (y/[n]) " -n 1 -r
if [[ $REPLY =~ ^[Yy]$ ]]; then
[..]
fi
Is there a way (upon executing the script) to already send the value which
read is going to read and handle?

your_script.sh <<< "Y"
This also supports multiple read's
your_script.sh <<< "YNYYNNY"

Related

Why is bash swallowing characters

I have the following code in a bash script:
ssh_management
if [[ "$PROMPT_SSH" == "true" ]]; then
read -p "Generate and Install SSH keys? [y/n]" -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
generate_container_ssh
install_ssh_keys
check_ssh_state
else
echo "Skipping SSH key install"
fi
fi
install_docker
But the script errors with:
Skipping container SSH key install
bash: line 102: nstall_docker: command not found
It appears that the read command is somehow swalling the first character on install_docker
Not sure why this is happening or how it fix it.
Seems something was messing up my STDIN.
read -p "Generate and Install SSH keys? [y/n]" -n 1 -r < /dev/tty
Fixes the problem.

How to properly iterate through a list using sshpass with a single ssh-login

Situation: we're feeding a list of filenames to an sshpass and it iterates correctly through a remote folder to check whether files with the given names actually exists, then build an updated list containing only the files that do exist, which is reused later in the bash script.
Problem: The list comprises sometimes tens of thousands of files, which means tens of thousands of ssh logins, which is harming performance and sometimes getting us blocked by our own security policies.
Intended solution: instead of starting the for-loop and calling sshpass each time, do it otherwise and pass the loop to an unique sshpass call.
I've got to pass the list to the sshpass instruction in the example test below:
#!/bin/bash
all_paths=(`/bin/cat /home/user/filenames_to_be_tested.list`)
existing_paths=()
sshpass -p PASSWORD ssh -n USER#HOST bash -c "'
for (( i=0; i<${#all_paths[#]}; i++ ))
do
echo ${all_paths[i]}
echo \"-->\"$i
if [[ -f ${all_paths[i]} ]]
then
echo ${all_paths[i]}
existing_paths=(${all_paths[i]})
fi
done
'
printf '%s\n' "${existing_paths[#]}"
The issue here is that it appears to loop (you see a series of echoed lines), but in the end it is not really iterating the i and is always checking/printing the same line.
Can someone help spot the bug? Thanks!
The problem is that bash first parses the string and substitutes the variables. That happens before it's sent to the server. If you want to stop bash from doing that, you should escape every variable that should be executed on the server.
#! /bin/bash
all_paths=(rootfs.tar derp a)
read -sp "pass? " PASS
echo
sshpass -p $PASS ssh -n $USER#$SERVER "
files=(${all_paths[#]})
existing_paths=()
for ((i=0; i<\${#files[#]}; i++)); do
echo -n \"\${files[#]} --> \$i\"
if [[ -f \${files[\$i]} ]]; then
echo \${files[\$i]}
existing_paths+=(\${files[\$i]})
else
echo 'Not found'
fi
done
printf '%s\n' \"\${existing_paths[#]}\"
This becomes hard to read very fast. However, there's an option I personally like to use. Create functions and export them to the server to be executed there to omit escaping a lot of stuff.
#! /bin/bash
all_paths=(rootfs.tar derp a)
function files_exist {
local files=($#)
local found=()
for file in ${files[#]}; do
echo -n "$file --> "
if [[ -f $file ]]; then
echo "exist"
found+=("$file")
else
echo "missing"
fi
done
printf '%s\n' "${found[#]}"
}
read -sp "pass? " PASS
echo
sshpass -p $PASS ssh -n $USER#$SERVER "
$(typeset -f files_exist)
files_exist ${all_paths[#]}
"

How can I prompt the user inside a while read loop? [duplicate]

This question already has answers here:
Read user input inside a loop
(6 answers)
How to read from user within while-loop read line?
(3 answers)
Closed 5 years ago.
For my purpose, I need to execute a shell command, achieve the output, and for each line ask user for prompt.
The problem is that on the read prompt, stdin buffer isn't empty
this is my code:
#!/bin/sh
git branch -a | sed 's/remotes\/origin\///g'
echo "############################"
git branch -a | sed 's/remotes\/origin\///g' | while read line
do
if [[ "$line" != *develop* ]] \
&& [[ "$line" != *master ]] \
&& [[ "$line" != *release/* ]] \
&& [[ "$line" != *hotfix* ]]
then
read -r -p "Do you want to delete branch $line <y/N>?" prompt
echo $prompt
fi
done
The line:
read -r -p "Do you want to delete branch $line <y/N>?" prompt
does not even display to video, and prompt variable show the result of line variable above.
How can I solve this problem?
Use a FD other than 0 (stdin), to leave the original stdin free for input from the user:
#!/usr/bin/env bash
# ^^^^- NOT /bin/sh; also, do not run with "sh scriptname"
while read -r line <&3; do
line=${line#remotes/origin/} # trim remotes/origin/ w/o needing sed
case $line in
*develop*|*master|*release/*|*hotfix*) continue ;;
*) read -r -p "Do you want to delete branch $line <y/N>?" prompt
echo "$prompt" ;;
esac
done 3< <(git branch -a)
Here, we're using FD 3 for output from git, such that FD 0 is still stdin, available to read from the user; and then redirecting <&3 on the explicit read where we want content from git.

read builtin doesn't work with pipe

I'd like to ask user a confirmation to read from stdin (Display output [Y/n]). It works Ok if some arguments were provided, or no arguments were provided but there was some input. However, if some data was piped to the script, there's no confirmation.
#!/bin/bash
output_file=$(mktemp)
cleanup() {
rm -f "$output_file"
}
trap cleanup 0 1 2 3 15
if [ $# -gt 0 ]; then
while [ $# -gt 0 ]; do
echo "$1" >> "$output_file"
shift
done
else
while read -r line; do
echo "$line" >> "$output_file"
done
fi
while true; do
read -p "Display output? [Y/n]" response
if [ -z "$response" ]; then
break
fi
case $response in
[Yy]*) break;;
[Nn]*) exit;;
esac
done
less "$output_file"
What prevent read -p to work? What should be done to provide consistent behavior?
The read command reads input from standard in. If you have standard in fed from a pipe then read looks for its data from the pipe, not from your terminal.
On most platforms you can work around this by redirecting the read command's input directly from the tty device, as in:
read -p "Display output? [Y/n]" response </dev/tty
If the script read everything from standard input, what is the read -p going to get? And it likely doesn't prompt if the input is not an 'interactive device' (aka terminal). Have you checked the Bash man page for read? It says:
-pprompt
Display prompt, without a trailing newline, before attempting to read any input. The prompt is displayed only if input is coming from a terminal.
When your input is from a pipe, it is not from a terminal.

Bash script support piping data into it

I have a bash script that I want to expand to support piping json into.
Example:
echo '{}' | myscript store
So, I tried the following:
local value="$1"
if [[ -z "$value" ]]; then
while read -r piped; do
value=$piped
done;
fi
Which works in a simple case above, but doing:
cat input.json | myscript store
Only get's the last line of the file input.json, it does not handle every line.
How can I support all cases of piping?
The following works:
if [[ -z "$value" && ! -t 0 ]]; then
while read -r piped; do
value+=$piped
done;
fi
The trick was using += and also checking ! -t 0 which checks if we are piping.
If you want to behave like cat, why not use it?
#! /bin/bash
value="$( cat "$#" )"

Resources