Expanded variable as argument of another command via concatenation in Bash [duplicate] - bash

This question already has answers here:
Bash doesn't parse quotes when converting a string to arguments
(5 answers)
Closed 1 year ago.
Imagine I want to call some command some-command via $() with an argument stored in another variable argument, the latter containing space.
With my current understanding, the fact that result="$(some-command $argument)" (e.g. expansion) leads to passing two arguments is as expected.
Question part: why does the result="$(some-command "$argument")" (e.g. concatenation) lead to the desired result of passing one single argument?
More details:
./some-command:
#!/usr/bin/env bash
echo "Arg 1: $1"
echo "Arg 2: $2"
./test-script:
#!/usr/bin/env bash
export PATH="`pwd -P`:$PATH"
argument="one two"
echo "Calling with expansion"
res="$(some-command $argument)"
echo $res
echo "Calling with concatenation"
res="$(some-command "$argument")"
echo $res
Calling test-script leads to the following output:
Calling with expansion
Arg 1: one Arg 2: two
Calling with concatenation
Arg 1: one two Arg 2:
I seem to not grasp when smth is expanded / evaluated and how the expanded variables are grouped into arguments passed to scripts.
Thank you!
P.S. Bonus curiosity is why result="$(some-command \"$argument\")" does not work at all.

That's how quoting and expansions work in bash. In fact, double quotes after = are not needed, as word-splitting is not performed, so
result=$(some-command "$argument")
should work the same way.
There's no "concatenation" going on. Bash treats the string inside $() as a command and runs all the expansions on it before running it.
So, what happens with some-command "$argument"? First, the parameter expansion expands $argument into a string containing spaces. When word-splitting happens, it notes the string was enclosed in double quotes, so it keeps it as a single string.

Related

How to execute a command that is the arguments to the script? [duplicate]

This question already has answers here:
What is the difference between $* and $#
(4 answers)
How do I pass on script arguments that contain quotes/spaces?
(2 answers)
Closed last month.
I want a bash script, call it args, to execute a command that is the arguments to the script.
In particular, I would like this command (note multiple blanks):
$./args echo 'foobar *0x0'
to execute this precise command:
echo 'foobar *0x0'
I tried this in args:
#!/bin/bash
set -x
$*
but it doesn't work:
./args echo 'foobar *0x0'
+ echo foobar '*0x0'
foobar *0x0
Witness the single space, as well as moved single quotes.
With $#, the result is exactly the same, so please don't close the question on the account of differences between $* and $#. Also, blanks are not my only problem, there is the *0x0.
#!/bin/bash
"$#"
This expands to all of the command-line arguments with spacing and quoting intact. $*, by contrast, is subject to unwanted word splitting and globbing since it's not quoted.

Error "command not found" when setting value to variable [duplicate]

