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

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")
}
}