Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I have a bash function to set the $PATH like this --

assign-path()
{
    str=$1
    # if the $PATH is empty, assign it directly.
    if [ -z $PATH ]; then
        PATH=$str;
    # if the $PATH does not contain the substring, append it with ':'.
    elif [[ $PATH != *$str* ]]; then
        PATH=$PATH:$str;
    fi
}

But the problem is, I have to write different function for different variables (for example, another function for $CLASSPATH like assign-classpath() etc.). I could not find a way to pass argument to the bash function so that I can access it by reference.

It would be better if I had something like --

assign( bigstr, substr )
{
    if [ -z bigstr ]; then
        bigstr=substr;
    elif [[ bigstr != *str* ]]; then
        bigstr=bigstr:substr;
    fi
}

Any idea, how to achieve something like above in bash?

share|improve this question
    
What "other languages"? –  choroba Apr 1 at 22:34
    
well, I tried to mean if bash allows "pass by reference" like in c/java etc. –  ramgorur Apr 1 at 22:39
1  
assign-path /abc will not append /abc to PATH if $PATH already contains /abc/def, /abcd, /def/abc etc. Especially you can't add /bin if PATH already contains /usr/bin. –  miracle173 Apr 2 at 5:33
    
@miracle173 - this is true, what you need to do is split $PATH and negate test against your arguments like: add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". In my answer I do it this way with as many arguments as you like only using IFS=:. –  mikeserv Apr 2 at 6:39

6 Answers 6

up vote 7 down vote accepted

In bash you can use ${!varname} to expand the variable referenced by the contents of another. Eg:

$ var=hello
$ foo () { echo "${!1}"; }
$ foo var
hello

From the man page:

${!prefix*}
${!prefix@}
       Names matching prefix.  Expands to the names of variables whose names
       begin with prefix, separated by the first character of the IFS special
       variable.  When @ is used  and the expansion appears within double quotes,
       each variable name expands to a separate word.

Also, to set a variable referenced by the contents (without the dangers of eval), you can use declare. Eg:

$ var=target
$ declare "$var=hello"
$ echo "$target"
hello

Thus, you could write your function like this (take care because if you use declare in a function, you must give -g or the variable will be local):

shopt -s extglob

assign()
{
  target=$1
  bigstr=${!1}
  substr=$2

  if [ -z "$bigstr" ]; then
    declare -g -- "$target=$substr"
  elif [[ $bigstr != @(|*:)$substr@(|:*) ]]; then
    declare -g -- "$target=$bigstr:$substr"
  fi
}

And use it like:

assign PATH /path/to/binaries

Note that I have also corrected an bug where if substr is already a substring of one of the colon separated members of bigstr, but not its own member, then it wouldn't be added. For example, this would allow adding /bin to a PATH variable already containing /usr/bin. It uses the extglob sets to match either the beginning/end of the string or a colon then anything else. Without extglob, the alternative would be:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]
share|improve this answer

New in bash 4.3, is the -n option to declare & local:

func() {
    local -n ref="$1"
    ref="hello, world"
}

var='goodbye world'
func var
echo "$var"

That prints out hello, world.

share|improve this answer
assign () 
{ 
    if [ -z ${!1} ]; then
        eval $1=$2
    else
        if [[ ${!1} != *$2* ]]; then
            eval $1=${!1}:$2
        fi
    fi
}

$ echo =$x=
==
$ assign x y
$ echo =$x=
=y=
$ assign x y
$ echo =$x=
=y=
$ assign x z
$ echo =$x=
=y:z=

Does this fit?

share|improve this answer
    
hi, I tried to do it like yours, but not working, sorry for being a newbie. Could you please tell me what is wrong with this script? –  ramgorur Apr 1 at 22:36
1  
Your usage of eval is susceptible to arbitrary command execution. –  Chris Down Apr 2 at 3:24
    
