bash: interpret string variable as file name/path
Asked Answered
R

4

35

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.

Raimondo answered 23/5, 2013 at 0:13 Comment(3)
"~/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....Exorcist
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.Wadmal
@Wadmal 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.Flown
L
58

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"?

Lactary answered 23/5, 2013 at 0:25 Comment(8)
file=~"/path with spaces/to/file" does not work too. It is necessary to write file=~/"path with spaces/to/file"Culpable
@Culpable for me it works on zsh and doesn't work on bash. I.e. ~/" is more portable.Desireedesiri
@Culpable (and Nick) fixedLactary
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.Chaucerian
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}Clynes
Oh the tilda ~ is not expanded when contained in stringSalvation
Use pattern substitution once on a whole array! I've just edited my answer!Serilda
@Salvation Depending on how, when and where: echo foo~ ~foo bin~ ~bin root~ ~root may not return what you expect! (to be compared with echo "foo~ ~foo bin~ ~bin root~ ~root"Serilda
S
1

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'
/bin/ls -1 "${file/#~\//$HOME\/}"   # This should 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

This could be done over an whole array too:

files=(~/*)
echo ${files[0]}
/home/user/afile.ext
printf '%s\n' "${files[@]/#"$HOME"/\~}"
~/afile.ext
~/anotherfile.ext
~/...

Please note: ${files...} is double-quoted and $HOME is double-quoted too! As brace ({ ... }) will determine order of expansion, there's no conflict.

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
...
Serilda answered 25/8, 2022 at 10:40 Comment(0)
C
0

Still not as glamorous, but worked for me:

home_folder="/home/$(logname)"

can work from there...

Cassette answered 3/1, 2021 at 6:39 Comment(1)
$HOME directory is not necessarly /home/$USER!!! Depending on your installation, your /etc/adduser.conf or else...Serilda
C
0

In support of "F. Hauri - Give Up GitHub", using pattern substitution is very useful when automating the use of paths as strings containing environment variables, ex. $HOME. For instance converting links made in KDE dolphin result in a file containing a URL to a text string target containing $HOME. A script to create a symbolic link from the dolphin link URL containing $HOME fails. My workaround was to to substitute /home/$USER for the environment variable $HOME using pattern substitution. As an example, the script below is used to create a custom action to change a KDE dolphin link selected in xfce4 Thunar to a symbolic link. The link name is simply a comma separated reverse list of the path to the target. The script did not recognize $HOME so it was changed to /home/$USER using pattern substitution.

#!/bin/bash
# file: cnv_kde-to-symb_link.sh
# date: 20240404
# desc: Convert a link created in KDE dolphin to a symbolic link
# usge: cnv_kde-to-symb_link.sh %ln
# usge: %ln is the KDE dolphin link

string=`grep URL $1 | sed 's/^.*binder\/\(.*\)$/\1/'`
fstring=`grep URL $1 | sed 's/^.*file:\(.*\)$/\1/'`
fstring="${fstring/#\$HOME\//\/home\/$USER\/}"

#Split the path using "/" into an array of directory names
readarray -d "/" -t array <<< "$string"

# Build the link name by reversing the directory names in the path
lnk="_"
sep=""
element=""
# To evaluate an integer expression use $(($var-int))
cnt=$((${#array[@]}-1))
# for ((i=${#array[@]}-1; i>=0; i--)); do
for ((i=$cnt; i>=0; i--)); do
    if [ $i -lt $cnt ]; then sep=","; fi
        # Strip leading or trailing spaces, condense multi-space to single space
        element=`echo ${array[$i]} | xargs`
        lnk=$lnk$sep$element
done

# Create the symbolic link
lstring=$lnk"_ln"
ln -s $fstring ./$lstring
Cribbing answered 5/4, 2024 at 1:2 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.