mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
Compare commits
1 Commits
chore/hous
...
4.7.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ad1a3c39f |
@@ -119,4 +119,4 @@ ssh -L 8080:localhost:80 user@remote
|
|||||||
- Use `go-git` for repository operations, not exec'ing git commands
|
- Use `go-git` for repository operations, not exec'ing git commands
|
||||||
- Platform-specific paths are handled in `internal/config/paths.go`
|
- Platform-specific paths are handled in `internal/config/paths.go`
|
||||||
- Color output uses ANSI codes via the Chroma library
|
- Color output uses ANSI codes via the Chroma library
|
||||||
- Test files use the `internal/mock` package for test data
|
- Test files use the `mocks` package for test data
|
||||||
@@ -88,7 +88,7 @@ The main configuration structure:
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Colorize bool `yaml:"colorize"`
|
Colorize bool `yaml:"colorize"`
|
||||||
Editor string `yaml:"editor"`
|
Editor string `yaml:"editor"`
|
||||||
Cheatpaths []cp.Cheatpath `yaml:"cheatpaths"`
|
Cheatpaths []cp.Path `yaml:"cheatpaths"`
|
||||||
Style string `yaml:"style"`
|
Style string `yaml:"style"`
|
||||||
Formatter string `yaml:"formatter"`
|
Formatter string `yaml:"formatter"`
|
||||||
Pager string `yaml:"pager"`
|
Pager string `yaml:"pager"`
|
||||||
@@ -97,7 +97,7 @@ type Config struct {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Key functions:
|
Key functions:
|
||||||
- `New(opts, confPath, resolve)` - Load config from file
|
- `New(confPath, resolve)` - Load config from file
|
||||||
- `Validate()` - Validate configuration values
|
- `Validate()` - Validate configuration values
|
||||||
- `Editor()` - Get editor from environment or defaults (package-level function)
|
- `Editor()` - Get editor from environment or defaults (package-level function)
|
||||||
- `Pager()` - Get pager from environment or defaults (package-level function)
|
- `Pager()` - Get pager from environment or defaults (package-level function)
|
||||||
@@ -107,7 +107,7 @@ Key functions:
|
|||||||
Represents a directory containing cheatsheets:
|
Represents a directory containing cheatsheets:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Cheatpath struct {
|
type Path struct {
|
||||||
Name string // Friendly name (e.g., "personal")
|
Name string // Friendly name (e.g., "personal")
|
||||||
Path string // Filesystem path
|
Path string // Filesystem path
|
||||||
Tags []string // Tags applied to all sheets in this path
|
Tags []string // Tags applied to all sheets in this path
|
||||||
@@ -202,7 +202,7 @@ go test ./... # Go test directly
|
|||||||
Test files follow Go conventions:
|
Test files follow Go conventions:
|
||||||
- `*_test.go` files in same package
|
- `*_test.go` files in same package
|
||||||
- Table-driven tests for multiple scenarios
|
- Table-driven tests for multiple scenarios
|
||||||
- Mock data in `internal/mock` package
|
- Mock data in `mocks` package
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
|
|||||||
17
Makefile
17
Makefile
@@ -44,6 +44,7 @@ releases := \
|
|||||||
$(dist_dir)/cheat-linux-arm7 \
|
$(dist_dir)/cheat-linux-arm7 \
|
||||||
$(dist_dir)/cheat-netbsd-amd64 \
|
$(dist_dir)/cheat-netbsd-amd64 \
|
||||||
$(dist_dir)/cheat-openbsd-amd64 \
|
$(dist_dir)/cheat-openbsd-amd64 \
|
||||||
|
$(dist_dir)/cheat-plan9-amd64 \
|
||||||
$(dist_dir)/cheat-solaris-amd64 \
|
$(dist_dir)/cheat-solaris-amd64 \
|
||||||
$(dist_dir)/cheat-windows-amd64.exe
|
$(dist_dir)/cheat-windows-amd64.exe
|
||||||
|
|
||||||
@@ -213,12 +214,12 @@ test-all: test test-integration
|
|||||||
## test-fuzz: run quick fuzz tests for security-critical functions
|
## test-fuzz: run quick fuzz tests for security-critical functions
|
||||||
.PHONY: test-fuzz
|
.PHONY: test-fuzz
|
||||||
test-fuzz:
|
test-fuzz:
|
||||||
@./build/fuzz.sh 15s
|
@./test/fuzz.sh 15s
|
||||||
|
|
||||||
## test-fuzz-long: run extended fuzz tests (10 minutes each)
|
## test-fuzz-long: run extended fuzz tests (10 minutes each)
|
||||||
.PHONY: test-fuzz-long
|
.PHONY: test-fuzz-long
|
||||||
test-fuzz-long:
|
test-fuzz-long:
|
||||||
@./build/fuzz.sh 10m
|
@./test/fuzz.sh 10m
|
||||||
|
|
||||||
## coverage: generate a test coverage report
|
## coverage: generate a test coverage report
|
||||||
.PHONY: coverage
|
.PHONY: coverage
|
||||||
@@ -240,22 +241,22 @@ coverage-text: .tmp
|
|||||||
## benchmark: run performance benchmarks
|
## benchmark: run performance benchmarks
|
||||||
.PHONY: benchmark
|
.PHONY: benchmark
|
||||||
benchmark: .tmp
|
benchmark: .tmp
|
||||||
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem ./cmd/cheat | tee .tmp/benchmark-latest.txt && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem ./test/integration | tee .tmp/benchmark-latest.txt && \
|
||||||
$(RM) -f cheat.test
|
$(RM) -f integration.test
|
||||||
|
|
||||||
## benchmark-cpu: run benchmarks with CPU profiling
|
## benchmark-cpu: run benchmarks with CPU profiling
|
||||||
.PHONY: benchmark-cpu
|
.PHONY: benchmark-cpu
|
||||||
benchmark-cpu: .tmp
|
benchmark-cpu: .tmp
|
||||||
$(GO) test -tags=integration -bench=. -benchtime=10s -cpuprofile=.tmp/cpu.prof ./cmd/cheat && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -cpuprofile=.tmp/cpu.prof ./test/integration && \
|
||||||
$(RM) -f cheat.test && \
|
$(RM) -f integration.test && \
|
||||||
echo "CPU profile saved to .tmp/cpu.prof" && \
|
echo "CPU profile saved to .tmp/cpu.prof" && \
|
||||||
echo "View with: go tool pprof -http=:8080 .tmp/cpu.prof"
|
echo "View with: go tool pprof -http=:8080 .tmp/cpu.prof"
|
||||||
|
|
||||||
## benchmark-mem: run benchmarks with memory profiling
|
## benchmark-mem: run benchmarks with memory profiling
|
||||||
.PHONY: benchmark-mem
|
.PHONY: benchmark-mem
|
||||||
benchmark-mem: .tmp
|
benchmark-mem: .tmp
|
||||||
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem -memprofile=.tmp/mem.prof ./cmd/cheat && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem -memprofile=.tmp/mem.prof ./test/integration && \
|
||||||
$(RM) -f cheat.test && \
|
$(RM) -f integration.test && \
|
||||||
echo "Memory profile saved to .tmp/mem.prof" && \
|
echo "Memory profile saved to .tmp/mem.prof" && \
|
||||||
echo "View with: go tool pprof -http=:8080 .tmp/mem.prof"
|
echo "View with: go tool pprof -http=:8080 .tmp/mem.prof"
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ The validation is performed at the application layer before any file operations
|
|||||||
|
|
||||||
### Validation Function
|
### Validation Function
|
||||||
|
|
||||||
The validation is implemented in `internal/cheatpath/validate.go`:
|
The validation is implemented in `internal/sheet/validate.go`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func ValidateSheetName(name string) error {
|
func Validate(name string) error {
|
||||||
// Reject empty names
|
// Reject empty names
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("cheatsheet name cannot be empty")
|
return fmt.Errorf("cheatsheet name cannot be empty")
|
||||||
@@ -133,7 +133,7 @@ The following patterns are explicitly allowed:
|
|||||||
|
|
||||||
Comprehensive tests ensure the validation works correctly:
|
Comprehensive tests ensure the validation works correctly:
|
||||||
|
|
||||||
1. **Unit tests** (`internal/cheatpath/validate_test.go`) verify the validation logic
|
1. **Unit tests** (`internal/sheet/validate_test.go`) verify the validation logic
|
||||||
2. **Integration tests** verify the actual binary blocks malicious inputs
|
2. **Integration tests** verify the actual binary blocks malicious inputs
|
||||||
3. **No system files are accessed** during testing - all tests use isolated directories
|
3. **No system files are accessed** during testing - all tests use isolated directories
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
"github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
"github.com/cheat/cheat/internal/sheet"
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
"github.com/cheat/cheat/internal/sheets"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
|||||||
cheatsheet := opts["--edit"].(string)
|
cheatsheet := opts["--edit"].(string)
|
||||||
|
|
||||||
// validate the cheatsheet name
|
// validate the cheatsheet name
|
||||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
if err := sheet.Validate(cheatsheet); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -29,8 +30,6 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatcheats by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
@@ -52,55 +51,36 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
|||||||
// if the sheet exists and is not read-only, edit it in place
|
// if the sheet exists and is not read-only, edit it in place
|
||||||
if ok && !sheet.ReadOnly {
|
if ok && !sheet.ReadOnly {
|
||||||
editpath = sheet.Path
|
editpath = sheet.Path
|
||||||
|
} else {
|
||||||
// if the sheet exists but is read-only, copy it before editing
|
// for read-only or non-existent sheets, resolve a writeable path
|
||||||
} else if ok && sheet.ReadOnly {
|
|
||||||
// compute the new edit path
|
|
||||||
// begin by getting a writeable cheatpath
|
|
||||||
writepath, err := cheatpath.Writeable(conf.Cheatpaths)
|
writepath, err := cheatpath.Writeable(conf.Cheatpaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to get writeable path: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to get writeable path: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute the new edit path
|
// use the existing title for read-only copies, the requested name otherwise
|
||||||
editpath = filepath.Join(writepath.Path, sheet.Title)
|
title := cheatsheet
|
||||||
|
if ok {
|
||||||
|
title = sheet.Title
|
||||||
|
}
|
||||||
|
editpath = filepath.Join(writepath.Path, title)
|
||||||
|
|
||||||
// create any necessary subdirectories
|
if ok {
|
||||||
dirs := filepath.Dir(editpath)
|
// copy the read-only sheet to the writeable path
|
||||||
if dirs != "." {
|
// (Copy handles MkdirAll internally)
|
||||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
if err := sheet.Copy(editpath); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to create directory: %s, %v\n", dirs, err)
|
fmt.Fprintf(os.Stderr, "failed to copy cheatsheet: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// create any necessary subdirectories for the new sheet
|
||||||
// copy the sheet to the new edit path
|
dirs := filepath.Dir(editpath)
|
||||||
err = sheet.Copy(editpath)
|
if dirs != "." {
|
||||||
if err != nil {
|
if err := os.MkdirAll(dirs, 0755); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to copy cheatsheet: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to create directory: %s, %v\n", dirs, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the sheet does not exist, create it
|
|
||||||
} else {
|
|
||||||
// compute the new edit path
|
|
||||||
// begin by getting a writeable cheatpath
|
|
||||||
writepath, err := cheatpath.Writeable(conf.Cheatpaths)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to get writeable path: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute the new edit path
|
|
||||||
editpath = filepath.Join(writepath.Path, cheatsheet)
|
|
||||||
|
|
||||||
// create any necessary subdirectories
|
|
||||||
dirs := filepath.Dir(editpath)
|
|
||||||
if dirs != "." {
|
|
||||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to create directory: %s, %v\n", dirs, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,78 +3,27 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
"github.com/cheat/cheat/internal/installer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdInit displays an example config file.
|
// cmdInit displays an example config file.
|
||||||
func cmdInit() {
|
func cmdInit(home string, envvars map[string]string) {
|
||||||
|
|
||||||
// get the user's home directory
|
// identify the os-specific paths at which configs may be located
|
||||||
home, err := homedir.Dir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to get user home directory: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read the envvars into a map of strings
|
|
||||||
envvars := map[string]string{}
|
|
||||||
for _, e := range os.Environ() {
|
|
||||||
pair := strings.SplitN(e, "=", 2)
|
|
||||||
envvars[pair[0]] = pair[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the config template
|
|
||||||
configs := configs()
|
|
||||||
|
|
||||||
// identify the os-specifc paths at which configs may be located
|
|
||||||
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to read config paths: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to read config paths: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine the appropriate paths for config data and (optional) community
|
|
||||||
// cheatsheets based on the user's platform
|
|
||||||
confpath := confpaths[0]
|
confpath := confpaths[0]
|
||||||
confdir := filepath.Dir(confpath)
|
|
||||||
|
|
||||||
// create paths for community, personal, and work cheatsheets
|
// expand template placeholders and comment out community cheatpath
|
||||||
community := filepath.Join(confdir, "cheatsheets", "community")
|
configs := installer.ExpandTemplate(configs(), confpath)
|
||||||
personal := filepath.Join(confdir, "cheatsheets", "personal")
|
configs = installer.CommentCommunity(configs, confpath)
|
||||||
work := filepath.Join(confdir, "cheatsheets", "work")
|
|
||||||
|
|
||||||
// template the above paths into the default configs
|
|
||||||
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
|
|
||||||
configs = strings.Replace(configs, "PERSONAL_PATH", personal, -1)
|
|
||||||
configs = strings.Replace(configs, "WORK_PATH", work, -1)
|
|
||||||
|
|
||||||
// locate and set a default pager
|
|
||||||
configs = strings.Replace(configs, "PAGER_PATH", config.Pager(), -1)
|
|
||||||
|
|
||||||
// locate and set a default editor
|
|
||||||
if editor, err := config.Editor(); err == nil {
|
|
||||||
configs = strings.Replace(configs, "EDITOR_PATH", editor, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// comment out the community cheatpath by default, since the directory
|
|
||||||
// won't exist until the user clones it
|
|
||||||
configs = strings.Replace(configs,
|
|
||||||
" - name: community\n"+
|
|
||||||
" path: "+community+"\n"+
|
|
||||||
" tags: [ community ]\n"+
|
|
||||||
" readonly: true",
|
|
||||||
" #- name: community\n"+
|
|
||||||
" # path: "+community+"\n"+
|
|
||||||
" # tags: [ community ]\n"+
|
|
||||||
" # readonly: true",
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
|
|
||||||
// output the templated configs
|
// output the templated configs
|
||||||
fmt.Println(configs)
|
fmt.Println(configs)
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatsheets by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
"github.com/cheat/cheat/internal/sheet"
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
"github.com/cheat/cheat/internal/sheets"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
|||||||
cheatsheet := opts["--rm"].(string)
|
cheatsheet := opts["--rm"].(string)
|
||||||
|
|
||||||
// validate the cheatsheet name
|
// validate the cheatsheet name
|
||||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
if err := sheet.Validate(cheatsheet); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -27,8 +27,6 @@ func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatcheats by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatcheats by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
@@ -80,7 +78,7 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
|||||||
// append the cheatsheet title
|
// append the cheatsheet title
|
||||||
sheet.Title,
|
sheet.Title,
|
||||||
// append the cheatsheet path
|
// append the cheatsheet path
|
||||||
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf.Color(opts)),
|
||||||
// indent each line of content
|
// indent each line of content
|
||||||
display.Indent(sheet.Text),
|
display.Indent(sheet.Text),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
|||||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter cheatcheats by tag if --tag was provided
|
|
||||||
if opts["--tag"] != nil {
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
@@ -42,7 +40,7 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
|||||||
// identify the matching cheatsheet
|
// identify the matching cheatsheet
|
||||||
out += fmt.Sprintf("%s %s\n",
|
out += fmt.Sprintf("%s %s\n",
|
||||||
sheet.Title,
|
sheet.Title,
|
||||||
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf.Color(opts)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// apply colorization if requested
|
// apply colorization if requested
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/cheat/cheat/internal/installer"
|
"github.com/cheat/cheat/internal/installer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "4.7.0"
|
const version = "4.7.1"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
@@ -26,13 +26,6 @@ func main() {
|
|||||||
panic(fmt.Errorf("docopt failed to parse: %v", err))
|
panic(fmt.Errorf("docopt failed to parse: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if --init was passed, we don't want to attempt to load a config file.
|
|
||||||
// Instead, just execute cmd_init and exit
|
|
||||||
if opts["--init"] != nil && opts["--init"] == true {
|
|
||||||
cmdInit()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the user's home directory
|
// get the user's home directory
|
||||||
home, err := homedir.Dir()
|
home, err := homedir.Dir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,6 +44,13 @@ func main() {
|
|||||||
envvars[pair[0]] = pair[1]
|
envvars[pair[0]] = pair[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if --init was passed, we don't want to attempt to load a config file.
|
||||||
|
// Instead, just execute cmd_init and exit
|
||||||
|
if opts["--init"] == true {
|
||||||
|
cmdInit(home, envvars)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// identify the os-specifc paths at which configs may be located
|
// identify the os-specifc paths at which configs may be located
|
||||||
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,7 +92,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initialize the configs
|
// initialize the configs
|
||||||
conf, err := config.New(opts, confpath, true)
|
conf, err := config.New(confpath, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -2,27 +2,10 @@
|
|||||||
// management.
|
// management.
|
||||||
package cheatpath
|
package cheatpath
|
||||||
|
|
||||||
import "fmt"
|
// Path encapsulates cheatsheet path information
|
||||||
|
type Path struct {
|
||||||
// Cheatpath encapsulates cheatsheet path information
|
|
||||||
type Cheatpath struct {
|
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
ReadOnly bool `yaml:"readonly"`
|
ReadOnly bool `yaml:"readonly"`
|
||||||
Tags []string `yaml:"tags"`
|
Tags []string `yaml:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate ensures that the Cheatpath is valid
|
|
||||||
func (c Cheatpath) Validate() error {
|
|
||||||
// Check that name is not empty
|
|
||||||
if c.Name == "" {
|
|
||||||
return fmt.Errorf("cheatpath name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that path is not empty
|
|
||||||
if c.Path == "" {
|
|
||||||
return fmt.Errorf("cheatpath path cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import (
|
|||||||
func TestCheatpathValidate(t *testing.T) {
|
func TestCheatpathValidate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cheatpath Cheatpath
|
cheatpath Path
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errMsg string
|
errMsg string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid cheatpath",
|
name: "valid cheatpath",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "personal",
|
Name: "personal",
|
||||||
Path: "/home/user/.config/cheat/personal",
|
Path: "/home/user/.config/cheat/personal",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -24,7 +24,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty name",
|
name: "empty name",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "",
|
Name: "",
|
||||||
Path: "/home/user/.config/cheat/personal",
|
Path: "/home/user/.config/cheat/personal",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -35,7 +35,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty path",
|
name: "empty path",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "personal",
|
Name: "personal",
|
||||||
Path: "",
|
Path: "",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -46,7 +46,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "both empty",
|
name: "both empty",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "",
|
Name: "",
|
||||||
Path: "",
|
Path: "",
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
@@ -57,7 +57,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "minimal valid",
|
name: "minimal valid",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "x",
|
Name: "x",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
},
|
},
|
||||||
@@ -65,7 +65,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with readonly and tags",
|
name: "with readonly and tags",
|
||||||
cheatpath: Cheatpath{
|
cheatpath: Path{
|
||||||
Name: "community",
|
Name: "community",
|
||||||
Path: "/usr/share/cheat",
|
Path: "/usr/share/cheat",
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
// Package cheatpath manages collections of cheat sheets organized in filesystem directories.
|
|
||||||
//
|
|
||||||
// A Cheatpath represents a directory containing cheat sheets, with associated
|
|
||||||
// metadata such as tags and read-only status. Multiple cheatpaths can be
|
|
||||||
// configured to organize sheets from different sources (personal, community, work, etc.).
|
|
||||||
//
|
|
||||||
// # Cheatpath Structure
|
|
||||||
//
|
|
||||||
// Each cheatpath has:
|
|
||||||
// - Name: A friendly identifier (e.g., "personal", "community")
|
|
||||||
// - Path: The filesystem path to the directory
|
|
||||||
// - Tags: Tags automatically applied to all sheets in this path
|
|
||||||
// - ReadOnly: Whether sheets in this path can be modified
|
|
||||||
//
|
|
||||||
// Example configuration:
|
|
||||||
//
|
|
||||||
// cheatpaths:
|
|
||||||
// - name: personal
|
|
||||||
// path: ~/cheat
|
|
||||||
// tags: []
|
|
||||||
// readonly: false
|
|
||||||
// - name: community
|
|
||||||
// path: ~/cheat/community
|
|
||||||
// tags: [community]
|
|
||||||
// readonly: true
|
|
||||||
//
|
|
||||||
// # Directory-Scoped Cheatpaths
|
|
||||||
//
|
|
||||||
// The package supports directory-scoped cheatpaths via `.cheat` directories.
|
|
||||||
// When running cheat, the tool walks upward from the current working directory
|
|
||||||
// to the filesystem root, stopping at the first `.cheat` directory found. That
|
|
||||||
// directory is temporarily added to the available cheatpaths.
|
|
||||||
//
|
|
||||||
// # Precedence and Overrides
|
|
||||||
//
|
|
||||||
// When multiple cheatpaths contain a sheet with the same name, the sheet
|
|
||||||
// from the most "local" cheatpath takes precedence. This allows users to
|
|
||||||
// override community sheets with personal versions.
|
|
||||||
//
|
|
||||||
// Key Functions
|
|
||||||
//
|
|
||||||
// - Filter: Filters cheatpaths by name
|
|
||||||
// - Validate: Ensures cheatpath configuration is valid
|
|
||||||
// - Writeable: Returns the first writeable cheatpath
|
|
||||||
//
|
|
||||||
// Example Usage
|
|
||||||
//
|
|
||||||
// // Filter cheatpaths to only "personal"
|
|
||||||
// filtered, err := cheatpath.Filter(paths, "personal")
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Find a writeable cheatpath
|
|
||||||
// writeable, err := cheatpath.Writeable(paths)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Validate cheatpath configuration
|
|
||||||
// if err := cheatpath.Validate(paths); err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
package cheatpath
|
|
||||||
@@ -5,15 +5,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Filter filters all cheatpaths that are not named `name`
|
// Filter filters all cheatpaths that are not named `name`
|
||||||
func Filter(paths []Cheatpath, name string) ([]Cheatpath, error) {
|
func Filter(paths []Path, name string) ([]Path, error) {
|
||||||
|
|
||||||
// if a path of the given name exists, return it
|
// if a path of the given name exists, return it
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
if path.Name == name {
|
if path.Name == name {
|
||||||
return []Cheatpath{path}, nil
|
return []Path{path}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, return an error
|
// otherwise, return an error
|
||||||
return []Cheatpath{}, fmt.Errorf("cheatpath does not exist: %s", name)
|
return []Path{}, fmt.Errorf("cheatpath does not exist: %s", name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
func TestFilterSuccess(t *testing.T) {
|
func TestFilterSuccess(t *testing.T) {
|
||||||
|
|
||||||
// init cheatpaths
|
// init cheatpaths
|
||||||
paths := []Cheatpath{
|
paths := []Path{
|
||||||
Cheatpath{Name: "foo"},
|
Path{Name: "foo"},
|
||||||
Cheatpath{Name: "bar"},
|
Path{Name: "bar"},
|
||||||
Cheatpath{Name: "baz"},
|
Path{Name: "baz"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter the paths
|
// filter the paths
|
||||||
@@ -39,10 +39,10 @@ func TestFilterSuccess(t *testing.T) {
|
|||||||
func TestFilterFailure(t *testing.T) {
|
func TestFilterFailure(t *testing.T) {
|
||||||
|
|
||||||
// init cheatpaths
|
// init cheatpaths
|
||||||
paths := []Cheatpath{
|
paths := []Path{
|
||||||
Cheatpath{Name: "foo"},
|
Path{Name: "foo"},
|
||||||
Cheatpath{Name: "bar"},
|
Path{Name: "bar"},
|
||||||
Cheatpath{Name: "baz"},
|
Path{Name: "baz"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter the paths
|
// filter the paths
|
||||||
|
|||||||
@@ -2,39 +2,15 @@ package cheatpath
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidateSheetName ensures that a cheatsheet name does not contain
|
// Validate ensures that the Path is valid
|
||||||
// directory traversal sequences or other potentially dangerous patterns.
|
func (c Path) Validate() error {
|
||||||
func ValidateSheetName(name string) error {
|
if c.Name == "" {
|
||||||
// Reject empty names
|
return fmt.Errorf("cheatpath name cannot be empty")
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot be empty")
|
|
||||||
}
|
}
|
||||||
|
if c.Path == "" {
|
||||||
// Reject names containing directory traversal
|
return fmt.Errorf("cheatpath path cannot be empty")
|
||||||
if strings.Contains(name, "..") {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot contain '..'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject absolute paths
|
|
||||||
if filepath.IsAbs(name) {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot be an absolute path")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject names that start with ~ (home directory expansion)
|
|
||||||
if strings.HasPrefix(name, "~") {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot start with '~'")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject hidden files (files that start with a dot)
|
|
||||||
// We don't display hidden files, so we shouldn't create them
|
|
||||||
filename := filepath.Base(name)
|
|
||||||
if strings.HasPrefix(filename, ".") {
|
|
||||||
return fmt.Errorf("cheatsheet name cannot start with '.' (hidden files are not supported)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Writeable returns a writeable Cheatpath
|
// Writeable returns a writeable Path
|
||||||
func Writeable(cheatpaths []Cheatpath) (Cheatpath, error) {
|
func Writeable(cheatpaths []Path) (Path, error) {
|
||||||
|
|
||||||
// iterate backwards over the cheatpaths
|
// iterate backwards over the cheatpaths
|
||||||
// NB: we're going backwards because we assume that the most "local"
|
// NB: we're going backwards because we assume that the most "local"
|
||||||
@@ -18,5 +18,5 @@ func Writeable(cheatpaths []Cheatpath) (Cheatpath, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, return an error
|
// otherwise, return an error
|
||||||
return Cheatpath{}, fmt.Errorf("no writeable cheatpaths found")
|
return Path{}, fmt.Errorf("no writeable cheatpaths found")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
func TestWriteableOK(t *testing.T) {
|
func TestWriteableOK(t *testing.T) {
|
||||||
|
|
||||||
// initialize some cheatpaths
|
// initialize some cheatpaths
|
||||||
cheatpaths := []Cheatpath{
|
cheatpaths := []Path{
|
||||||
Cheatpath{Path: "/foo", ReadOnly: true},
|
Path{Path: "/foo", ReadOnly: true},
|
||||||
Cheatpath{Path: "/bar", ReadOnly: false},
|
Path{Path: "/bar", ReadOnly: false},
|
||||||
Cheatpath{Path: "/baz", ReadOnly: true},
|
Path{Path: "/baz", ReadOnly: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the writeable cheatpath
|
// get the writeable cheatpath
|
||||||
@@ -34,10 +34,10 @@ func TestWriteableOK(t *testing.T) {
|
|||||||
func TestWriteableNotOK(t *testing.T) {
|
func TestWriteableNotOK(t *testing.T) {
|
||||||
|
|
||||||
// initialize some cheatpaths
|
// initialize some cheatpaths
|
||||||
cheatpaths := []Cheatpath{
|
cheatpaths := []Path{
|
||||||
Cheatpath{Path: "/foo", ReadOnly: true},
|
Path{Path: "/foo", ReadOnly: true},
|
||||||
Cheatpath{Path: "/bar", ReadOnly: true},
|
Path{Path: "/bar", ReadOnly: true},
|
||||||
Cheatpath{Path: "/baz", ReadOnly: true},
|
Path{Path: "/baz", ReadOnly: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the writeable cheatpath
|
// get the writeable cheatpath
|
||||||
|
|||||||
@@ -2,158 +2,16 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
cp "github.com/cheat/cheat/internal/cheatpath"
|
cp "github.com/cheat/cheat/internal/cheatpath"
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config encapsulates configuration parameters
|
// Config encapsulates configuration parameters
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Colorize bool `yaml:"colorize"`
|
Colorize bool `yaml:"colorize"`
|
||||||
Editor string `yaml:"editor"`
|
Editor string `yaml:"editor"`
|
||||||
Cheatpaths []cp.Cheatpath `yaml:"cheatpaths"`
|
Cheatpaths []cp.Path `yaml:"cheatpaths"`
|
||||||
Style string `yaml:"style"`
|
Style string `yaml:"style"`
|
||||||
Formatter string `yaml:"formatter"`
|
Formatter string `yaml:"formatter"`
|
||||||
Pager string `yaml:"pager"`
|
Pager string `yaml:"pager"`
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Config struct
|
|
||||||
func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error) {
|
|
||||||
|
|
||||||
// read the config file
|
|
||||||
buf, err := os.ReadFile(confPath)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("could not read config file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize a config object
|
|
||||||
conf := Config{}
|
|
||||||
|
|
||||||
// store the config path
|
|
||||||
conf.Path = confPath
|
|
||||||
|
|
||||||
// unmarshal the yaml
|
|
||||||
err = yaml.Unmarshal(buf, &conf)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("could not unmarshal yaml: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if a .cheat directory exists in the current directory or any ancestor,
|
|
||||||
// append it to the cheatpaths
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("failed to get cwd: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if local := findLocalCheatpath(cwd); local != "" {
|
|
||||||
path := cp.Cheatpath{
|
|
||||||
Name: "cwd",
|
|
||||||
Path: local,
|
|
||||||
ReadOnly: false,
|
|
||||||
Tags: []string{},
|
|
||||||
}
|
|
||||||
conf.Cheatpaths = append(conf.Cheatpaths, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// process cheatpaths
|
|
||||||
var validPaths []cp.Cheatpath
|
|
||||||
for _, cheatpath := range conf.Cheatpaths {
|
|
||||||
|
|
||||||
// expand ~ in config paths
|
|
||||||
expanded, err := homedir.Expand(cheatpath.Path)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("failed to expand ~: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// follow symlinks
|
|
||||||
//
|
|
||||||
// NB: `resolve` is an ugly kludge that exists for the sake of unit-tests.
|
|
||||||
// It's necessary because `EvalSymlinks` will error if the symlink points
|
|
||||||
// to a non-existent location on the filesystem. When unit-testing,
|
|
||||||
// however, we don't want to have dependencies on the filesystem. As such,
|
|
||||||
// `resolve` is a switch that allows us to turn off symlink resolution when
|
|
||||||
// running the config tests.
|
|
||||||
if resolve {
|
|
||||||
evaled, err := filepath.EvalSymlinks(expanded)
|
|
||||||
if err != nil {
|
|
||||||
// if the path simply doesn't exist, warn and skip it
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
fmt.Fprintf(os.Stderr,
|
|
||||||
"WARNING: cheatpath '%s' does not exist, skipping\n",
|
|
||||||
expanded,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return Config{}, fmt.Errorf(
|
|
||||||
"failed to resolve symlink: %s: %v",
|
|
||||||
expanded,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
expanded = evaled
|
|
||||||
}
|
|
||||||
|
|
||||||
cheatpath.Path = expanded
|
|
||||||
validPaths = append(validPaths, cheatpath)
|
|
||||||
}
|
|
||||||
conf.Cheatpaths = validPaths
|
|
||||||
|
|
||||||
// determine the editor: env vars override the config file value,
|
|
||||||
// following standard Unix convention (see #589)
|
|
||||||
if v := os.Getenv("VISUAL"); v != "" {
|
|
||||||
conf.Editor = v
|
|
||||||
} else if v := os.Getenv("EDITOR"); v != "" {
|
|
||||||
conf.Editor = v
|
|
||||||
} else {
|
|
||||||
conf.Editor = strings.TrimSpace(conf.Editor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if an editor was still not determined, attempt to choose one
|
|
||||||
// that's appropriate for the environment
|
|
||||||
if conf.Editor == "" {
|
|
||||||
if conf.Editor, err = Editor(); err != nil {
|
|
||||||
return Config{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = "terminal"
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the pager
|
|
||||||
conf.Pager = strings.TrimSpace(conf.Pager)
|
|
||||||
|
|
||||||
return conf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// findLocalCheatpath walks upward from dir looking for a .cheat directory.
|
|
||||||
// It returns the path to the first .cheat directory found, or an empty string
|
|
||||||
// if none exists. This mirrors the discovery pattern used by git for .git
|
|
||||||
// directories.
|
|
||||||
func findLocalCheatpath(dir string) string {
|
|
||||||
for {
|
|
||||||
candidate := filepath.Join(dir, ".cheat")
|
|
||||||
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
parent := filepath.Dir(dir)
|
|
||||||
if parent == dir {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
dir = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestConfigYAMLErrors tests YAML parsing errors
|
// TestConfigYAMLErrors tests YAML parsing errors
|
||||||
@@ -24,7 +24,7 @@ func TestConfigYAMLErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to load invalid YAML
|
// Attempt to load invalid YAML
|
||||||
_, err = New(map[string]interface{}{}, invalidYAML, false)
|
_, err = New(invalidYAML, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for invalid YAML, got nil")
|
t.Error("expected error for invalid YAML, got nil")
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func TestConfigYAMLErrors(t *testing.T) {
|
|||||||
// TestConfigDefaults tests default values
|
// TestConfigDefaults tests default values
|
||||||
func TestConfigDefaults(t *testing.T) {
|
func TestConfigDefaults(t *testing.T) {
|
||||||
// Load empty config
|
// Load empty config
|
||||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
conf, err := New(mocks.Path("conf/empty.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to load config: %v", err)
|
t.Errorf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ cheatpaths:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load config with symlink resolution
|
// Load config with symlink resolution
|
||||||
conf, err := New(map[string]interface{}{}, configFile, true)
|
conf, err := New(configFile, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to load config: %v", err)
|
t.Errorf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ cheatpaths:
|
|||||||
|
|
||||||
// Load config with symlink resolution should skip the broken cheatpath
|
// Load config with symlink resolution should skip the broken cheatpath
|
||||||
// (warn to stderr) rather than hard-error
|
// (warn to stderr) rather than hard-error
|
||||||
conf, err := New(map[string]interface{}{}, configFile, true)
|
conf, err := New(configFile, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error for broken symlink (should skip), got: %v", err)
|
t.Errorf("expected no error for broken symlink (should skip), got: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
"github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestFindLocalCheatpathInCurrentDir tests that .cheat in the given dir is found
|
// TestFindLocalCheatpathInCurrentDir tests that .cheat in the given dir is found
|
||||||
@@ -286,7 +286,7 @@ func TestConfigSuccessful(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// initialize a config
|
// initialize a config
|
||||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/conf.yml"), false)
|
conf, err := New(mocks.Path("conf/conf.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to parse config file: %v", err)
|
t.Errorf("failed to parse config file: %v", err)
|
||||||
}
|
}
|
||||||
@@ -306,18 +306,18 @@ func TestConfigSuccessful(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// assert that the cheatpaths are correct
|
// assert that the cheatpaths are correct
|
||||||
want := []cheatpath.Cheatpath{
|
want := []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Path: filepath.Join(home, ".dotfiles", "cheat", "community"),
|
Path: filepath.Join(home, ".dotfiles", "cheat", "community"),
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
Tags: []string{"community"},
|
Tags: []string{"community"},
|
||||||
},
|
},
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Path: filepath.Join(home, ".dotfiles", "cheat", "work"),
|
Path: filepath.Join(home, ".dotfiles", "cheat", "work"),
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{"work"},
|
Tags: []string{"work"},
|
||||||
},
|
},
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Path: filepath.Join(home, ".dotfiles", "cheat", "personal"),
|
Path: filepath.Join(home, ".dotfiles", "cheat", "personal"),
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{"personal"},
|
Tags: []string{"personal"},
|
||||||
@@ -338,7 +338,7 @@ func TestConfigSuccessful(t *testing.T) {
|
|||||||
func TestConfigFailure(t *testing.T) {
|
func TestConfigFailure(t *testing.T) {
|
||||||
|
|
||||||
// attempt to read a non-existent config file
|
// attempt to read a non-existent config file
|
||||||
_, err := New(map[string]interface{}{}, "/does-not-exit", false)
|
_, err := New("/does-not-exit", false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("failed to error on unreadable config")
|
t.Errorf("failed to error on unreadable config")
|
||||||
}
|
}
|
||||||
@@ -358,7 +358,7 @@ func TestEditorEnvOverride(t *testing.T) {
|
|||||||
// with no env vars, the config file value should be used
|
// with no env vars, the config file value should be used
|
||||||
os.Unsetenv("VISUAL")
|
os.Unsetenv("VISUAL")
|
||||||
os.Unsetenv("EDITOR")
|
os.Unsetenv("EDITOR")
|
||||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/conf.yml"), false)
|
conf, err := New(mocks.Path("conf/conf.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to init configs: %v", err)
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
}
|
}
|
||||||
@@ -368,7 +368,7 @@ func TestEditorEnvOverride(t *testing.T) {
|
|||||||
|
|
||||||
// $EDITOR should override the config file value
|
// $EDITOR should override the config file value
|
||||||
os.Setenv("EDITOR", "nano")
|
os.Setenv("EDITOR", "nano")
|
||||||
conf, err = New(map[string]interface{}{}, mock.Path("conf/conf.yml"), false)
|
conf, err = New(mocks.Path("conf/conf.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to init configs: %v", err)
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
}
|
}
|
||||||
@@ -378,7 +378,7 @@ func TestEditorEnvOverride(t *testing.T) {
|
|||||||
|
|
||||||
// $VISUAL should override both $EDITOR and the config file value
|
// $VISUAL should override both $EDITOR and the config file value
|
||||||
os.Setenv("VISUAL", "emacs")
|
os.Setenv("VISUAL", "emacs")
|
||||||
conf, err = New(map[string]interface{}{}, mock.Path("conf/conf.yml"), false)
|
conf, err = New(mocks.Path("conf/conf.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to init configs: %v", err)
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
}
|
}
|
||||||
@@ -401,7 +401,7 @@ func TestEditorEnvFallback(t *testing.T) {
|
|||||||
// set $EDITOR and assert it's used when config has no editor
|
// set $EDITOR and assert it's used when config has no editor
|
||||||
os.Unsetenv("VISUAL")
|
os.Unsetenv("VISUAL")
|
||||||
os.Setenv("EDITOR", "foo")
|
os.Setenv("EDITOR", "foo")
|
||||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
conf, err := New(mocks.Path("conf/empty.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to init configs: %v", err)
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
}
|
}
|
||||||
@@ -411,7 +411,7 @@ func TestEditorEnvFallback(t *testing.T) {
|
|||||||
|
|
||||||
// set $VISUAL and assert it takes precedence over $EDITOR
|
// set $VISUAL and assert it takes precedence over $EDITOR
|
||||||
os.Setenv("VISUAL", "bar")
|
os.Setenv("VISUAL", "bar")
|
||||||
conf, err = New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
conf, err = New(mocks.Path("conf/empty.yml"), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to init configs: %v", err)
|
t.Fatalf("failed to init configs: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
// Package config manages application configuration and settings.
|
|
||||||
//
|
|
||||||
// The config package provides functionality to:
|
|
||||||
// - Load configuration from YAML files
|
|
||||||
// - Validate configuration values
|
|
||||||
// - Manage platform-specific configuration paths
|
|
||||||
// - Handle editor and pager settings
|
|
||||||
// - Configure colorization and formatting options
|
|
||||||
//
|
|
||||||
// # Configuration Structure
|
|
||||||
//
|
|
||||||
// The main configuration file (conf.yml) contains:
|
|
||||||
// - Editor preferences
|
|
||||||
// - Pager settings
|
|
||||||
// - Colorization options
|
|
||||||
// - Cheatpath definitions
|
|
||||||
// - Formatting preferences
|
|
||||||
//
|
|
||||||
// Example configuration:
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// editor: vim
|
|
||||||
// colorize: true
|
|
||||||
// style: monokai
|
|
||||||
// formatter: terminal256
|
|
||||||
// pager: less -FRX
|
|
||||||
// cheatpaths:
|
|
||||||
// - name: personal
|
|
||||||
// path: ~/cheat
|
|
||||||
// tags: []
|
|
||||||
// readonly: false
|
|
||||||
// - name: community
|
|
||||||
// path: ~/cheat/.cheat
|
|
||||||
// tags: [community]
|
|
||||||
// readonly: true
|
|
||||||
//
|
|
||||||
// # Platform-Specific Paths
|
|
||||||
//
|
|
||||||
// The package automatically detects configuration paths based on the operating system:
|
|
||||||
// - Linux/Unix: $XDG_CONFIG_HOME/cheat/conf.yml or ~/.config/cheat/conf.yml
|
|
||||||
// - macOS: ~/Library/Application Support/cheat/conf.yml
|
|
||||||
// - Windows: %APPDATA%\cheat\conf.yml
|
|
||||||
//
|
|
||||||
// # Environment Variables
|
|
||||||
//
|
|
||||||
// The following environment variables are respected:
|
|
||||||
// - CHEAT_CONFIG_PATH: Override the configuration file location
|
|
||||||
// - CHEAT_USE_FZF: Enable fzf integration when set to "true"
|
|
||||||
// - EDITOR: Default editor if not specified in config
|
|
||||||
// - VISUAL: Fallback editor if EDITOR is not set
|
|
||||||
// - PAGER: Default pager if not specified in config
|
|
||||||
package config
|
|
||||||
147
internal/config/new.go
Normal file
147
internal/config/new.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
cp "github.com/cheat/cheat/internal/cheatpath"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a new Config struct
|
||||||
|
func New(confPath string, resolve bool) (Config, error) {
|
||||||
|
|
||||||
|
// read the config file
|
||||||
|
buf, err := os.ReadFile(confPath)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("could not read config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize a config object
|
||||||
|
conf := Config{}
|
||||||
|
|
||||||
|
// store the config path
|
||||||
|
conf.Path = confPath
|
||||||
|
|
||||||
|
// unmarshal the yaml
|
||||||
|
err = yaml.Unmarshal(buf, &conf)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("could not unmarshal yaml: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a .cheat directory exists in the current directory or any ancestor,
|
||||||
|
// append it to the cheatpaths
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("failed to get cwd: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if local := findLocalCheatpath(cwd); local != "" {
|
||||||
|
path := cp.Path{
|
||||||
|
Name: "cwd",
|
||||||
|
Path: local,
|
||||||
|
ReadOnly: false,
|
||||||
|
Tags: []string{},
|
||||||
|
}
|
||||||
|
conf.Cheatpaths = append(conf.Cheatpaths, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process cheatpaths
|
||||||
|
var validPaths []cp.Path
|
||||||
|
for _, cheatpath := range conf.Cheatpaths {
|
||||||
|
|
||||||
|
// expand ~ in config paths
|
||||||
|
expanded, err := homedir.Expand(cheatpath.Path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("failed to expand ~: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// follow symlinks
|
||||||
|
//
|
||||||
|
// NB: `resolve` is an ugly kludge that exists for the sake of unit-tests.
|
||||||
|
// It's necessary because `EvalSymlinks` will error if the symlink points
|
||||||
|
// to a non-existent location on the filesystem. When unit-testing,
|
||||||
|
// however, we don't want to have dependencies on the filesystem. As such,
|
||||||
|
// `resolve` is a switch that allows us to turn off symlink resolution when
|
||||||
|
// running the config tests.
|
||||||
|
if resolve {
|
||||||
|
evaled, err := filepath.EvalSymlinks(expanded)
|
||||||
|
if err != nil {
|
||||||
|
// if the path simply doesn't exist, warn and skip it
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"WARNING: cheatpath '%s' does not exist, skipping\n",
|
||||||
|
expanded,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return Config{}, fmt.Errorf(
|
||||||
|
"failed to resolve symlink: %s: %v",
|
||||||
|
expanded,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded = evaled
|
||||||
|
}
|
||||||
|
|
||||||
|
cheatpath.Path = expanded
|
||||||
|
validPaths = append(validPaths, cheatpath)
|
||||||
|
}
|
||||||
|
conf.Cheatpaths = validPaths
|
||||||
|
|
||||||
|
// determine the editor: env vars override the config file value,
|
||||||
|
// following standard Unix convention (see #589)
|
||||||
|
if v := os.Getenv("VISUAL"); v != "" {
|
||||||
|
conf.Editor = v
|
||||||
|
} else if v := os.Getenv("EDITOR"); v != "" {
|
||||||
|
conf.Editor = v
|
||||||
|
} else {
|
||||||
|
conf.Editor = strings.TrimSpace(conf.Editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if an editor was still not determined, attempt to choose one
|
||||||
|
// that's appropriate for the environment
|
||||||
|
if conf.Editor == "" {
|
||||||
|
if conf.Editor, err = Editor(); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = "terminal"
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the pager
|
||||||
|
conf.Pager = strings.TrimSpace(conf.Pager)
|
||||||
|
|
||||||
|
return conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findLocalCheatpath walks upward from dir looking for a .cheat directory.
|
||||||
|
// It returns the path to the first .cheat directory found, or an empty string
|
||||||
|
// if none exists. This mirrors the discovery pattern used by git for .git
|
||||||
|
// directories.
|
||||||
|
func findLocalCheatpath(dir string) string {
|
||||||
|
for {
|
||||||
|
candidate := filepath.Join(dir, ".cheat")
|
||||||
|
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ cheatpaths:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the config
|
// Load the config
|
||||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
conf, err := New(configPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load config: %v", err)
|
t.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ cheatpaths:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the config
|
// Load the config
|
||||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
conf, err := New(configPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// It's OK if this fails due to no editor being found
|
// It's OK if this fails due to no editor being found
|
||||||
// The important thing is it doesn't panic
|
// The important thing is it doesn't panic
|
||||||
@@ -123,7 +123,7 @@ cheatpaths:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load the config
|
// Load the config
|
||||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
conf, err := New(configPath, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to load config: %v", err)
|
t.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ func TestValidateCorrect(t *testing.T) {
|
|||||||
Colorize: true,
|
Colorize: true,
|
||||||
Editor: "vim",
|
Editor: "vim",
|
||||||
Formatter: "terminal16m",
|
Formatter: "terminal16m",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -38,8 +38,8 @@ func TestInvalidateMissingEditor(t *testing.T) {
|
|||||||
conf := Config{
|
conf := Config{
|
||||||
Colorize: true,
|
Colorize: true,
|
||||||
Formatter: "terminal16m",
|
Formatter: "terminal16m",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -80,8 +80,8 @@ func TestInvalidateInvalidFormatter(t *testing.T) {
|
|||||||
Colorize: true,
|
Colorize: true,
|
||||||
Editor: "vim",
|
Editor: "vim",
|
||||||
Formatter: "html",
|
Formatter: "html",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -105,14 +105,14 @@ func TestInvalidateDuplicateCheatpathNames(t *testing.T) {
|
|||||||
Colorize: true,
|
Colorize: true,
|
||||||
Editor: "vim",
|
Editor: "vim",
|
||||||
Formatter: "terminal16m",
|
Formatter: "terminal16m",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
},
|
},
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/bar",
|
Path: "/bar",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -136,14 +136,14 @@ func TestInvalidateDuplicateCheatpathPaths(t *testing.T) {
|
|||||||
Colorize: true,
|
Colorize: true,
|
||||||
Editor: "vim",
|
Editor: "vim",
|
||||||
Formatter: "terminal16m",
|
Formatter: "terminal16m",
|
||||||
Cheatpaths: []cheatpath.Cheatpath{
|
Cheatpaths: []cheatpath.Path{
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
},
|
},
|
||||||
cheatpath.Cheatpath{
|
cheatpath.Path{
|
||||||
Name: "bar",
|
Name: "bar",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// Package display handles output formatting and presentation for the cheat application.
|
|
||||||
//
|
|
||||||
// The display package provides utilities for:
|
|
||||||
// - Writing output to stdout or a pager
|
|
||||||
// - Formatting text with indentation
|
|
||||||
// - Creating faint (dimmed) text for de-emphasis
|
|
||||||
// - Managing colored output
|
|
||||||
//
|
|
||||||
// # Pager Integration
|
|
||||||
//
|
|
||||||
// The package integrates with system pagers (less, more, etc.) to handle
|
|
||||||
// long output. If a pager is configured and the output is to a terminal,
|
|
||||||
// content is automatically piped through the pager.
|
|
||||||
//
|
|
||||||
// # Text Formatting
|
|
||||||
//
|
|
||||||
// Various formatting utilities are provided:
|
|
||||||
// - Faint: Creates dimmed text using ANSI escape codes
|
|
||||||
// - Indent: Adds consistent indentation to text blocks
|
|
||||||
// - Write: Intelligent output that uses stdout or pager as appropriate
|
|
||||||
//
|
|
||||||
// Example Usage
|
|
||||||
//
|
|
||||||
// // Write output, using pager if configured
|
|
||||||
// if err := display.Write(output, config); err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Create faint text for de-emphasis
|
|
||||||
// fainted := display.Faint("(read-only)", config)
|
|
||||||
//
|
|
||||||
// // Indent a block of text
|
|
||||||
// indented := display.Indent(text, " ")
|
|
||||||
//
|
|
||||||
// # Color Support
|
|
||||||
//
|
|
||||||
// The package respects the colorization settings from the config.
|
|
||||||
// When colorization is disabled, formatting functions like Faint
|
|
||||||
// return unmodified text.
|
|
||||||
//
|
|
||||||
// # Terminal Detection
|
|
||||||
//
|
|
||||||
// The package uses isatty to detect if output is to a terminal,
|
|
||||||
// which affects decisions about using a pager and applying colors.
|
|
||||||
package display
|
|
||||||
@@ -2,17 +2,13 @@
|
|||||||
// cheatsheet content to stdout, or alternatively the system pager.
|
// cheatsheet content to stdout, or alternatively the system pager.
|
||||||
package display
|
package display
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Faint returns a faintly-colored string that's used to de-prioritize text
|
// Faint returns a faintly-colored string that's used to de-prioritize text
|
||||||
// written to stdout
|
// written to stdout
|
||||||
func Faint(str string, conf config.Config) string {
|
func Faint(str string, colorize bool) string {
|
||||||
// make `str` faint only if colorization has been requested
|
// make `str` faint only if colorization has been requested
|
||||||
if conf.Colorize {
|
if colorize {
|
||||||
return fmt.Sprintf("\033[2m%s\033[0m", str)
|
return fmt.Sprintf("\033[2m%s\033[0m", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
package display
|
package display
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestFaint asserts that Faint applies faint formatting
|
// TestFaint asserts that Faint applies faint formatting
|
||||||
func TestFaint(t *testing.T) {
|
func TestFaint(t *testing.T) {
|
||||||
|
|
||||||
// case: apply colorization
|
// case: apply colorization
|
||||||
conf := config.Config{Colorize: true}
|
|
||||||
want := "\033[2mfoo\033[0m"
|
want := "\033[2mfoo\033[0m"
|
||||||
got := Faint("foo", conf)
|
got := Faint("foo", true)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
// case: do not apply colorization
|
// case: do not apply colorization
|
||||||
conf.Colorize = false
|
|
||||||
want = "foo"
|
want = "foo"
|
||||||
got = Faint("foo", conf)
|
got = Faint("foo", false)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func Write(out string, conf config.Config) {
|
|||||||
|
|
||||||
// writeToPager writes output through a pager command
|
// writeToPager writes output through a pager command
|
||||||
func writeToPager(out string, conf config.Config) {
|
func writeToPager(out string, conf config.Config) {
|
||||||
parts := strings.Split(conf.Pager, " ")
|
parts := strings.Fields(conf.Pager)
|
||||||
pager := parts[0]
|
pager := parts[0]
|
||||||
args := parts[1:]
|
args := parts[1:]
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package installer
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
"github.com/cheat/cheat/internal/repo"
|
"github.com/cheat/cheat/internal/repo"
|
||||||
@@ -13,27 +11,11 @@ import (
|
|||||||
// Run runs the installer
|
// Run runs the installer
|
||||||
func Run(configs string, confpath string) error {
|
func Run(configs string, confpath string) error {
|
||||||
|
|
||||||
// determine the appropriate paths for config data and (optional) community
|
// expand template placeholders with platform-appropriate paths
|
||||||
// cheatsheets based on the user's platform
|
configs = ExpandTemplate(configs, confpath)
|
||||||
confdir := filepath.Dir(confpath)
|
|
||||||
|
|
||||||
// create paths for community, personal, and work cheatsheets
|
// determine cheatsheet directory paths
|
||||||
community := filepath.Join(confdir, "cheatsheets", "community")
|
community, personal, work := cheatsheetDirs(confpath)
|
||||||
personal := filepath.Join(confdir, "cheatsheets", "personal")
|
|
||||||
work := filepath.Join(confdir, "cheatsheets", "work")
|
|
||||||
|
|
||||||
// set default cheatpaths
|
|
||||||
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
|
|
||||||
configs = strings.Replace(configs, "PERSONAL_PATH", personal, -1)
|
|
||||||
configs = strings.Replace(configs, "WORK_PATH", work, -1)
|
|
||||||
|
|
||||||
// locate and set a default pager
|
|
||||||
configs = strings.Replace(configs, "PAGER_PATH", config.Pager(), -1)
|
|
||||||
|
|
||||||
// locate and set a default editor
|
|
||||||
if editor, err := config.Editor(); err == nil {
|
|
||||||
configs = strings.Replace(configs, "EDITOR_PATH", editor, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prompt the user to download the community cheatsheets
|
// prompt the user to download the community cheatsheets
|
||||||
yes, err := Prompt(
|
yes, err := Prompt(
|
||||||
@@ -51,19 +33,7 @@ func Run(configs string, confpath string) error {
|
|||||||
return fmt.Errorf("failed to clone cheatsheets: %v", err)
|
return fmt.Errorf("failed to clone cheatsheets: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// comment out the community cheatpath in the config since
|
configs = CommentCommunity(configs, confpath)
|
||||||
// the directory won't exist
|
|
||||||
configs = strings.Replace(configs,
|
|
||||||
" - name: community\n"+
|
|
||||||
" path: "+community+"\n"+
|
|
||||||
" tags: [ community ]\n"+
|
|
||||||
" readonly: true",
|
|
||||||
" #- name: community\n"+
|
|
||||||
" # path: "+community+"\n"+
|
|
||||||
" # tags: [ community ]\n"+
|
|
||||||
" # readonly: true",
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// always create personal and work directories
|
// always create personal and work directories
|
||||||
|
|||||||
58
internal/installer/template.go
Normal file
58
internal/installer/template.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/cheat/cheat/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cheatsheetDirs returns the community, personal, and work cheatsheet
|
||||||
|
// directory paths derived from a config file path.
|
||||||
|
func cheatsheetDirs(confpath string) (community, personal, work string) {
|
||||||
|
confdir := filepath.Dir(confpath)
|
||||||
|
community = filepath.Join(confdir, "cheatsheets", "community")
|
||||||
|
personal = filepath.Join(confdir, "cheatsheets", "personal")
|
||||||
|
work = filepath.Join(confdir, "cheatsheets", "work")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandTemplate replaces placeholder tokens in the config template with
|
||||||
|
// platform-appropriate paths derived from confpath.
|
||||||
|
func ExpandTemplate(configs string, confpath string) string {
|
||||||
|
community, personal, work := cheatsheetDirs(confpath)
|
||||||
|
|
||||||
|
// substitute paths
|
||||||
|
configs = strings.ReplaceAll(configs, "COMMUNITY_PATH", community)
|
||||||
|
configs = strings.ReplaceAll(configs, "PERSONAL_PATH", personal)
|
||||||
|
configs = strings.ReplaceAll(configs, "WORK_PATH", work)
|
||||||
|
|
||||||
|
// locate and set a default pager
|
||||||
|
configs = strings.ReplaceAll(configs, "PAGER_PATH", config.Pager())
|
||||||
|
|
||||||
|
// locate and set a default editor
|
||||||
|
if editor, err := config.Editor(); err == nil {
|
||||||
|
configs = strings.ReplaceAll(configs, "EDITOR_PATH", editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentCommunity comments out the community cheatpath block in the config
|
||||||
|
// template. This is used when the community cheatsheets directory won't exist
|
||||||
|
// (either because the user declined to download them, or because the config
|
||||||
|
// is being output as an example).
|
||||||
|
func CommentCommunity(configs string, confpath string) string {
|
||||||
|
community, _, _ := cheatsheetDirs(confpath)
|
||||||
|
|
||||||
|
return strings.ReplaceAll(configs,
|
||||||
|
" - name: community\n"+
|
||||||
|
" path: "+community+"\n"+
|
||||||
|
" tags: [ community ]\n"+
|
||||||
|
" readonly: true",
|
||||||
|
" #- name: community\n"+
|
||||||
|
" # path: "+community+"\n"+
|
||||||
|
" # tags: [ community ]\n"+
|
||||||
|
" # readonly: true",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Package mock implements mock functions used in unit-tests.
|
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"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 mock path
|
|
||||||
file, err := filepath.Abs(
|
|
||||||
filepath.Join(
|
|
||||||
filepath.Dir(thisfile),
|
|
||||||
"../../mocks",
|
|
||||||
filename,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to resolve mock path: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package repo
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Package sheet provides functionality for parsing and managing individual cheat sheets.
|
|
||||||
//
|
|
||||||
// A sheet represents a single cheatsheet file containing helpful commands, notes,
|
|
||||||
// or documentation. Sheets can include optional YAML frontmatter for metadata
|
|
||||||
// such as tags and syntax highlighting preferences.
|
|
||||||
//
|
|
||||||
// # Sheet Format
|
|
||||||
//
|
|
||||||
// Sheets are plain text files that may begin with YAML frontmatter:
|
|
||||||
//
|
|
||||||
// ---
|
|
||||||
// syntax: bash
|
|
||||||
// tags: [networking, linux, ssh]
|
|
||||||
// ---
|
|
||||||
// # Connect to remote server
|
|
||||||
// ssh user@hostname
|
|
||||||
//
|
|
||||||
// # Copy files over SSH
|
|
||||||
// scp local_file user@hostname:/remote/path
|
|
||||||
//
|
|
||||||
// The frontmatter is optional. If omitted, the sheet will use default values.
|
|
||||||
//
|
|
||||||
// # Core Types
|
|
||||||
//
|
|
||||||
// The Sheet type contains:
|
|
||||||
// - Title: The sheet's name (derived from filename)
|
|
||||||
// - Path: Full filesystem path to the sheet
|
|
||||||
// - Text: The content of the sheet (without frontmatter)
|
|
||||||
// - Tags: Categories assigned to the sheet
|
|
||||||
// - Syntax: Language hint for syntax highlighting
|
|
||||||
// - ReadOnly: Whether the sheet can be modified
|
|
||||||
//
|
|
||||||
// Key Functions
|
|
||||||
//
|
|
||||||
// - New: Creates a new Sheet from a file path
|
|
||||||
// - Parse: Extracts frontmatter and content from sheet text
|
|
||||||
// - Search: Searches sheet content using regular expressions
|
|
||||||
// - Colorize: Applies syntax highlighting to sheet content
|
|
||||||
//
|
|
||||||
// # Syntax Highlighting
|
|
||||||
//
|
|
||||||
// The package integrates with the Chroma library to provide syntax highlighting.
|
|
||||||
// Supported languages include bash, python, go, javascript, and many others.
|
|
||||||
// The syntax can be specified in the frontmatter or auto-detected.
|
|
||||||
//
|
|
||||||
// Example Usage
|
|
||||||
//
|
|
||||||
// // Load a sheet from disk
|
|
||||||
// s, err := sheet.New("/path/to/sheet", []string{"personal"}, false)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Search for content
|
|
||||||
// matches, err := s.Search("ssh", false)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Apply syntax highlighting
|
|
||||||
// colorized, err := s.Colorize(config)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
package sheet
|
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSheetSuccess asserts that sheets initialize properly
|
// TestSheetSuccess asserts that sheets initialize properly
|
||||||
@@ -14,7 +14,7 @@ func TestSheetSuccess(t *testing.T) {
|
|||||||
sheet, err := New(
|
sheet, err := New(
|
||||||
"foo",
|
"foo",
|
||||||
"community",
|
"community",
|
||||||
mock.Path("sheet/foo"),
|
mocks.Path("sheet/foo"),
|
||||||
[]string{"alpha", "bravo"},
|
[]string{"alpha", "bravo"},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -27,10 +27,10 @@ func TestSheetSuccess(t *testing.T) {
|
|||||||
t.Errorf("failed to init title: want: foo, got: %s", sheet.Title)
|
t.Errorf("failed to init title: want: foo, got: %s", sheet.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sheet.Path != mock.Path("sheet/foo") {
|
if sheet.Path != mocks.Path("sheet/foo") {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"failed to init path: want: %s, got: %s",
|
"failed to init path: want: %s, got: %s",
|
||||||
mock.Path("sheet/foo"),
|
mocks.Path("sheet/foo"),
|
||||||
sheet.Path,
|
sheet.Path,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ func TestSheetFailure(t *testing.T) {
|
|||||||
_, err := New(
|
_, err := New(
|
||||||
"foo",
|
"foo",
|
||||||
"community",
|
"community",
|
||||||
mock.Path("/does-not-exist"),
|
mocks.Path("/does-not-exist"),
|
||||||
[]string{"alpha", "bravo"},
|
[]string{"alpha", "bravo"},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -80,7 +80,7 @@ func TestSheetFrontMatterFailure(t *testing.T) {
|
|||||||
_, err := New(
|
_, err := New(
|
||||||
"foo",
|
"foo",
|
||||||
"community",
|
"community",
|
||||||
mock.Path("sheet/bad-fm"),
|
mocks.Path("sheet/bad-fm"),
|
||||||
[]string{"alpha", "bravo"},
|
[]string{"alpha", "bravo"},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
package sheet
|
package sheet
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
// Tagged returns true if a sheet was tagged with `needle`
|
// Tagged returns true if a sheet was tagged with `needle`
|
||||||
func (s *Sheet) Tagged(needle string) bool {
|
func (s *Sheet) Tagged(needle string) bool {
|
||||||
|
return slices.Contains(s.Tags, needle)
|
||||||
// if any of the tags match `needle`, return `true`
|
|
||||||
for _, tag := range s.Tags {
|
|
||||||
if tag == needle {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, return `false`
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
40
internal/sheet/validate.go
Normal file
40
internal/sheet/validate.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package sheet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate ensures that a cheatsheet name does not contain
|
||||||
|
// directory traversal sequences or other potentially dangerous patterns.
|
||||||
|
func Validate(name string) error {
|
||||||
|
// Reject empty names
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject names containing directory traversal
|
||||||
|
if strings.Contains(name, "..") {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot contain '..'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject absolute paths
|
||||||
|
if filepath.IsAbs(name) {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot be an absolute path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject names that start with ~ (home directory expansion)
|
||||||
|
if strings.HasPrefix(name, "~") {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot start with '~'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject hidden files (files that start with a dot)
|
||||||
|
// We don't display hidden files, so we shouldn't create them
|
||||||
|
filename := filepath.Base(name)
|
||||||
|
if strings.HasPrefix(filename, ".") {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot start with '.' (hidden files are not supported)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cheatpath
|
package sheet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FuzzValidateSheetName tests the ValidateSheetName function with fuzzing
|
// FuzzValidate tests the Validate function with fuzzing
|
||||||
// to ensure it properly prevents path traversal and other security issues
|
// to ensure it properly prevents path traversal and other security issues
|
||||||
func FuzzValidateSheetName(f *testing.F) {
|
func FuzzValidate(f *testing.F) {
|
||||||
// Add seed corpus with various valid and malicious inputs
|
// Add seed corpus with various valid and malicious inputs
|
||||||
// Valid names
|
// Valid names
|
||||||
f.Add("docker")
|
f.Add("docker")
|
||||||
@@ -84,11 +84,11 @@ func FuzzValidateSheetName(f *testing.F) {
|
|||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("ValidateSheetName panicked with input %q: %v", input, r)
|
t.Errorf("Validate panicked with input %q: %v", input, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := ValidateSheetName(input)
|
err := Validate(input)
|
||||||
|
|
||||||
// Security invariants that must always hold
|
// Security invariants that must always hold
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -129,8 +129,8 @@ func FuzzValidateSheetName(f *testing.F) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FuzzValidateSheetNamePathTraversal specifically targets path traversal bypasses
|
// FuzzValidatePathTraversal specifically targets path traversal bypasses
|
||||||
func FuzzValidateSheetNamePathTraversal(f *testing.F) {
|
func FuzzValidatePathTraversal(f *testing.F) {
|
||||||
// Seed corpus focusing on path traversal variations
|
// Seed corpus focusing on path traversal variations
|
||||||
f.Add("..", "/", "")
|
f.Add("..", "/", "")
|
||||||
f.Add("", "..", "/")
|
f.Add("", "..", "/")
|
||||||
@@ -153,11 +153,11 @@ func FuzzValidateSheetNamePathTraversal(f *testing.F) {
|
|||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("ValidateSheetName panicked with constructed input %q: %v", input, r)
|
t.Errorf("Validate panicked with constructed input %q: %v", input, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := ValidateSheetName(input)
|
err := Validate(input)
|
||||||
|
|
||||||
// If the input contains literal "..", it must be rejected
|
// If the input contains literal "..", it must be rejected
|
||||||
if strings.Contains(input, "..") && err == nil {
|
if strings.Contains(input, "..") && err == nil {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cheatpath
|
package sheet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateSheetName(t *testing.T) {
|
func TestValidate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
@@ -98,14 +98,14 @@ func TestValidateSheetName(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err := ValidateSheetName(tt.input)
|
err := Validate(tt.input)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("ValidateName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
t.Errorf("Validate(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil && tt.errMsg != "" {
|
if err != nil && tt.errMsg != "" {
|
||||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||||
t.Errorf("ValidateName(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
t.Errorf("Validate(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Package sheets manages collections of cheat sheets across multiple cheatpaths.
|
|
||||||
//
|
|
||||||
// The sheets package provides functionality to:
|
|
||||||
// - Load sheets from multiple cheatpaths
|
|
||||||
// - Consolidate duplicate sheets (with precedence rules)
|
|
||||||
// - Filter sheets by tags
|
|
||||||
// - Sort sheets alphabetically
|
|
||||||
// - Extract unique tags across all sheets
|
|
||||||
//
|
|
||||||
// # Loading Sheets
|
|
||||||
//
|
|
||||||
// Sheets are loaded recursively from cheatpath directories, excluding:
|
|
||||||
// - Hidden files (starting with .)
|
|
||||||
// - Files in .git directories
|
|
||||||
// - Files with extensions (sheets have no extension)
|
|
||||||
//
|
|
||||||
// # Consolidation
|
|
||||||
//
|
|
||||||
// When multiple cheatpaths contain sheets with the same name, consolidation
|
|
||||||
// rules apply based on the order of cheatpaths. Sheets from earlier paths
|
|
||||||
// override those from later paths, allowing personal sheets to override
|
|
||||||
// community sheets.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// cheatpaths:
|
|
||||||
// 1. personal: ~/cheat
|
|
||||||
// 2. community: ~/cheat/community
|
|
||||||
//
|
|
||||||
// If both contain "git", the version from "personal" is used.
|
|
||||||
//
|
|
||||||
// # Filtering
|
|
||||||
//
|
|
||||||
// Sheets can be filtered by:
|
|
||||||
// - Tags: Include only sheets with specific tags
|
|
||||||
// - Cheatpath: Include only sheets from specific paths
|
|
||||||
//
|
|
||||||
// Key Functions
|
|
||||||
//
|
|
||||||
// - Load: Loads all sheets from the given cheatpaths
|
|
||||||
// - Filter: Filters sheets by tag
|
|
||||||
// - Consolidate: Merges sheets from multiple paths with precedence
|
|
||||||
// - Sort: Sorts sheets alphabetically by title
|
|
||||||
// - Tags: Extracts all unique tags from sheets
|
|
||||||
//
|
|
||||||
// Example Usage
|
|
||||||
//
|
|
||||||
// // Load sheets from all cheatpaths
|
|
||||||
// allSheets, err := sheets.Load(cheatpaths)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Consolidate to handle duplicates
|
|
||||||
// consolidated := sheets.Consolidate(allSheets)
|
|
||||||
//
|
|
||||||
// // Filter by tag
|
|
||||||
// filtered := sheets.Filter(consolidated, "networking")
|
|
||||||
//
|
|
||||||
// // Sort alphabetically
|
|
||||||
// sheets.Sort(filtered)
|
|
||||||
//
|
|
||||||
// // Get all unique tags
|
|
||||||
// tags := sheets.Tags(consolidated)
|
|
||||||
package sheets
|
|
||||||
@@ -8,12 +8,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
cp "github.com/cheat/cheat/internal/cheatpath"
|
cp "github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/repo"
|
|
||||||
"github.com/cheat/cheat/internal/sheet"
|
"github.com/cheat/cheat/internal/sheet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load produces a map of cheatsheet titles to filesystem paths
|
// Load produces a map of cheatsheet titles to filesystem paths
|
||||||
func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
func Load(cheatpaths []cp.Path) ([]map[string]sheet.Sheet, error) {
|
||||||
|
|
||||||
// create a slice of maps of sheets. This structure will store all sheets
|
// create a slice of maps of sheets. This structure will store all sheets
|
||||||
// that are associated with each cheatpath.
|
// that are associated with each cheatpath.
|
||||||
@@ -27,10 +26,10 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
|||||||
|
|
||||||
// recursively iterate over the cheatpath, and load each cheatsheet
|
// recursively iterate over the cheatpath, and load each cheatsheet
|
||||||
// encountered along the way
|
// encountered along the way
|
||||||
err := filepath.Walk(
|
err := filepath.WalkDir(
|
||||||
cheatpath.Path, func(
|
cheatpath.Path, func(
|
||||||
path string,
|
path string,
|
||||||
info os.FileInfo,
|
d fs.DirEntry,
|
||||||
err error) error {
|
err error) error {
|
||||||
|
|
||||||
// fail if an error occurred while walking the directory
|
// fail if an error occurred while walking the directory
|
||||||
@@ -38,8 +37,12 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
|||||||
return fmt.Errorf("failed to walk path: %v", err)
|
return fmt.Errorf("failed to walk path: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't register directories as cheatsheets
|
if d.IsDir() {
|
||||||
if info.IsDir() {
|
// skip .git directories to avoid hundreds/thousands of
|
||||||
|
// needless syscalls (see repo.GitDir for full history)
|
||||||
|
if filepath.Base(path) == ".git" {
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,17 +66,6 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
|||||||
string(os.PathSeparator),
|
string(os.PathSeparator),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Don't walk the `.git` directory. Doing so creates
|
|
||||||
// hundreds/thousands of needless syscalls and could
|
|
||||||
// potentially harm performance on machines with slow disks.
|
|
||||||
skip, err := repo.GitDir(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to identify .git directory: %v", err)
|
|
||||||
}
|
|
||||||
if skip {
|
|
||||||
return fs.SkipDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse the cheatsheet file into a `sheet` struct
|
// parse the cheatsheet file into a `sheet` struct
|
||||||
s, err := sheet.New(
|
s, err := sheet.New(
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
"github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/mock"
|
"github.com/cheat/cheat/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestLoad asserts that sheets on valid cheatpaths can be loaded successfully
|
// TestLoad asserts that sheets on valid cheatpaths can be loaded successfully
|
||||||
func TestLoad(t *testing.T) {
|
func TestLoad(t *testing.T) {
|
||||||
|
|
||||||
// mock cheatpaths
|
// mock cheatpaths
|
||||||
cheatpaths := []cheatpath.Cheatpath{
|
cheatpaths := []cheatpath.Path{
|
||||||
{
|
{
|
||||||
Name: "community",
|
Name: "community",
|
||||||
Path: path.Join(mock.Path("cheatsheets"), "community"),
|
Path: path.Join(mocks.Path("cheatsheets"), "community"),
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "personal",
|
Name: "personal",
|
||||||
Path: path.Join(mock.Path("cheatsheets"), "personal"),
|
Path: path.Join(mocks.Path("cheatsheets"), "personal"),
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ func TestLoad(t *testing.T) {
|
|||||||
func TestLoadBadPath(t *testing.T) {
|
func TestLoadBadPath(t *testing.T) {
|
||||||
|
|
||||||
// mock a bad cheatpath
|
// mock a bad cheatpath
|
||||||
cheatpaths := []cheatpath.Cheatpath{
|
cheatpaths := []cheatpath.Path{
|
||||||
{
|
{
|
||||||
Name: "badpath",
|
Name: "badpath",
|
||||||
Path: "/cheat/test/path/does/not/exist",
|
Path: "/cheat/test/path/does/not/exist",
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ func Tags(cheatpaths []map[string]sheet.Sheet) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sort the slice
|
// sort the slice
|
||||||
sort.Slice(sorted, func(i, j int) bool {
|
sort.Strings(sorted)
|
||||||
return sorted[i] < sorted[j]
|
|
||||||
})
|
|
||||||
|
|
||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|||||||
23
mocks/path.go
Normal file
23
mocks/path.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Package mocks provides test fixture data and helpers for unit tests.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path returns the absolute path to the specified mock file within
|
||||||
|
// the mocks/ directory.
|
||||||
|
func Path(filename string) string {
|
||||||
|
_, thisfile, _, _ := runtime.Caller(0)
|
||||||
|
|
||||||
|
file, err := filepath.Abs(
|
||||||
|
filepath.Join(filepath.Dir(thisfile), filename),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to resolve mock path: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
@@ -16,14 +16,12 @@ DURATION="${1:-15s}"
|
|||||||
# Define fuzz tests: "TestName:Package:Description"
|
# Define fuzz tests: "TestName:Package:Description"
|
||||||
TESTS=(
|
TESTS=(
|
||||||
"FuzzParse:./internal/sheet:YAML frontmatter parsing"
|
"FuzzParse:./internal/sheet:YAML frontmatter parsing"
|
||||||
"FuzzValidateSheetName:./internal/cheatpath:sheet name validation (path traversal protection)"
|
"FuzzValidate:./internal/sheet:sheet name validation (path traversal protection)"
|
||||||
"FuzzSearchRegex:./internal/sheet:regex search operations"
|
"FuzzSearchRegex:./internal/sheet:regex search operations"
|
||||||
"FuzzSearchCatastrophicBacktracking:./internal/sheet:catastrophic backtracking"
|
|
||||||
"FuzzTagged:./internal/sheet:tag matching with malicious input"
|
"FuzzTagged:./internal/sheet:tag matching with malicious input"
|
||||||
"FuzzFilter:./internal/sheets:tag filtering operations"
|
"FuzzFilter:./internal/sheets:tag filtering operations"
|
||||||
"FuzzTags:./internal/sheets:tag aggregation and sorting"
|
"FuzzTags:./internal/sheets:tag aggregation and sorting"
|
||||||
"FuzzFindLocalCheatpath:./internal/config:recursive .cheat directory discovery"
|
"FuzzFindLocalCheatpath:./internal/config:recursive .cheat directory discovery"
|
||||||
"FuzzFindLocalCheatpathNearestWins:./internal/config:nearest .cheat wins invariant"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
echo "Running fuzz tests ($DURATION each)..."
|
echo "Running fuzz tests ($DURATION each)..."
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -18,7 +18,8 @@ func TestBriefFlagIntegration(t *testing.T) {
|
|||||||
|
|
||||||
// Build the cheat binary once for all sub-tests.
|
// Build the cheat binary once for all sub-tests.
|
||||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
build := exec.Command("go", "build", "-o", binPath, ".")
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
if output, err := build.CombinedOutput(); err != nil {
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -32,7 +32,8 @@ func TestLocalCheatpathIntegration(t *testing.T) {
|
|||||||
|
|
||||||
// Build the cheat binary once for all sub-tests.
|
// Build the cheat binary once for all sub-tests.
|
||||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
build := exec.Command("go", "build", "-o", binPath, ".")
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
if output, err := build.CombinedOutput(); err != nil {
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -19,7 +19,8 @@ func TestFirstRunIntegration(t *testing.T) {
|
|||||||
binName += ".exe"
|
binName += ".exe"
|
||||||
}
|
}
|
||||||
binPath := filepath.Join(t.TempDir(), binName)
|
binPath := filepath.Join(t.TempDir(), binName)
|
||||||
build := exec.Command("go", "build", "-o", binPath, ".")
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
if output, err := build.CombinedOutput(); err != nil {
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
30
test/integration/helpers_test.go
Normal file
30
test/integration/helpers_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// repoRoot returns the absolute path to the repository root.
|
||||||
|
// It derives this from the known location of this source file
|
||||||
|
// (test/integration/) relative to the repo root.
|
||||||
|
func repoRoot(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
_, file, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("failed to determine repo root via runtime.Caller")
|
||||||
|
}
|
||||||
|
// file is <repo>/test/integration/helpers_test.go → go up two dirs
|
||||||
|
return filepath.Dir(filepath.Dir(filepath.Dir(file)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// repoRootBench is the same as repoRoot but for use in benchmarks.
|
||||||
|
func repoRootBench(b *testing.B) string {
|
||||||
|
b.Helper()
|
||||||
|
_, file, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
b.Fatal("failed to determine repo root via runtime.Caller")
|
||||||
|
}
|
||||||
|
return filepath.Dir(filepath.Dir(filepath.Dir(file)))
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -19,7 +19,9 @@ func TestPathTraversalIntegration(t *testing.T) {
|
|||||||
|
|
||||||
// Build the cheat binary
|
// Build the cheat binary
|
||||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
t.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +161,9 @@ func TestPathTraversalRealWorld(t *testing.T) {
|
|||||||
|
|
||||||
// Build cheat
|
// Build cheat
|
||||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
||||||
|
build.Dir = repoRoot(t)
|
||||||
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
t.Fatalf("Failed to build: %v\n%s", err, output)
|
t.Fatalf("Failed to build: %v\n%s", err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build integration
|
//go:build integration
|
||||||
|
|
||||||
package main
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -16,12 +16,10 @@ import (
|
|||||||
|
|
||||||
// BenchmarkSearchCommand benchmarks the actual cheat search command
|
// BenchmarkSearchCommand benchmarks the actual cheat search command
|
||||||
func BenchmarkSearchCommand(b *testing.B) {
|
func BenchmarkSearchCommand(b *testing.B) {
|
||||||
|
root := repoRootBench(b)
|
||||||
|
|
||||||
// Build the cheat binary in .tmp (using absolute path)
|
// Build the cheat binary in .tmp (using absolute path)
|
||||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
tmpDir := filepath.Join(root, ".tmp", "bench-test")
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to get root dir: %v", err)
|
|
||||||
}
|
|
||||||
tmpDir := filepath.Join(rootDir, ".tmp", "bench-test")
|
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
b.Fatalf("Failed to create temp dir: %v", err)
|
b.Fatalf("Failed to create temp dir: %v", err)
|
||||||
}
|
}
|
||||||
@@ -34,7 +32,7 @@ func BenchmarkSearchCommand(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||||
cmd.Dir = rootDir
|
cmd.Dir = root
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
@@ -126,12 +124,10 @@ cheatpaths:
|
|||||||
|
|
||||||
// BenchmarkListCommand benchmarks the list command for comparison
|
// BenchmarkListCommand benchmarks the list command for comparison
|
||||||
func BenchmarkListCommand(b *testing.B) {
|
func BenchmarkListCommand(b *testing.B) {
|
||||||
|
root := repoRootBench(b)
|
||||||
|
|
||||||
// Build the cheat binary in .tmp (using absolute path)
|
// Build the cheat binary in .tmp (using absolute path)
|
||||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
tmpDir := filepath.Join(root, ".tmp", "bench-test")
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("Failed to get root dir: %v", err)
|
|
||||||
}
|
|
||||||
tmpDir := filepath.Join(rootDir, ".tmp", "bench-test")
|
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
b.Fatalf("Failed to create temp dir: %v", err)
|
b.Fatalf("Failed to create temp dir: %v", err)
|
||||||
}
|
}
|
||||||
@@ -144,7 +140,7 @@ func BenchmarkListCommand(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||||
cmd.Dir = rootDir
|
cmd.Dir = root
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user