Expand tilde to home directory
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))
Related videos on Youtube
Comments
-
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 almost 11 yearsThanks, I didn't know the shell does that.
-
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 readfmt.Sprintf("some-binary '%q'", someArg)
-
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 almost 11 yearsFails for valid paths like
~/a~z
. -
joshlf almost 11 yearsReally? The last parameter defines how many instances to replace; it should stop after the first one.
-
Nick Craig-Wood almost 11 yearsNote also that the shell supports
~user/foo
which refers tofoo
inuser
's home directory too. -
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 tofmt.Sprintf
produces will be a single argument, so there's no need to further quote it. -
lukad almost 11 yearsWhat about
somedir/~/someotherdir
? -
Michael over 9 yearsThe code should read
dir+"/"
, otherwise the result is missing one/
-
joshlf over 9 yearsIf 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 almost 9 yearsRather than
strings.Replace
you can usefilepath.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 over 7 yearsThis answer is 99% right. But
if strings.HasPrefix(path, "~/")
is more accurate thanif 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 about 7 yearsAwesome! 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 about 6 yearsThis may be true if you get the path as CLI argument, but not if you get a path string from anywhere else.
-
Ivan Black almost 6 years
-
joshlf almost 6 yearsGood catch; fixed.
-
shad over 3 yearsThis is both simpler, and more correct than either the accepted answer, or the most upvoted answer.
-
user2804197 about 3 yearsThe 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 theos/user
package only checks the user's home directory and ignores$HOME
.