Re-wrote from scratch in Golang

- Re-implemented the project in Golang, and deprecated Python entirely
- Implemented several new, long-requested features
- Refactored cheatsheets into a separate repository
This commit is contained in:
Chris Lane
2019-10-20 10:02:28 -04:00
parent 307c4e6ad6
commit e5114a3e76
271 changed files with 2630 additions and 7834 deletions

View File

@ -0,0 +1,9 @@
package cheatpath
// Cheatpath encapsulates cheatsheet path information
type Cheatpath struct {
Name string `yaml:name`
Path string `yaml:path`
ReadOnly bool `yaml:readonly`
Tags []string `yaml:tags`
}

View File

@ -0,0 +1,19 @@
package cheatpath
import (
"fmt"
)
// Filter filters all cheatpaths that are not named `name`
func Filter(paths []Cheatpath, name string) ([]Cheatpath, error) {
// if a path of the given name exists, return it
for _, path := range paths {
if path.Name == name {
return []Cheatpath{path}, nil
}
}
// otherwise, return an error
return []Cheatpath{}, fmt.Errorf("cheatpath does not exist: %s", name)
}

View File

@ -0,0 +1,53 @@
package cheatpath
import (
"testing"
)
// TestFilterSuccess asserts that the proper cheatpath is returned when the
// requested cheatpath exists
func TestFilterSuccess(t *testing.T) {
// init cheatpaths
paths := []Cheatpath{
Cheatpath{Name: "foo"},
Cheatpath{Name: "bar"},
Cheatpath{Name: "baz"},
}
// filter the paths
paths, err := Filter(paths, "bar")
if err != nil {
t.Errorf("failed to filter paths: %v", err)
}
// assert that the expected path was returned
if len(paths) != 1 {
t.Errorf(
"failed to return correct path count: want: 1, got: %d",
len(paths),
)
}
if paths[0].Name != "bar" {
t.Errorf("failed to return correct path: want: bar, got: %s", paths[0].Name)
}
}
// TestFilterFailure asserts that an error is returned when a non-existent
// cheatpath is requested
func TestFilterFailure(t *testing.T) {
// init cheatpaths
paths := []Cheatpath{
Cheatpath{Name: "foo"},
Cheatpath{Name: "bar"},
Cheatpath{Name: "baz"},
}
// filter the paths
paths, err := Filter(paths, "qux")
if err == nil {
t.Errorf("failed to return an error on non-existent cheatpath")
}
}

View File

@ -0,0 +1,18 @@
package cheatpath
import (
"fmt"
)
// Validate returns an error if the cheatpath is invalid
func (c *Cheatpath) Validate() error {
if c.Name == "" {
return fmt.Errorf("invalid cheatpath: name must be specified")
}
if c.Path == "" {
return fmt.Errorf("invalid cheatpath: path must be specified")
}
return nil
}

View File

@ -0,0 +1,56 @@
package cheatpath
import (
"testing"
)
// TestValidateValid asserts that valid cheatpaths validate successfully
func TestValidateValid(t *testing.T) {
// initialize a valid cheatpath
cheatpath := Cheatpath{
Name: "foo",
Path: "/foo",
ReadOnly: false,
Tags: []string{},
}
// assert that no errors are returned
if err := cheatpath.Validate(); err != nil {
t.Errorf("failed to validate valid cheatpath: %v", err)
}
}
// TestValidateMissingName asserts that paths that are missing a name fail to
// validate
func TestValidateMissingName(t *testing.T) {
// initialize a valid cheatpath
cheatpath := Cheatpath{
Path: "/foo",
ReadOnly: false,
Tags: []string{},
}
// assert that no errors are returned
if err := cheatpath.Validate(); err == nil {
t.Errorf("failed to invalidate cheatpath without name")
}
}
// TestValidateMissingPath asserts that paths that are missing a path fail to
// validate
func TestValidateMissingPath(t *testing.T) {
// initialize a valid cheatpath
cheatpath := Cheatpath{
Name: "foo",
ReadOnly: false,
Tags: []string{},
}
// assert that no errors are returned
if err := cheatpath.Validate(); err == nil {
t.Errorf("failed to invalidate cheatpath without path")
}
}

View File

@ -0,0 +1,24 @@
package cheatpath
import (
"fmt"
)
// Writeable returns a writeable Cheatpath
func Writeable(cheatpaths []Cheatpath) (Cheatpath, error) {
// iterate backwards over the cheatpaths
// NB: we're going backwards because we assume that the most "local"
// cheatpath will be specified last in the configs
for i := len(cheatpaths) - 1; i >= 0; i-- {
// if the cheatpath is not read-only, it is writeable, and thus returned
if cheatpaths[i].ReadOnly == false {
return cheatpaths[i], nil
}
}
// otherwise, return an error
return Cheatpath{}, fmt.Errorf("no writeable cheatpaths found")
}

View File