You have an idea to make zhe eval lines safer? Normally, when I think I need eval, I decide not to use *sh and switch to a different language instead. On the other hand, using this in scrips to append entries to some PATH-like variables, it will run with string constants and not user input... –  yeti Apr 2 at 4:31
1  
you can do eval safely - but it takes a lot of thought. If you're just trying to reference a parameter, you'd wanna do something like this: eval "$1=\"\$2\"" that way on eval's first pass it only evaluates $1 and on the second it does value="$2". But you need to do something else - this is unnecessary here. –  mikeserv Apr 2 at 5:20
    
Actually, my above comment is wrong, too. You've gotta do "${1##*[;"$IFS"]}=\"\$2\"" - and even that comes with no warranty. Or eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". It's not easy. –  mikeserv Apr 2 at 9:51

Named arguments simply aren't how Bash's syntax was designed. Bash was designed to be an iterative improvement upon the Bourne shell. As such it needs to ensure certain things work between the two shells as much as possible. So it isn't meant to be easier to script with overall, it's just meant to be better than Bourne while ensuring that if you take a script from a Bourne environment over to bash it's as easy as possible. That isn't trivial since a lot of shells still treat Bourne as a de facto standard. Since people write their scripts to be Bourne-compatible (for this portability) the need remains in force and is unlikely to ever change.

You're probably better off looking at a different shell script (like python or something) entirely if it's at all feasible. If you're running up against a language's limitations, you need to start using a new language.

share|improve this answer
    
Maybe early in bash's life this was true. But now specific provisions are made. Full reference variables are now available in bash 4.3 - see derobert's answer. –  Graeme Apr 2 at 1:38
    
And if you look here, you'll see you can do this kind of thing really easily even with just POSIX portable code: unix.stackexchange.com/a/120531/52934 –  mikeserv Apr 2 at 5:27

ON NAMED ARGS:

This is very simply done and bash is not required at all - this is the basic POSIX specified behavior of assignment via parameter expansion:

: ${PATH:=this is only assigned to \$PATH if \$PATH is null or unset}

To demo in a similar vein to @Graeme, but in a portable way:

_fn() { echo "$1 ${2:-"$1"} $str" ; }

% str= ; _fn "${str:=hello}"
> hello hello hello

And there I only do str= to ensure it has a null value, because parameter expansion has the built-in safeguard against reassigning shell environment inline if it's already set.

SOLUTION:

For your specific problem, I don't believe named arguments are necessary, though they're certainly possible. Use $IFS instead:

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- $PATH ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH= ; echo "${PATH:="$*"}" ; IFS=$oFS
}

Here's what I get when I run it:

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% dir="/some crazy/dir"
% p=`assign /usr/bin /usr/bin/new "$dir"`
% echo "$p" ; echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new

Notice it only added the arguments that weren't already in $PATH or that came before? Or even that it took more than one argument at all? $IFS is handy.

share|improve this answer
    
hi, I failed to follow, so could you please elaborate it a little bit more? thanks. –  ramgorur Apr 1 at 22:12
    
I'm already doing so... Just a few more moments please... –  mikeserv Apr 1 at 22:13
    
@ramgorur Any better? Sorry, but real life intruded and it took me a little longer than I expected to finish the write-up. –  mikeserv Apr 2 at 0:58
    
same here as well, succumbed to real life. Looks like lots of different approaches to code this thing, let me give me sometime to settle on the best one. –  ramgorur Apr 2 at 4:52
    
@ramgorur - of course, just wanted to make sure I didn't leave you hanging. About this - you choose whatever you want, man. I will say none of the other answers I can see offer as concise, portable, or as robust a solution as assign does here. If you have questions about how it works, I'd be happy to answer. And by the way, if you really want named arguments, you might wanna look at this other answer of mine in which I show how to declare a function named for another function's arguments: unix.stackexchange.com/a/120531/52934 –  mikeserv Apr 2 at 5:10

You can use eval to set a parameter. A description of this command can be found here. The following usage of eval is wrong:

wrong(){
  eval $1=$2
}

With respect to the additional evaluation evaldoes you should use

assign(){
  eval $1='$2'
}

Check the results of using these functions:

