3

I would like to set an environment variable (that we'll call "TEST") to store the path to a sub-directory of my home directory : something like /home/myself/dir/. I would like to use the $HOME variable so that the system can get a path that is valid for any user on the system (provided they have a directory called "dir" in their home folder). I tried several possibilities, but could not get it working. Here is one of my tries:

export TEST='$HOME/dir'

We can check that the variable has indeed been created :

env | grep TEST

Which returns:

TEST=$HOME/bin

So far so good. However this is where things get tough :

cd $TEST

Returns:

bash: cd: $HOME/bin: No such file or directory

However following variant works:

eval cd $TEST

Is there any way to obtain the same result, but without invoking eval?

slm
  • 369,824
  • Did you try using double quotes? That is by saying: export TEST="$HOME/dir" – devnull Mar 15 '14 at 11:11
  • He's saying he wants $HOME evaluated only when the variable $TEST is evaluated - he wants its value linked. – mikeserv Mar 15 '14 at 16:32
  • 1
    This is not a duplicate! Even if the basis of problem is same! This is not same question and not same bug... – F. Hauri - Give Up GitHub Mar 18 '14 at 21:25
  • @devnull : the problem if I do this is that the $TEST variable has a "hard coded" value in the sense that it will only use the home path of the user who invoques "export", and will not adapt to the home path of current user.say for example you run your command as user A then log off and eventually log back in as a different user). In other words, that is not exactly what I am trying to achieve... – Gael Lorieul Mar 20 '14 at 09:55

3 Answers3

2

You can do this, but it requires at least 2 shell variable evaluations, and, normally, all you ever get is one. This is by design - I mean, the shell is where all of your stuff is, so it's best to limit the unexpected to a minimum and avoid opening doors behind doors if you can help it. In any case, you definitely have at your disposal the eval shell built-in:

% one=two ; two=three ; eval echo \$$one
> three

% eval "one=\$$two" ; echo $one
> three

It does not store a one -> two pointer, but rather it evaluates its arguments as a single shell statement. If that's a little unclear to you, you're not alone. It basically means that the shell runs the same line twice, or at least as near as I can figure it does, and all of the second pass's assignments are affected by the first pass's expansions - which is not normally the case, as you demonstrate in your question. It also means that two's literal value from a certain point in time is stored in one - copied, not linked, so:

% one=two ; two=three
% eval echo \$$one ; two=four ; eval echo \$$one
> three
> three
% echo $two
> four

But what's up with the backslash? Well, this is where it gets confusing - because you're making two passes you have to quote your quotes. The first pass expands $one to two and strips the first level of quotes, so bye-bye \backslash, but the quoted dollar-sign remains and so $two becomes three on the second pass. This gets really crazy really fast.

Shell quotes are hard enough to deal with as it is - just try to quote a single quote. Layer them and you're asking for trouble, strip, evaluate, strip, evaluate and you're going nowhere safe at all - because quotes are there to protect the values of shell parameters and environment variables and the shell from their values.

So what you've done is store a literal $ dollar sign within the value of your variable. You didn't store a path to $HOME because the shell did not expand its value during assignment - again, by design.

% dir=some_dir ; eval "$dir=\$HOME" ; echo $some_dir 
> /your/$HOME/path/

You don't have to ask around much about this stuff before you hear of the evil eval. It's commonly called that, and for good reason. Look:

two=var\ \;rm\ -f\ /all/your/stuff ; eval home=$two 

Above home is assigned to the expansion of $two. The home= portion of the statement is evaluated twice - after the first pass and before the second, home briefly equals "$two" ; but upon the second, after the quotes are stripped, $home expands to var and /all/your/stuff equals 0. That's a super basic quoting example as well, it can go really wrong in a more complicated situation - and just requirement of eval tends to denote complicated.

There are other ways to accomplish this - GNOUC notes the ${!bash specific} value inversion assignment (I think that's what it's called...) which probably just does the same thing but in a way that bash has attempted to safeguard. That's a good thing. You could also - and this is probably a good idea - write a function to spit out the values you want when it's called:

_home() { exec 2>/dev/null ; set -- /home/*/ ; for h do  (
            cd "$h" && cd "$dir" && printf %s\\n "$dir" ) 
        done
} ; var="$(_home)" ; echo "$var"
> probably...
> the...
> result...
> you...
> wanted...

Almost as dangerous as eval can be a little imagination in combination with use of the shell's . as well. Because .'s job is to execute commands in the current shell environment rather than subshelling out as do pipes and etc, it's possible to construct a twice-evaluated statement that can affect your environment variables with little fuss.

For instance, if you do not quote the limiter on a here-document its contents are subject to shell expansion. If you take that one further and . source it you can execute the results of its expansions. Like this:

% . <<HERE /dev/stdin
> ${one=two}three=four
> home='$HOME'
> HERE
% [ "$home" = "$HOME" ] && echo $twothree
> four

See, in this way, at least, the quotes are easier because they're not stripped except from within an expansion, but they can still get tricky when your expansions do contain quotes. Anyway, that about wraps it up for what I know on the subject.

Actually, one more thing, and this is super-duper risky, but it's your computer:

 % var=eval\ echo\ \$HOME
 % $var
 > /YOUR/HOME

But...

 % cd eval echo $HOME 

obviously doesn't work...

mikeserv
  • 58,310
1

That is not possible in general because environment variables do not work that way. Applications use their content literally. You need an application which breaks this rule.

Maybe it is possible for you to use a wrapper script for calling that application. The wrapper script can solve the problem by using eval.

If this is not for a single application then you may set this variable in the start-up files of the shell.

This is not going to solve every possible problem. E.g. it is not going to work with sudo, cron and so on (unless calling a shell or shell script).

Hauke Laging
  • 90,279
0

The problem is because you assigned to TEST string $HOME/dir, then when shell expand TEST variable, it doesn't expand $HOME. The simple solution is let the shell expand $HOME when you assigned to TEST:

export TEST="$HOME/dir"

I don't know why you want to delay the expanding of $HOME, maybe you want use TEST as global variable and for each user, $TEST will expand to correct their home folder. In this case, you can use bash indirect expansion, but for $HOME only:

$ export TEST="HOME"
$ echo ${!TEST}
/home/cuonglm

Then you can use:

cd ${!TEST}/dir
cuonglm
  • 153,898