Expand tilde to home directory

33,067

Solution 1

Normally, the ~ is expanded by the shell before your program sees it.
Adjust how your program acquires its arguments from the command line in a way compatible with the shell expansion mechanism.

One of the possible problems is using exec.Command like this:

cmd := exec.Command("some-binary", someArg) // say 'someArg' is "~/foo"

which will not get expanded. You can, for example use instead:

cmd := exec.Command("sh", "-c", fmt.Sprintf("'some-binary %q'", someArg))

which will get the standard ~ expansion from the shell.

EDIT: fixed the 'sh -c' example.

Solution 2

Go provides the package os/user, which allows you to get the current user, and for any user, their home directory:

usr, _ := user.Current()
dir := usr.HomeDir

Then, use path/filepath to combine both strings to a valid path:

if path == "~" {
    // In case of "~", which won't be caught by the "else if"
    path = dir
} else if strings.HasPrefix(path, "~/") {
    // Use strings.HasPrefix so we don't match paths like
    // "/something/~/something/"
    path = filepath.Join(dir, path[2:])
}

(Note that user.Current() is not implemented in the go playground (likely for security reasons), so I can't give an easily runnable example).

Solution 3

In general the ~ is expanded by your shell before it gets to your program. But there are some limitations.

In general is ill-advised to do it manually in Go.

I had the same problem in a program of mine and what I have understood is that if I use the flag format as --flag=~/myfile, it is not expanded. But if you run --flag ~/myfile it is expanded by the shell (the = is missing and the filename appears as a separate "word").

Solution 4

If you are expanding tilde '~' for use with exec.Command() you should use the users local shell for expansion.

// 'sh', 'bash' and 'zsh' all respect the '-c' argument
cmd := exec.Command(os.Getenv("SHELL"), "-c", "cat ~/.myrc")
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
    fmt.Fprintln(os.Stderr, err)
}

However; when loading application config files such as ~./myrc this solution is not acceptable. The following has worked well for me across multiple platforms

import "os/user"
import "path/filepath"

func expand(path string) (string, error) {
    if len(path) == 0 || path[0] != '~' {
        return path, nil
    }

    usr, err := user.Current()
    if err != nil {
        return "", err
    }
    return filepath.Join(usr.HomeDir, path[1:]), nil
}

NOTE: usr.HomeDir does not respect $HOME instead determines the home directory by reading the /etc/passwd file via the getpwuid_r syscall on (osx/linux). On windows it uses the OpenCurrentProcessToken syscall to determine the users home directory.

Solution 5

I know this is an old question but there is another option now. You can use go-homedir to expand the tidle to the user's homedir:

myPath := "~/.ssh"
fmt.Printf("path: %s; with expansion: %s", myPath, homedir.Expand(myPath))
Share:
33,067

Related videos on Youtube

lukad
Author by

lukad

Handel mit es.

Updated on July 09, 2022

Comments

  • lukad
    lukad almost 2 years

    I have a program that accepts a destination folder where files will be created. My program should be able to handle absolute paths as well as relative paths. My problem is that I don't know how to expand ~ to the home directory.

    My function to expand the destination looks like this. If the path given is absolute it does nothing otherwise it joins the relative path with the current working directory.

    import "path"
    import "os"
    
    // var destination *String is the user input
    
    func expandPath() {
            if path.IsAbs(*destination) {
                    return
            }
            cwd, err := os.Getwd()
            checkError(err)
            *destination = path.Join(cwd, *destination)
    }
    

    Since path.Join doesn't expand ~ it doesn't work if the user passes something like ~/Downloads as the destination.

    How should I solve this in a cross platform way?

  • lukad
    lukad almost 11 years
    Thanks, I didn't know the shell does that.
  • kostix
    kostix almost 11 years
    @jnml, I'd say the first single quote in the format string is misplaced and should instead be right before the %q placeholder to read fmt.Sprintf("some-binary '%q'", someArg)
  • zzzz
    zzzz almost 11 years
    @kostix: I don't think so. The value returned from Sprintf in the above example should be 'some-binary "/home/login/foo"'. Didn't tested it, maybe the outer single quotes should be actually removed. They are correct on the command line, though.
  • zzzz
    zzzz almost 11 years
    Fails for valid paths like ~/a~z.
  • joshlf
    joshlf almost 11 years
    Really? The last parameter defines how many instances to replace; it should stop after the first one.
  • Nick Craig-Wood
    Nick Craig-Wood almost 11 years
    Note also that the shell supports ~user/foo which refers to foo in user's home directory too.
  • kostix
    kostix almost 11 years
    @jnml, yes, you're correct: the %q itself produces a single-quoted string (I missed that). But I think my initial idea still holds: whatever string the call to fmt.Sprintf produces will be a single argument, so there's no need to further quote it.
  • lukad
    lukad almost 11 years
    What about somedir/~/someotherdir?
  • Michael
    Michael over 9 years
    The code should read dir+"/", otherwise the result is missing one /
  • joshlf
    joshlf over 9 years
    If I'm not mistaken, it shouldn't matter. Any utility which uses filepaths (ie, os.Open, unix commands, etc) should treat /path/to/dir and /path/to/dir/ the same way.
  • ralfoide
    ralfoide almost 9 years
    Rather than strings.Replace you can use filepath.Join(usr.HomeDir, path[2:]) to make sure the directory separator is properly added. On Windows at least the HomeDir is not terminated by a slash.
  • Paul Hankin
    Paul Hankin over 7 years
    This answer is 99% right. But if strings.HasPrefix(path, "~/") is more accurate than if path[:2] == "~/". The latter will fail if the path is zero or one characters long. Also the path "~" is not handled correctly -- it should also expand to $HOME.
  • Gwyneth Llewelyn
    Gwyneth Llewelyn about 7 years
    Awesome! Thanks for providing a full example (with error checking and all!) But you also need to import "path/filepath" or your code won't work.
  • thobens
    thobens about 6 years
    This may be true if you get the path as CLI argument, but not if you get a path string from anywhere else.
  • Ivan Black
    Ivan Black almost 6 years
    Yep, if path == "~" { return usr.HomeDir } should be before the HasPrefix check. Play
  • joshlf
    joshlf almost 6 years
    Good catch; fixed.
  • shad
    shad over 3 years
    This is both simpler, and more correct than either the accepted answer, or the most upvoted answer.
  • user2804197
    user2804197 about 3 years
    The behavior of this is different than what for example bash and Python do to expand ~: Usually, ~ is expanded to $HOME if it's set, and as a fallback it's set to the user's home directory (as defined in /etc/passwd). But the os/userpackage only checks the user's home directory and ignores $HOME.