Skip to content

Commit 5206d72

Browse files
boldandbradJanDeDobbeleer
authored andcommitted
feat: add symlink support
1 parent 9c004ba commit 5206d72

File tree

17 files changed

+301
-17
lines changed

17 files changed

+301
-17
lines changed

src/config/unmarshal.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Aliae struct {
1717
Envs shell.Envs `yaml:"env"`
1818
Paths shell.Paths `yaml:"path"`
1919
Scripts shell.Scripts `yaml:"script"`
20+
Links shell.Links `yaml:"link"`
2021
}
2122

2223
type FuncMap []StringFunc

src/core/init.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func Init(configPath, sh string, printOutput bool) string {
2727
aliae.Envs.Render()
2828
aliae.Paths.Render()
2929
aliae.Aliae.Render()
30+
aliae.Links.Render()
3031
aliae.Scripts.Render()
3132

3233
script := shell.DotFile.String()

src/shell/cmd.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// doskey np=notepad++.exe $*
2-
31
package shell
42

53
const (
@@ -15,8 +13,7 @@ func (a *Alias) cmd() *Alias {
1513
}
1614

1715
func cmdAliasPre() string {
18-
return `
19-
local filename = os.tmpname()
16+
return `local filename = os.tmpname()
2017
local macrofile = io.open(filename, "w+")
2118
`
2219
}
@@ -25,8 +22,7 @@ func cmdAliasPost() string {
2522
return `
2623
macrofile:close()
2724
local _ = io.popen(string.format("doskey /macrofile=%s", filename)):close()
28-
os.remove(filename)
29-
`
25+
os.remove(filename)`
3026
}
3127

3228
func (e *Echo) cmd() *Echo {
@@ -42,6 +38,12 @@ func (e *Env) cmd() *Env {
4238
return e
4339
}
4440

41+
func (l *Link) cmd() *Link {
42+
template := `os.execute("{{ $source := (escapeString .Name) }}mklink {{ if isDir $source }}/d{{ else }}/h{{ end }} {{ $source }} {{ escapeString .Target }} > nul 2>&1")`
43+
l.template = template
44+
return l
45+
}
46+
4547
func (p *Path) cmd() *Path {
4648
p.template = `os.setenv("PATH", "{{ escapeString .Value }};" .. os.getenv("PATH"))`
4749
return p

src/shell/link.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package shell
2+
3+
import (
4+
"github.com/jandedobbeleer/aliae/src/context"
5+
)
6+
7+
type Links []*Link
8+
9+
type Link struct {
10+
Name Template `yaml:"name"`
11+
Target Template `yaml:"target"`
12+
If If `yaml:"if"`
13+
14+
template string
15+
}
16+
17+
func (l *Link) string() string {
18+
switch context.Current.Shell {
19+
case ZSH, BASH, FISH, XONSH:
20+
return l.zsh().render()
21+
case PWSH, POWERSHELL:
22+
return l.pwsh().render()
23+
case NU:
24+
return l.nu().render()
25+
case TCSH:
26+
return l.tcsh().render()
27+
case CMD:
28+
return l.cmd().render()
29+
default:
30+
return ""
31+
}
32+
}
33+
34+
func (l *Link) render() string {
35+
script, err := parse(l.template, l)
36+
if err != nil {
37+
return err.Error()
38+
}
39+
40+
return script
41+
}
42+
43+
func (l Links) Render() {
44+
if len(l) == 0 {
45+
return
46+
}
47+
48+
first := true
49+
for _, link := range l {
50+
script := link.string()
51+
if len(script) == 0 || link.If.Ignore() {
52+
continue
53+
}
54+
55+
if first && DotFile.Len() > 0 {
56+
DotFile.WriteString("\n")
57+
}
58+
59+
DotFile.WriteString("\n")
60+
DotFile.WriteString(script)
61+
62+
first = false
63+
}
64+
}

src/shell/link_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package shell
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/jandedobbeleer/aliae/src/context"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestLinkCommand(t *testing.T) {
12+
link := &Link{Name: "foo", Target: "bar"}
13+
cases := []struct {
14+
Case string
15+
Shell string
16+
Expected string
17+
OS string
18+
}{
19+
{
20+
Case: "PWSH",
21+
Shell: PWSH,
22+
Expected: "New-Item -Path \"foo\" -ItemType SymbolicLink -Value \"bar\" -Force",
23+
},
24+
{
25+
Case: "CMD",
26+
Shell: CMD,
27+
Expected: `os.execute("mklink /h foo bar > nul 2>&1")`,
28+
},
29+
{
30+
Case: "FISH",
31+
Shell: FISH,
32+
Expected: "ln -sf bar foo",
33+
},
34+
{
35+
Case: "NU",
36+
Shell: NU,
37+
Expected: "ln -sf bar foo out+err>| ignore",
38+
},
39+
{
40+
Case: "NU Windows",
41+
Shell: NU,
42+
OS: context.WINDOWS,
43+
Expected: "mklink /h foo bar out+err>| ignore",
44+
},
45+
{
46+
Case: "TCSH",
47+
Shell: TCSH,
48+
Expected: "ln -sf bar foo;",
49+
},
50+
{
51+
Case: "XONSH",
52+
Shell: XONSH,
53+
Expected: "ln -sf bar foo",
54+
},
55+
{
56+
Case: "ZSH",
57+
Shell: ZSH,
58+
Expected: `ln -sf bar foo`,
59+
},
60+
{
61+
Case: "BASH",
62+
Shell: BASH,
63+
Expected: `ln -sf bar foo`,
64+
},
65+
}
66+
67+
for _, tc := range cases {
68+
link.template = ""
69+
context.Current = &context.Runtime{Shell: tc.Shell, OS: tc.OS}
70+
assert.Equal(t, tc.Expected, link.string(), tc.Case)
71+
}
72+
}
73+
74+
func TestLinkRender(t *testing.T) {
75+
cases := []struct {
76+
Case string
77+
Expected string
78+
Links Links
79+
}{
80+
{
81+
Case: "Single link",
82+
Links: Links{
83+
&Link{Name: "FOO", Target: "bar"},
84+
},
85+
Expected: "ln -sf bar FOO",
86+
},
87+
{
88+
Case: "Double link",
89+
Links: Links{
90+
&Link{Name: "FOO", Target: "bar"},
91+
&Link{Name: "BAR", Target: "foo"},
92+
},
93+
Expected: `ln -sf bar FOO
94+
ln -sf foo BAR`,
95+
},
96+
{
97+
Case: "Filtered out",
98+
Links: Links{
99+
&Link{Name: "FOO", Target: "bar", If: `eq .Shell "fish"`},
100+
},
101+
},
102+
}
103+
104+
for _, tc := range cases {
105+
DotFile.Reset()
106+
context.Current = &context.Runtime{Shell: BASH}
107+
tc.Links.Render()
108+
assert.Equal(t, tc.Expected, strings.TrimSpace(DotFile.String()), tc.Case)
109+
}
110+
}
111+
112+
func TestLinkWithTemplate(t *testing.T) {
113+
cases := []struct {
114+
Case string
115+
Target Template
116+
Expected string
117+
}{
118+
{
119+
Case: "No template",
120+
Target: "~/dotfiles/zshrc",
121+
Expected: `ln -sf ~/dotfiles/zshrc /tmp/l`,
122+
},
123+
{
124+
Case: "Home in template",
125+
Target: "{{ .Home }}/.aliae.yaml",
126+
Expected: `ln -sf /Users/jan/.aliae.yaml /tmp/l`,
127+
},
128+
{
129+
Case: "Advanced template",
130+
Target: "{{ .Home }}/go/bin/aliae{{ if eq .OS \"windows\" }}.exe{{ end }}",
131+
Expected: `ln -sf /Users/jan/go/bin/aliae.exe /tmp/l`,
132+
},
133+
}
134+
135+
for _, tc := range cases {
136+
link := &Link{Name: "/tmp/l", Target: tc.Target}
137+
context.Current = &context.Runtime{Shell: BASH, Home: "/Users/jan", OS: context.WINDOWS}
138+
assert.Equal(t, tc.Expected, link.string(), tc.Case)
139+
}
140+
}

src/shell/nu.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ func (e *Env) nu() *Env {
4545
return e
4646
}
4747

48+
func (l *Link) nu() *Link {
49+
template := `ln -sf {{ .Target }} {{ .Name }} out+err>| ignore`
50+
if context.Current.OS == context.WINDOWS {
51+
template = `{{ $source := (escapeString .Name) }}mklink {{ if isDir $source }}/d{{ else }}/h{{ end }} {{ $source }} {{ escapeString .Target }} out+err>| ignore`
52+
}
53+
54+
l.template = template
55+
return l
56+
}
57+
4858
func (p *Path) nu() *Path {
4959
template := `$env.%s = ($env.%s | prepend {{ formatString .Value }})`
5060
pathName := "PATH"

src/shell/path.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,10 @@ func (p Paths) Render() {
100100
}
101101

102102
if first && DotFile.Len() > 0 {
103-
DotFile.WriteString("\n\n")
104-
}
105-
106-
if !first {
107103
DotFile.WriteString("\n")
108104
}
109105

106+
DotFile.WriteString("\n")
110107
DotFile.WriteString(script)
111108

112109
first = false

src/shell/path_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package shell
22

33
import (
4+
"strings"
45
"testing"
56

67
"github.com/jandedobbeleer/aliae/src/context"
@@ -228,7 +229,7 @@ $env:PATH = '/Users/jan/.tools/bin:' + $env:PATH`,
228229
}
229230
context.Current = &context.Runtime{Shell: tc.Shell, Path: &context.Path{}}
230231
tc.Paths.Render()
231-
assert.Equal(t, tc.Expected, DotFile.String(), tc.Case)
232+
assert.Equal(t, tc.Expected, strings.TrimSpace(DotFile.String()), tc.Case)
232233
}
233234
}
234235

src/shell/pwsh.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ func (e *Env) pwsh() *Env {
7272
return e
7373
}
7474

75+
func (l *Link) pwsh() *Link {
76+
template := `New-Item -Path {{ formatString .Name }} -ItemType SymbolicLink -Value {{ formatString .Target }} -Force`
77+
l.template = template
78+
return l
79+
}
80+
7581
func (p *Path) pwsh() *Path {
7682
template := fmt.Sprintf(`$env:PATH = '{{ .Value }}%s' + $env:PATH`, context.PathDelimiter())
7783
p.template = template

src/shell/script.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,10 @@ func (s Scripts) Render() {
2525
}
2626

2727
if first && DotFile.Len() > 0 {
28-
DotFile.WriteString("\n\n")
29-
}
30-
31-
if !first {
3228
DotFile.WriteString("\n")
3329
}
3430

31+
DotFile.WriteString("\n")
3532
DotFile.WriteString(scriptBlock)
3633

3734
first = false

0 commit comments

Comments
 (0)