Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/cheatmd/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func buildComposeMarkdown(name, desc, command string) string {

func determineComposeTargetFile(file string) (string, error) {
if file != "" {
return file, nil
return config.ExpandHomePath(file)
}

cfgPath := config.Get().Path
Expand Down
23 changes: 3 additions & 20 deletions cmd/cheatmd/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/cheatmd-dev/cheatmd/pkg/config"
"github.com/cheatmd-dev/cheatmd/pkg/parser"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -104,25 +101,11 @@ func dumpVars(vars []parser.VarDef) []dumpVar {
}

func parseDumpIndex(args []string) (*parser.CheatIndex, error) {
path := "."
if len(args) > 0 {
path = args[0]
} else if config.Get().Path != "." {
path = config.Get().Path
}
absPath, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("error resolving path: %w", err)
}
info, err := os.Stat(absPath)
path, err := resolveCheatPath(args)
if err != nil {
return nil, fmt.Errorf("path error: %w", err)
}
p := parser.NewParser()
if info.IsDir() {
return p.ParseDirectory(absPath)
return nil, err
}
return p.ParseSingleFile(absPath)
return loadAndParseCheats(path)
}

func writeDumpCSV(cmd *cobra.Command, entries []dumpEntry) error {
Expand Down
95 changes: 95 additions & 0 deletions cmd/cheatmd/path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package main

import (
"os"
"path/filepath"
"testing"

"github.com/cheatmd-dev/cheatmd/pkg/config"
"github.com/spf13/viper"
)

func TestHomePathInputs(t *testing.T) {
old := *config.Get()
t.Cleanup(func() { *config.Get() = old; viper.Reset() })
dir := t.TempDir()
path := filepath.Join(dir, "greeting.md")
if err := os.WriteFile(path, []byte("# Greeting\n```sh\necho hello\n```\n"), 0o600); err != nil {
t.Fatal(err)
}
home, err := os.UserHomeDir()
if err != nil {
t.Fatal(err)
}
relative, err := filepath.Rel(home, path)
if err != nil {
t.Fatal(err)
}
homePath := "~/" + filepath.ToSlash(relative)
expandedPath := home + "/" + filepath.ToSlash(relative)
configFile := filepath.Join(dir, "cheatmd.yaml")
if err := os.WriteFile(configFile, []byte("{}\n"), 0o600); err != nil {
t.Fatal(err)
}

for _, configured := range []bool{false, true} {
name := "explicit"
if configured {
name = "configured"
}
t.Run(name, func(t *testing.T) {
args, composeFile := []string{homePath}, homePath
viper.Reset()
viper.SetConfigFile(configFile)
if configured {
t.Setenv("CHEATMD_PATH", homePath)
args, composeFile = nil, ""
} else {
t.Setenv("CHEATMD_PATH", ".")
}
if err := config.Init(); err != nil {
t.Fatal(err)
}
t.Run("root", func(t *testing.T) {
got, err := resolveCheatPath(args)
if err != nil || got != path {
t.Fatalf("resolved path = %q, %v; want %q", got, err, path)
}
})
t.Run("dump", func(t *testing.T) {
index, err := parseDumpIndex(args)
if err != nil {
t.Fatal(err)
}
if len(index.Cheats) != 1 || index.Cheats[0].Command != "echo hello" {
t.Fatalf("unexpected cheats: %+v", index.Cheats)
}
})
t.Run("compose", func(t *testing.T) {
got, err := determineComposeTargetFile(composeFile)
if err != nil || got != expandedPath {
t.Fatalf("compose target = %q, %v; want %q", got, err, expandedPath)
}
})
})
}

for _, tc := range []struct{ name, path, want string }{
{"absolute second path", homePath + ",/unused/second.md", expandedPath},
{"parent in second path", homePath + ",/second/../../third.md", expandedPath},
{"bare home first", "~,/second", filepath.Join(home, "snippets.md")},
} {
t.Run("compose configured path list/"+tc.name, func(t *testing.T) {
viper.Reset()
viper.SetConfigFile(configFile)
t.Setenv("CHEATMD_PATH", tc.path)
if err := config.Init(); err != nil {
t.Fatal(err)
}
got, err := determineComposeTargetFile("")
if err != nil || got != tc.want {
t.Fatalf("compose target = %q, %v; want first path %q", got, err, tc.want)
}
})
}
}
4 changes: 4 additions & 0 deletions cmd/cheatmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ func resolveCheatPath(args []string) (string, error) {
path = config.Get().Path
}

path, err := config.ExpandHomePath(path)
if err != nil {
return "", fmt.Errorf("error resolving path: %w", err)
}
absPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("error resolving path: %w", err)
Expand Down
30 changes: 29 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,36 @@ func Init() error {
}); err != nil {
return err
}
if configErr != nil {
return configErr
}
path, remaining, multiple := strings.Cut(cfg.Path, ",")
path, err := ExpandHomePath(path)
if err != nil {
return fmt.Errorf("cheat path: %w", err)
}
if multiple {
path += "," + remaining
}
cfg.Path = path

return nil
}

return configErr
// ExpandHomePath resolves a leading ~ or ~/ against the user's home directory.
// Other paths are preserved, including relative paths and literal dollar signs.
func ExpandHomePath(path string) (string, error) {
if path != "~" && !strings.HasPrefix(path, "~/") && !strings.HasPrefix(path, "~"+string(filepath.Separator)) {
return path, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
if path == "~" {
return home, nil
}
return home + path[1:], nil
}

// registerDefaults seeds viper with a default for every config key, sourced
Expand Down
48 changes: 48 additions & 0 deletions pkg/config/path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package config

import (
"os"
"path/filepath"
"testing"

"github.com/spf13/viper"
)

func TestConfiguredHomePaths(t *testing.T) {
old := *Get()
t.Cleanup(func() { *Get() = old; viper.Reset() })
file := filepath.Join(t.TempDir(), "cheatmd.yaml")
if err := os.WriteFile(file, []byte("{}\n"), 0o600); err != nil {
t.Fatal(err)
}
home, err := os.UserHomeDir()
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct{ path, want string }{
{"~", home},
{"~/cheats", filepath.Join(home, "cheats")},
{"~/first/../cheats", home + "/first/../cheats"},
{"~,/second", home + ",/second"},
{"~/first,/second/../third", home + "/first,/second/../third"},
{"~/first,/second/../../third", home + "/first,/second/../../third"},
{"~other/cheats", "~other/cheats"},
{"cheats/~archive", "cheats/~archive"},
{"$HOME/cheats", "$HOME/cheats"},
} {
t.Run(tc.path, func(t *testing.T) {
viper.Reset()
viper.SetConfigFile(file)
t.Setenv("CHEATMD_PATH", tc.path)
if err := Init(); err != nil {
t.Fatal(err)
}
if got := Get().Path; got != tc.want {
t.Errorf("configured path = %q, want %q", got, tc.want)
}
if got := CheatsInstallDir(); got != tc.want {
t.Errorf("install path = %q, want %q", got, tc.want)
}
})
}
}
Loading