Bash : read / readarray multiline input - bash

I'm new on bash and I'm trying to write a bash script that will save user's multilines inputs (a text with newlines, somes lines of code, etc.). I need to allow newline (when you press "Enter"), multiline paste (when you paste few lines "Ctrl+V") and set a new key, instead of "Enter", to validate, send the input and continue to the next step of the script.
I tried with read but you can not do multiline.
echo "Enter content :"
read content
I found an example with readarray here (How to delete a character in bash read -d multiline input?) that allow to press "Enter" for newline but each words separate by space are separate in the array. I would like to have only the lines separated.
echo "Enter package names, one per line: hit Ctrl-D on a blank line to stop"
readarray -t pkgs
Do you have any ideas ? Or there is maybe a completely different way to do it ? Thank you for your help.

You can set IFS to newline so that only newlines will separate items in the array.
IFS=$'\n' readarray lines
The first line read will be ${lines[0]}, the second ${lines[1]}, etc. ${#lines[#]} tells you how many lines, and the last one will be ${lines[${#lines[#]}-1]}.
To loop over the array, you should use "${lines[#]}", not ${lines[*]}; the latter will take you right back to looping over individual words.

Related

Using space-separated arguments from a field in a tab-separated file

I'm writing a shell script intended to edit audio files using the sox command. I've been running into a strange problem I never encountered in bash scripting before: When defining space separated effects in sox, the command will work when that effect is written directly, but not when it's stored in a variable. This means the following works fine and without any issues:
sox ./test.in.wav ./test.out.wav delay 5
Yet for some reason the following will not work:
IFS=' ' # set IFS to only have a tab character because file is tab-separated
while read -r file effects text; do
sox $file.in.wav $file.out.wav $effects
done <in.txt
...when its in.txt is created with:
printf '%s\t%s\t%s\n' "test" "delay 5" "other text here" >in.txt
The error indicates this is causing it to see the output file as another input.
sox FAIL formats: can't open input file `./output.wav': No such file or directory
I tried everything I could think of: Using quotation marks (sox "$file.in.wav" "$file.out.wav" "$effects"), echoing the variable in-line (sox $file.in.wav $file.out.wav $(echo $effects)), even escaping the space inside the variable (effects="delay\ 5"). Nothing seems to work, everything produces the error. Why does one command work but not the other, what am I missing and how do I solve it?
IFS does not only change the behavior of read; it also changes the behavior of unquoted expansions.
In particular, unquoted expansions' content are split on characters found in IFS, before each element resulting from that split is expanded as a glob.
Thus, if you want the space between delay and 5 to be used for word splitting, you need to have a regular space, not just a tab, in IFS. If you move your IFS assignment to be part of the same simple command as the read, as in IFS=$'\t' read -r file effects text; do, that will stop it from changing behavior in the rest of the script.
However, it's not good practice to use unquoted expansions for word-splitting at all. Use an array instead. You can split your effects string into an array with:
IFS=' ' read -r -a effects_arr <<<"$effects"
...and then run sox "$file.in.wav" "$file.out.wav" "${effects_arr[#]}" to expand each item in the array as a separate word.
By contrast, if you need quotes/escapes/etc to be allowed in effects, see Reading quoted/escaped arguments correctly from a string

Remove carriage return end of variable

I'm getting really strange output for this program. What is the "Carriage Return" doing, and how to remove it - missing single quote in the end? Why is the letter "T" missing? How to write code to correct this?
code i'm using
#!/bin/bash
export DATABASE_LIST="/opt/halogen/crontab/etc/db_stat_list.cfg"
export v3=""
while read -r USERID ORACLE_SID2
do
v3="This is '${ORACLE_SID2}' "
echo $v3
done < <(tac $DATABASE_LIST)
output
'his is 'OT1SL80
'his is 'OT1SL010
The file I'm reading from is not corrupt and is small one with two lines
[oracle#ot1sldbm001v test2]$ cat /opt/halogen/crontab/etc/db_stat_list.cfg
asp_dba/dba OT1SL010
asp_dba/dba OT1SL80
Thank you
Your DATABASE_LIST file is in DOS/Windows format, with carriage return + linefeed at the end of each line. Unix uses just linefeed as a line terminator, so unix tools treat the carriage return as part of the content of the line. You can keep this from being a problem by telling the read command to treat the carriage return as whitespace (like spaces, tabs, etc), since read automatically removes whitespace from the beginning and end of lines:
...
while IFS="$IFS"$'\r' read -r USERID ORACLE_SID2
...
Note that since this assignment to IFS (which basically lists the whitespace characters) is a prefix to the read command, it only applies to that one command and doesn't have to be set back to normal afterward.

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

Bash Variables within text block

I am trying to get variables into a block of text which will later be echoed to a file.
Problem is the $VAR is not getting converted into the variable's value ??
VAR="SOME Value"
read -d '' WPA <<"BLOCK"
Description='WIFI'
Interface=wlan0
Connection=wireless
IP=dhcp
Security=wpa
Key=$VAR
BLOCK
echo "$WPA"
Also, is it possible to append further text to the WPA Block ?
When you quote the delimeter of a heredoc, variables are not interpolated. Just drop the quotes:
read -d '' WPA <<BLOCK
Description='WIFI'
Interface=wlan0
Connection=wireless
IP=dhcp
Security=wpa
Key=$VAR
BLOCK
Why don't you just say
WPA="Description='WIFI'
Interface=wlan0
Connection=wireless
IP=dhcp
Security=wpa
Key=$VAR
"
?
There's not really a need to use read in your case.
If you want to echo append text to $WPA, do it like this:
WPA="$WPA
first appended line
second appended line
"
but be aware that you insert an extra newline this way - $WPA had a newline at the end and there's another one at the beginning of the new text. To avoid this, use
WPA="${WPA}first appended line
second appended line
"
The {} quotation delimits the variable name. Using
WPA="$WPAfirst appended line
would look for a variable named WPAfirst.
is it possible to append further text to the WPA Block ?
$WPA is just a normal shell variable (that happens to contain a multi-line string), so you can append to it with +=; e.g.:
WPA+=$'\nanother line\nand another'
If you wanted to append the content of another heredoc, assign it to a separate variable and append that to WPA (but, as #GuntramBlohm points out, you can just as easily assign/append a multi-line string directly).

bash dynamic dialog

I have a task to write simple bash script that adds deletes and views entries from file.
The requirement is to use "dialog"
data structure in file:
Name Surname mymail#mail.com
Another New person#database.loc
basically i have accomplished everything except delete, i know how to do the delete itself(with "sed" i think?)
But i need to use dialog --menu to display the search results.
The menu item should be whole line of text i think as after selection of an item i will use "grep" again to filter out the unique entry.
Maybe anyone can put me on the right direction?
Thanks.
I used dialog never before, but maybe I still can help. Try this:
declare -a args=()
while read
do
args+=("$REPLY" "")
done < <( grep '#' example.txt )
dialog --menu "Please select the line you want to edit" 40 60 34 "${args[#]}"
How does this work?
dialog --menu takes the following arguments:
question text
height and width of the window
height of the menu (which should be 7 less then the window height to use it fully, in my experience)
pairs of tag string and description.
The selected tag string is then output (on stderr) at the end.
How to create such a list strings from our grep output? A failed try is described below, here the working one.
The read command reads one line a time from standard input (to which we redirected the grep output), and puts it (if we don't give other options or arguments) in the REPLY variable.
We then add this value (quoted to be one element) to the array args , and additionally a single "" to add an empty string to the array, too.
We have to use the < <( ... ) syntax for redirection, since the normal | creates a subshell for the second command, which has the effect that changes to the variables are not propagated back to the original shell. (< means read input from file, and <( ... ) creates a pipe to read the output of the command and results in its filename.)
Then we use the "${args[#]}" parameter expansion - # has the effect that each element is individually quoted as the result. So for your example, the command line now looks like
dialog --menu "Please select the line you want to edit" 40 60 34 "Name Surname mymail#mail.com" "" "Another New person#database.loc" ""
This creates a two line menu, with the complete lines as the "tag", and an empty string as the clarification.
You will need some way to capture it's standard error output, as it puts the result there.
Another idea which does not work:
The question is how to to get the output of grep in the command line of dialog, so that it forms two arguments for each line.
What helps here are the following syntactic constructs:
Command substitution: $( cmd ) executes the command and converts the result to a string, which is then used at the point in the command line.
So, we need some command which produces two "words" for each line of grep output (since your file would give three words). As you are already using sed, why not use it here too?
The sed command s/^.*$/"&" ""/ replaces each line with the line enclosed in "", followed by another two quotes.
"Name Surname mymail#mail.com" ""
"Another New person#database.loc" ""
The idea would now be to use
dialog --menu "Please select the line you want to edit" 40 60 34 $( sed -e 's/^.*$/"&" ""/' < input )
but unfortunately the word-splitting of bash does not respect "" after command-substitution, so bash gives the six arguments "Name, Surname, mymail#mail.com", "", "Another, New, person#database.loc" and "" to the dialog program. (In fact, using "" to inhibit splitting seems to work only for quotes given literal in the source or in eval - but eval does not work here since we have line breaks, too.)

Resources