reading multiple items semi-interactively from the user in bash - bash

I'm trying to read multiple items from the user in a shell script, with no luck. The intention is to read a list of files first (which are read from the stdin pipe), and then read twice more to get two strings interactively. What I'm trying to do is read a list of files to attach in an email, then the subject and finally the email body.
So far I have this:
photos=($(< /dev/stdin))
echo "Enter message subject"
subject=$(< /dev/stdin)
echo "Enter message body"
body=$(< /dev/stdin)
(plus error checking code that I omit for succintness)
However, this gets an empty subject and body presumably because the second and third redirections get EOF.
I've been trying to close and reopen stdin with <&- and stuff but it doesn't seem to work that way.
I even tried using a separator for the list of files, using a "while; read line" loop and break out of the loop when the separator was detected. But that didn't work either (??).
Any ideas how to build something like this?

So what I ended up doing is based on ezpz's answer and this doc: http://www.faqs.org/docs/abs/HTML/io-redirection.html Basically I prompt for the fields first from /dev/tty, and then read stdin, using the dup-and-close trick:
# close stdin after dup'ing it to FD 6
exec 6<&0
# open /dev/tty as stdin
exec 0</dev/tty
# now read the fields
echo "Enter message subject"
read subject
echo "Enter message body"
read body
# done reading interactively; now read from the pipe
exec 0<&6 6<&-
photos=($(< /dev/stdin))
Thanks!

You should be able to use read to prompt for the subject and body:
photos=($(< /dev/stdin))
read -rp "Enter message subject" subject
read -rp "Enter message body" body

Since it is possible that you have a varying number of photos, why not just prompt for the known fields first and then read 'everything else'. It is much easier than trying to get the last two fields of an unknown length in an interactive manner.

# Prompt and read two things from the terminal (not from stdin), then read stdin.
# The last line uses arrays, so is BASH-specific. The read lines are portable.
# - Ian! D. Allen - idallen#idallen.ca - www.idallen.com
read -p "Enter message subject: " subject </dev/tty
read -p "Enter message body: " body </dev/tty
photos=($(</dev/stdin))

Related

Dynamic variable names based on value