@ -0,0 +1,50 @@
package cheatpath
import (
"testing"
)
// TestWriteableOK asserts that Writeable returns the appropriate cheatpath
// when a writeable cheatpath exists
func TestWriteableOK(t *testing.T) {
// initialize some cheatpaths
cheatpaths := []Cheatpath{
Cheatpath{Path: "/foo", ReadOnly: true},
Cheatpath{Path: "/bar", ReadOnly: false},
Cheatpath{Path: "/baz", ReadOnly: true},
}
// get the writeable cheatpath
got, err := Writeable(cheatpaths)
// assert that no errors were returned
if err != nil {
t.Errorf("failed to get cheatpath: %v", err)
}
// assert that the path is correct
if got.Path != "/bar" {
t.Errorf("incorrect cheatpath returned: got: %s", got.Path)
}
}
// TestWriteableOK asserts that Writeable returns an error when no writeable
// cheatpaths exist
func TestWriteableNotOK(t *testing.T) {
// initialize some cheatpaths
cheatpaths := []Cheatpath{
Cheatpath{Path: "/foo", ReadOnly: true},
Cheatpath{Path: "/bar", ReadOnly: true},
Cheatpath{Path: "/baz", ReadOnly: true},
}
// get the writeable cheatpath
_, err := Writeable(cheatpaths)
// assert that no errors were returned
if err == nil {
t.Errorf("failed to return an error when no writeable paths found")
}
}

74
internal/config/config.go Normal file
View File

@ -0,0 +1,74 @@
package config
import (
"fmt"
"io/ioutil"
"os"
cp "github.com/cheat/cheat/internal/cheatpath"
"github.com/mitchellh/go-homedir"
"gopkg.in/yaml.v2"
)
// Config encapsulates configuration parameters
type Config struct {
Colorize bool `yaml:colorize`
Editor string `yaml:editor`
Cheatpaths []cp.Cheatpath `yaml:cheatpaths`
Style string `yaml:style`
Formatter string `yaml:formatter`
}
// New returns a new Config struct
func New(opts map[string]interface{}, confPath string) (Config, error) {
// read the config file
buf, err := ioutil.ReadFile(confPath)
if err != nil {
return Config{}, fmt.Errorf("could not read config file: %v", err)
}
// initialize a config object
conf := Config{}
// unmarshal the yaml
err = yaml.UnmarshalStrict(buf, &conf)
if err != nil {
return Config{}, fmt.Errorf("could not unmarshal yaml: %v", err)
}
// expand ~ in config paths
for i, cheatpath := range conf.Cheatpaths {
expanded, err := homedir.Expand(cheatpath.Path)
if err != nil {
return Config{}, fmt.Errorf("failed to expand ~: %v", err)
}
conf.Cheatpaths[i].Path = expanded
}
// if an editor was not provided in the configs, look to envvars
if conf.Editor == "" {
if os.Getenv("VISUAL") != "" {
conf.Editor = os.Getenv("VISUAL")
} else if os.Getenv("EDITOR") != "" {
conf.Editor = os.Getenv("EDITOR")
} else {
return Config{}, fmt.Errorf("no editor set")
}
}
// if a chroma style was not provided, set a default
if conf.Style == "" {
conf.Style = "bw"
}
// if a chroma formatter was not provided, set a default
if conf.Formatter == "" {
conf.Formatter = "terminal16m"
}
return conf, nil
}

View File

@ -0,0 +1,111 @@
package config
import (
"os"
"path/filepath"
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/mitchellh/go-homedir"
"github.com/cheat/cheat/internal/cheatpath"
"github.com/cheat/cheat/internal/mock"
)
// TestConfig asserts that the configs are loaded correctly
func TestConfigSuccessful(t *testing.T) {
// initialize a config
conf, err := New(map[string]interface{}{}, mock.Path("conf/conf.yml"))
if err != nil {
t.Errorf("failed to parse config file: %v", err)
}
// assert that the expected values were returned
if conf.Editor != "vim" {
t.Errorf("failed to set editor: want: vim, got: %s", conf.Editor)
}
if !conf.Colorize {
t.Errorf("failed to set colorize: want: true, got: %t", conf.Colorize)
}
// get the user's home directory (with ~ expanded)
home, err := homedir.Dir()
if err != nil {
t.Errorf("failed to get homedir: %v", err)
}
// assert that the cheatpaths are correct
want := []cheatpath.Cheatpath{
cheatpath.Cheatpath{
Path: filepath.Join(home, ".dotfiles/cheat/community"),
ReadOnly: true,
Tags: []string{"community"},
},
cheatpath.Cheatpath{
Path: filepath.Join(home, ".dotfiles/cheat/work"),
ReadOnly: false,
Tags: []string{"work"},
},
cheatpath.Cheatpath{
Path: filepath.Join(home, ".dotfiles/cheat/personal"),
ReadOnly: false,
Tags: []string{"personal"},
},
}
if !reflect.DeepEqual(conf.Cheatpaths, want) {
t.Errorf(
"failed to return expected results: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(conf.Cheatpaths),
)
}
}
// TestConfigFailure asserts that an error is returned if the config file
// cannot be read.
func TestConfigFailure(t *testing.T) {
// attempt to read a non-existent config file
_, err := New(map[string]interface{}{}, "/does-not-exit")
if err == nil {
t.Errorf("failed to error on unreadable config")
}
}
// TestEmptyEditor asserts that envvars are respected if an editor is not
// specified in the configs
func TestEmptyEditor(t *testing.T) {
// clear the environment variables
os.Setenv("VISUAL", "")
os.Setenv("EDITOR", "")
// initialize a config
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"))
if err == nil {
t.Errorf("failed to return an error on empty editor")
}
// set editor, and assert that it is respected
os.Setenv("EDITOR", "foo")
conf, err = New(map[string]interface{}{}, mock.Path("conf/empty.yml"))
if err != nil {
t.Errorf("failed to init configs: %v", err)
}
if conf.Editor != "foo" {
t.Errorf("failed to respect editor: want: foo, got: %s", conf.Editor)
}
// set visual, and assert that it overrides editor
os.Setenv("VISUAL", "bar")
conf, err = New(map[string]interface{}{}, mock.Path("conf/empty.yml"))
if err != nil {
t.Errorf("failed to init configs: %v", err)
}
if conf.Editor != "bar" {
t.Errorf("failed to respect editor: want: bar, got: %s", conf.Editor)
}
}

