31

My bash script receives a filename (or relative path) as a string, but must then read from that file. I can only read from a filename if I declare it as a literal directly in the script (without quotes)...which is impossible for arguments since they are implicitly strings to begin with. Observe:

a="~/test.txt"
#Look for it
if [[ -a $a ]] ; then
    echo "A Found it"
else
    echo "A Error"
fi
#Try to use it
while read line; do
    echo $line
done < $a

b='~/test.txt'
#Look for it
if [[ -a $b ]] ; then
    echo "B Found it"
else
    echo "B Error"
fi
#Try to use it
while read line; do
    echo $line
done < $b

c=~/test.txt
#Look for it
if [[ -a $c ]] ; then
    echo "C Found it"
else
    echo "C Error"
fi
#Try to use it
while read line; do
    echo $line
done < $c

YIELDS:

A Error
./test.sh: line 10: ~/test.txt: No such file or directory
B Error
./test: line 12: ~/test.txt: No such file or directory
C Found it
Hello

As stated above, I can't pass a command line argument to the routines above since I get the same behavior that I get on the quoted strings.

Keith Wiley
  • 683
  • 2
  • 6
  • 14
  • 4
    "~/test.txt" and '~/test.txt' stop expansion of the ~ into your home directory. ~/test.txt works because it is unquoted. Stop using the ~ notation or stop using quotes.... – jim mcnamara May 23 '13 at 00:20
  • 1
    If you just use the script's command line argument (`$1`), then everything will work fine because the home directory expansion will have already been done before the script is called. – rici May 23 '13 at 01:45
  • @rici Yes, provided that the `~` is not quoted when his script is called from the command line or from another script. It's a different story when his script is called from some other program that passes _a literal_ `~` That would be a mistake that should be fixed in the other program; but if he want's to deal with such a case in his own script, he probably needs `eval`. – Uwe May 23 '13 at 06:15

3 Answers3

53

This is part of the rules of ~-expansion. It is clearly stated in the Bash manual that this expansion is not performed when the ~ is quoted.

Workaround 1

Don't quote the ~.

file=~/path/to/file

If you need to quote the rest of the filename:

file=~/"path with spaces/to/file"

(This is perfectly legal in a garden-variety shell.)

Workaround 2

Use $HOME instead of ~.

file="$HOME/path/to/file"

BTW: Shell variable types

You seem to be a little confused about the types of shell variables.

Everything is a string.

Repeat until it sinks in: Everything is a string. (Except integers, but they're mostly hacks on top of strings AFAIK. And arrays, but they're arrays of strings.)

This is a shell string: "foo". So is "42". So is 42. So is foo. If you don't need to quote things, it's reasonable not to; who wants to type "ls" "-la" "some/dir"?

  • 2
    `file=~"/path with spaces/to/file"` does not work too. It is necessary to write `file=~/"path with spaces/to/file"` – sercxjo Nov 07 '16 at 10:47
  • 1
    @sercxjo for me it works on zsh and doesn't work on bash. I.e. `~/"` is more portable. – Nick Volynkin Nov 07 '16 at 10:51
  • 1
    @sercxjo (and Nick) fixed – michaelb958--GoFundMonica Nov 07 '16 at 11:16
  • Personally I'd flip these recommendations - using `$HOME` is much more consistent and intuitive. `~` should be treated as a convenience only where it works. Jumping through hoops like taking advantage of quoting semantics may "work" but it's brittle and confusing. – dimo414 Feb 23 '19 at 18:48
  • Just to re-highlight the actual issue as stated above 'This is part of the rules of ~-expansion. It is clearly stated in the Bash manual that this expansion is not performed when the ~ is quoted' OP can you maybe put in big text!! @michaelb958-gofundmonica my fix unixpath=${unixpath/\~/$HOME} – zzapper Dec 03 '20 at 14:32
  • Use ***pattern substitution***, see [my answer](https://stackoverflow.com/a/73486101/1765658)! – F. Hauri - Give Up GitHub Aug 25 '22 at 10:57
  • Oh the tilda `~` is not expanded when contained in string – KoCMoHaBTa Jun 28 '23 at 10:30
0

Still not as glamorous, but worked for me:

home_folder="/home/$(logname)"

can work from there...

Imran Rafiq Rather
  • 7,677
  • 1
  • 16
  • 35
0

Final workaround

In complement of michaelb958's correct answer, I would suggest to use pattern substitution, from bash's Parameter Expansion (see man -P'less +/pattern\ substitution' bash):

file='~/test.txt'
ls "${file/#~\//$HOME\/}"   # This could be double-quoted
/home/user/test.txt

Of course this could be used in both way:

file="$HOME/test.txt"
echo "${file/#$HOME\//\~\/}"
~/test.txt

Filtering output by using sed:

In order to avoid conflict with any kind of $HOME path, I will use ; as sed's s command:

ls -ld ~/* | sed "s;$HOME;~;"
drwxr-xr-x   5 user user   4096 12 jui 16:36 ~/Documents
drwxr-xr-x   5 user user   4096 12 jui 16:36 ~/Desktop
-rw-r--r--   1 user user      0 10 sep 12:34 ~/test.txt
...
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137