bug-bash
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Bash $@ Parameter Variable Breaking Out of Strings


From: Greg Wooledge
Subject: Re: Bash $@ Parameter Variable Breaking Out of Strings
Date: Tue, 22 Mar 2016 11:33:25 -0400
User-agent: Mutt/1.4.2.3i

Today's lesson is: intent.  Always tell us what you are trying to do.
In a script, that means writing comments.  In an email, you can write
plain text anywhere you want.

It's helpful!  Try it!

On Tue, Mar 22, 2016 at 10:47:28AM -0400, Adam Danischewski wrote:
> $ alias t2='_() { /tmp/m.bsh -d "clarify $@"; }; _'

I don't understand why you are creating an alias.  Just create a
function named t2 and call it, if that's your intent.

$ t2() { /tmp/m.bsh -d "clarify $@"; }
$ t2 hi there

However, see below.

> $ t2 hi there
> 3
> the optarg is clarify hi, optind is 3
>  ### Incorrectly breaks the argument array variable out as separate
>      single string arguments.

It's almost never desirable to combine "$@" with any other string,
such as "clarify $@".  "$@" is special magic which expands to each
positional parameter as a separate word.  If you place that inside
another string, then the first or last positional parameter (or both)
become "contaminated" by the other parts of the string.

imadev:~$ set -- hi there
imadev:~$ args "$@"
2 args: <hi> <there>
imadev:~$ args "clarify $@"
2 args: <clarify hi> <there>
imadev:~$ args "$@ mom"
2 args: <hi> <there mom>
imadev:~$ args "clarify $@ mom"
2 args: <clarify hi> <there mom>

If your intent was to pass 3 arguments, the first being "clarify" and
the others being "$@", then you need to separate the two strings:

... /tmp/m.bsh -d clarify "$@"

That said, I don't quite understand what you are trying to do in t2.
Did you want 3 arguments?  Or did you want 2 arguments with the constant
string prefixed to each one ("clarify hi" "clarify there")?  Or something
else entirely?

"args" is a script I use to show how the arguments of a command are
expanding.

imadev:~$ cat bin/args
#!/bin/sh
printf "%d args:" "$#"
printf " <%s>" "$@"
echo

> I noticed another interesting occurrence as well, I'm not sure if they are
> related, to variable names:
> 
> function update() {
> local -i VAR=45
> VAR+=-1
> VAR+=$1
> echo $VAR
> }

I recommend *against* using the -i attribute on variables, because it
makes code do surprising things.  Your intent may be clear in this
simple example, but in larger scripts you'll be unable to tell whether
a given line that contains var+=... is doing string concatenation or
integer addition.

I recommend always using $((...)) or other math context syntax to do
arithmetic.  That way the reader can immediately tell just by looking
at the line, without having to trace the origin of the variable across
hundreds of lines of code, that you are doing math.

Of course, that's just my opinion.

> $ VAR2=2
> $ update VAR2
>   47

I don't get this result, nor do I see how you got it.  I get 46.

> $ VAR=3
> $ update VAR
>   88 ### !?

Remember, you are not passing the *value* of VAR (you did not write $VAR).
You are passing the actual string "VAR" as the argument.

Therefore, within your function VAR+=$1 becomes VAR+=VAR.  The VAR on the
right hand side is the local variable, shadowing the global variable,
so you're just doubling the content of (local) VAR.  44*2 = 88.

The value 3 is never used.

If your intent (see lesson) is to play around with "passing variables by
reference", the short answer is you can't.  Read below for the long
answer.

Bash has no way to pass variables by reference.  The closest it has is
"declare -n", which creates a "nameref", which is sort of like a symbolic
link from one variable name to another.  But the names are both resolved
in the same scope -- it does NOT let you safely link a local variable in
a function to a variable in the caller's scope.

Namerefs have exactly the same problem as your example above: if the
function's local variable and the caller's-scope variable both have the
same name, the reference points to itself.  There's no way to get at
the caller's variable, because the local variable is shadowing it.

In other words, bash doesn't have an [upvar] command (a la Tcl).
Freddy Vulto tried to make one; see his work at
http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference

It's an ugly hack at best.

I've never used Freddy's upvar trick in a script.  My own preference
for returning values from functions is to use a variable with a specific
name, and document that this name is special, and should not be used
for any other purpose.  For example:

# Pick unbiased random number from 0 to N-1 ($1 = N)
# Returns value in global variable r.
rand() {
  local max=$((32768 / $1 * $1))
  while (( (r=$RANDOM) >= max )); do :; done
  r=$(( r % $1 ))
}

The caller is then responsible to ensure that it does not use the
variable r for any purpose other than retrieving the "return value" from
function rand.  The caller should make a copy of r after calling rand,
because r will be overwritten the next time rand is called.  (You could
use _r or _rand or whatever you want; the important part is that it's
documented, and that you consistently obey your own restrictions.)

This works tolerably well as long as you use a different variable for
each function's return value.  Don't just use "r" for all of them, or
you won't be able to call one function from within another.  (The
script in which I use rand only has that one function.)

This does not work for recursive functions, unfortunately.  But that's
a topic for a different email.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]