Probably a noob question, but I'm having a particular challenge where I want to dynamically generate variable names based on a value. Eg. If I'd require 3 variables, variable names to be incremented dynamically var_0,var_1,var_3 respectively.
The solution I have right now is barebones. I'm just using read -p get user input and save it to variable. So for 5 hosts, I've just duplicated this 5 times to get the job done, but not a clean solution. I was looking around and reading through declare and eval but haven't got anywhere
Thinking of a solution where I input no. of hosts first and this dynamically picks up and asks for user input based on number of hosts and saves to variables incremented dynamically
Any help is appreciated, cheers
Use arrays. However, you don't have to ask the user for the number of hosts. Let them enter as many hosts as they like and finish by entering an empty line:
echo "Please enter hosts, one per line."
echo "Press enter on an empty line to finish."
hosts=()
while read -p "host ${#hosts[#]}: " host && [ -n "$host" ]; do
hosts+=("$host")
done
echo "Finished"
Alternatively, let the user enter all hosts on the same line separated by whitespace:
read -p "Please enter hosts, separated by whitespace: " -a hosts
To access the individual hosts use ${hosts[0]}, ${hosts[1]}, ..., ${hosts[${#hosts[#]}]}.
To list them all at once use ${hosts[#]}.
Use an array instead!
But read can actualy create dynamic vars, just like this:
for i in {1..3}; do
read -p 'helo: ' var_$i
done

UNIX: What are these ====== for?

What are the ============= in the below program? Is that section needed or is it a comment block and can be taken out? I'm somewhat new to UNIX so I am trying to read code to understand it and don't know why this is like this. Also, what is the EOF below it mean. I'm not sure about the meaning of that line either. Can anyone explain please? Thanks in advance
#!/bin/bash
usage() {
cat <<-EOF
========================================================
Usage: $0
Choose either y or n in "do you want to continue"
Choose from option A - E in mainmenu to perform actions.
========================================================
EOF
}
This question "How does “cat << EOF” work in bash?" explains how cat can be used for "here documents".
This question on How can I write a heredoc to a file in Bash script?
also has detailed answers on specifically using cat for this and contains this example that writes the content to a file:
cat << EOF > /tmp/yourfilehere
These contents will be written to the file.
This line is indented.
EOF
From the Advanced Bash-Scripting Guide section on here-docs:
A here document is a special-purpose code block. It uses a form of I/O
redirection to feed a command list to an interactive program or a
command, such as ftp, cat, or the ex text editor.
In this case, the ======================================================== is simply text content to be displayed.
Looking at your code, the heredoc is defined within a method called usage that appears to be called from error_exit(), so I would imagine it's there to display a message about the use of the script to users that input incorrect options.
EOF is an "end of file" - think of it as the beginning and end of the heredoc content.
In answer to your question whether the ======================================================== can be taken out - yes, it can. All that will happen is that it will no longer be displayed to the user.
However, don't remove the EOF!

Customize argument input routine for shell script

I have written a shell script for automating some tasks that I run from the terminal as -
v#ubuntu:$ ./automate.sh from:a1 to:a2 msg:'edited'
How can I (if at all) customize the script so as to enter each argument in a custom format on a separate line and execute it by pressing some other key to execute the shell script? So, I would do -
v#ubuntu:$ ./automate.sh
from : a1
to : a2
msg : 'next change'
... and then hit say Ctrl+Enter or F5 to execute this particular script?
NOTE : I know there is a hacky work around by simply typing ./automate.sh \ and hitting Enter after the trailing backslash to get a new line, but I was hoping to find a more elegant way to do this from within the script itself.
Also, I've purposely changed each argument to include whitespaces and the msg argument to include a string with spaces. So if anyone can point me in the right direction as to how to accomplish that as an added bonus, I'll be really grateful :)
If you know the number of arguments it is easy. Basics first.
#!/bin/bash
if [ $# == 0 ]
then
read v1 # gets onto new line. reads the whole line until ENTER
read v2 # same
read v3 # same
fi
# Parse $v1, $v2, $v3 as needed and run your script
echo ""
echo "Got |$v1|, |$v2|, |$v3|"
When you type automate.sh and hit enter the script is started, having received no arguments. With no arguments ($# == 0) the first read is executed, which prints a new line, waits, and gets the line typed in (once enter is hit) into $v1. The control goes back to the script, the next read gets the next typed line ... after the last one it drops out of if-else and continues. Parse your variables and run the script.
Session:
> automate.sh Enter
typed line Enter
more items Enter
yet more Enter
Got |typed line| |more items| |yet more|
>
You don't need Control-Enter or F5, it continues after 3 (three) lines.
This also allows you to provide both behaviors. Add an else, which will be executed if there are some arguments. You can then use the script by either supplying arguments on the first line (invocation you have so far), or in this new way.
If you need an unspecified number of arguments this approach will need more work.
Read words in input line into variables
If read is followed by variable names, like read v1 v2, then it reads each word into a variable, and the last variable gets everything that may have remained on the line. So replace read lines with
read k1 p1 s1
read k2 p2 s2
read k3 p3 s3
Now $k1 contains the first word (from), and $k2 and $k3 have the first words on their lines; then $p1 (etc) have the second word (:), and $s1 (etc) have everything else to the end of their lines (a1, a2, 'next change'). So you don't need those single quotes. All this is simple to modify if you want the script to print something on each line before input.
Based on the clarification in the comment, it is indeed desirable to not have to enter the whole strings, as one might think. This is "simple to modify"
read -p 'from :' s1
read -p 'to :' s2
read -p 'msg :' s3
Now the user only needs to enter the part after :, captured in $s variables. All else is the same.
See, for example: The section on user input in the Bash Guide; Their Advanced Guide (special variables); For a far more involved user interaction, this post. And, of course, man read.
It will be hard to bind Control-Enter in bash. If you are ok to change it for Control-D then everything might look like:
#!/usr/local/bin/bash
read -p 'From: ' from
read -p 'To: ' to
read -p 'Msg: ' msg
read keystroke
if [ "$keystroke" == "^D" ]; then
echo "$from $to $msg"
# do something else
fi

Unix Shell Script does not send intended email

I have a shell script like this. The purpose of this script is to tail+head out a certain amount of data from file.csv and then send it to email Bob#123.com. DataFunction seems to work fine alone however when I try to call DataFunction within the email function body. It seems it sends a empty email with the correct Title and destination. The body of the email is missing which should be the data from DataFunction. Is there a workaround for this ? Thank you in advance.
#!/bin/bash
DataFunction()
{
tail -10 /folder/"file.csv" | head -19
}
fnEmailFunction()
{
echo ${DataFunction}| mail -s Title Bob#123.com
}
fnEmailFunction
You are echoing an unset variable, $DataFunction (written ${DataFunction}), not invoking the function.
You should use:
DataFunction | mail -s Title Bob#123.com
You may have been trying to use:
echo $(DataFunction) | mail -s Title Bob#123.com
but that is misguided for several reasons. The primary problem is that it converts the 10 lines of output from the DataFunction function into a single line of input to mail. If you enclosed the $(DataFunction) in double quotes, that would preserve the 'shape' of the input, but it wastes time and energy compared to running the command (function) directly as shown.
Try this:
mail -s "Title" "bob#123com" <<EOF
${DataFunction}
EOF
I tried #0xAX's answer and couldnt get it to work properly within a bash script. You simply need to save the output of DataFunction() in some variable. You can simply use these three lines to achieve this.
#!/bin/bash
VAR1=`tail -10 "/folder/file.csv" | head -19`
echo $VAR1 | mail -s Title Bob#123.com

Writing to an IO stream in Ruby instead of sanitizing user input and sending to the shell?

I was just reading a blog post about sanitizing user input in Ruby before sending it to the command line. The author's conclusion was: don't send user input it to the command line in the first place.
In creating a contact form, he said he learned that
What I should do instead is open a
pipe to the mail command as an IO
stream and just write to it like any
other file handle.
This is the code he used:
open( %Q{| mail -s "#{subject}" "#{recipient}" }, 'w' ) do |msg|
msg << body
end
(I actually added the quotes around recipient - they're needed, right?)
I don't quite understand how this works. Could anybody walk me through it?
OK, I'll explain it with the caveat that I don't think it's the best way to accomplish that task (see comments to your question).
open() with a pipe/vertical bar as the first character will spawn a shell, execute the command, and pass your input into the command through a unix-style pipe. For example the unix command cat file.txt | sort will send the contents of the file to the sort command. Similarly, open("| sort", 'w') {|cmd| cmd << file} will take the content of the file variable and send it to sort. (The 'w' means it is opened for writing).
The %Q() is an alternate way to quote a Ruby string. This way it doesn't interfere with literal quote characters in the string which can result in ugly escaping. So the mail -s command is being executed with a subject and a recipient.
Quotes are needed around the subject, because the mail command will be interpreted by the shell, and arguments are separated by spaces, so if you want spaces in an argument, you surround it with quotes. Since the -s argument is for the subject, it needs to be in quotes because it will likely contain spaces. On the other hand, the recipient is an email address and email addresses don't contain spaces, so they're not necessary.
The block is providing the piped input to the command. Everything you add to the block variable (in this case msg) is sent into the pipe. Thus the email body is being appended (via the << operator) to the message, and therefore piped to the mail command. The unix equivalent is something like this: cat body.txt | mail -s "subject" recipient#a.com

Resources