Get string from variable lists in Bash - bash

I have a file (file.env) similar to this:
kw_var1='string1'
kw_var2='string 2'
kw_var3='this is string 3'
kw_var4='does not matter'
kw_var5='maybe'
w_var1=qwert_1
w_var2=qwert_2
w_var3=qwert_3
w_var4=qwert_4
and I need to create a string list_of_values which contains the values of all variables that start with kw_, i.e.
$ echo -e $list_of_values
should output:
'string1' 'string 2' 'this is string 3' 'does not matter' 'maybe'
I tried to iterate over them, but cannot get this to work. My code:
list_of_values=$(for param in $(cat $file.env | grep "kw\_"); do echo $(echo -e '$'$param | cut -s -d '=' -f1); done)
but this is what I get:
$kw_var1 $kw_var2 $kw_var3 $kw_var4 $kw_var5
Note that:
the variable values will contain spaces;
list_of_values will be used as an argument to another function
Any ideas with what is wrong?
UPDATE:
When doing the final echo I used:
$ echo -e $list_of_values | tr '\n' ' '
to get everything in one line.

Trying your code
I tried your command and get this as output :
$kw_var1
$kw_var2
$kw_var3
$kw_var4
$kw_var5
You had the wrong output because you chose the first field when you used cut instead of the second.
Fixing cut command
for param in $(cat test.txt | grep "kw\_"); do echo $(echo '$'$param | cut -s -d '=' -f2); done
Returns :
'string1'
'string
'this
'does
'maybe'
Fixing IFS
You used a for in loop but it does not iterate over newlines, it iterates over spaces. You need to change the IFS (Internal Field Separator) variable first :
IFS=$'\n'; for param in $(cat <file> | grep "kw\_"); do echo $(echo $param | cut -s -d '=' -f2); done
Output :
'string1'
'string 2'
'this is string 3'
'does not matter'
'maybe'
Using printf
To get the output on one line, you can use printf instead of echo :
for param in $(cat <file> | grep "kw\_"); do printf "$(echo $param | cut -s -d '=' -f2) "; done; printf "\n"
Output :
'string1' 'string 2' 'this is string 3' 'does not matter' 'maybe'
Using while
You could simplify the command and use a while read statement that iterates directly over lines :
cat <file> | grep "kw\_" | cut -d"=" -f2 | while read line; do printf "${line} "; done; printf "\n"
Using awk
Last but not least, you can use awk which radically simplifies your code:
awk -F"=" '/kw_/{printf "%s ", $2}END{print ""}' <file>
Output :
'string1' 'string 2' 'this is string 3' 'does not matter' 'maybe'
If the extra space at the end of the line is annoying, you can do this :
awk -F"=" '/kw_/{printf "%s%s", delim, $2; delim=" "}END{print ""}' <file>
Awk explained :
# Using = as delimiter
awk -F"=" '
# If line contains kw_
/kw_/{
# Prints second field
printf "%s%s", delim, $2;
delim=" "
}
END{
# Prints newline
print ""
}' <file>
Final code
list_of_values=$(awk -F"=" '/kw_/{printf "%s%s", delim, $2; delim=" "}END{print ""}' $file.env)

