Expand tilde to home directory

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?


Solution 1:

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 2:

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 3:

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 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))