Skip to content

Commit

Permalink
feat: add symlink support
Browse files Browse the repository at this point in the history
  • Loading branch information
boldandbrad authored and JanDeDobbeleer committed Dec 27, 2024
1 parent 9c004ba commit 5206d72
Show file tree
Hide file tree
Showing 17 changed files with 301 additions and 17 deletions.
1 change: 1 addition & 0 deletions src/config/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Aliae struct {
Envs shell.Envs `yaml:"env"`
Paths shell.Paths `yaml:"path"`
Scripts shell.Scripts `yaml:"script"`
Links shell.Links `yaml:"link"`
}

type FuncMap []StringFunc
Expand Down
1 change: 1 addition & 0 deletions src/core/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func Init(configPath, sh string, printOutput bool) string {
aliae.Envs.Render()
aliae.Paths.Render()
aliae.Aliae.Render()
aliae.Links.Render()
aliae.Scripts.Render()

script := shell.DotFile.String()
Expand Down
14 changes: 8 additions & 6 deletions src/shell/cmd.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// doskey np=notepad++.exe $*

package shell

const (
Expand All @@ -15,8 +13,7 @@ func (a *Alias) cmd() *Alias {
}

func cmdAliasPre() string {
return `
local filename = os.tmpname()
return `local filename = os.tmpname()
local macrofile = io.open(filename, "w+")
`
}
Expand All @@ -25,8 +22,7 @@ func cmdAliasPost() string {
return `
macrofile:close()
local _ = io.popen(string.format("doskey /macrofile=%s", filename)):close()
os.remove(filename)
`
os.remove(filename)`
}

func (e *Echo) cmd() *Echo {
Expand All @@ -42,6 +38,12 @@ func (e *Env) cmd() *Env {
return e
}

func (l *Link) cmd() *Link {
template := `os.execute("{{ $source := (escapeString .Name) }}mklink {{ if isDir $source }}/d{{ else }}/h{{ end }} {{ $source }} {{ escapeString .Target }} > nul 2>&1")`
l.template = template
return l
}

func (p *Path) cmd() *Path {
p.template = `os.setenv("PATH", "{{ escapeString .Value }};" .. os.getenv("PATH"))`
return p
Expand Down
64 changes: 64 additions & 0 deletions src/shell/link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package shell

import (
"github.com/jandedobbeleer/aliae/src/context"
)

type Links []*Link

type Link struct {
Name Template `yaml:"name"`
Target Template `yaml:"target"`
If If `yaml:"if"`

template string
}

func (l *Link) string() string {
switch context.Current.Shell {
case ZSH, BASH, FISH, XONSH:
return l.zsh().render()
case PWSH, POWERSHELL:
return l.pwsh().render()
case NU:
return l.nu().render()
case TCSH:
return l.tcsh().render()
case CMD:
return l.cmd().render()
default:
return ""
}
}

func (l *Link) render() string {
script, err := parse(l.template, l)
if err != nil {
return err.Error()
}

return script
}

func (l Links) Render() {
if len(l) == 0 {
return
}

first := true
for _, link := range l {
script := link.string()
if len(script) == 0 || link.If.Ignore() {
continue
}

if first && DotFile.Len() > 0 {
DotFile.WriteString("\n")
}

DotFile.WriteString("\n")
DotFile.WriteString(script)

first = false
}
}
140 changes: 140 additions & 0 deletions src/shell/link_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package shell

import (
"strings"
"testing"

"github.com/jandedobbeleer/aliae/src/context"
"github.com/stretchr/testify/assert"
)

func TestLinkCommand(t *testing.T) {
link := &Link{Name: "foo", Target: "bar"}
cases := []struct {
Case string
Shell string
Expected string
OS string
}{
{
Case: "PWSH",
Shell: PWSH,
Expected: "New-Item -Path \"foo\" -ItemType SymbolicLink -Value \"bar\" -Force",
},
{
Case: "CMD",
Shell: CMD,
Expected: `os.execute("mklink /h foo bar > nul 2>&1")`,
},
{
Case: "FISH",
Shell: FISH,
Expected: "ln -sf bar foo",
},
{
Case: "NU",
Shell: NU,
Expected: "ln -sf bar foo out+err>| ignore",
},
{
Case: "NU Windows",
Shell: NU,
OS: context.WINDOWS,
Expected: "mklink /h foo bar out+err>| ignore",
},
{
Case: "TCSH",
Shell: TCSH,
Expected: "ln -sf bar foo;",
},
{
Case: "XONSH",
Shell: XONSH,
Expected: "ln -sf bar foo",
},
{
Case: "ZSH",
Shell: ZSH,
Expected: `ln -sf bar foo`,
},
{
Case: "BASH",
Shell: BASH,
Expected: `ln -sf bar foo`,
},
}

for _, tc := range cases {
link.template = ""
context.Current = &context.Runtime{Shell: tc.Shell, OS: tc.OS}
assert.Equal(t, tc.Expected, link.string(), tc.Case)
}
}

func TestLinkRender(t *testing.T) {
cases := []struct {
Case string
Expected string
Links Links
}{
{
Case: "Single link",
Links: Links{
&Link{Name: "FOO", Target: "bar"},
},
Expected: "ln -sf bar FOO",
},
{
Case: "Double link",
Links: Links{
&Link{Name: "FOO", Target: "bar"},
&Link{Name: "BAR", Target: "foo"},
},
Expected: `ln -sf bar FOO
ln -sf foo BAR`,
},
{
Case: "Filtered out",
Links: Links{
&Link{Name: "FOO", Target: "bar", If: `eq .Shell "fish"`},
},
},
}

for _, tc := range cases {
DotFile.Reset()
context.Current = &context.Runtime{Shell: BASH}
tc.Links.Render()
assert.Equal(t, tc.Expected, strings.TrimSpace(DotFile.String()), tc.Case)
}
}

func TestLinkWithTemplate(t *testing.T) {
cases := []struct {
Case string
Target Template
Expected string
}{
{
Case: "No template",
Target: "~/dotfiles/zshrc",
Expected: `ln -sf ~/dotfiles/zshrc /tmp/l`,
},
{
Case: "Home in template",
Target: "{{ .Home }}/.aliae.yaml",
Expected: `ln -sf /Users/jan/.aliae.yaml /tmp/l`,
},
{
Case: "Advanced template",
Target: "{{ .Home }}/go/bin/aliae{{ if eq .OS \"windows\" }}.exe{{ end }}",
Expected: `ln -sf /Users/jan/go/bin/aliae.exe /tmp/l`,
},
}

for _, tc := range cases {
link := &Link{Name: "/tmp/l", Target: tc.Target}
context.Current = &context.Runtime{Shell: BASH, Home: "/Users/jan", OS: context.WINDOWS}
assert.Equal(t, tc.Expected, link.string(), tc.Case)
}
}
10 changes: 10 additions & 0 deletions src/shell/nu.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ func (e *Env) nu() *Env {
return e
}

func (l *Link) nu() *Link {
template := `ln -sf {{ .Target }} {{ .Name }} out+err>| ignore`
if context.Current.OS == context.WINDOWS {
template = `{{ $source := (escapeString .Name) }}mklink {{ if isDir $source }}/d{{ else }}/h{{ end }} {{ $source }} {{ escapeString .Target }} out+err>| ignore`
}

l.template = template
return l
}

func (p *Path) nu() *Path {
template := `$env.%s = ($env.%s | prepend {{ formatString .Value }})`
pathName := "PATH"
Expand Down
5 changes: 1 addition & 4 deletions src/shell/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,10 @@ func (p Paths) Render() {
}

if first && DotFile.Len() > 0 {
DotFile.WriteString("\n\n")
}

if !first {
DotFile.WriteString("\n")
}

DotFile.WriteString("\n")
DotFile.WriteString(script)

first = false
Expand Down
3 changes: 2 additions & 1 deletion src/shell/path_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package shell

import (
"strings"
"testing"

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

Expand Down
6 changes: 6 additions & 0 deletions src/shell/pwsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ func (e *Env) pwsh() *Env {
return e
}

func (l *Link) pwsh() *Link {
template := `New-Item -Path {{ formatString .Name }} -ItemType SymbolicLink -Value {{ formatString .Target }} -Force`
l.template = template
return l
}

func (p *Path) pwsh() *Path {
template := fmt.Sprintf(`$env:PATH = '{{ .Value }}%s' + $env:PATH`, context.PathDelimiter())
p.template = template
Expand Down
5 changes: 1 addition & 4 deletions src/shell/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,10 @@ func (s Scripts) Render() {
}

if first && DotFile.Len() > 0 {
DotFile.WriteString("\n\n")
}

if !first {
DotFile.WriteString("\n")
}

DotFile.WriteString("\n")
DotFile.WriteString(scriptBlock)

first = false
Expand Down
3 changes: 2 additions & 1 deletion src/shell/script_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package shell

import (
"strings"
"testing"

"github.com/jandedobbeleer/aliae/src/context"
Expand Down Expand Up @@ -76,6 +77,6 @@ func TestScriptRender(t *testing.T) {
}
context.Current = &context.Runtime{Shell: PWSH}
tc.Scripts.Render()
assert.Equal(t, tc.Expected, DotFile.String(), tc.Case)
assert.Equal(t, tc.Expected, strings.TrimSpace(DotFile.String()), tc.Case)
}
}
6 changes: 6 additions & 0 deletions src/shell/tcsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ func (e *Env) tcsh() *Env {
return e
}

func (l *Link) tcsh() *Link {
template := `ln -sf {{ .Target }} {{ .Name }};`
l.template = template
return l
}

func (p *Path) tcsh() *Path {
p.template = `set path = ( {{ .Value }} $path );`
return p
Expand Down
13 changes: 12 additions & 1 deletion src/shell/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func funcMap() template.FuncMap {
"env": os.Getenv,
"match": match,
"hasCommand": hasCommand,
"isDir": isDir,
}
return template.FuncMap(funcMap)
}
Expand Down Expand Up @@ -121,7 +122,8 @@ func escapeString(variable interface{}) interface{} {

switch v := variable.(type) {
case Template:
return clean(string(v))
value := v.String()
return clean(value)
case string:
return clean(v)
default:
Expand All @@ -142,3 +144,12 @@ func hasCommand(command string) bool {
_, err := exec.LookPath(command)
return err == nil
}

func isDir(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}

return info.IsDir()
}
Loading

0 comments on commit 5206d72

Please sign in to comment.