68
internal/config/path.go Normal file
View File

@ -0,0 +1,68 @@
package config
import (
"fmt"
"os"
"path"
"github.com/mitchellh/go-homedir"
)
// Path returns the config file path
func Path(sys string) (string, error) {
var paths []string
// if CHEAT_CONFIG_PATH is set, return it
if os.Getenv("CHEAT_CONFIG_PATH") != "" {
// expand ~
expanded, err := homedir.Expand(os.Getenv("CHEAT_CONFIG_PATH"))
if err != nil {
return "", fmt.Errorf("failed to expand ~: %v", err)
}
return expanded, nil
// OSX config paths
} else if sys == "darwin" {
paths = []string{
path.Join(os.Getenv("XDG_CONFIG_HOME"), "/cheat/conf.yml"),
path.Join(os.Getenv("HOME"), ".config/cheat/conf.yml"),
path.Join(os.Getenv("HOME"), ".cheat/conf.yml"),
}
// Linux config paths
} else if sys == "linux" {
paths = []string{
path.Join(os.Getenv("XDG_CONFIG_HOME"), "/cheat/conf.yml"),
path.Join(os.Getenv("HOME"), ".config/cheat/conf.yml"),
path.Join(os.Getenv("HOME"), ".cheat/conf.yml"),
"/etc/cheat/conf.yml",
}
// Windows config paths
} else if sys == "windows" {
paths = []string{
fmt.Sprintf("%s/cheat/conf.yml", os.Getenv("APPDATA")),
fmt.Sprintf("%s/cheat/conf.yml", os.Getenv("PROGRAMDATA")),
}
// Unsupported platforms
} else {
return "", fmt.Errorf("unsupported os: %s", sys)
}
// check if the config file exists on any paths
for _, p := range paths {
if _, err := os.Stat(p); err == nil {
return p, nil
}
}
// we can't find the config file if we make it this far
return "", fmt.Errorf("could not locate config file")
}

View File

@ -0,0 +1,64 @@
package config
import (
"fmt"
)
// Validate returns an error if the config is invalid
func (c *Config) Validate() error {
// assert that an editor was specified
if c.Editor == "" {
return fmt.Errorf("config error: editor unspecified")
}
// assert that at least one cheatpath was specified
if len(c.Cheatpaths) == 0 {
return fmt.Errorf("config error: no cheatpaths specified")
}
// assert that each path and name is unique
names := make(map[string]bool)
paths := make(map[string]bool)
// assert that each cheatpath is valid
for _, cheatpath := range c.Cheatpaths {
// assert that the cheatpath is valid
if err := cheatpath.Validate(); err != nil {
return fmt.Errorf("config error: %v", err)
}
// assert that the name is unique
if _, ok := names[cheatpath.Name]; ok {
return fmt.Errorf(
"config error: cheatpath name is not unique: %s",
cheatpath.Name,
)
}
names[cheatpath.Name] = true
// assert that the path is unique
if _, ok := paths[cheatpath.Path]; ok {
return fmt.Errorf(
"config error: cheatpath path is not unique: %s",
cheatpath.Path,
)
}
paths[cheatpath.Path] = true
}
// TODO: assert valid styles?
// assert that the formatter is valid
formatters := map[string]bool{
"terminal": true,
"terminal256": true,
"terminal16m": true,
}
if _, ok := formatters[c.Formatter]; !ok {
return fmt.Errorf("config error: formatter is invalid: %s", c.Formatter)
}
return nil
}

View File

