From db778877e6273c3d878eb9f3858531ddfb68242c Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Sat, 17 Jan 2026 11:07:37 +0100 Subject: refactor(open): extract findCommand --- go.mod | 2 + go.sum | 2 + hashes.nix | 2 +- programs/open/main.go | 55 ++++++++--------- programs/open/main_test.go | 150 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 programs/open/main_test.go diff --git a/go.mod b/go.mod index 1bbc9da..43de4d8 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module push-f.com/cmd50 go 1.25.5 require github.com/BurntSushi/toml v1.6.0 + +require github.com/google/go-cmp v0.7.0 // indirect diff --git a/go.sum b/go.sum index f74b269..7d2396d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/hashes.nix b/hashes.nix index 3196db1..9326d68 100644 --- a/hashes.nix +++ b/hashes.nix @@ -1,3 +1,3 @@ { - go = "sha256-pbA/AlBz3cQYRTMnQ/qBPcinYOKokrBLNhkbRTq54gE="; + go = "sha256-XXgXzv6MARTUse1lf4RAaMp9xg8FfysaPMM7wq5zdlw="; } diff --git a/programs/open/main.go b/programs/open/main.go index 415f62a..ad3c3ca 100644 --- a/programs/open/main.go +++ b/programs/open/main.go @@ -40,23 +40,35 @@ func main() { fatal("unknown config keys: %v", meta.Undecoded()) } - for scheme, cmd := range cfg.UriSchemes { + args, err := cfg.findCommand(arg) + if err != nil { + fatal(err.Error()) + } + args = append(args, arg) + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Run() + if err != nil { + log.Fatal(err) + } +} + +func (c Config) findCommand(arg string) ([]string, error) { + for scheme, cmd := range c.UriSchemes { if strings.HasPrefix(arg, scheme+":") { - cmd = append(cmd, arg) - err := run(cmd) - if err != nil { - log.Fatal(err) - } - return + return cmd, nil } } stat, err := os.Stat(arg) if err != nil { if os.IsNotExist(err) && strings.Contains(arg, ":") { - fatal("file not found or URI scheme not registered") + return nil, fmt.Errorf("file not found or URI scheme not registered") } - fatal("stat failed: %v", err) + return nil, fmt.Errorf("stat failed: %v", err) } var mimeType string @@ -67,28 +79,23 @@ func main() { ext := path.Ext(arg) mimeType = mime.TypeByExtension(ext) if mimeType == "" { - fatal("no associated mime type for file extension %s", ext) + return nil, fmt.Errorf("no associated mime type for file extension %s", ext) } // strip e.g. `; charset=utf-8` mimeType, _, _ = strings.Cut(mimeType, ";") } - for mimeTypePat, cmd := range cfg.MimeTypes { + for mimeTypePat, cmd := range c.MimeTypes { matched, err := path.Match(mimeTypePat, mimeType) if err != nil { - log.Fatal(err) + return nil, err } if matched { - cmd = append(cmd, arg) - err := run(cmd) - if err != nil { - log.Fatal(err) - } - return + return cmd, nil } } - fatal("no opener configured for mime %s", mimeType) + return nil, fmt.Errorf("no opener configured for mime %s", mimeType) } func fatal(msg string, v ...any) { @@ -96,16 +103,6 @@ func fatal(msg string, v ...any) { os.Exit(1) } -func run(args []string) error { - cmd := exec.Command(args[0], args[1:]...) - - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - return cmd.Run() -} - func xdgConfigPath(appName string) (string, error) { xdgHome := os.Getenv("XDG_CONFIG_HOME") diff --git a/programs/open/main_test.go b/programs/open/main_test.go new file mode 100644 index 0000000..213c2e4 --- /dev/null +++ b/programs/open/main_test.go @@ -0,0 +1,150 @@ +package main + +import ( + "fmt" + "os" + "path" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestFindCommand_Uri(t *testing.T) { + cfg := Config{ + MimeTypes: map[string][]string{}, + UriSchemes: map[string][]string{ + "http": {"my-browser"}, + }, + } + + cases := []string{ + "http://example.com", + "http:example.com", + "http:", + } + + for _, arg := range cases { + t.Run(arg, func(t *testing.T) { + cmd, err := cfg.findCommand(arg) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(cmd, cfg.UriSchemes["http"]); diff != "" { + t.Error("unexpected command", diff) + } + }) + } +} + +func TestFindCommand_MimeFile(t *testing.T) { + err := os.Chdir(t.TempDir()) + if err != nil { + t.Fatal(err) + } + filename := "test.html" + _, err = os.Create(filename) + if err != nil { + t.Fatal(err) + } + + cases := []string{ + "text/html", + "text/*", + } + + for _, pattern := range cases { + t.Run(pattern, func(t *testing.T) { + cfg := Config{ + MimeTypes: map[string][]string{ + pattern: {"the-program"}, + }, + UriSchemes: map[string][]string{}, + } + cmd, err := cfg.findCommand(filename) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(cmd, cfg.MimeTypes[pattern]); diff != "" { + t.Error("unexpected command", diff) + } + }) + } +} + +func TestFindCommand_MimeDir(t *testing.T) { + path := t.TempDir() + + cfg := Config{ + MimeTypes: map[string][]string{ + "inode/directory": {"the-program"}, + }, + UriSchemes: map[string][]string{}, + } + cmd, err := cfg.findCommand(path) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(cmd, cfg.MimeTypes["inode/directory"]); diff != "" { + t.Error("unexpected command", diff) + } +} + +func TestFindCommand_FileMissing(t *testing.T) { + filePath := path.Join(t.TempDir(), "test.html") + + cfg := Config{ + MimeTypes: map[string][]string{}, + UriSchemes: map[string][]string{}, + } + _, err := cfg.findCommand(filePath) + expectedPrefix := "stat failed:" + if !strings.HasPrefix(err.Error(), expectedPrefix) { + t.Fatalf("expected error to start with %q but got %q", expectedPrefix, err.Error()) + } +} + +func TestFindCommand_UriCheckedFirst(t *testing.T) { + cases := []struct { + uriHandlerExists bool + expectedCommand []string + }{ + {true, []string{"uri-program"}}, + {false, []string{"mime-program"}}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("uriHandlerExists=%v", c.uriHandlerExists), func(t *testing.T) { + scheme := "uri-or" + arg := scheme + ":file.txt" + + cfg := Config{ + MimeTypes: map[string][]string{ + "text/plain": {"mime-program"}, + }, + UriSchemes: map[string][]string{}, + } + + if c.uriHandlerExists { + cfg.UriSchemes[scheme] = []string{"uri-program"} + } + + err := os.Chdir(t.TempDir()) + if err != nil { + t.Fatal(err) + } + _, err = os.Create(arg) + if err != nil { + t.Fatal(err) + } + + cmd, err := cfg.findCommand(arg) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(cmd, c.expectedCommand); diff != "" { + t.Error("unexpected command", diff) + } + }) + } +} -- cgit v1.3.1