4

I write a script where must find some files in a user-defined directory which may contain tilde (thus, it's possible to have user_defined_directory='~/foo'). The construct looks like

found_files=$(find "$user_defined_directory" -type f … )

I use quotes to cover possible spaces in that path, but tilde expansion does not work in quotes according to man page. I know about : operator that probably can do this expansion, but I can’t figure out how to use it here.

The ‘user-defined-directory’ is being taken from another configuration file in user $HOME directory. It is not passing to my script as a parameter, it’s being parsed from that another config in the script I write.

Charles Duffy
  • 235,655
  • 34
  • 305
  • 356
tijagi
  • 1,006
  • 1
  • 10
  • 30
  • How about using eval? `user_defined_directory=$(eval echo "'$user_defined_directory'")` – Vaughn Cato Apr 07 '13 at 04:24
  • @vaughn-cato no, variable contains the same value it has before. – tijagi Apr 07 '13 at 04:47
  • 2
    FYI: There are a number of 'related' question links on the right, and most of them do not help, including: [Tilde expansion in environment variable](http://stackoverflow.com/questions/3984074/), [Bash Tilde Expansion](http://stackoverflow.com/questions/4651856/), [Bash problem with with cd using tilde expansion](http://stackoverflow.com/questions/5748216/), [Tilde for home directory doesn't expand within quotes](http://stackoverflow.com/questions/8409024/). Not a trivial problem! – Jonathan Leffler Apr 07 '13 at 05:58

3 Answers3

6

You can use "${user_defined_directory/#~/$HOME}" to replace a "~" at the beginning of the string with the current user's home directory. Note that this won't handle the ~username/subdir format, only a plain ~. If you need to handle the more complex versions, you'll need to write a much more complex converter.

Gordon Davisson
  • 95,980
  • 14
  • 99
  • 125
6

This works given some fairly plausible assumptions, but it is far from obvious code (and isn't a one-liner, either):

# Working function - painful, but can you simplify any of it?
# NB: Assumes that ~user does not expand to a name with double spaces or
#     tabs or newlines, etc.

expand_tilde()
{
    case "$1" in
    (\~)        echo "$HOME";;
    (\~/*)      echo "$HOME/${1#\~/}";;
    (\~[^/]*/*) local user=$(eval echo ${1%%/*})
                echo "$user/${1#*/}";;
    (\~[^/]*)   eval echo ${1};;
    (*)         echo "$1";;
    esac
}

# Test cases

name1="~/Documents/over  enthusiastic"
name2="~crl/Documents/double  spaced"
name3="/work/whiffle/two  spaces  are  better  than one"

expand_tilde "$name1"
expand_tilde "$name2"
expand_tilde "$name3"
expand_tilde "~"
expand_tilde "~/"
expand_tilde "~crl"
expand_tilde "~crl/"

# This is illustrative of the 'normal use' of expand_tilde function
x=$(expand_tilde "$name1")
echo "x=[$x]"

When run on my machine (where there is a user crl), the output is:

/Users/jleffler/Documents/over  enthusiastic
/Users/crl/Documents/double  spaced
/work/whiffle/two  spaces  are  better  than one
/Users/jleffler
/Users/jleffler/
/Users/crl
/Users/crl/
x=[/Users/jleffler/Documents/over  enthusiastic]

The function tilde_expansion deals with the various cases separately and differently. The first clause deals with a value ~ and simply substitutes $HOME. The second is a case of paranoia: ~/ is mapped to $HOME/. The third deals with ~/anything (including an empty 'anything'). The next case deals with ~user. The catch-all * deals with everything else.

Note that the code makes the (plausible) assumption that ~user will not expand to a value containing any double spaces, nor any tabs or newlines (and possibly other space-like characters). If you have to deal with that, life is going to be hell.

Note the answer to chdir() to home directory, which explains that POSIX requires ~ to expand to the current value of $HOME, but ~user expands to the value of the home directory from the password database.

Community
  • 1
  • 1
Jonathan Leffler
  • 666,971
  • 126
  • 813
  • 1,185
  • Very nice. Possible simplifications: the second case (`\~/`) can be removed, since the third (`\~/*`) handles it ok. If you use my answer (`"${1/#~/$HOME}"`), you could unify the first three cases (although you'd need two patterns in the case, so it's not really much of a simplification and it's far less readable). – Gordon Davisson Apr 07 '13 at 07:21
  • I tried to combine the second and third cases and didn't get the result I expected, but I'll have another go since I may have been fumble-fingering it. I wasn't delighted with the number of cases, to be polite about it. – Jonathan Leffler Apr 07 '13 at 07:24
  • It's working for me without the second. BTW, it's even working with multiple spaces, tabs, and linefeeds in the `~joeuser` cases (except for linefeeds at the end of the path)! Although it does have the usual risks associated with `eval` in cases like `expand_tilde '~$(rm something)'` – Gordon Davisson Apr 07 '13 at 07:33
  • Updated; testing today showed I was suffering from 'late night programming problems' when I was assembling the answer. – Jonathan Leffler Apr 08 '13 at 18:55
  • Might consider using `printf %q` to quote the username before handing it to `eval`. That should always be a noop for usernames using only characters in the portable set, but if we're concerned about malicious input... – Charles Duffy May 19 '16 at 18:00
  • @CharlesDuffy: AFAICT, `printf` with `%q` is a non-standard extension, presumably for GNU/Linux, compared with the POSIX standard [`printf`](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/printf.html) command. That's not to say it shouldn't be used where it is available; it just isn't available everywhere, necessarily (for instance, it isn't documented in BSD / Mac OS X `printf` command). – Jonathan Leffler May 19 '16 at 18:04
  • It's a ksh-ism adopted by bash and zsh, yes. Notably, this is a question tagged bash. – Charles Duffy May 19 '16 at 18:05
  • ...in bash on MacOS, it'll still be the bash-builtin printf in use, so the lack of support in the external `/usr/bin/printf` provided by the OS is rather moot. And the ksh93 build that ships with MacOS actually provides a printf builtin with a %q implementation significantly superior to that of bash (specifically, one that tries harder to generate POSIX-compliant output where possible; AFAIK, the ksh93 implementation may do so entirely without exception). – Charles Duffy May 19 '16 at 18:06
  • (GNU's `/usr/bin/printf` doesn't provide `%q` either, btw, so far as I'm aware). – Charles Duffy May 19 '16 at 18:10
1

Tilde definitely doesn't expand inside quote. There might be some other bash trick but what I do in this situation is this:

find ~/"$user_defined_directory" -type f

i.e. move starting ~/ outside quotes and keep rest of the path in the quotes.

anubhava
  • 664,788
  • 59
  • 469
  • 547
  • 2
    Ha, if it would be so simple I wouldn’t ask here. The point is that directory _may be_ starting with a tilde and _may not_ as well, leading to some absolute path. – tijagi Apr 07 '13 at 04:50
  • I cannot, because that way means escaping spaces and other stuff that shell may recognize as a special character. It’s more harder and will take more code than a colon in right place. The idea suggested by @vaughn-cato with pre-parsing user_defined_dir is better as it adds just one move. – tijagi Apr 07 '13 at 06:18