diff --git a/src/config/unmarshal.go b/src/config/unmarshal.go index a8493c9..13def13 100644 --- a/src/config/unmarshal.go +++ b/src/config/unmarshal.go @@ -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 diff --git a/src/core/init.go b/src/core/init.go index ae8417c..f760cbb 100644 --- a/src/core/init.go +++ b/src/core/init.go @@ -28,6 +28,7 @@ func Init(configPath, sh string, printOutput bool) string { aliae.Paths.Render() aliae.Aliae.Render() aliae.Scripts.Render() + aliae.Links.Render() script := shell.DotFile.String() diff --git a/src/shell/cmd.go b/src/shell/cmd.go index f20804c..8f8c00e 100644 --- a/src/shell/cmd.go +++ b/src/shell/cmd.go @@ -42,6 +42,12 @@ func (e *Env) cmd() *Env { return e } +func (l *Link) cmd() *Link { + template := `os.execute("mklink {{ if eq .Type "hard" }}/H{{ else }}/D{{ end }} {{ .Name }} {{ .Target }}")` + l.template = template + return l +} + func (p *Path) cmd() *Path { p.template = `os.setenv("PATH", "{{ escapeString .Value }};" .. os.getenv("PATH"))` return p diff --git a/src/shell/link.go b/src/shell/link.go new file mode 100644 index 0000000..2a09754 --- /dev/null +++ b/src/shell/link.go @@ -0,0 +1,73 @@ +package shell + +import ( + "github.com/jandedobbeleer/aliae/src/context" +) + +type Links []*Link + +const ( + Hard Type = "hard" + Soft Type = "soft" +) + +type Link struct { + Name Template `yaml:"name"` + Target Template `yaml:"target"` + If If `yaml:"if"` + Type Type `yaml:"type"` + + 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\n") + } + + if !first { + DotFile.WriteString("\n") + } + + DotFile.WriteString(script) + + first = false + } +} diff --git a/src/shell/link_test.go b/src/shell/link_test.go new file mode 100644 index 0000000..24e7d82 --- /dev/null +++ b/src/shell/link_test.go @@ -0,0 +1,139 @@ +package shell + +import ( + "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 /D foo bar")`, + }, + { + Case: "FISH", + Shell: FISH, + Expected: "ln -sf bar foo", + }, + { + Case: "NU", + Shell: NU, + Expected: "ln -sf bar foo", + }, + { + Case: "NU Windows", + Shell: NU, + OS: context.WINDOWS, + Expected: "mklink /D foo bar", + }, + { + 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 + Links Links + Expected string + }{ + { + 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, 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) + } +} diff --git a/src/shell/nu.go b/src/shell/nu.go index f0b1183..06d1850 100644 --- a/src/shell/nu.go +++ b/src/shell/nu.go @@ -45,6 +45,16 @@ func (e *Env) nu() *Env { return e } +func (l *Link) nu() *Link { + template := `ln -sf {{ .Target }} {{ .Name }}` + if context.Current.OS == context.WINDOWS { + template = `mklink {{ if eq .Type "hard" }}/H{{ else }}/D{{ end }} {{ .Name }} {{ .Target }}` + } + + l.template = template + return l +} + func (p *Path) nu() *Path { template := `$env.%s = ($env.%s | prepend {{ formatString .Value }})` pathName := "PATH" diff --git a/src/shell/pwsh.go b/src/shell/pwsh.go index 2c6f95d..2081e12 100644 --- a/src/shell/pwsh.go +++ b/src/shell/pwsh.go @@ -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 diff --git a/src/shell/tcsh.go b/src/shell/tcsh.go index 1274f61..6567049 100644 --- a/src/shell/tcsh.go +++ b/src/shell/tcsh.go @@ -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 diff --git a/src/shell/zsh.go b/src/shell/zsh.go index d607f91..22fc201 100644 --- a/src/shell/zsh.go +++ b/src/shell/zsh.go @@ -42,6 +42,12 @@ func (e *Env) zsh() *Env { return e } +func (l *Link) zsh() *Link { + template := `ln -sf {{ .Target }} {{ .Name }}` + l.template = template + return l +} + func (p *Path) zsh() *Path { template := fmt.Sprintf(`export PATH="{{ .Value }}%s$PATH"`, context.PathDelimiter()) p.template = template diff --git a/website/docs/introduction.mdx b/website/docs/introduction.mdx index 025835b..3d946b1 100644 --- a/website/docs/introduction.mdx +++ b/website/docs/introduction.mdx @@ -17,6 +17,7 @@ It allows you to use a single, template enabled `YAML` [configuration][config] t - [Environment variable][env] - [PATH entry][path] - [Script][script] +- [Symbolic Link][link] ## I want in! @@ -29,4 +30,5 @@ for your platform and have a look at the alias [configuration][config] page to g [env]: setup/env.mdx [path]: setup/path.mdx [script]: setup/script.mdx +[link]: setup/link.mdx diff --git a/website/docs/setup/configuration.mdx b/website/docs/setup/configuration.mdx index 412ab53..89d6791 100644 --- a/website/docs/setup/configuration.mdx +++ b/website/docs/setup/configuration.mdx @@ -65,6 +65,11 @@ script: - value: | eval "$(oh-my-posh init {{ .Shell }})" if: match .Shell "bash" "zsh" +link: + - name: ~/.aliae.yaml + target: ~/dotfiles/aliae.yaml + - name: ~/.zshrc + target: $DOTFILES/config/zsh/zshrc ``` You can find out more about the configuration options below. @@ -74,8 +79,10 @@ You can find out more about the configuration options below. - [Environment variable][env] - [PATH entry][path] - [Script][script] +- [Symbolic link][link] [alias]: setup/alias.mdx [env]: setup/env.mdx [path]: setup/path.mdx [script]: setup/script.mdx +[link]: setup/link.mdx diff --git a/website/docs/setup/link.mdx b/website/docs/setup/link.mdx new file mode 100644 index 0000000..21c8ab5 --- /dev/null +++ b/website/docs/setup/link.mdx @@ -0,0 +1,32 @@ +--- +id: link +title: Symbolic Link +sidebar_label: 🔗 Symbolic Link +--- + +Create symlinks to files and directories. Useful for dotfiles. + +### Syntax + +```yaml +link: + - name: ~/.aliae.yaml + target: ~/dotfiles/aliae.yaml + - name: ~/.zshrc + target: $DOTFILES/config/zsh/zshrc + - name: ~/Brewfile + value: /some/location/Brewfile + if: eq .OS "darwin" +``` + +### Link + +| Name | Type | Description | +| -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | the link name, supports [templating][templates] | +| `target` | `string` | the name of the file or directory to link to, supports [templating][templates] | +| `if` | `string` | golang [template][go-text-template] conditional statement, see [if][if] | + +[go-text-template]: https://golang.org/pkg/text/template/ +[if]: if.mdx +[templates]: templates.mdx