@ -0,0 +1,150 @@
package config
import (
"testing"
"github.com/cheat/cheat/internal/cheatpath"
)
// TestValidateCorrect asserts that valid configs are validated successfully
func TestValidateCorrect(t *testing.T) {
// mock a config
conf := Config{
Colorize: true,
Editor: "vim",
Formatter: "terminal16m",
Cheatpaths: []cheatpath.Cheatpath{
cheatpath.Cheatpath{
Name: "foo",
Path: "/foo",
ReadOnly: false,
Tags: []string{},
},
},
}
// assert that no errors are returned
if err := conf.Validate(); err != nil {
t.Errorf("failed to validate valid config: %v", err)
}
}
// TestInvalidateMissingEditor asserts that configs with unspecified editors
// are invalidated
func TestInvalidateMissingEditor(t *testing.T) {
// mock a config
conf := Config{
Colorize: true,
Formatter: "terminal16m",
Cheatpaths: []cheatpath.Cheatpath{
cheatpath.Cheatpath{
Name: "foo",
Path: "/foo",
ReadOnly: false,
Tags: []string{},
},
},
}
// assert that no errors are returned
if err := conf.Validate(); err == nil {
t.Errorf("failed to invalidate config with unspecified editor")
}
}
// TestInvalidateMissingCheatpaths asserts that configs without cheatpaths are
// invalidated
func TestInvalidateMissingCheatpaths(t *testing.T) {
// mock a config
conf := Config{
Colorize: true,
Editor: "vim",
Formatter: "terminal16m",
}
// assert that no errors are returned
if err := conf.Validate(); err == nil {
t.Errorf("failed to invalidate config without cheatpaths")
}
}
// TestMissingInvalidFormatters asserts that configs which contain invalid
// formatters are invalidated
func TestMissingInvalidFormatters(t *testing.T) {
// mock a config
conf := Config{
Colorize: true,
Editor: "vim",
}
// assert that no errors are returned
if err := conf.Validate(); err == nil {
t.Errorf("failed to invalidate config without formatter")
}
}
// TestInvalidateDuplicateCheatpathNames asserts that configs which contain
// cheatpaths with duplcated names are invalidated
func TestInvalidateDuplicateCheatpathNames(t *testing.T) {
// mock a config
conf := Config{
Colorize: true,
Editor: "vim",
Formatter: "terminal16m",
Cheatpaths: []cheatpath.Cheatpath{
cheatpath.Cheatpath{
Name: "foo",
Path: "/foo",
ReadOnly: false,
Tags: []string{},
},
cheatpath.Cheatpath{
Name: "foo",
Path: "/bar",
ReadOnly: false,
Tags: []string{},
},
},
}
// assert that no errors are returned
if err := conf.Validate(); err == nil {
t.Errorf("failed to invalidate config with cheatpaths with duplicate names")
}
}
// TestInvalidateDuplicateCheatpathPaths asserts that configs which contain
// cheatpaths with duplcated paths are invalidated
func TestInvalidateDuplicateCheatpathPaths(t *testing.T) {
// mock a config
conf := Config{
Colorize: true,
Editor: "vim",
Formatter: "terminal16m",
Cheatpaths: []cheatpath.Cheatpath{
cheatpath.Cheatpath{
Name: "foo",
Path: "/foo",
ReadOnly: false,
Tags: []string{},
},
cheatpath.Cheatpath{
Name: "bar",
Path: "/foo",
ReadOnly: false,
Tags: []string{},
},
},
}
// assert that no errors are returned
if err := conf.Validate(); err == nil {
t.Errorf("failed to invalidate config with cheatpaths with duplicate paths")
}
}

29
internal/mock/path.go Normal file
View File

@ -0,0 +1,29 @@
package mock
import (
"fmt"
"path"
"path/filepath"
"runtime"
)
// Path returns the absolute path to the specified mock file.
func Path(filename string) string {
// determine the path of this file during runtime
_, thisfile, _, _ := runtime.Caller(0)
// compute the config path
file, err := filepath.Abs(
path.Join(
filepath.Dir(thisfile),
"../../mocks",
filename,
),
)
if err != nil {
panic(fmt.Errorf("failed to resolve config path: %v", err))
}
return file
}

51
internal/sheet/copy.go Normal file
View File

@ -0,0 +1,51 @@
package sheet
import (
"fmt"
"io"
"os"
"path"
)
// Copy copies a cheatsheet to a new location
func (s *Sheet) Copy(dest string) error {
// NB: while the `infile` has already been loaded and parsed into a `sheet`
// struct, we're going to read it again here. This is a bit wasteful, but
// necessary if we want the "raw" file contents (including the front-matter).
// This is because the frontmatter is parsed and then discarded when the file
// is loaded via `sheets.Load`.
infile, err := os.Open(s.Path)
if err != nil {
return fmt.Errorf("failed to open cheatsheet: %s, %v", s.Path, err)
}
defer infile.Close()
// create any necessary subdirectories
dirs := path.Dir(dest)
if dirs != "." {
if err := os.MkdirAll(dirs, 0755); err != nil {
return fmt.Errorf("failed to create directory: %s, %v", dirs, err)
}
}
// create the outfile
outfile, err := os.Create(dest)
if err != nil {
return fmt.Errorf("failed to create outfile: %s, %v", dest, err)
}
defer outfile.Close()
// copy file contents
_, err = io.Copy(outfile, infile)
if err != nil {
return fmt.Errorf(
"failed to copy file: infile: %s, outfile: %s, err: %v",
s.Path,
dest,
err,
)
}
return nil
}

