Nested quotes in Bash - bash

Please let me explain my scenario with some sample script which is much simpler than my actual application but shows the same behavior:
For the demo I use two bash shell scripts. The first one is called argtest.sh simply outputs the command line parameters:
#/bin/bash
echo "Argument 1: $1"
echo "Argument 2: $2"
Test:
root#test:~# ./argtest.sh 1 2
Argument 1: 1
Argument 2: 2
and
root#test:~# ./argtest.sh "1 2" 3
Argument 1: 1 2
Argument 2: 3
This works as expected
I've created another command like this:
./argtest.sh $(for i in ` seq 1 2 ` ; do echo Number $i; done)
This is the result
Argument 1: Number
Argument 2: 1
I've tried many different combinations of quotation marks (") and escaped quotation marks (\") around Number, but no combination produced the desired output of:
Argument 1: Number 1
Argument 2: Number 2
How can I get to that output?

You can't. The command substitution is subject to word-splitting. Its output is two lines
Number 1
Number 2
but newlines are treated as arbitrary whitespace during word splitting. As a result, the command substitution is split into 4 words (Number, 1, Number, and 2), each of which is a separate argument to argtest.sh.
Additional quotes would treated literally during word-splitting; a string like "a b" c is split into 3 words ("a, b", and c), not 2 (a b, c).
To do what you want, you need to use a while loop to read one line of output at a time from your loop. For example:
for i in $(seq 1 2); do
echo "Number $i"
done | while read -r line; do
./argtest.sh "$line"
done

because arguments are subject to word splitting, you may change $IFS and put the the new separator into your echo
IFS=$'_\n'
./argtest.sh $(for i in {1..2} ; do echo "Number ${i}_"; done )
don't forget to restore the old $IFS
oIFS=$IFS
...
IFS=$oIFS
btw {1..2} is equivalent to seq 1 2 but you don't need an external command

As chepner mentioned, command substitution is subjected to word splitting.
Below are few examples to better understand this
Here is the behaviour when I replace echo with printf and escape the double quotes:-
for i in ` seq 1 2 ` ; do printf "\"Number %d\" " "$i"; done; printf "\n"
"Number 1" "Number 2"
./arg.sh $(for i in ` seq 1 2 ` ; do printf "\"Number %d\" " "$i"; done; printf "\n")
Argument 1: "Number
Argument 2: 1"
Now if I try to enclose the whole command substitution in double quotes, it will consider them as one single argument:-
./arg.sh "$(for i in ` seq 1 2 ` ; do printf "\"Number %d\" " "$i"; done; printf "\n")"
Argument 1: "Number 1" "Number 2"
Argument 2:

You can use xargs
for i in ` seq 1 2 ` ; do echo Number $i; done | xargs -d $'\n' ./argtest.sh

Related

How to get everything after two specific characters in bash

I want to put the numbers into different variables
So I have three lines for example with $1 being -a in the first line
number -a 3 1245 1234
second line, $1= -b
number -b 2 2345 64353
third line, $1= -a
number -a 5 -b 3 54525 545252
Is it possible to put the numbers in one variable if a and b are separated like line one and two and in two variables if they are combined
So for line one
number=3
line 2
number=2
line 3
number1=5
number2=3
and then to use in a echo statement
Just do normal shell parsing for command line parameters. Not practical to create conditions for parsing for 3rd pattern "number=". Best to keep it to one for each of the options involved, that way you keep the distinct relationship with the original option flags.
#!/bin/sh
echo "number -a 3 1245 1234
number -b 2 2345 64353
number -a 5 -b 3 54525 545252" |
while read line
do
set -- ${line}
shift # eliminate command from line
while [ $# -gt 1 ]
do
case $1 in
-a ) echo "number1=$2" ; shift ; shift ;;
-b ) echo "number2=$2" ; shift ; shift ;;
* ) break ;;
esac
done
echo ""
done

When use $# to pass all arguments in bash, why '-n' cannot be passed