$ X1='$X2'
$ X2='$X3'
$ X3='xxx'
$ 
$ echo :$X1:
:$X2:
$ echo :$X2:
:$X3:
$ echo :$X3:
:xxx:
$ 
$ wrong Y $X1
$ echo :$Y:
:$X3:
$ 
$ assign Y $X1
$ echo :$Y:
:$X2:
$ 
$ assign Y "hallo world"
$echo :$Y:
:hallo world:
$ # the following may be unexpected
$ assign Z $Y
$ echo ":$Z:"
:hallo:
$ # so you have to quote the second argument if its a variable
$ assign Z "$Y"
$ echo ":$Z:"
:hallo world:

But you can achieve your goal without the usage of eval. I prefer this way that is more simple.

The following function makes the substitution in the right way (I hope)

augment(){
  local CURRENT=$1
  local AUGMENT=$2
  local NEW
  if [[ -z $CURRENT ]]; then
    NEW=$AUGMENT
  elif [[ ! ( ( $CURRENT = $AUGMENT ) || ( $CURRENT = $AUGMENT:* ) || \
    ( $CURRENT = *:$AUGMENT ) || ( $CURRENT = *:$AUGMENT:* ) ) ]]; then
    NEW=$CURRENT:$AUGMENT
  else
    NEW=$CURRENT
    fi
  echo "$NEW"
}

Check the following output

augment /usr/bin /bin
/usr/bin:/bin

augment /usr/bin:/bin /bin
/usr/bin:/bin

augment /usr/bin:/bin:/usr/local/bin /bin
/usr/bin:/bin:/usr/local/bin

augment /bin:/usr/bin /bin
/bin:/usr/bin

augment /bin /bin
/bin


augment /usr/bin: /bin
/usr/bin::/bin

augment /usr/bin:/bin: /bin
/usr/bin:/bin:

augment /usr/bin:/bin:/usr/local/bin: /bin
/usr/bin:/bin:/usr/local/bin:

augment /bin:/usr/bin: /bin
/bin:/usr/bin:

augment /bin: /bin
/bin:


augment : /bin
::/bin


augment "/usr lib" "/usr bin"
/usr lib:/usr bin

augment "/usr lib:/usr bin" "/usr bin"
/usr lib:/usr bin

Now you can use the augment function in the following way to set a variable:

PATH=`augment PATH /bin`
CLASSPATH=`augment CLASSPATH /bin`
LD_LIBRARY_PATH=`augment LD_LIBRARY_PATH /usr/lib`
share|improve this answer
    
Even your eval statement is wrong. This, for example: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - would echo "OHNO! before assigning var. You could "${v##*[;"$IFS"]}='$l'" to ensure that the string can't expand to anything that won't be evaluated with the =. –  mikeserv Apr 2 at 9:42
    
@mikeserv thank you for your comment but I think that is not a valid example. The first argument of the assign script should be a variable name or a variable that contains a variable name that is used on the left hand side of the = assignement statement. You can argue that my scrip does not check its argument. That is true. I do not even check if there is any argument or if the number of arguments is valid. But this was by intention. The OP can add such checks if he wants. –  miracle173 Apr 2 at 13:04
    
@mikeserv: I think that your proposal to transform the first argument silently to a valid variable name is not a good idea: 1) some variable is set/overwritten that was not intended by the user. 2) the error is hidden from the user of the function. That is never a good idea. One should simply raise an error if that happens. –  miracle173 Apr 2 at 13:06
    
@mikeserv: It is interesting when one wants to use your variable v (better its value) as second argument of the assign function. So its value should be on the right hand side of an assignement. It is necessaary to quote the argument of the function assign. I added this subtlety to my posting. –  miracle173 Apr 2 at 13:09
    
Probably true - and you're not actually using eval in your final example - which is wise - so it doesn't really matter. But what I'm saying is that any code that uses eval and takes user input is inherently risky - and if you did use your example, I could make the function designed to change my path to rm my path with little effort. –  mikeserv Apr 2 at 13:10

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.