102
internal/sheet/copy_test.go Normal file
View File

@ -0,0 +1,102 @@
package sheet
import (
"io/ioutil"
"os"
"path"
"testing"
)
// TestCopyFlat asserts that Copy correctly copies files at a single level of
// depth
func TestCopyFlat(t *testing.T) {
// mock a cheatsheet file
text := "this is the cheatsheet text"
src, err := ioutil.TempFile("", "foo-src")
if err != nil {
t.Errorf("failed to mock cheatsheet: %v", err)
}
defer src.Close()
defer os.Remove(src.Name())
if _, err := src.WriteString(text); err != nil {
t.Errorf("failed to write to mock cheatsheet: %v", err)
}
// mock a cheatsheet struct
sheet, err := New("foo", src.Name(), []string{}, false)
if err != nil {
t.Errorf("failed to init cheatsheet: %v", err)
}
// compute the outfile's path
outpath := path.Join(os.TempDir(), sheet.Title)
defer os.Remove(outpath)
// attempt to copy the cheatsheet
err = sheet.Copy(outpath)
if err != nil {
t.Errorf("failed to copy cheatsheet: %v", err)
}
// assert that the destination file contains the correct text
got, err := ioutil.ReadFile(outpath)
if err != nil {
t.Errorf("failed to read destination file: %v", err)
}
if string(got) != text {
t.Errorf(
"destination file contained wrong text: want: '%s', got: '%s'",
text,
got,
)
}
}
// TestCopyDeep asserts that Copy correctly copies files at several levels of
// depth
func TestCopyDeep(t *testing.T) {
// mock a cheatsheet file
text := "this is the cheatsheet text"
src, err := ioutil.TempFile("", "foo-src")
if err != nil {
t.Errorf("failed to mock cheatsheet: %v", err)
}
defer src.Close()
defer os.Remove(src.Name())
if _, err := src.WriteString(text); err != nil {
t.Errorf("failed to write to mock cheatsheet: %v", err)
}
// mock a cheatsheet struct
sheet, err := New("/cheat-tests/alpha/bravo/foo", src.Name(), []string{}, false)
if err != nil {
t.Errorf("failed to init cheatsheet: %v", err)
}
// compute the outfile's path
outpath := path.Join(os.TempDir(), sheet.Title)
defer os.RemoveAll(path.Join(os.TempDir(), "cheat-tests"))
// attempt to copy the cheatsheet
err = sheet.Copy(outpath)
if err != nil {
t.Errorf("failed to copy cheatsheet: %v", err)
}
// assert that the destination file contains the correct text
got, err := ioutil.ReadFile(outpath)
if err != nil {
t.Errorf("failed to read destination file: %v", err)
}
if string(got) != text {
t.Errorf(
"destination file contained wrong text: want: '%s', got: '%s'",
text,
got,
)
}
}

7
internal/sheet/match.go Normal file
View File

@ -0,0 +1,7 @@
package sheet
// Match encapsulates search matches within cheatsheets
type Match struct {
Line int
Text string
}

45
internal/sheet/search.go Normal file
View File

@ -0,0 +1,45 @@
package sheet
import (
"regexp"
"strings"
"github.com/mgutz/ansi"
)
// Search searches for regexp matches in a cheatsheet's text, and optionally
// colorizes matching strings.
func (s *Sheet) Search(reg *regexp.Regexp, colorize bool) []Match {
// record matches
matches := []Match{}
// search through the cheatsheet's text line by line
// TODO: searching line-by-line is surely the "naive" approach. Revisit this
// later with an eye for performance improvements.
for linenum, line := range strings.Split(s.Text, "\n") {
// exit early if the line doesn't match the regex
if !reg.MatchString(line) {
continue
}
// init the match
m := Match{
Line: linenum + 1,
Text: strings.TrimSpace(line),
}
// colorize the matching text if so configured
if colorize {
m.Text = reg.ReplaceAllStringFunc(m.Text, func(matched string) string {
return ansi.Color(matched, "red+b")
})
}
// record the match
matches = append(matches, m)
}
return matches
}

View File

