diff --git a/cmd/cheat/cmd_remove.go b/cmd/cheat/cmd_remove.go new file mode 100644 index 0000000..caaa09a --- /dev/null +++ b/cmd/cheat/cmd_remove.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/cheat/cheat/internal/config" + "github.com/cheat/cheat/internal/sheets" +) + +// cmdRemove opens a cheatsheet for editing (or creates it if it doesn't exist). +func cmdRemove(opts map[string]interface{}, conf config.Config) { + + cheatsheet := opts["--rm"].(string) + + // load the cheatsheets + cheatsheets, err := sheets.Load(conf.Cheatpaths) + if err != nil { + fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err)) + os.Exit(1) + } + + // filter cheatcheats by tag if --tag was provided + if opts["--tag"] != nil { + cheatsheets = sheets.Filter( + cheatsheets, + strings.Split(opts["--tag"].(string), ","), + ) + } + + // consolidate the cheatsheets found on all paths into a single map of + // `title` => `sheet` (ie, allow more local cheatsheets to override less + // local cheatsheets) + consolidated := sheets.Consolidate(cheatsheets) + + // fail early if the requested cheatsheet does not exist + sheet, ok := consolidated[cheatsheet] + if !ok { + fmt.Fprintln(os.Stderr, fmt.Sprintf("no cheatsheet found for '%s'.\n", cheatsheet)) + os.Exit(1) + } + + // fail early if the sheet is read-only + if sheet.ReadOnly { + fmt.Fprintln(os.Stderr, fmt.Sprintf("cheatsheet '%s' is read-only.", cheatsheet)) + os.Exit(1) + } + + // otherwise, attempt to delete the sheet + if err := os.Remove(sheet.Path); err != nil { + fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to delete sheet: %s, %v", sheet.Title, err)) + os.Exit(1) + } +} diff --git a/cmd/cheat/cmd_search.go b/cmd/cheat/cmd_search.go index 89652b1..ebc53e1 100644 --- a/cmd/cheat/cmd_search.go +++ b/cmd/cheat/cmd_search.go @@ -55,7 +55,7 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) { // compile the regex reg, err := regexp.Compile(pattern) if err != nil { - fmt.Errorf("failed to compile regexp: %s, %v", pattern, err) + fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to compile regexp: %s, %v", pattern, err)) os.Exit(1) } diff --git a/cmd/cheat/cmd_tags.go b/cmd/cheat/cmd_tags.go new file mode 100644 index 0000000..a17c87c --- /dev/null +++ b/cmd/cheat/cmd_tags.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "os" + + "github.com/cheat/cheat/internal/config" + "github.com/cheat/cheat/internal/sheets" +) + +// cmdTags lists all tags in use. +func cmdTags(opts map[string]interface{}, conf config.Config) { + + // load the cheatsheets + cheatsheets, err := sheets.Load(conf.Cheatpaths) + if err != nil { + fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err)) + os.Exit(1) + } + + // write sheet tags to stdout + for _, tag := range sheets.Tags(cheatsheets) { + fmt.Println(tag) + } +} diff --git a/cmd/cheat/docopt.txt b/cmd/cheat/docopt.txt index effe2cc..609f209 100644 --- a/cmd/cheat/docopt.txt +++ b/cmd/cheat/docopt.txt @@ -5,13 +5,15 @@ Options: --init Write a default config file to stdout -c --colorize Colorize output -d --directories List cheatsheet directories - -e --edit= Edit cheatsheet + -e --edit= Edit -l --list List cheatsheets -p --path= Return only sheets found on path -r --regex Treat search as a regex -s --search= Search cheatsheets for -t --tag= Return only sheets matching + -T --tags List all tags in use -v --version Print the version number + --rm= Remove (delete) Examples: @@ -33,6 +35,9 @@ Examples: To list all available cheatsheets: cheat -l + To list all tags in use: + cheat -T + To list available cheatsheets that are tagged as "personal": cheat -l -t personal @@ -41,3 +46,6 @@ Examples: To search (by regex) for cheatsheets that contain an IP address: cheat -c -r -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}' + + To remove (delete) the foo/bar cheatsheet: + cheat --rm foo/bar diff --git a/cmd/cheat/main.go b/cmd/cheat/main.go index a4608ab..036e1c3 100755 --- a/cmd/cheat/main.go +++ b/cmd/cheat/main.go @@ -13,7 +13,7 @@ import ( "github.com/cheat/cheat/internal/config" ) -const version = "3.0.7" +const version = "3.1.0" func main() { @@ -76,9 +76,15 @@ func main() { case opts["--list"].(bool): cmd = cmdList + case opts["--tags"].(bool): + cmd = cmdTags + case opts["--search"] != nil: cmd = cmdSearch + case opts["--rm"] != nil: + cmd = cmdRemove + case opts[""] != nil: cmd = cmdView diff --git a/cmd/cheat/str_usage.go b/cmd/cheat/str_usage.go index 49baa38..dfcc96c 100644 --- a/cmd/cheat/str_usage.go +++ b/cmd/cheat/str_usage.go @@ -14,13 +14,15 @@ Options: --init Write a default config file to stdout -c --colorize Colorize output -d --directories List cheatsheet directories - -e --edit= Edit cheatsheet + -e --edit= Edit -l --list List cheatsheets -p --path= Return only sheets found on path -r --regex Treat search as a regex -s --search= Search cheatsheets for -t --tag= Return only sheets matching + -T --tags List all tags in use -v --version Print the version number + --rm= Remove (delete) Examples: @@ -42,6 +44,9 @@ Examples: To list all available cheatsheets: cheat -l + To list all tags in use: + cheat -T + To list available cheatsheets that are tagged as "personal": cheat -l -t personal @@ -50,5 +55,8 @@ Examples: To search (by regex) for cheatsheets that contain an IP address: cheat -c -r -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}' + + To remove (delete) the foo/bar cheatsheet: + cheat --rm foo/bar `) } diff --git a/internal/sheets/tags.go b/internal/sheets/tags.go new file mode 100644 index 0000000..1d7b0e7 --- /dev/null +++ b/internal/sheets/tags.go @@ -0,0 +1,36 @@ +package sheets + +import ( + "sort" + + "github.com/cheat/cheat/internal/sheet" +) + +// Tags returns a slice of all tags in use in any sheet +func Tags(cheatpaths []map[string]sheet.Sheet) []string { + + // create a map of all tags in use in any sheet + tags := make(map[string]bool) + + // iterate over all tags on all sheets on all cheatpaths + for _, path := range cheatpaths { + for _, sheet := range path { + for _, tag := range sheet.Tags { + tags[tag] = true + } + } + } + + // restructure the map into a slice + sorted := []string{} + for tag := range tags { + sorted = append(sorted, tag) + } + + // sort the slice + sort.Slice(sorted, func(i, j int) bool { + return sorted[i] < sorted[j] + }) + + return sorted +} diff --git a/internal/sheets/tags_test.go b/internal/sheets/tags_test.go new file mode 100644 index 0000000..54d156b --- /dev/null +++ b/internal/sheets/tags_test.go @@ -0,0 +1,51 @@ +package sheets + +import ( + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + + "github.com/cheat/cheat/internal/sheet" +) + +// TestTags asserts that cheetsheet tags are properly returned +func TestTags(t *testing.T) { + + // mock cheatsheets available on multiple cheatpaths + cheatpaths := []map[string]sheet.Sheet{ + + // mock community cheatsheets + map[string]sheet.Sheet{ + "foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha"}}, + "bar": sheet.Sheet{Title: "bar", Tags: []string{"alpha", "bravo"}}, + }, + + // mock local cheatsheets + map[string]sheet.Sheet{ + "bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}}, + "baz": sheet.Sheet{Title: "baz", Tags: []string{"delta"}}, + }, + } + + // consolidate the cheatsheets + tags := Tags(cheatpaths) + + // specify the expected output + want := []string{ + "alpha", + "bravo", + "charlie", + "delta", + } + + // assert that the cheatsheets properly consolidated + if !reflect.DeepEqual(tags, want) { + t.Errorf( + "failed to return tags: want:\n%s, got:\n%s", + spew.Sdump(want), + spew.Sdump(tags), + ) + } + +}