diff --git a/cow.go b/cow.go index ebe1aed..9e729dd 100644 --- a/cow.go +++ b/cow.go @@ -10,7 +10,7 @@ import ( type Cow struct { eyes string tongue string - typ string + typ *CowFile thoughts rune thinking bool bold bool @@ -25,10 +25,14 @@ type Cow struct { // New returns pointer of Cow struct that made by options func New(options ...Option) (*Cow, error) { cow := &Cow{ - eyes: "oo", - tongue: " ", - thoughts: '\\', - typ: "cows/default.cow", + eyes: "oo", + tongue: " ", + thoughts: '\\', + typ: &CowFile{ + Name: "default", + BasePath: "cows", + LocationType: InBinary, + }, ballonWidth: 40, } for _, o := range options { @@ -103,13 +107,18 @@ func adjustTo2Chars(s string) string { return " " } -func containCows(t string) bool { - for _, cow := range AssetNames() { - if t == cow { - return true +func containCows(target string) (*CowFile, error) { + cowPaths, err := Cows() + if err != nil { + return nil, err + } + for _, cowPath := range cowPaths { + cowfile, ok := cowPath.Lookup(target) + if ok { + return cowfile, nil } } - return false + return nil, nil } // NotFound is indicated not found the cowfile. @@ -126,21 +135,17 @@ func (n *NotFound) Error() string { // Type specify name of the cowfile func Type(s string) Option { if s == "" { - s = "cows/default.cow" - } - if !strings.HasSuffix(s, ".cow") { - s += ".cow" - } - if !strings.HasPrefix(s, "cows/") { - s = "cows/" + s + s = "default" } return func(c *Cow) error { - if containCows(s) { - c.typ = s + cowfile, err := containCows(s) + if err != nil { + return err + } + if cowfile != nil { + c.typ = cowfile return nil } - s = strings.TrimPrefix(s, "cows/") - s = strings.TrimSuffix(s, ".cow") return &NotFound{Cowfile: s} } } @@ -165,20 +170,30 @@ func Thoughts(thoughts rune) Option { // Random specifies something .cow from cows directory func Random() Option { - pick := pickCow() + pick, err := pickCow() return func(c *Cow) error { + if err != nil { + return err + } c.typ = pick return nil } } -func pickCow() string { - cows := AssetNames() - n := len(cows) - rand.Shuffle(n, func(i, j int) { - cows[i], cows[j] = cows[j], cows[i] - }) - return cows[rand.Intn(n)] +func pickCow() (*CowFile, error) { + cowPaths, err := Cows() + if err != nil { + return nil, err + } + cowPath := cowPaths[rand.Intn(len(cowPaths))] + + n := len(cowPath.CowFiles) + cowfile := cowPath.CowFiles[rand.Intn(n)] + return &CowFile{ + Name: cowfile, + BasePath: cowPath.Name, + LocationType: cowPath.LocationType, + }, nil } // Bold enables bold mode diff --git a/cowsay.go b/cowsay.go index 91d1a23..affd05b 100644 --- a/cowsay.go +++ b/cowsay.go @@ -1,7 +1,10 @@ package cowsay import ( + "io/ioutil" "math/rand" + "os" + "path/filepath" "sort" "strings" "time" @@ -20,21 +23,111 @@ func Say(phrase string, options ...Option) (string, error) { return cow.Say(phrase) } +// LocationType indicates the type of COWPATH. +type LocationType int + +const ( + // InBinary indicates the COWPATH in binary. + InBinary LocationType = iota + + // InDirectory indicates the COWPATH in your directory. + InDirectory +) + +// CowPath is information of the COWPATH. +type CowPath struct { + // Name is name of the COWPATH. + // If you specified `COWPATH=/foo/bar`, Name is `/foo/bar`. + Name string + // CowFiles are name of the cowfile which are trimmed ".cow" suffix. + CowFiles []string + // LocationType is the type of COWPATH + LocationType LocationType +} + +// Lookup will look for the target cowfile in the specified path. +// If it exists, it returns the cowfile information and true value. +func (c *CowPath) Lookup(target string) (*CowFile, bool) { + for _, cowfile := range c.CowFiles { + if cowfile == target { + return &CowFile{ + Name: cowfile, + BasePath: c.Name, + LocationType: c.LocationType, + }, true + } + } + return nil, false +} + +// CowFile is information of the cowfile. +type CowFile struct { + // Name is name of the cowfile. + Name string + // BasePath is the path which the cowpath is in. + BasePath string + // LocationType is the type of COWPATH + LocationType LocationType +} + +// ReadAll reads the cowfile content. +// If LocationType is InBinary, the file read from binary. +// otherwise reads from file system. +func (c *CowFile) ReadAll() ([]byte, error) { + joinedPath := filepath.Join(c.BasePath, c.Name+".cow") + if c.LocationType == InBinary { + return Asset(joinedPath) + } + return ioutil.ReadFile(joinedPath) +} + // Cows to get list of cows -func Cows() []string { - assets := AssetNames() - cows := make([]string, 0, len(assets)) - for _, key := range assets { - cows = append(cows, strings.TrimSuffix(strings.TrimPrefix(key, "cows/"), ".cow")) +func Cows() ([]*CowPath, error) { + cowPaths, err := cowsFromCowPath() + if err != nil { + return nil, err } + cowPaths = append(cowPaths, &CowPath{ + Name: "cows", + CowFiles: CowsInBinary(), + LocationType: InBinary, + }) + return cowPaths, nil +} - sort.Strings(cows) - return cows +func cowsFromCowPath() ([]*CowPath, error) { + cowPaths := make([]*CowPath, 0) + cowPath := os.Getenv("COWPATH") + if cowPath == "" { + return cowPaths, nil + } + paths := splitPath(cowPath) + for _, path := range paths { + dirEntries, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + path := &CowPath{ + Name: path, + CowFiles: []string{}, + LocationType: InDirectory, + } + for _, entry := range dirEntries { + name := entry.Name() + if strings.HasSuffix(name, ".cow") { + name = strings.TrimSuffix(name, ".cow") + path.CowFiles = append(path.CowFiles, name) + } + } + sort.Strings(path.CowFiles) + cowPaths = append(cowPaths, path) + } + return cowPaths, nil } // GetCow to get cow's ascii art func (cow *Cow) GetCow() (string, error) { - src, err := Asset(cow.typ) + src, err := cow.typ.ReadAll() if err != nil { return "", err } diff --git a/cowsay_test.go b/cowsay_test.go new file mode 100644 index 0000000..800d000 --- /dev/null +++ b/cowsay_test.go @@ -0,0 +1,154 @@ +package cowsay + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestCows(t *testing.T) { + t.Run("no set COWPATH env", func(t *testing.T) { + cowPaths, err := Cows() + if err != nil { + t.Fatal(err) + } + if len(cowPaths) != 1 { + t.Fatalf("want 1, but got %d", len(cowPaths)) + } + cowPath := cowPaths[0] + if len(cowPath.CowFiles) == 0 { + t.Fatalf("no cowfiles") + } + + wantCowPath := &CowPath{ + Name: "cows", + LocationType: InBinary, + } + if diff := cmp.Diff(wantCowPath, cowPath, + cmpopts.IgnoreFields(CowPath{}, "CowFiles"), + ); diff != "" { + t.Errorf("(-want, +got)\n%s", diff) + } + }) + + t.Run("set COWPATH env", func(t *testing.T) { + cowpath := filepath.Join("testdata", "testdir") + + os.Setenv("COWPATH", cowpath) + defer os.Unsetenv("COWPATH") + + cowPaths, err := Cows() + if err != nil { + t.Fatal(err) + } + if len(cowPaths) != 2 { + t.Fatalf("want 2, but got %d", len(cowPaths)) + } + + wants := []*CowPath{ + { + Name: "testdata/testdir", + LocationType: InDirectory, + }, + { + Name: "cows", + LocationType: InBinary, + }, + } + if diff := cmp.Diff(wants, cowPaths, + cmpopts.IgnoreFields(CowPath{}, "CowFiles"), + ); diff != "" { + t.Errorf("(-want, +got)\n%s", diff) + } + + if len(cowPaths[0].CowFiles) != 1 { + t.Fatalf("unexpected cowfiles len = %d, %+v", + len(cowPaths[0].CowFiles), cowPaths[0].CowFiles, + ) + } + + if cowPaths[0].CowFiles[0] != "test" { + t.Fatalf("want %q but got %q", "test", cowPaths[0].CowFiles[0]) + } + }) + + t.Run("set COWPATH env", func(t *testing.T) { + os.Setenv("COWPATH", "notfound") + defer os.Unsetenv("COWPATH") + + _, err := Cows() + if err == nil { + t.Fatal("want error") + } + }) + +} + +func TestCowPath_Lookup(t *testing.T) { + t.Run("looked for cowfile", func(t *testing.T) { + c := &CowPath{ + Name: "basepath", + CowFiles: []string{"test"}, + LocationType: InBinary, + } + got, ok := c.Lookup("test") + if !ok { + t.Errorf("want %v", ok) + } + want := &CowFile{ + Name: "test", + BasePath: "basepath", + LocationType: InBinary, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("(-want, +got)\n%s", diff) + } + }) + + t.Run("no cowfile", func(t *testing.T) { + c := &CowPath{ + Name: "basepath", + CowFiles: []string{"test"}, + LocationType: InBinary, + } + got, ok := c.Lookup("no cowfile") + if ok { + t.Errorf("want %v", !ok) + } + if got != nil { + t.Error("want nil") + } + }) +} + +func TestCowFile_ReadAll(t *testing.T) { + fromTestData := &CowFile{ + Name: "test", + BasePath: filepath.Join("testdata", "testdir"), + LocationType: InDirectory, + } + fromTestdataContent, err := fromTestData.ReadAll() + if err != nil { + t.Fatal(err) + } + + fromBinary := &CowFile{ + Name: "default", + BasePath: "cows", + LocationType: InBinary, + } + fromBinaryContent, err := fromBinary.ReadAll() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(fromTestdataContent, fromBinaryContent) { + t.Fatalf("testdata\n%s\n\nbinary%s\n", string(fromTestdataContent), string(fromBinaryContent)) + } + +} diff --git a/embed.go b/embed.go index 2b763e7..e8bf42a 100644 --- a/embed.go +++ b/embed.go @@ -2,7 +2,8 @@ package cowsay import ( "embed" - "path/filepath" + "sort" + "strings" ) //go:embed cows/* @@ -15,17 +16,25 @@ func Asset(path string) ([]byte, error) { return cowsDir.ReadFile(path) } -// AssetNames returns the names of the assets. +// AssetNames returns the list of filename of the assets. func AssetNames() []string { - const cows = "cows" - entries, err := cowsDir.ReadDir(cows) + entries, err := cowsDir.ReadDir("cows") if err != nil { panic(err) } names := make([]string, 0, len(entries)) for _, entry := range entries { - filename := filepath.Join(cows, entry.Name()) - names = append(names, filename) + name := strings.TrimSuffix(entry.Name(), ".cow") + names = append(names, name) } + sort.Strings(names) return names } + +var cowsInBinary = AssetNames() + +// CowsInBinary returns the list of cowfiles which are in binary. +// the list is memoized. +func CowsInBinary() []string { + return cowsInBinary +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index c0a3877..5109ae2 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -84,7 +84,19 @@ func (c *CLI) mow(argv []string) error { } if opts.List { - fmt.Println(wordwrap.WrapString(strings.Join(cowsay.Cows(), " "), 80)) + cowPaths, err := cowsay.Cows() + if err != nil { + return err + } + for _, cowPath := range cowPaths { + if cowPath.LocationType == cowsay.InBinary { + fmt.Fprintf(c.stdout, "Cow files in binary:\n") + } else { + fmt.Fprintf(c.stdout, "Cow files in %s:\n", cowPath.Name) + } + fmt.Fprintln(c.stdout, wordwrap.WrapString(strings.Join(cowPath.CowFiles, " "), 80)) + fmt.Fprintln(c.stdout) + } return nil } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index f7b0178..4c6d178 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -125,7 +125,7 @@ func TestCLI_Run(t *testing.T) { if exit != 0 { t.Fatalf("unexpected exit code: %d", exit) } - testpath := filepath.Join("testdata", cli.name, tt.testfile) + testpath := filepath.Join("..", "..", "testdata", cli.name, tt.testfile) content, err := ioutil.ReadFile(testpath) if err != nil { t.Fatal(err) diff --git a/split.go b/split.go new file mode 100644 index 0000000..532a596 --- /dev/null +++ b/split.go @@ -0,0 +1,10 @@ +//go:build !windows +// +build !windows + +package cowsay + +import "strings" + +func splitPath(s string) []string { + return strings.Split(s, ":") +} diff --git a/split_windows.go b/split_windows.go new file mode 100644 index 0000000..4f000e0 --- /dev/null +++ b/split_windows.go @@ -0,0 +1,10 @@ +//go:build windows +// +build windows + +package cowsay + +import "strings" + +func splitPath(s string) []string { + return strings.Split(s, ";") +} diff --git a/internal/cli/testdata/cowsay/W_option.txt b/testdata/cowsay/W_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/W_option.txt rename to testdata/cowsay/W_option.txt diff --git a/internal/cli/testdata/cowsay/b_option.txt b/testdata/cowsay/b_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/b_option.txt rename to testdata/cowsay/b_option.txt diff --git a/internal/cli/testdata/cowsay/d_option.txt b/testdata/cowsay/d_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/d_option.txt rename to testdata/cowsay/d_option.txt diff --git a/internal/cli/testdata/cowsay/eyes_option.txt b/testdata/cowsay/eyes_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/eyes_option.txt rename to testdata/cowsay/eyes_option.txt diff --git a/internal/cli/testdata/cowsay/f_tux_option.txt b/testdata/cowsay/f_tux_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/f_tux_option.txt rename to testdata/cowsay/f_tux_option.txt diff --git a/internal/cli/testdata/cowsay/g_option.txt b/testdata/cowsay/g_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/g_option.txt rename to testdata/cowsay/g_option.txt diff --git a/internal/cli/testdata/cowsay/n_option.txt b/testdata/cowsay/n_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/n_option.txt rename to testdata/cowsay/n_option.txt diff --git a/internal/cli/testdata/cowsay/p_option.txt b/testdata/cowsay/p_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/p_option.txt rename to testdata/cowsay/p_option.txt diff --git a/internal/cli/testdata/cowsay/s_option.txt b/testdata/cowsay/s_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/s_option.txt rename to testdata/cowsay/s_option.txt diff --git a/internal/cli/testdata/cowsay/t_option.txt b/testdata/cowsay/t_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/t_option.txt rename to testdata/cowsay/t_option.txt diff --git a/internal/cli/testdata/cowsay/tongue_option.txt b/testdata/cowsay/tongue_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/tongue_option.txt rename to testdata/cowsay/tongue_option.txt diff --git a/internal/cli/testdata/cowsay/wired_option.txt b/testdata/cowsay/wired_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/wired_option.txt rename to testdata/cowsay/wired_option.txt diff --git a/internal/cli/testdata/cowsay/y_option.txt b/testdata/cowsay/y_option.txt similarity index 100% rename from internal/cli/testdata/cowsay/y_option.txt rename to testdata/cowsay/y_option.txt diff --git a/internal/cli/testdata/cowthink/W_option.txt b/testdata/cowthink/W_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/W_option.txt rename to testdata/cowthink/W_option.txt diff --git a/internal/cli/testdata/cowthink/b_option.txt b/testdata/cowthink/b_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/b_option.txt rename to testdata/cowthink/b_option.txt diff --git a/internal/cli/testdata/cowthink/d_option.txt b/testdata/cowthink/d_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/d_option.txt rename to testdata/cowthink/d_option.txt diff --git a/internal/cli/testdata/cowthink/eyes_option.txt b/testdata/cowthink/eyes_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/eyes_option.txt rename to testdata/cowthink/eyes_option.txt diff --git a/internal/cli/testdata/cowthink/f_tux_option.txt b/testdata/cowthink/f_tux_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/f_tux_option.txt rename to testdata/cowthink/f_tux_option.txt diff --git a/internal/cli/testdata/cowthink/g_option.txt b/testdata/cowthink/g_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/g_option.txt rename to testdata/cowthink/g_option.txt diff --git a/internal/cli/testdata/cowthink/n_option.txt b/testdata/cowthink/n_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/n_option.txt rename to testdata/cowthink/n_option.txt diff --git a/internal/cli/testdata/cowthink/p_option.txt b/testdata/cowthink/p_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/p_option.txt rename to testdata/cowthink/p_option.txt diff --git a/internal/cli/testdata/cowthink/s_option.txt b/testdata/cowthink/s_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/s_option.txt rename to testdata/cowthink/s_option.txt diff --git a/internal/cli/testdata/cowthink/t_option.txt b/testdata/cowthink/t_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/t_option.txt rename to testdata/cowthink/t_option.txt diff --git a/internal/cli/testdata/cowthink/tongue_option.txt b/testdata/cowthink/tongue_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/tongue_option.txt rename to testdata/cowthink/tongue_option.txt diff --git a/internal/cli/testdata/cowthink/wired_option.txt b/testdata/cowthink/wired_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/wired_option.txt rename to testdata/cowthink/wired_option.txt diff --git a/internal/cli/testdata/cowthink/y_option.txt b/testdata/cowthink/y_option.txt similarity index 100% rename from internal/cli/testdata/cowthink/y_option.txt rename to testdata/cowthink/y_option.txt diff --git a/testdata/testdir/test.cow b/testdata/testdir/test.cow new file mode 100644 index 0000000..684b69d --- /dev/null +++ b/testdata/testdir/test.cow @@ -0,0 +1,7 @@ +$the_cow = <