This question already has answers here:
Indirect variable assignment in bash
(7 answers)
What is indirect expansion? What does ${!var*} mean?
(6 answers)
Closed 6 years ago.
I have the following test.sh script:
#!/bin/bash
foo=0
bar=foo;
${bar}=1
echo $foo;
Output:
./test.sh: line 4: foo=1: command not found
0
Why the "command not found" error? How to change script to "echo $foo" outputs 1?
That's not the way to do indirection unfortunately. To do what you want you could use printf like so
printf -v "$bar" "1"
which will store the value printed (here 1 in the variable name given as an argument to -v which when $bar expands here will be foo
Also, you could use declare like
declare "$bar"=1
which will do variable substitution before executing the declare command.
In your attempt the order of bash processing is biting you. Before variable expansion is done the line is split into commands. A command can include variable assignments, however, at that point you do not have a variable assignment of the form name=value so that part of the command is not treated as an assignment. After that, variable expansion is done and it becomes foo=1 but by then we're done deciding if it's an assignment or not, so just because it now looks like one doesn't mean it gets treated as such.
Since it was not processed as a variable assignment, it must not be treated as a command. You don't have a command named foo=1 in your path, so you get the error of command not found.
You need to use the eval function, like
#!/bin/bash
foo=0
bar=foo;
eval "${bar}=1"
echo $foo;
The ${bar}=1 will first go through the substitution process so it becomes foo=1, and then the eval will evaluate that in context of your shell

Nested quotes in bash -- passing single argument containing spaces [duplicate]

This question already has answers here:
How to substitute quoted, multi-word strings as arguments?
(4 answers)
Closed 7 years ago.
script.sh:
#!/bin/bash
echo "First argument: $1"
wrapper.sh:
#!/bin/bash
CALLER='./script.sh "this should be one argument"'
$CALLER
what happens:
$ ./wrapper.sh
First argument: "this
what I was expecting:
$ ./wrapper.sh
First argument: this should be one argument
I tried different exercises to make it work the way I want it, but I can't find the way to invoke script.sh with single argument containing spaces from within wrapper.sh.
I would also like to understand the way nested quotes are interpreted.
This works instead (only last line changed):
#!/bin/bash
CALLER='./script.sh "this should be one argument"'
eval "$CALLER"
The reason for this is that quoting is applied at a different place in the parsing process than variable substitution, so you need to re-run the result of the substitution ($CALLER) through the parsing process (using eval), but quoted (the "…" around $CALLER) to avoid the field splitting that comes with the substitution already.
Further reading: the POSIX documentation on this, and the links already given in comments.

Use the output of a shell command as a variable [duplicate]

This question already has answers here:
Create variable from string/nameonly parameter to extract data in bash?
(3 answers)
Closed 8 years ago.
I want to use the output of a echo command as variable name. Like,
var1="test"
var2="script"
echo ${$1}
If $1 is var1 echo should print test.
${$1} throws error "bad substitution"
What you want is called variable expansion (or indirect expansion). You have to use the syntax ${!var}:
~$ cat s.sh
var1="test"
var2="script"
echo ${!1}
~$ ./s.sh var1
test
~$ ./s.sh var2
script
From man bash:
${parameter}
The value of parameter is substituted. The braces are required when parameter is a positional parameter with more than one digit, or when parameter is followed by a character which is not to be interpreted as part of its name.
If the first character of parameter is an exclamation point (!), a level of variable indirection is introduced. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion. The exceptions to this are the expansions of ${!prefix*} and ${!name[#]} described below. The exclamation point must immediately follow the left brace in order to introduce indirection.
You can do this:
$ foo='bar'
$ baz='foo'
$ echo ${!baz}
bar

How to pass all arguments passed to my Bash script to a function of mine? [duplicate]

This question already has answers here:
Propagate all arguments in a Bash shell script
(12 answers)
Closed 3 years ago.
Let's say I have a function abc() that will handle the logic related to analyzing the arguments passed to my script.
How can I pass all arguments my Bash script has received to abc()? The number of arguments is variable, so I can't just hard-code the arguments passed like this:
abc $1 $2 $3 $4
Better yet, is there any way for my function to have access to the script arguments' variables?
The $# variable expands to all command-line parameters separated by spaces. Here is an example.
abc "$#"
When using $#, you should (almost) always put it in double-quotes to avoid misparsing of arguments containing spaces or wildcards (see below). This works for multiple arguments. It is also portable to all POSIX-compliant shells.
It is also worth noting that $0 (generally the script's name or path) is not in $#.
The Bash Reference Manual Special Parameters Section says that $# expands to the positional parameters starting from one. When the expansion occurs within double quotes, each parameter expands to a separate word. That is "$#" is equivalent to "$1" "$2" "$3"....
Passing some arguments:
If you want to pass all but the first arguments, you can first use shift to "consume" the first argument and then pass "$#" to pass the remaining arguments to another command. In Bash (and zsh and ksh, but not in plain POSIX shells like dash), you can do this without messing with the argument list using a variant of array slicing: "${#:3}" will get you the arguments starting with "$3". "${#:3:4}" will get you up to four arguments starting at "$3" (i.e. "$3" "$4" "$5" "$6"), if that many arguments were passed.
Things you probably don't want to do:
"$*" gives all of the arguments stuck together into a single string (separated by spaces, or whatever the first character of $IFS is). This looses the distinction between spaces within arguments and the spaces between arguments, so is generally a bad idea. Although it might be ok for printing the arguments, e.g. echo "$*", provided you don't care about preserving the space within/between distinction.
Assigning the arguments to a regular variable (as in args="$#") mashes all the arguments together like "$*" does. If you want to store the arguments in a variable, use an array with args=("$#") (the parentheses make it an array), and then reference them as e.g. "${args[0]}" etc. Note that in Bash and ksh, array indexes start at 0, so $1 will be in args[0], etc. zsh, on the other hand, starts array indexes at 1, so $1 will be in args[1]. And more basic shells like dash don't have arrays at all.
Leaving off the double-quotes, with either $# or $*, will try to split each argument up into separate words (based on whitespace or whatever's in $IFS), and also try to expand anything that looks like a filename wildcard into a list of matching filenames. This can have really weird effects, and should almost always be avoided. (Except in zsh, where this expansion doesn't take place by default.)
I needed a variation on this, which I expect will be useful to others:
function diffs() {
diff "${#:3}" <(sort "$1") <(sort "$2")
}
The "${#:3}" part means all the members of the array starting at 3. So this function implements a sorted diff by passing the first two arguments to diff through sort and then passing all other arguments to diff, so you can call it similarly to diff:
diffs file1 file2 [other diff args, e.g. -y]
Use the $# variable, which expands to all command-line parameters separated by spaces.
abc "$#"
Here's a simple script:
#!/bin/bash
args=("$#")
echo Number of arguments: $#
echo 1st argument: ${args[0]}
echo 2nd argument: ${args[1]}
$# is the number of arguments received by the script. I find easier to access them using an array: the args=("$#") line puts all the arguments in the args array. To access them use ${args[index]}.
It's worth mentioning that you can specify argument ranges with this syntax.
function example() {
echo "line1 ${#:1:1}"; #First argument
echo "line2 ${#:2:1}"; #Second argument
echo "line3 ${#:3}"; #Third argument onwards
}
I hadn't seen it mentioned.
abc "$#" is generally the correct answer.
But I was trying to pass a parameter through to an su command, and no amount of quoting could stop the error su: unrecognized option '--myoption'. What actually worked for me was passing all the arguments as a single string :
abc "$*"
My exact case (I'm sure someone else needs this) was in my .bashrc
# run all aws commands as Jenkins user
aws ()
{
sudo su jenkins -c "aws $*"
}
abc "$#"
$# represents all the parameters given to your bash script.

Resources