$ cat tst.awk
/^kw_/ {
sub(/[^=]+=/,"")
str = str sep $0
sep = " "
}
END {
print str
}
e.g. note that it handles this=that in your desired output string correctly:
$ cat file
kw_var1='string1'
kw_var2='string 2'
kw_var3='this is string 3'
kw_var4='does not matter'
kw_var5='maybe'
kw_var6='this=that'
w_var1=qwert_1
w_var2=qwert_2
w_var3=qwert_3
w_var4=qwert_4
$ awk -f tst.awk file
'string1' 'string 2' 'this is string 3' 'does not matter' 'maybe' 'this=that'
Updated: given what you've now told us in comments, here's how I'd do it assuming you need to access individual values by their tags sometimes, otherwise you could use a regular array instead of associative:
$ cat tst.sh
#!/bin/env bash
declare -A kw
declare -A w
while IFS= read -r line; do
tag="${line%%=*}"
val="${line#*=}"
case "$tag" in
kw* ) kw["$tag"]="$val" ;;
w* ) w["$tag"]="$val" ;;
?* ) printf 'Error: unexpected contents: "%s"\n' "$line"; exit 1;;
esac
done < file.env
printf '\nAll kw indices => values:\n'
for idx in "${!kw[#]}"; do
printf '\t%s => %s\n' "$idx" "${kw[$idx]}"
done
printf '\nAll kw values passed to a function (printf) at once:\n'
printf '\t%s\n' "${kw[#]}"
printf '\nAll w indices => values:\n'
for idx in "${!w[#]}"; do
printf '\t%s => %s\n' "$idx" "${w[$idx]}"
done
printf '\nAll w values passed to a function (printf) at once:\n'
printf '\t%s\n' "${w[#]}"
.
$ ./tst.sh
All kw indices => values:
kw_var4 => does not matter
kw_var5 => maybe
kw_var6 => this=that
kw_var1 => string1
kw_var2 => string 2
kw_var3 => this is string 3
All kw values passed to a function (printf) at once:
does not matter
maybe
this=that
string1
string 2
this is string 3
All w indices => values:
w_var3 => qwert_3
w_var2 => qwert_2
w_var1 => qwert_1
w_var4 => qwert_4
All w values passed to a function (printf) at once:
qwert_3
qwert_2
qwert_1
qwert_4
The above was run on this file.env without the redundant single quotes around the values, otherwise you'd just remove them in the script:
$ cat file.env
kw_var1=string1
kw_var2=string 2
kw_var3=this is string 3
kw_var4=does not matter
kw_var5=maybe
kw_var6=this=that
w_var1=qwert_1
w_var2=qwert_2
w_var3=qwert_3
w_var4=qwert_4
wrt our discussion in the comments and using printf '<%s>\n' in place of fitsort which I don't know and don't have:
$ list[0]='foo bar'; list[1]='etc'
$ printf '<%s>\n' "${list[#]}"
<foo bar>
<etc>
$ printf '<%s>\n' $(printf '%s\n' "${list[#]}")
<foo>
<bar>
<etc>
$ printf '<%s>\n' "$(printf '%s\n' "${list[#]}")"
<foo bar
etc>
See how the first version correctly simply passes the contents of list[] to the fitsort-replacement command while the others pass the strings output by printf to it instead?

Make two arrays of your bunch of variables, then you can easily iterate over them like this
#!/bin/bash
kw=(
'string1'
'string 2'
'this is string 3'
'does not matter'
'maybe'
)
w=(
'qwert_1'
'qwert_2'
'qwert_3'
'qwert_4'
)
for i in {1..5}
do
echo -n "\"${kw[$i]}\" "
done
echo
for i in {1..4}
do
echo -n "\"${w[$i]}\" "
done
echo

I used dynamic references.
$: out="$( . file.env; for r in ${!kw_*}; do printf "'%s' " "${!r}"; done; echo )"
$: echo "$out"
'string1' 'string 2' 'this is string 3' 'does not matter' 'maybe'

Your attempt uses a few practices that aren't recommended, and contains a few syntax errors:
You use $file.env, but it should be just file.env
Don't use for to read lines from a file (see this article)
echo $(cmd) is the same as just cmd plus globbing and word splitting, which often isn't what you want
echo -e '$'$param is going to print a literal $ sign
cut -f1 is selecting the first field, but you want the second one
This is a solution "in the spirit" of what you tried, but using just Bash:
list=$(
while IFS='=' read -r key val; do
[[ $key == kw_* ]] && printf '%s ' "$val"
done < file.env
)
list=${list% } # Remove trailing blank
If you deal with strings containing spaces, though, it's generally advisable to use an array instead. Since file.env is valid Bash, we can source the lines we're interested in and then build an array with the values of all the kw_ variables:
source <(grep '^kw_' file.env)
declare -n var
for var in "${!kw_#}"; do list+=("$var"); done
The array now contains one string per element, without the literal single quotes:
$ printf '%s\n' "${list[#]}"
string1
string 2
this is string 3
does not matter
maybe
declare -n sets var to a nameref: it is treated as if it actually were the variable whose name it holds. This requires Bash 4.3 or newer.

Related

Take multiple (any number of input) input strings and concatenate in shell

I want to input multiple strings.
For example:
abc
xyz
pqr
and I want output like this (including quotes) in a file:
"abc","xyz","pqr"
I tried the following code, but it doesn't give the expected output.
NextEmail=","
until [ "a$NextEmail" = "a" ];do
echo "Enter next E-mail: "
read NextEmail
Emails="\"$Emails\",\"$NextEmail\""
done
echo -e $Emails
This seems to work:
#!/bin/bash
# via https://stackoverflow.com/questions/1527049/join-elements-of-an-array
function join_by { local IFS="$1"; shift; echo "$*"; }
emails=()
while read line
do
if [[ -z $line ]]; then break; fi
emails+=("$line")
done
join_by ',' "${emails[#]}"
$ bash vvuv.sh
my-email
another-email
third-email
my-email,another-email,third-email
$
With sed and paste:
sed 's/.*/"&"/' infile | paste -sd,
The sed command puts "" around each line; paste does serial pasting (-s) and uses , as the delimiter (-d,).
If input is from standard input (and not a file), you can just remove the input filename (infile) from the command; to store in a file, add a redirection at the end (> outfile).
If you can withstand a trailing comma, then printf can convert an array, with no loop required...
$ readarray -t a < <(printf 'abc\nxyx\npqr\n' )
$ declare -p a
declare -a a=([0]="abc" [1]="xyx" [2]="pqr")
$ printf '"%s",' "${a[#]}"; echo
"abc","xyx","pqr",
(To be fair, there's a loop running inside bash, to step through the array, but it's written in C, not bash. :) )
If you wanted, you could replace the final line with:
$ printf -v s '"%s",' "${a[#]}"
$ s="${s%,}"
$ echo "$s"
"abc","xyx","pqr"
This uses printf -v to store the imploded text into a variable, $s, which you can then strip the trailing comma off using Parameter Expansion.

How to append the rest of the command from a variable?

In the sample bellow, I would like to print text on the screen and also append this text into file, when the variable cpstdout is set to 1. Otherwise only print the text on screen. I need to have the echo part flexible to the append variable. Is there any way to correct my code?
#!/bin/ksh
cpstdout=1
if [ $cpstdout -eq 1 ]; then
append="| tee somefile"
else
append=""
fi
echo "test string" $append
Now the result is just like this:
./test.sh
test string | tee somefile
-no file is created of course
example of print function:
print_output(){
printf "\t/-------------------------------------------------\\ \n"
for i in "$#"; do
printf "\t| %-14s %-32s |\n" "$(echo $i | awk -F, '{print $1}')" "$(echo $i | awk -F, '{print $2}')"
shift
done
printf "\t\-------------------------------------------------/\n"
}
Define your appending command as a function:
output_with_append() {
tee -a somefile <<<"$1"
}
Then, in the if, set a variable to the appropriate outputting function:
if [ $cpstdout -eq 1 ]; then
output=output_with_append
else
output=echo
fi
Finally, use variable expansion to run the command:
$output "test_string"
Note that I've used tee -a since you said you wanted to append to a file and not overwrite it.
Setting cpstdout to $1 so we can control it through a command-line parameter:
cpstdout="$1"
A example session then looks like this:
$ ./test.sh 1
test_string
$ ./test.sh 1
test_string
$ cat somefile
test_string
test_string
$ ./test.sh 0
test_string
$ cat somefile
test_string
test_string

What is wrong in this bash read -n?

I'm learning Bash and am looking at the read command. I thought the difference between the -N and the -n option was that -N would overwrite the IFS while -n wouldn't. In the following example I expected var6 to take the value of "ijfz", because I thought the space would act as field separator.
But it seems to have value "ijfz e". The space wasn't used as field separator
printf "%s\n" "ijfz eszev enacht" | {
read -n 6 var6
printf "%s\n" "$var6"
}
I wanted to see what was $IFS, but the following printf command doesn't learn me too much:
printf ":%s:\n" "$IFS"
gives the following output
:
:
What am I not understanding...?
By delimiter it means using option -d. See this difference:
printf "%s\n" "ijfz eszev enacht" | { read -d ' ' -N 6 var6; printf "[%s]\n" "$var6"; }
[ijfz e]
printf "%s\n" "ijfz eszev enacht" | { read -d ' ' -n 6 var6; printf "[%s]\n" "$var6"; }
[ijfz]
In first case when using -N it ignores -d ' ' and reads exactly 6 characters.
In second case when using -n it respects -d ' ' and reads until a space is read.

shell bash - How to split a string acoring to a delimeter and echo each substring to a file [duplicate]

This question already has answers here:
How do I split a string on a delimiter in Bash?
(37 answers)
Closed 9 years ago.
Hi I am trying to split a string am getting from a file, using the delimiter "<" I then want to echo each string to a file. I am sort of there, but am not sure how to best split the string and then loop echo each substring (there may be up to 10 substrings) I am guessing I need to create an array to store these strings and then have a loop to echo each value?
Here is what I have so far:
while read line
do
# ceck if the line begins with client_values=
if[["$line" == *client_values=*]]
CLIENT_VALUES = 'echo "${line}" | cut -d'=' -f 2'
#Now need to split the CLIENT_VALUES using "<" as a delimeter.
# for each substring
echo "Output" >> ${FILE}
echo "Value $substring" >> ${FILE}
echo "End" >> ${FILE}
done < ${PROP_FILE}
grep '^client_values=' < "${PROP_FILE}" | while IFS='=' read name value
do
IFS='<' read -ra parts <<< "$value"
for part in "${parts[#]}"
do
echo "Output"
echo "Value $part"
echo "End"
done >> "${FILE}"
done
One line awk might be simpler here (and you get the added bonus of having the angry face regex separator =<)
$ awk -F "[=<]" '/^client_values/ { print "Output"; for (i = 2; i <= NF; i++) print "Value " $i; print "End"}' input.txt >> output.txt
$ cat input.txt
client_values=firstvalue1<secondvalue2<thirdvalue3
some text
client_values=someothervalue1<someothervalue2
$ cat output.txt
Output
Value firstvalue1
Value secondvalue2
Value thirdvalue3
End
Output
Value someothervalue1
Value someothervalue2
End
Your answer could probably also work, I think with minimal modification, you would want something like
#!/bin/bash
PROP_FILE='input.txt'
FILE="output2.txt"
while read line
do
# ceck if the line begins with client_values=
if [[ "$line" == "client_values="* ]]
then
CLIENT_VALUES=`echo "${line}" | cut -d'=' -f 2`
IFS='<' read -ra CLIENT_VALUES <<< "$CLIENT_VALUES"
for substring in "${CLIENT_VALUES[#]}"; do
echo "Output" >> "${FILE}"
echo "Value $substring" >> "${FILE}"
echo "End" >> "${FILE}"
done
fi
done < "${PROP_FILE}"
Which produces
$ cat output2.txt
Output
Value firstvalue1
End
Output
Value secondvalue2
End
Output
Value thirdvalue3
End
Output
Value someothervalue1
End
Output
Value someothervalue2
End
Though again, not sure if that's what you want or not.

How to get output of grep in single line in shell script?

Here is a script which reads words from the file replaced.txt and displays the output each word in each line, But I want to display all the outputs in a single line.
#!/bin/sh
echo
echo "Enter the word to be translated"
read a
IFS=" " # Set the field separator
set $a # Breaks the string into $1, $2, ...
for a # a for loop by default loop through $1, $2, ...
do
{
b= grep "$a" replaced.txt | cut -f 2 -d" "
}
done
Content of "replaced.txt" file is given below:
hllo HELLO
m AM
rshbh RISHABH
jn JAIN
hw HOW
ws WAS
ur YOUR
dy DAY
This question can't be appropriate to what I asked, I just need the help to put output of the script in a single line.
Your entire script can be replaced by:
#!/bin/bash
echo
read -r -p "Enter the words to be translated: " a
echo $(printf "%s\n" $a | grep -Ff - replaced.txt | cut -f 2 -d ' ')
No need for a loop.
The echo with an unquoted argument removes embedded newlines and replaces each sequence of multiple spaces and/or tabs with one space.
One hackish-but-simple way to remove trailing newlines from the output of a command is to wrap it in printf %s "$(...) ". That is, you can change this:
b= grep "$a" replaced.txt | cut -f 2 -d" "
to this:
printf %s "$(grep "$a" replaced.txt | cut -f 2 -d" ") "
and add an echo command after the loop completes.
The $(...) notation sets up a "command substitution": the command grep "$a" replaced.txt | cut -f 2 -d" " is run in a subshell, and its output, minus any trailing newlines, is substituted into the argument-list. So, for example, if the command outputs DAY, then the above is equivalent to this:
printf %s "DAY "
(The printf %s ... notation is equivalent to echo -n ... — it outputs a string without adding a trailing newline — except that its behavior is more portably consistent, and it won't misbehave if the string you want to print happens to start with -n or -e or whatnot.)
You can also use
awk 'BEGIN { OFS=": "; ORS=" "; } NF >= 2 { print $2; }'
in a pipe after the cut.

Resources