When I try to use the $# to pass arguments in bash, it seems if -n come first in the arguments, the return symbol '\n' is removed.
[...]$ test(){ echo "$#";}
[...]$ test 1 2 3
return
[...]$ 1 2 3
[...]$
but
[...]$ test -n 1 2 3
return
[...]$1 2 3[...]$
-n disappear and seems return symbol \n is removed due to '-n'
Is '-n' an special option for $#? How can I pass -n with $#
With test -n 1 2 3, echo "$#" will become echo -n 1 2 3 4 where -n become echo's option which prevents echo from printing a NEWLINE char.
You can write like this:
Test() { printf '%s\n' "$*"; }
(Note that Test() { printf -- "$*"; } mentioned in another deleted answer may not work. Try Test %d a b c and you'll find out why.)
-nis special option for echo (remove final \n)
You need use $* instead of $#
test(){ echo "$*";}
$# expanded as "$1" "$2" "$3" ... "$n"
$* expanded as "$1x$2x$3x...$n", where x is the value of IFS variable i.e.
"$*" is one long string and $IFS act as an separator or token delimiters.
echo "$#" ==> echo "-n" "1" "2" "3" ==> 1 2 3 (without "\n")
echo "$*" ==> echo "-n 1 2 3" ==> -n 1 2 3 (with "\n")

in bash,now i have two strings,one is 'a b c' while another is '1 2 3' any good way to combine them to 'a=1 b=2 c=3' I

in bash,now i have two strings,one is 'a b c' while another is '1 2 3'
any good way to combine them to 'a=1 b=2 c=3'
I tried string to array and combined them.but if i don't know the IFS?
IFS=' ' read -r -a array1 <<< "$upvote_count"
IFS=' ' read -r -a array0 <<< "$qids"
tLen=${#array[#]}
for (( i=0; i<${tLen}; i++ ));
do
echo "${array0[$i]}"" ""${array1[$i]}">>a.txt
done
You can create arrays from each string[1] and then use eval to create the variables with names from string 1 and values from string 2 by looping over each array element and evaluating array1[i]=array2[i] (pseudocode). A short script would look like the following:
#!/bin/bash
v1="a b c" ## original string variables
v2="1 2 3"
ar1=( $(echo $v1) ) ## create arrays 1 & 2 from strings
ar2=( $(echo $v2) )
for ((i=0; i<${#ar1[#]}; i++)); do
eval "${ar1[i]}=${ar2[i]}" ## eval to create variables
done ## (be careful with eval)
printf "a=%s\nb=%s\nc=%s\n" $a $b $c ## confirm
Output
$ bash evalabc.sh
a=1
b=2
c=3
You would want to add validations that the you have the same number of elements in each array, that the elements of the first don't begin with numbers, etc.. and that they do not contain anything that would be harmful when you run eval!
As noted in the comment of the script, take great care in using eval (or avoid it altogether) because it will do exactly what you tell it to do. You would not want to have, e.g. a=sudo rm, b="-rf", c=/* and then eval "$a $b $c" -- very bad things can happen.
Footnotes:
[1] (adjust IFS as needed - not needed for space separation)
Give this tested version a try:
unset letters; unset figures; IFS=' ' read -r -a letters <<< "a b c" ; \
IFS=' ' read -r -a figures <<< '1 2 3' ; \
for i in "${!letters[#]}" ; do \
printf "%s=%s\n" ${letters[i]} ${figures[i]}; \
done
a=1
b=2
c=3
A general method that works in any Bourne shell, (not just bash): Use a for loop for one list (a b c), match it up with piped input for the second list (1 2 3), produce a string of shell code, and eval that.
eval $(seq 3 | for f in a b c ; do read x ; echo $f=$x ; done) ;
echo $a $b $c
which prints:
1 2 3
eval is needed because variables assigned in a loop are forgotten once the loop is over.
Caution: never let eval execute unknown code. If either list contained unwanted shell code delimiters or commands, further parsing would be necessary, i.e. a prophylactic pipe after '; done'.
Applied to the specific example in the starting question, with two strings, containing space separated lists, we get:
qids="a b c" # our variable names
unset $qids # clear them if need be
upvote_count="1 2 3" # the values to be assigned
# generate code to assign the names list to the values list,
# via a 'for' loop, the index $var_name is for the $qids,
# and assign $var_name to each $upvote_count value which we pipe in.
# Since an assignment can't leave the loop, we 'echo'
# the code for assignment, and 'eval' that code after the loop
# is done.
eval $(
echo "${upvote_count}" |
tr ' ' '\n' |
for var_name in $qids ; do
read value
echo "$var_name=$value"
done
)
# The loop is done, so test if the code worked:
for var_name in $qids
do
echo -n $var_name=
eval echo \$$var_name
done
...which outputs:
a=1
b=2
c=3

Preserve argument splitting when storing command with whitespaces in variable

I'd like to store a command line in a variable, and then execute that command line. The problem is, the command line has arguments with spaces in them. If I do
$ x='command "complex argument"'
$ $x
it calls command with "complex and argument". I tried using "$x" thinking it would preserve the argument splittings, but it only tries to execute a program with the file name command "complex argument". I also tried variations of the quotes (' vs ") and using exec, but it didn't help. Any ideas?
Edit: eval "$x" almost works, but if the whitespaces separating arguments are newlines and not spaces, then it treats the lines as separate commands.
Edit2: The extra " quotes were too much, and made eval interpret the newlines not as spaces, but as command delimiters. The solutions in the answers all work.
For testing purposes, I define:
$ function args() { while [[ "$1" != "" ]]; do echo arg: $1; shift; done }
This works as expected:
$ args "1 2" 3
arg: 1 2
arg: 3
$ x="arg 4 5 6"
$ $x
arg: 4
arg: 5
arg: 6
This doesnt:
$ x="args \"3 4\" 5"
$ $x
arg: "3
arg: 4"
arg: 5
A safe approach is to store your command line in a BASH array:
arr=( command "complex argument" )
Then execute it:
"${arr[#]}"
OR else another approach is to use BASH function:
cmdfunc() {
command "complex argument"
}
cmdfunc
Another solution is
x='command "complex argument"'
eval $x

AIX (KSH) Dynamic Variable Assignment - Split long string into multiple lines and assign each line to variables

On AIX (Korn Shell), how could I achieve dynamic variable name generation and assignment?
I basically have a string as "LINE 1 LINE 2 LINE 3 LINE 4 LINE 5" and I want this long string to be split into multiple lines (each 7 Characters Long) and assign those to dynamically generated variables such as msg_txt_line_1, msg_txt_line_2 and so on.
I looked for information on Internet and using some help from Building Dynamic Variable Names in KornShell i built this snippet so far but it gives errors.
foo.sh
TEXT='LINE 1 LINE 2 LINE 3 LINE 4 LINE 5'
counter=1
echo $TEXT | fmt -7 | while read line ; do eval msg_txt_line_$counter=$line;counter=$(( counter += 1 )) ; done
echo $msg_txt_line_1
echo $msg_txt_line_2
echo $msg_txt_line_3
echo $msg_txt_line_4
echo $msg_txt_line_5
The error is
AIX:>foo.sh
foo.sh[4]: 1: not found.
foo.sh[4]: 2: not found.
foo.sh[4]: 3: not found.
foo.sh[4]: 4: not found.
foo.sh[4]: 5: not found.
Thanks for your guidance.
I have been working on this and with the comments from JS, I have managed to write the following script which works fine. This can still be improved though for example if the long line contains characters such as `, ", ', and Shell special characters? Appreciate if someone could help me improve this snippet.
x=1
TEXT="No one is going to hand me success. I must go out & get it myself. That's why I'm here. To dominate. To conquer. Both the world, and myself."
echo "$TEXT" | fmt -30 | while IFS=$'\n' read -r line; do export msg_txt_line_$x="$line"; let "x=x+1";done
echo "$msg_txt_line_1"
echo "$msg_txt_line_2"
echo "$msg_txt_line_3"
echo "$msg_txt_line_4"
echo "$msg_txt_line_5"
You can create an array and then assign values. Something like:
$ TEXT='LINE 1 LINE 2 LINE 3 LINE 4 LINE 5'
$ echo "$TEXT" | fmt -w7 > myfile
$ while IFS=$'\n' read -r line; do export msg_txt_line_$((++x))="$line"; done <myfile
$ echo "$msg_txt_line_1"
LINE 1
Update:
$ TEXT='LINE 1 LINE 2 LINE 3 LINE 4 LINE 5'
$ echo "$TEXT" | fmt -w7 > myfile
$ while IFS=$'\n' read -r line; do export msg_txt_line_$((++x))="$line"; done <myfile
$ echo "$msg_txt_line_1"
LINE 1

Resources