The $HOME
environment variable is commonly set and exported by login
to the pathname of a user's home directory when a user logs in. A POSIX-compatible shell will use the value of this environment variable in a context when it should perform a ~
tilde expansion to complete a path to a username's home directory but the actual expanded field is otherwise null. Aside from its association with the shell's tilde expansion and with /bin/login
the $HOME
shell variable is in no other case exceptional - it is handled by the shell as any other shell variable might be. The shell's tilde expansion, however, is unique as shell expansions go in almost every way.
This is a little a difficult to grasp at first - and is probably why the tilde expansion is so underused despite its immunity to the many varied kinds of shell-expansion vulnerabilities that can often negatively affect the more common kinds.
The shell might portably consider a tilde eligible for expansion if read in any of the following contexts:
- Immediately follows either the first
=
or any colon-character in a shell-variable assignment.
- Begins any shell argument word.
- Begins a shell path and either completes at least one full properly delimited path component all alone or can verifiably do so with trailing context.
Even in these cases the ~
tilde will not be expanded if it is quoted, or if the context somehow precludes its expanding wholly to some user's home directory or the current value of the $HOME
environment variable. In other words, the shell will not perform any tilde expansions if the context of the expansion is at all uncertain. While the shell will happily expand $var_not_var
away to null, it will refuse every time to make similar guesses about a ~
.
The $HOME
substitution, for example, can only occur if the tilde's possible expansion field is null - which is to say that its position in the argument list unequivocally guarantees its eligibility for expansion because it is delimited on all sides.
The only other case that a tilde might be expanded is when its trailing context completely matches a verified system username - in which case it is substituted for a path to said user's assigned home directory as reported-on by the system. The shell should compare the context against a list of the system's registered users and only expand if the trailing context matches one of them.
To clarify what I mean by a null or not-null ~
expansion field, here are a few examples:
printf %s\\n ~some_user_name ~some_user_name/some_more_stuff
Both of the above expansion contexts are not null - the tilde can only expand to the pathname of some_user_name's home directory and only if there is such a registered username on the current system. The second ~
should expand (or not expand) identically to the first because the path /
separator character delimits the trailing context. If the shell cannot verify the existence of some_user_name then the ~
should remain just a tilde.
printf %s\\n ~/some_user_name /~/ ~
But here the expansion fields for the first and third tildes are null. The first is delimited by the path separator char. The second is similarly delimited, but is not even considered for expansion because it does not lead the shell word. The first and third tildes should both expand to the current value of the $HOME
environment variable, and the results of the expansion should be equivalent to:
printf %s\\n "$HOME"/some_user_name /~/ "$HOME"
Note that the "$HOME"/some_user_name
path above need not actually exist to qualify the tilde for expansion - as is also true of the previous ~some_user_name/some_more_stuff
example - which is a unique attribute among portable path expansions. Typical filename generation or shell globbing behavior as might be attempted with the special shell chars ?[*
is only to expand these characters away if they match a resolved pathname but otherwise to let them remain literally. The tilde, however, instead adopts an arbitrary preassigned value - as specified in $HOME
- when a match cannot be had so long as its expansion eligibility is absolute.
Note also that the quotes used above are intentional - while the ~
tilde expansion will not occur in quotes, the value of its expansion is always literal and never subject to further pathname expansions or shell splitting of any kind. It expands in the same way most other expansions require quotes to do - when it expands at all. This is (possibly very usefully) contrasted, of course, by the expansion behavior of the actual $HOME
environment variable.
As I previously hinted, the ~
tilde expansion can only occur in either an argument or assignment context unless the expansion completes a delimited path component of an executable command. And so a ~
passed alone in command position will not be subject to expansion. It is therefore possible to assign a ~
shell alias
such that the command:
~ ~
...results in two independently and uniquely expanded values which are processed entirely based on input context. A user might even define the ~
alias
to define the value of the $HOME
shell variable when called such that the second tilde's expansion is altered by the first (and possibly even vice-versa with recursion).
In fact, I like to think of the ~
tilde as a kind of argument alias. I often use it in combination with $IFS
- to the effects of which its expansions are immune - and $*
and eval
. I consider that it lends itself remarkably well to $IFS
contexts because - as a single character - it can either be removed from a string entirely with a single expansion, or else safely substitute in any number of other characters per occurrence as I desire it. An argument array that looks like:
"$argv1" ~ "$argv2" ~ "$argv3" ~
...might be (safely) any number of things at any given time by merely temporarily reassigning the value of $HOME
to whatever I wish and reeval
uating it. Such an argument array can even be generated from any other on the original argument delimiter points with a little dextrous application of $*
substitutions.
Another construct I like to use is:
for HOME in ./* ~; do stuff w/ $HOME and ~; done
...which gets me two values for each iteration's assignment. To one of these I can freely apply parsing in the form of $IFS
splitting or additional glob characters immediately alongisde the other which will always literally refer to the iteration's original argument value. And what's more, I can do these things side-by-side in the same command's argument list and without overly cluttering it with shell-quotes.
It is this last point, I think, which most clearly demonstrates the difference between $HOME
and ~
.