@ -0,0 +1,185 @@
package sheet
import (
"reflect"
"regexp"
"testing"
"github.com/davecgh/go-spew/spew"
)
// TestSearchNoMatch ensures that the expected output is returned when no
// matches are found
func TestSearchNoMatch(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
reg, err := regexp.Compile("(?i)foo")
if err != nil {
t.Errorf("failed to compile regex: %v", err)
}
// search the sheet
matches := sheet.Search(reg, false)
// assert that no matches were found
if len(matches) != 0 {
t.Errorf("failure: expected no matches: got: %s", spew.Sdump(matches))
}
}
// TestSearchSingleMatchNoColor asserts that the expected output is returned
// when a single match is returned, and no colorization is applied.
func TestSearchSingleMatchNoColor(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
reg, err := regexp.Compile("(?i)fox")
if err != nil {
t.Errorf("failed to compile regex: %v", err)
}
// search the sheet
matches := sheet.Search(reg, false)
// specify the expected results
want := []Match{
Match{
Line: 1,
Text: "The quick brown fox",
},
}
// assert that the correct matches were returned
if !reflect.DeepEqual(matches, want) {
t.Errorf(
"failed to return expected matches: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(matches),
)
}
}
// TestSearchSingleMatchColorized asserts that the expected output is returned
// when a single match is returned, and colorization is applied
func TestSearchSingleMatchColorized(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
reg, err := regexp.Compile("(?i)fox")
if err != nil {
t.Errorf("failed to compile regex: %v", err)
}
// search the sheet
matches := sheet.Search(reg, true)
// specify the expected results
want := []Match{
Match{
Line: 1,
Text: "The quick brown \x1b[1;31mfox\x1b[0m",
},
}
// assert that the correct matches were returned
if !reflect.DeepEqual(matches, want) {
t.Errorf(
"failed to return expected matches: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(matches),
)
}
}
// TestSearchMultiMatchNoColor asserts that the expected output is returned
// when a multiple matches are returned, and no colorization is applied
func TestSearchMultiMatchNoColor(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
reg, err := regexp.Compile("(?i)the")
if err != nil {
t.Errorf("failed to compile regex: %v", err)
}
// search the sheet
matches := sheet.Search(reg, false)
// specify the expected results
want := []Match{
Match{
Line: 1,
Text: "The quick brown fox",
},
Match{
Line: 3,
Text: "the lazy dog.",
},
}
// assert that the correct matches were returned
if !reflect.DeepEqual(matches, want) {
t.Errorf(
"failed to return expected matches: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(matches),
)
}
}
// TestSearchMultiMatchColorized asserts that the expected output is returned
// when a multiple matches are returned, and colorization is applied
func TestSearchMultiMatchColorized(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
reg, err := regexp.Compile("(?i)the")
if err != nil {
t.Errorf("failed to compile regex: %v", err)
}
// search the sheet
matches := sheet.Search(reg, true)
// specify the expected results
want := []Match{
Match{
Line: 1,
Text: "\x1b[1;31mThe\x1b[0m quick brown fox",
},
Match{
Line: 3,
Text: "\x1b[1;31mthe\x1b[0m lazy dog.",
},
}
// assert that the correct matches were returned
if !reflect.DeepEqual(matches, want) {
t.Errorf(
"failed to return expected matches: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(matches),
)
}
}

64
internal/sheet/sheet.go Normal file
View File

@ -0,0 +1,64 @@
package sheet
import (
"fmt"
"io/ioutil"
"sort"
"strings"
"github.com/tj/front"
)
// frontmatter is an un-exported helper struct used in parsing cheatsheets
type frontmatter struct {
Tags []string
Syntax string
}
// Sheet encapsulates sheet information
type Sheet struct {
Title string
Path string
Text string
Tags []string
Syntax string
ReadOnly bool
}
// New initializes a new Sheet
func New(
title string,
path string,
tags []string,
readOnly bool,
) (Sheet, error) {
// read the cheatsheet file
markdown, err := ioutil.ReadFile(path)
if err != nil {
return Sheet{}, fmt.Errorf("failed to read file: %s, %v", path, err)
}
// parse the front-matter
var fm frontmatter
text, err := front.Unmarshal(markdown, &fm)
if err != nil {
return Sheet{}, fmt.Errorf("failed to parse front-matter: %v", err)
}
// merge the sheet-specific tags into the cheatpath tags
tags = append(tags, fm.Tags...)
// sort strings so they pretty-print nicely
sort.Strings(tags)
// initialize and return a sheet
return Sheet{
Title: title,
Path: path,
Text: strings.TrimSpace(string(text)) + "\n",
Tags: tags,
Syntax: fm.Syntax,
ReadOnly: readOnly,
}, nil
}

View File

@ -0,0 +1,71 @@
package sheet
import (
"reflect"
"testing"
"github.com/cheat/cheat/internal/mock"
)
// TestSheetSuccess asserts that sheets initialize properly
func TestSheetSuccess(t *testing.T) {
// initialize a sheet
sheet, err := New(
"foo",
mock.Path("sheet/foo"),
[]string{"alpha", "bravo"},
false,
)
if err != nil {
t.Errorf("failed to load sheet: %v", err)
}
// assert that the sheet loaded correctly
if sheet.Title != "foo" {
t.Errorf("failed to init title: want: foo, got: %s", sheet.Title)
}
if sheet.Path != mock.Path("sheet/foo") {
t.Errorf(
"failed to init path: want: %s, got: %s",
mock.Path("sheet/foo"),
sheet.Path,
)
}
wantText := "# To foo the bar:\n foo bar\n"
if sheet.Text != wantText {
t.Errorf("failed to init text: want: %s, got: %s", wantText, sheet.Text)
}
// NB: tags should sort alphabetically
wantTags := []string{"alpha", "bar", "baz", "bravo", "foo"}
if !reflect.DeepEqual(sheet.Tags, wantTags) {
t.Errorf("failed to init tags: want: %v, got: %v", wantTags, sheet.Tags)
}
if sheet.Syntax != "sh" {
t.Errorf("failed to init syntax: want: sh, got: %s", sheet.Syntax)
}
if sheet.ReadOnly != false {
t.Errorf("failed to init readonly")
}
}
// TestSheetFailure asserts that an error is returned if the sheet cannot be
// read
func TestSheetFailure(t *testing.T) {
// initialize a sheet
_, err := New(
"foo",
mock.Path("/does-not-exist"),
[]string{"alpha", "bravo"},
false,
)
if err == nil {
t.Errorf("failed to return an error on unreadable sheet")
}
}

