Skip to content

Commit adf90cb

Browse files
enhance Executable function to handle relative paths (#537)
* refactor: enhance Executable function to handle relative paths and symlinks * refactor: update executable resolution logic and add tests for edge cases
1 parent 859da21 commit adf90cb

File tree

6 files changed

+202
-7
lines changed

6 files changed

+202
-7
lines changed

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ ignore:
22
- shell/mock/
33
- lock/test/
44
- priority/check/
5+
- util/test_executable/
56
- term/remote.go
67
- deprecation.go
78
- logger.go

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ sonar.projectName=resticprofile
44
sonar.projectVersion=0.32.0
55

66
sonar.sources=.
7-
sonar.exclusions=**/*_test.go,/docs/**
7+
sonar.exclusions=**/*_test.go,/docs/**,**/mocks/*.go,shell/mock/**,lock/test/**,priority/check/**,util/test_executable/**
88

99
sonar.tests=.
1010
sonar.test.inclusions=**/*_test.go

util/executable_linux.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,38 @@ package util
55
import (
66
"errors"
77
"os"
8+
"os/exec"
89
"path/filepath"
910
)
1011

1112
// Executable returns the path name for the executable that started the current process.
1213
// On non-Linux systems, it behaves like os.Executable.
1314
// On Linux, it returns the path to the executable as specified in the command line arguments.
1415
func Executable() (string, error) {
15-
executable := os.Args[0]
16+
return resolveExecutable(os.Args[0])
17+
}
18+
19+
func resolveExecutable(executable string) (string, error) {
1620
if len(executable) == 0 {
1721
return "", errors.New("executable path is empty")
1822
}
1923
if executable[0] != '/' {
20-
wd, err := os.Getwd()
21-
if err != nil {
22-
return "", err
24+
// If the path is relative, we need to resolve it to an absolute path
25+
if executable[0] == '.' {
26+
wd, err := os.Getwd()
27+
if err != nil {
28+
return "", err
29+
}
30+
// If the executable path is relative, prepend the current working directory to form an absolute path.
31+
executable = filepath.Join(wd, executable)
32+
} else {
33+
// If the path is not absolute, we assume it is in the PATH and resolve it.
34+
found, err := exec.LookPath(executable)
35+
if err != nil {
36+
return "", err
37+
}
38+
executable = filepath.Clean(found)
2339
}
24-
// If the executable path is relative, prepend the current working directory to form an absolute path.
25-
executable = filepath.Join(wd, executable)
2640
}
2741
return executable, nil
2842
}

util/executable_linux_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//go:build linux
2+
3+
package util
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestResolveExecutable(t *testing.T) {
15+
t.Run("empty path", func(t *testing.T) {
16+
path, err := resolveExecutable("")
17+
assert.Equal(t, "", path)
18+
assert.Error(t, err)
19+
assert.Equal(t, "executable path is empty", err.Error())
20+
})
21+
22+
t.Run("absolute path", func(t *testing.T) {
23+
path, err := resolveExecutable("/usr/bin/ls")
24+
assert.NoError(t, err)
25+
assert.Equal(t, "/usr/bin/ls", path)
26+
})
27+
28+
t.Run("relative path with dot", func(t *testing.T) {
29+
wd, err := os.Getwd()
30+
require.NoError(t, err)
31+
32+
path, err := resolveExecutable("./test")
33+
assert.NoError(t, err)
34+
expected := filepath.Join(wd, "test")
35+
assert.Equal(t, expected, path)
36+
})
37+
38+
t.Run("command in PATH", func(t *testing.T) {
39+
// Testing with "ls" which should be available on most Linux systems
40+
path, err := resolveExecutable("ls")
41+
assert.NoError(t, err)
42+
assert.NotEmpty(t, path)
43+
t.Log(path)
44+
// The exact path can vary by system, but it should be an absolute path
45+
assert.True(t, filepath.IsAbs(path), "Path should be absolute")
46+
})
47+
48+
t.Run("command not in PATH", func(t *testing.T) {
49+
path, err := resolveExecutable("this_command_should_not_exist_anywhere")
50+
assert.Equal(t, "", path)
51+
assert.Error(t, err)
52+
})
53+
}

util/executable_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package util
22

33
import (
4+
"os"
5+
"os/exec"
46
"path/filepath"
57
"testing"
68

9+
"github.com/creativeprojects/resticprofile/platform"
710
"github.com/stretchr/testify/assert"
811
"github.com/stretchr/testify/require"
912
)
@@ -15,3 +18,110 @@ func TestExecutableIsAbsolute(t *testing.T) {
1518

1619
assert.True(t, filepath.IsAbs(executable))
1720
}
21+
22+
func TestExecutable(t *testing.T) {
23+
if platform.IsWindows() {
24+
t.Skip("Executable test is not applicable on Windows")
25+
}
26+
27+
tempDir, err := os.MkdirTemp("", "resticprofile-executable")
28+
if err != nil {
29+
t.Fatalf("failed to create temp dir: %v", err)
30+
}
31+
32+
t.Cleanup(func() {
33+
if err := os.RemoveAll(tempDir); err != nil {
34+
t.Errorf("failed to remove temp dir: %v", err)
35+
}
36+
})
37+
38+
helperBinary := filepath.Join(tempDir, "executable_test_helper")
39+
assert.True(t, filepath.IsAbs(helperBinary), "Helper binary path should be absolute")
40+
41+
cmd := exec.Command("go", "build", "-buildvcs=false", "-o", helperBinary, "./test_executable")
42+
if err := cmd.Run(); err != nil {
43+
t.Fatalf("Error building helper binary: %s\n", err)
44+
}
45+
46+
symlinkBinary := filepath.Join(tempDir, "executable_test_symlink")
47+
err = os.Symlink(helperBinary, symlinkBinary)
48+
require.NoError(t, err, "Failed to create symlink for helper binary")
49+
50+
t.Run("absolute", func(t *testing.T) {
51+
cmd = exec.Command(helperBinary)
52+
output, err := cmd.Output()
53+
if err != nil {
54+
t.Fatalf("Error executing helper binary: %s\n", err)
55+
}
56+
t.Log(string(output))
57+
assert.Equal(t, string(output), "\""+helperBinary+"\"\n", "Output should match the helper binary path")
58+
})
59+
60+
t.Run("absolute symlink", func(t *testing.T) {
61+
cmd = exec.Command(symlinkBinary)
62+
output, err := cmd.Output()
63+
if err != nil {
64+
t.Fatalf("Error executing helper binary: %s\n", err)
65+
}
66+
t.Log(string(output))
67+
assert.Equal(t, string(output), "\""+symlinkBinary+"\"\n", "Output should match the helper binary path")
68+
})
69+
70+
t.Run("relative", func(t *testing.T) {
71+
cmd = exec.Command("./" + filepath.Base(helperBinary))
72+
cmd.Dir = tempDir // Set the working directory to the temp directory
73+
output, err := cmd.Output()
74+
if err != nil {
75+
t.Fatalf("Error executing helper binary: %s\n", err)
76+
}
77+
t.Log(string(output))
78+
assert.Equal(t, string(output), "\""+helperBinary+"\"\n", "Output should match the helper binary path")
79+
})
80+
81+
t.Run("relative symlink", func(t *testing.T) {
82+
cmd = exec.Command("./" + filepath.Base(symlinkBinary))
83+
cmd.Dir = tempDir // Set the working directory to the temp directory
84+
output, err := cmd.Output()
85+
if err != nil {
86+
t.Fatalf("Error executing helper binary: %s\n", err)
87+
}
88+
t.Log(string(output))
89+
assert.Equal(t, string(output), "\""+symlinkBinary+"\"\n", "Output should match the helper binary path")
90+
})
91+
92+
t.Run("from PATH", func(t *testing.T) {
93+
path := os.Getenv("PATH")
94+
t.Cleanup(func() {
95+
os.Setenv("PATH", path) // Restore original PATH after test
96+
})
97+
os.Setenv("PATH", tempDir+string(os.PathListSeparator)+path) // Add tempDir to PATH for this test
98+
t.Logf("Using PATH: %s", os.Getenv("PATH"))
99+
100+
cmd = exec.Command(filepath.Base(helperBinary))
101+
cmd.Dir = tempDir // Set the working directory to the temp directory
102+
output, err := cmd.Output()
103+
if err != nil {
104+
t.Fatalf("Error executing helper binary: %s\n", err)
105+
}
106+
t.Log(string(output))
107+
assert.Equal(t, string(output), "\""+helperBinary+"\"\n", "Output should match the helper binary path")
108+
})
109+
110+
t.Run("symlink from PATH", func(t *testing.T) {
111+
path := os.Getenv("PATH")
112+
t.Cleanup(func() {
113+
os.Setenv("PATH", path) // Restore original PATH after test
114+
})
115+
os.Setenv("PATH", tempDir+string(os.PathListSeparator)+path) // Add tempDir to PATH for this test
116+
t.Logf("Using PATH: %s", os.Getenv("PATH"))
117+
118+
cmd = exec.Command(filepath.Base(symlinkBinary))
119+
cmd.Dir = tempDir // Set the working directory to the temp directory
120+
output, err := cmd.Output()
121+
if err != nil {
122+
t.Fatalf("Error executing helper binary: %s\n", err)
123+
}
124+
t.Log(string(output))
125+
assert.Equal(t, string(output), "\""+symlinkBinary+"\"\n", "Output should match the helper binary path")
126+
})
127+
}

util/test_executable/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/creativeprojects/resticprofile/util"
8+
)
9+
10+
func main() {
11+
executable, err := util.Executable()
12+
if err != nil {
13+
fmt.Fprintf(os.Stderr, "Error getting executable: %s\n", err)
14+
os.Exit(1)
15+
}
16+
fmt.Printf("%q\n", executable)
17+
}

0 commit comments

Comments
 (0)