Why Is a Tilde in a Path Not Expanded in a Shell Script

Why is a tilde in a path not expanded in a shell script?

In the bash manual, note that brace expansion during parameter substitution, but not recursively:

The order of expansions is: brace expansion; tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution (done in a left-to-right fashion); word splitting; and filename expansion.

This implies that any tilde (or parameter references or command substitution) stored unexpanded in a bash variable will not automatically resolve. Your JAVA_HOME variable contains a literal tilde, so bash will not expand it automatically.

It is likely that your fix worked because tilde expansion does not apply in quotes:

$ echo "~"
~
$ echo ~
/home/jeffbowman

...but parameter expansion like $HOME does occur in quotes. Replacing it with $HOME expands to your home directory during the assignment of JAVA_HOME.

FOO=~/bar        # stores /home/jeffbowman/bar
FOO="~/bar" # stores ~/bar
FOO=$HOME/bar # stores /home/jeffbowman/bar
FOO="$HOME/bar" # stores /home/jeffbowman/bar

Though the better option is to ensure your assignment is correct, if you want to expand it manually, these SO questions have some good options:

  • "Tilde expansion in quotes"
  • "How to manually expand a special variable (ex: ~ tilde) in bash"

How to manually expand a special variable (ex: ~ tilde) in bash

Due to the nature of StackOverflow, I can't just make this answer unaccepted, but in the intervening 5 years since I posted this there have been far better answers than my admittedly rudimentary and pretty bad answer (I was young, don't kill me).

The other solutions in this thread are safer and better solutions. Preferably, I'd go with either of these two:

  • Charle's Duffy's solution
  • Håkon Hægland's solution

Original answer for historic purposes (but please don't use this)

If I'm not mistaken, "~" will not be expanded by a bash script in that manner because it is treated as a literal string "~". You can force expansion via eval like this.

#!/bin/bash

homedir=~
eval homedir=$homedir
echo $homedir # prints home path

Alternatively, just use ${HOME} if you want the user's home directory.

How to use `~` inside a double quote path reference

A tilde is only expanded if it's unquoted. Quoting (or, equivalent, prepending a backslash) disables expansion and turns them into literal tildes.

It's permissible to begin and end quotes mid-argument. You can quote the spaces while leaving the tilde unquoted. These are all equivalent:

wc -l ~/"file name with spaces.txt"
wc -l ~/'file name with spaces'.txt
wc -l ~/file\ name\ with\ spaces.txt

Tilde (~/) not working on if then statement in Shell script

Double quotes (really, any quotes) prevent ~ from being expanded. Use instead:

checkfile=~/mysql_backup/"$file"

...or, even better, use $HOME in all scripts, and consider ~ to be a facility for interactive/human use only.

Why use $HOME over ~ (tilde) in a shell script?

Tilde expansion doesn't work in some situations, like in the middle of strings like /foo/bar:~/baz

Why is zsh not able to read ~ (tilde) from a path in a script?

Use the following code to get what you want:

export PATH=~/Desktop/Capture/
echo $PATH

# Result: /Users/swift/Desktop/Capture/

Although, when you're using a string, you'll get this:

export PATH="~/Desktop/Capture/"
echo $PATH

# Result: ~/Desktop/Capture/

So to get it right, you'll have to try this approach:

tilde=~
export PATH="${tilde}/Desktop/Capture/"
echo $PATH

# Result: /Users/swift/Desktop/Capture/

P.S. Also, there's one useful command for tilde to be expanded.

Here's an example:

echo tilda=~

# Result: tilda=~

Use magicequalsubst command in zsh:

set -o magicequalsubst
echo tilda=~

# Result: tilda=/Users/swift

How to expand tilde (~) in path

Original answer

Try this:

eval directory="$directory"

Since nothing can interpret the special shell characters better than the shell itself, it is a good idea to ask the shell to evaluate the expression for us. And eval is just the command that evaluates shell expressions.

Alternative #1: program in C

However, eval is unsafe, as it has been mentioned many times, - it may execute malicious code, or cause unwanted effects. Then, for a POSIX environment, you can write a simple program in C:

tildeexp.c

#include <stdio.h>
#include <stdlib.h>
#include <wordexp.h>

int
main(int argc, char **argv)
{
wordexp_t p;
int rc;

rc = wordexp(argv[1], &p, 0);
if (rc) {
fprintf(stderr, "Failed to expand %s: %d\n",
argv[1], rc);
} else {
printf("%s\n", p.we_wordc ? p.we_wordv[0] : "");
}
wordfree(&p);

return (rc ? 1 : 0);
}

Compiling

gcc -Wall -g -O2 -o tildeexp tildeexp.c

Usage

directory=$(/path/to/tildeexp "$directory")
if [ $? -eq 0 ]; then
# success
else
# failed to expand
fi

Alternative #2: Perl's glob

directory="${directory//$"'"/$"\\'"}"
directory=$(perl -e "print glob('$directory')")

Expand variable before tilde

bash expands the tilde before the variable. See https://www.gnu.org/software/bash/manual/bash.html#Shell-Expansions

The shell will see if the literal characters $ o m a r are a login name. As they are not, the tilde is not expanded. The shell eventually sees $omar as a variable and substitutes that. It then hands the expanded word ~xyz123 to echo which just prints it.

Similarly, it hands the word ~xyz123 to ls. Since ls does not do its own tilde expansion, it is looking for a file in your current directory named ~xyz123 with a literal tilde. Since such a file does not exist you get that error.

If you want ls ~$var to list files, you need eval ls ~$var. Or, since eval is considered very unsafe to use casually, you could do this instead:

ls "$(getent passwd "$omar" | cut -d: -f6)"

Bash tilde not expanding in certain arguments, such as --home_dir=~

bash is somewhat mistakenly treating home_dir=~ as an assignment. As such, the ~ is eligible for expansion:

Each variable assignment is checked for unquoted tilde-prefixes immediately following a : or the first =. In these cases, tilde expansion is
also performed.

Since --home_dir is not a valid identifier, that string is not mistaken for an assignment.

Arguably, you have uncovered a bug in bash. (I say arguably, because if you use set -k, then home_dir=~ is an assignment, even though it is after, not before, the command name.)


However, when in doubt, quote a string that is meant to be treated literally whether or not it is subject to any sort of shell processing.

echo '--home_dir=~'

Update: This is intentional, according to the maintainer, to allow assignment-like argument for commands like make to take advantage of tilde-expansion. (And commands like export, which for some reason I was thinking were special because they are builtins, but tilde expansion would have to occur before the actual command is necessarily known.)



Related Topics



Leave a reply



Submit