15
internal/sheet/tagged.go Normal file
View File

@ -0,0 +1,15 @@
package sheet
// Tagged returns true if a sheet was tagged with `needle`
func (s *Sheet) Tagged(needle string) bool {
// if any of the tags match `needle`, return `true`
for _, tag := range s.Tags {
if tag == needle {
return true
}
}
// otherwise, return `false`
return false
}

View File

@ -0,0 +1,26 @@
package sheet
import (
"testing"
)
// TestTagged ensures that tags are properly recognized as being absent or
// present
func TestTagged(t *testing.T) {
// initialize a cheatsheet
tags := []string{"foo", "bar", "baz"}
sheet := Sheet{Tags: tags}
// assert that set tags are recognized as set
for _, tag := range tags {
if sheet.Tagged(tag) == false {
t.Errorf("failed to recognize tag: %s", tag)
}
}
// assert that unset tags are recognized as unset
if sheet.Tagged("qux") {
t.Errorf("failed to recognize absent tag")
}
}

View File

@ -0,0 +1,23 @@
package sheets
import (
"github.com/cheat/cheat/internal/sheet"
)
// Consolidate applies cheatsheet "overrides", resolving title conflicts that
// exist among cheatpaths by preferring more local cheatsheets over less local
// cheatsheets.
func Consolidate(
cheatpaths []map[string]sheet.Sheet,
) map[string]sheet.Sheet {
consolidated := make(map[string]sheet.Sheet)
for _, cheatpath := range cheatpaths {
for title, sheet := range cheatpath {
consolidated[title] = sheet
}
}
return consolidated
}

View File

@ -0,0 +1,50 @@
package sheets
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/cheat/cheat/internal/sheet"
)
// TestConsolidate asserts that cheatsheets are properly consolidated
func TestConsolidate(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", Path: "community/foo"},
"bar": sheet.Sheet{Title: "bar", Path: "community/bar"},
},
// mock local cheatsheets
map[string]sheet.Sheet{
"bar": sheet.Sheet{Title: "bar", Path: "local/bar"},
"baz": sheet.Sheet{Title: "baz", Path: "local/baz"},
},
}
// consolidate the cheatsheets
consolidated := Consolidate(cheatpaths)
// specify the expected output
want := map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo", Path: "community/foo"},
"bar": sheet.Sheet{Title: "bar", Path: "local/bar"},
"baz": sheet.Sheet{Title: "baz", Path: "local/baz"},
}
// assert that the cheatsheets properly consolidated
if !reflect.DeepEqual(consolidated, want) {
t.Errorf(
"failed to consolidate cheatpaths: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(consolidated),
)
}
}

53
internal/sheets/filter.go Normal file
View File

@ -0,0 +1,53 @@
package sheets
import (
"strings"
"github.com/cheat/cheat/internal/sheet"
)
// Filter filters cheatsheets that do not match `tag(s)`
func Filter(
cheatpaths []map[string]sheet.Sheet,
tags []string,
) []map[string]sheet.Sheet {
// buffer a map of filtered cheatsheets
filtered := make([]map[string]sheet.Sheet, 0, len(cheatpaths))
// iterate over each cheatpath
for _, cheatsheets := range cheatpaths {
// create a map of cheatsheets for each cheatpath. The filtering will be
// applied to each cheatpath individually.
pathFiltered := make(map[string]sheet.Sheet)
// iterate over each cheatsheet that exists on each cheatpath
for title, sheet := range cheatsheets {
// assume that the sheet should be kept (ie, should not be filtered)
keep := true
// iterate over each tag. If the sheet does not match *all* tags, filter
// it out.
for _, tag := range tags {
if !sheet.Tagged(strings.TrimSpace(tag)) {
keep = false
}
}
// if the sheet does match all tags, it passes the filter
if keep {
pathFiltered[title] = sheet
}
}
// the sheets on this individual cheatpath have now been filtered. Now,
// store those alongside the sheets on the other cheatpaths that also made
// it passed the filter.
filtered = append(filtered, pathFiltered)
}
// return the filtered cheatsheets on all paths
return filtered
}

View File

