mirror of
https://github.com/cheat/cheat.git
synced 2025-09-04 19:18: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:
74
internal/config/config.go
Normal file
74
internal/config/config.go
Normal 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
|
||||
}
|
111
internal/config/config_test.go
Normal file
111
internal/config/config_test.go
Normal 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
68
internal/config/path.go
Normal 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")
|
||||
}
|
64
internal/config/validate.go
Normal file
64
internal/config/validate.go
Normal 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
|
||||
}
|
150
internal/config/validate_test.go
Normal file
150
internal/config/validate_test.go
Normal 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")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user