With a username passed to a script, find the user's home directory

I am writing a script that gets called when a user logs in and check if a certain folder exists or is a broken symlink. (This is on a Mac OS X system, but the question is purely bash).

It is not elegant, and it is not working, but right now it looks like this:

#!/bin/bash

# Often users have a messed up cache folder -- one that was redirected
# but now is just a broken symlink.  This script checks to see if
# the cache folder is all right, and if not, deletes it
# so that the system can recreate it.

USERNAME=$3
if [ "$USERNAME" == "" ] ; then
    echo "This script must be run at login!" >&2
    exit 1
fi

DIR="~$USERNAME/Library/Caches"

cd $DIR || rm $DIR && echo "Removed misdirected Cache folder" && exit 0

echo "Cache folder was fine."

The crux of the problem is that the tilde expansion is not working as I'd like.

Let us say that I have a user named george, and that his home folder is /a/path/to/georges_home. If, at a shell, I type:

cd ~george

it takes me to the appropriate directory. If I type:

HOME_DIR=~george
echo $HOME_DIR

It gives me:

/a/path/to/georges_home

However, if I try to use a variable, it does not work:

USERNAME="george"
cd ~$USERNAME
-bash: cd: ~george: No such file or directory

I've tried using quotes and backticks, but can't figure out how to make it expand properly. How do I make this work?


Addendum

I just wanted to post my completed script (really, it isn't as ugly as the work in progress above!) and say that it appears to be working right.

#!/bin/bash

# Often users have a messed up cache folder -- one that was redirected
# but now is just a broken symlink.  This script checks to see if
# the cache folder is all right, and if not, deletes it
# so that the system can recreate it.

#set -x # turn on to help debug

USERNAME=$3 # Casper passes the user name as parameter 3
if [ "$USERNAME" == "" ] ; then
    echo "This script must be run at login!" >&2
    exit 1  # bail out, indicating failure
fi

CACHEDIR=`echo $(eval echo ~$USERNAME/Library/Caches)`

# Show what we've got
ls -ldF "$CACHEDIR"

if [ -d "$CACHEDIR" ] ; then
    # The cache folder either exists or is a working symlink
    # It doesn't really matter, but might as well output a message stating which
    if [ -L "$CACHEDIR" ] ; then
        echo "Working symlink found at $CACHEDIR was not removed."
    else
        echo "Normal directory found at $CACHEDIR was left untouched."
    fi
else
    # We almost certainly have a broken symlink instead of the directory
    if [ -L "$CACHEDIR" ] ; then
        echo "Removing broken symlink at $CACHEDIR."
        rm "$CACHEDIR"
    else
        echo "Abnormality found at $CACHEDIR.  Trying to remove." >&2
        rm -rf "$CACHEDIR"
        exit 2  # mark this as a bad attempt to fix things; it isn't clear if the fix worked
    fi
fi

# exit, indicating that the script ran successfully,
# and that the Cache folder is (almost certainly) now in a good state
exit 0  

Solution 1:

Use $(eval echo ...):

michael:~> USERNAME=michael
michael:~> echo ~michael
/home/michael
michael:~> echo ~$USERNAME
~michael
michael:~> echo $(eval echo ~$USERNAME)
/home/michael

So your code should look like:

HOMEDIR="$(eval echo ~$USERNAME)"

Solution 2:

It's because it's going to enclose the ~george within singlequotes when set as a variable. set -x is useful for debugging.

Remove the quoting when setting DIR and the shell will expand when setting the variable, which will give you your desired performance.

lrwxrwxrwx  1 root root 4 Sep 10  2004 /bin/sh -> bash*
wmoore@bitbucket(/tmp)$ cat test.sh
#!/bin/bash
set -x

cd ~root

DIR=~root

cd $DIR

DIR="~root"

cd $DIR
wmoore@bitbucket(/tmp)$ sh test.sh
+ cd /root
test.sh: line 4: cd: /root: Permission denied
+ DIR=/root
+ cd /root
test.sh: line 8: cd: /root: Permission denied
+ DIR=~root
+ cd '~root'
test.sh: line 12: cd: ~root: No such file or directory

Solution 3:

Bash has built-in exported variables for username and user's home directory. If you are calling your script when the user logs in from their ~/.bash_profile for example, you won't need to pass the values as arguments to your script.

You can use $USER and $HOME since they're already set and available in the environment of your script since they're marked as exported. I think tilde expansion is meant to be more of a command-line convenience than something that's used in scripts.

DIR="$HOME/Library/Caches"

cd "$DIR" || rm "$DIR" && echo "Removed misdirected Cache folder" && exit 1

It might be more reliable to get the user's home directory in one of the following ways:

getent passwd $USER | awk -F: '{print $(NF - 1)}'

or

awk -F: -v user=$USER 'user == $1 {print $(NF - 1)}' /etc/passwd

Also, exit 0 indicates success. In a way, your process is successful in deleting the directory, but the fact that it needs deleting is an error of a sort. In any case, if you exit 0 at this point you won't be able to tell the difference when the script exits after the final echo since the exit code there will most likely be zero.

Solution 4:

Judging by the path $HOME/Library/Caches, this is Mac OS X, so dscl is your friend.

As noted above, bash does it for you, though, and if it’s a Mac, you can guarantee bash will be available (and so you don’t need to worry about a strictly-conforming /bin/sh not being able to cope with it).