@ -0,0 +1,94 @@
package sheets
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/cheat/cheat/internal/sheet"
)
// TestFilterSingleTag asserts that Filter properly filters results when passed
// a single tag
func TestFilterSingleTag(t *testing.T) {
// mock cheatsheets available on multiple cheatpaths
cheatpaths := []map[string]sheet.Sheet{
map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
"bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}},
},
map[string]sheet.Sheet{
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha", "bravo"}},
"bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}},
},
}
// filter the cheatsheets
filtered := Filter(cheatpaths, []string{"bravo"})
// assert that the expect results were returned
want := []map[string]sheet.Sheet{
map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
"bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}},
},
map[string]sheet.Sheet{
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha", "bravo"}},
"bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}},
},
}
if !reflect.DeepEqual(filtered, want) {
t.Errorf(
"failed to return expected results: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(filtered),
)
}
}
// TestFilterSingleTag asserts that Filter properly filters results when passed
// multiple tags
func TestFilterMultiTag(t *testing.T) {
// mock cheatsheets available on multiple cheatpaths
cheatpaths := []map[string]sheet.Sheet{
map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
"bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}},
},
map[string]sheet.Sheet{
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha", "bravo"}},
"bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}},
},
}
// filter the cheatsheets
filtered := Filter(cheatpaths, []string{"alpha", "bravo"})
// assert that the expect results were returned
want := []map[string]sheet.Sheet{
map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
},
map[string]sheet.Sheet{
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha", "bravo"}},
},
}
if !reflect.DeepEqual(filtered, want) {
t.Errorf(
"failed to return expected results: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(filtered),
)
}
}

77
internal/sheets/load.go Normal file
View File

@ -0,0 +1,77 @@
package sheets
import (
"fmt"
"os"
"path/filepath"
"strings"
cp "github.com/cheat/cheat/internal/cheatpath"
"github.com/cheat/cheat/internal/sheet"
)
// Load produces a map of cheatsheet titles to filesystem paths
func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
// create a slice of maps of sheets. This structure will store all sheets
// that are associated with each cheatpath.
sheets := make([]map[string]sheet.Sheet, len(cheatpaths))
// iterate over each cheatpath
for _, cheatpath := range cheatpaths {
// vivify the map of cheatsheets on this specific cheatpath
pathsheets := make(map[string]sheet.Sheet)
// recursively iterate over the cheatpath, and load each cheatsheet
// encountered along the way
err := filepath.Walk(
cheatpath.Path, func(
path string,
info os.FileInfo,
err error) error {
// fail if an error occurred while walking the directory
if err != nil {
return fmt.Errorf("error walking path: %v", err)
}
// don't register directories as cheatsheets
if info.IsDir() {
return nil
}
// calculate the cheatsheet's "title" (the phrase with which it may be
// accessed. Eg: `cheat tar` - `tar` is the title)
title := strings.TrimPrefix(
strings.TrimPrefix(path, cheatpath.Path),
"/",
)
// ignore dotfiles. Otherwise, we'll likely load .git/*
if strings.HasPrefix(title, ".") {
return nil
}
// parse the cheatsheet file into a `sheet` struct
s, err := sheet.New(title, path, cheatpath.Tags, cheatpath.ReadOnly)
if err != nil {
return fmt.Errorf("could not create sheet: %v", err)
}
// register the cheatsheet on its cheatpath, keyed by its title
pathsheets[title] = s
return nil
})
if err != nil {
return sheets, fmt.Errorf("failed to load cheatsheets: %v", err)
}
// store the sheets on this cheatpath alongside the other cheatsheets on
// other cheatpaths
sheets = append(sheets, pathsheets)
}
// return the cheatsheets, grouped by cheatpath
return sheets, nil
}

View File

@ -0,0 +1,3 @@
package sheets
// TODO

32
internal/sheets/sort.go Normal file
View File

@ -0,0 +1,32 @@
package sheets
import (
"sort"
"github.com/cheat/cheat/internal/sheet"
)
// Sort organizes the cheatsheets into an alphabetically-sorted slice
func Sort(cheatsheets map[string]sheet.Sheet) []sheet.Sheet {
// create a slice that contains the cheatsheet titles
var titles []string
for title := range cheatsheets {
titles = append(titles, title)
}
// sort the slice of titles
sort.Strings(titles)
// create a slice of sorted cheatsheets
sorted := []sheet.Sheet{}
// iterate over the sorted slice of titles, and append cheatsheets to
// `sorted` in an identical (alabetically sequential) order
for _, title := range titles {
sorted = append(sorted, cheatsheets[title])
}
// return the sorted slice of cheatsheets
return sorted
}

View File

@ -0,0 +1,34 @@
package sheets
import (
"testing"
"github.com/cheat/cheat/internal/sheet"
)
// TestSort asserts that Sort properly sorts sheets
func TestSort(t *testing.T) {
// mock a map of cheatsheets
sheets := map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo"},
"bar": sheet.Sheet{Title: "bar"},
"baz": sheet.Sheet{Title: "baz"},
}
// sort the sheets
sorted := Sort(sheets)
// assert that the sheets sorted properly
want := []string{"bar", "baz", "foo"}
for i, got := range sorted {
if got.Title != want[i] {
t.Errorf(
"sort returned incorrect value: want: %s, got: %s",
want[i],
got.Title,
)
}
}
}