mirror of
https://github.com/cheat/cheat.git
synced 2025-09-04 11:08:29 +02:00
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:
51
internal/sheet/copy.go
Normal file
51
internal/sheet/copy.go
Normal 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
102
internal/sheet/copy_test.go
Normal 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
7
internal/sheet/match.go
Normal 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
45
internal/sheet/search.go
Normal 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
|
||||
}
|
185
internal/sheet/search_test.go
Normal file
185
internal/sheet/search_test.go
Normal 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
64
internal/sheet/sheet.go
Normal 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
|
||||
}
|
71
internal/sheet/sheet_test.go
Normal file
71
internal/sheet/sheet_test.go
Normal 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
15
internal/sheet/tagged.go
Normal 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
|
||||
}
|
26
internal/sheet/tagged_test.go
Normal file
26
internal/sheet/tagged_test.go
Normal 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")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user