mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
chore: bump version to 4.5.0
Bug fixes: - Fix inverted pager detection logic (returned error instead of path) - Fix repo.Clone ignoring destination directory parameter - Fix sheet loading using append on pre-sized slices - Clean up partial files on copy failure - Trim whitespace from editor config Security: - Add path traversal protection for cheatsheet names Performance: - Move regex compilation outside search loop - Replace string concatenation with strings.Join in search Build: - Remove go:generate; embed config and usage as string literals - Parallelize release builds - Add fuzz testing infrastructure Testing: - Improve test coverage from 38.9% to 50.2% - Add fuzz tests for search, filter, tags, and validation Documentation: - Fix inaccurate code examples in HACKING.md - Add missing --conf and --all options to man page - Add ADRs for path traversal, env parsing, and search parallelization - Update CONTRIBUTING.md to reflect project policy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,12 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
cheatsheet := opts["--edit"].(string)
|
||||
|
||||
// validate the cheatsheet name
|
||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,15 +5,22 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cheat/cheat/internal/cheatpath"
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
// cmdRemove opens a cheatsheet for editing (or creates it if it doesn't exist).
|
||||
// cmdRemove removes (deletes) a cheatsheet.
|
||||
func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
cheatsheet := opts["--rm"].(string)
|
||||
|
||||
// validate the cheatsheet name
|
||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
|
||||
@@ -31,6 +31,21 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||
)
|
||||
}
|
||||
|
||||
// prepare the search pattern
|
||||
pattern := "(?i)" + phrase
|
||||
|
||||
// unless --regex is provided, in which case we pass the regex unaltered
|
||||
if opts["--regex"] == true {
|
||||
pattern = phrase
|
||||
}
|
||||
|
||||
// compile the regex once, outside the loop
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// iterate over each cheatpath
|
||||
out := ""
|
||||
for _, pathcheats := range cheatsheets {
|
||||
@@ -44,21 +59,6 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||
continue
|
||||
}
|
||||
|
||||
// assume that we want to perform a case-insensitive search for <phrase>
|
||||
pattern := "(?i)" + phrase
|
||||
|
||||
// unless --regex is provided, in which case we pass the regex unaltered
|
||||
if opts["--regex"] == true {
|
||||
pattern = phrase
|
||||
}
|
||||
|
||||
// compile the regex
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// `Search` will return text entries that match the search terms.
|
||||
// We're using it here to overwrite the prior cheatsheet Text,
|
||||
// filtering it to only what is relevant.
|
||||
|
||||
73
cmd/cheat/config.go
Normal file
73
cmd/cheat/config.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
// configs returns the default configuration template
|
||||
func configs() string {
|
||||
return `---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: EDITOR_PATH
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: false
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
# https://github.com/alecthomas/chroma/tree/master/styles
|
||||
style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal256
|
||||
|
||||
# Through which pager should output be piped?
|
||||
# 'less -FRX' is recommended on Unix systems
|
||||
# 'more' is recommended on Windows
|
||||
pager: PAGER_PATH
|
||||
|
||||
# The paths at which cheatsheets are available. Tags associated with a cheatpath
|
||||
# are automatically attached to all cheatsheets residing on that path.
|
||||
#
|
||||
# Whenever cheatsheets share the same title (like 'tar'), the most local
|
||||
# cheatsheets (those which come later in this file) take precedence over the
|
||||
# less local sheets. This allows you to create your own "overides" for
|
||||
# "upstream" cheatsheets.
|
||||
#
|
||||
# But what if you want to view the "upstream" cheatsheets instead of your own?
|
||||
# Cheatsheets may be filtered by 'tags' in combination with the '--tag' flag.
|
||||
#
|
||||
# Example: 'cheat tar --tag=community' will display the 'tar' cheatsheet that
|
||||
# is tagged as 'community' rather than your own.
|
||||
#
|
||||
# Paths that come earlier are considered to be the most "global", and paths
|
||||
# that come later are considered to be the most "local". The most "local" paths
|
||||
# take precedence.
|
||||
#
|
||||
# See: https://github.com/cheat/cheat/blob/master/doc/cheat.1.md#cheatpaths
|
||||
cheatpaths:
|
||||
|
||||
# Cheatsheets that are tagged "personal" are stored here by default:
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# Cheatsheets that are tagged "work" are stored here by default:
|
||||
- name: work
|
||||
path: WORK_PATH
|
||||
tags: [ work ]
|
||||
readonly: false
|
||||
|
||||
# Community cheatsheets are stored here by default:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# You can also use glob patterns to automatically load cheatsheets from all
|
||||
# directories that match.
|
||||
#
|
||||
# Example: overload cheatsheets for projects under ~/src/github.com/example/*/
|
||||
#- name: example-projects
|
||||
# path: ~/src/github.com/example/**/.cheat
|
||||
# tags: [ example ]
|
||||
# readonly: true`
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
Usage:
|
||||
cheat [options] [<cheatsheet>]
|
||||
|
||||
Options:
|
||||
--init Write a default config file to stdout
|
||||
-a --all Search among all cheatpaths
|
||||
-c --colorize Colorize output
|
||||
-d --directories List cheatsheet directories
|
||||
-e --edit=<cheatsheet> Edit <cheatsheet>
|
||||
-l --list List cheatsheets
|
||||
-p --path=<name> Return only sheets found on cheatpath <name>
|
||||
-r --regex Treat search <phrase> as a regex
|
||||
-s --search=<phrase> Search cheatsheets for <phrase>
|
||||
-t --tag=<tag> Return only sheets matching <tag>
|
||||
-T --tags List all tags in use
|
||||
-v --version Print the version number
|
||||
--rm=<cheatsheet> Remove (delete) <cheatsheet>
|
||||
--conf Display the config file path
|
||||
|
||||
Examples:
|
||||
|
||||
To initialize a config file:
|
||||
mkdir -p ~/.config/cheat && cheat --init > ~/.config/cheat/conf.yml
|
||||
|
||||
To view the tar cheatsheet:
|
||||
cheat tar
|
||||
|
||||
To edit (or create) the foo cheatsheet:
|
||||
cheat -e foo
|
||||
|
||||
To edit (or create) the foo/bar cheatsheet on the "work" cheatpath:
|
||||
cheat -p work -e foo/bar
|
||||
|
||||
To view all cheatsheet directories:
|
||||
cheat -d
|
||||
|
||||
To list all available cheatsheets:
|
||||
cheat -l
|
||||
|
||||
To list all cheatsheets whose titles match "apt":
|
||||
cheat -l apt
|
||||
|
||||
To list all tags in use:
|
||||
cheat -T
|
||||
|
||||
To list available cheatsheets that are tagged as "personal":
|
||||
cheat -l -t personal
|
||||
|
||||
To search for "ssh" among all cheatsheets, and colorize matches:
|
||||
cheat -c -s ssh
|
||||
|
||||
To search (by regex) for cheatsheets that contain an IP address:
|
||||
cheat -c -r -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
|
||||
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
cheat --rm foo/bar
|
||||
|
||||
To view the configuration file path:
|
||||
cheat --conf
|
||||
@@ -1,8 +1,6 @@
|
||||
// Package main serves as the executable entrypoint.
|
||||
package main
|
||||
|
||||
//go:generate go run ../../build/embed.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -17,7 +15,7 @@ import (
|
||||
"github.com/cheat/cheat/internal/installer"
|
||||
)
|
||||
|
||||
const version = "4.4.2"
|
||||
const version = "4.5.0"
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -45,6 +43,7 @@ func main() {
|
||||
// read the envvars into a map of strings
|
||||
envvars := map[string]string{}
|
||||
for _, e := range os.Environ() {
|
||||
// os.Environ() guarantees "key=value" format (see ADR-002)
|
||||
pair := strings.SplitN(e, "=", 2)
|
||||
if runtime.GOOS == "windows" {
|
||||
pair[0] = strings.ToUpper(pair[0])
|
||||
|
||||
216
cmd/cheat/path_traversal_integration_test.go
Normal file
216
cmd/cheat/path_traversal_integration_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPathTraversalIntegration tests that the cheat binary properly blocks
|
||||
// path traversal attempts when invoked as a subprocess.
|
||||
func TestPathTraversalIntegration(t *testing.T) {
|
||||
// Build the cheat binary
|
||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment
|
||||
testDir := t.TempDir()
|
||||
sheetsDir := filepath.Join(testDir, "sheets")
|
||||
os.MkdirAll(sheetsDir, 0755)
|
||||
|
||||
// Create config
|
||||
config := fmt.Sprintf(`---
|
||||
editor: echo
|
||||
colorize: false
|
||||
pager: cat
|
||||
cheatpaths:
|
||||
- name: test
|
||||
path: %s
|
||||
readonly: false
|
||||
`, sheetsDir)
|
||||
|
||||
configPath := filepath.Join(testDir, "config.yml")
|
||||
if err := os.WriteFile(configPath, []byte(config), 0644); err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Test table
|
||||
tests := []struct {
|
||||
name string
|
||||
command []string
|
||||
wantFail bool
|
||||
wantMsg string
|
||||
}{
|
||||
// Blocked patterns
|
||||
{
|
||||
name: "block parent traversal edit",
|
||||
command: []string{"--edit", "../evil"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block absolute path edit",
|
||||
command: []string{"--edit", "/etc/passwd"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot be an absolute path",
|
||||
},
|
||||
{
|
||||
name: "block home dir edit",
|
||||
command: []string{"--edit", "~/.ssh/config"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot start with '~'",
|
||||
},
|
||||
{
|
||||
name: "block parent traversal remove",
|
||||
command: []string{"--rm", "../evil"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block complex traversal",
|
||||
command: []string{"--edit", "foo/../../bar"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block just dots",
|
||||
command: []string{"--edit", ".."},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block empty name",
|
||||
command: []string{"--edit", ""},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot be empty",
|
||||
},
|
||||
// Allowed patterns
|
||||
{
|
||||
name: "allow simple name",
|
||||
command: []string{"--edit", "docker"},
|
||||
wantFail: false,
|
||||
},
|
||||
{
|
||||
name: "allow nested name",
|
||||
command: []string{"--edit", "lang/go"},
|
||||
wantFail: false,
|
||||
},
|
||||
{
|
||||
name: "block hidden file",
|
||||
command: []string{"--edit", ".gitignore"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot start with '.'",
|
||||
},
|
||||
{
|
||||
name: "allow current dir",
|
||||
command: []string{"--edit", "./local"},
|
||||
wantFail: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := exec.Command(binPath, tc.command...)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configPath),
|
||||
fmt.Sprintf("HOME=%s", testDir),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if tc.wantFail {
|
||||
if err == nil {
|
||||
t.Errorf("Expected failure but command succeeded. Output: %s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Expected 'invalid cheatsheet name' error, got: %s", output)
|
||||
}
|
||||
if tc.wantMsg != "" && !strings.Contains(string(output), tc.wantMsg) {
|
||||
t.Errorf("Expected message %q in output, got: %s", tc.wantMsg, output)
|
||||
}
|
||||
} else {
|
||||
// Command might fail for other reasons (e.g., editor not found)
|
||||
// but should NOT fail with "invalid cheatsheet name"
|
||||
if strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Command incorrectly blocked. Output: %s", output)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPathTraversalRealWorld tests with more realistic scenarios
|
||||
func TestPathTraversalRealWorld(t *testing.T) {
|
||||
// This test ensures our protection works with actual file operations
|
||||
|
||||
// Build cheat
|
||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("Failed to build: %v\n%s", err, output)
|
||||
}
|
||||
|
||||
// Create test structure
|
||||
testRoot := t.TempDir()
|
||||
sheetsDir := filepath.Join(testRoot, "cheatsheets")
|
||||
secretDir := filepath.Join(testRoot, "secrets")
|
||||
os.MkdirAll(sheetsDir, 0755)
|
||||
os.MkdirAll(secretDir, 0755)
|
||||
|
||||
// Create a "secret" file that should not be accessible
|
||||
secretFile := filepath.Join(secretDir, "secret.txt")
|
||||
os.WriteFile(secretFile, []byte("SECRET DATA"), 0644)
|
||||
|
||||
// Create config using vim in non-interactive mode
|
||||
config := fmt.Sprintf(`---
|
||||
editor: vim -u NONE -n --cmd "set noswapfile" --cmd "wq"
|
||||
colorize: false
|
||||
pager: cat
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: %s
|
||||
readonly: false
|
||||
`, sheetsDir)
|
||||
|
||||
configPath := filepath.Join(testRoot, "config.yml")
|
||||
os.WriteFile(configPath, []byte(config), 0644)
|
||||
|
||||
// Test 1: Try to edit a file outside cheatsheets using traversal
|
||||
cmd := exec.Command(binPath, "--edit", "../secrets/secret")
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configPath),
|
||||
fmt.Sprintf("HOME=%s", testRoot),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err == nil || !strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Path traversal was not blocked! Output: %s", output)
|
||||
}
|
||||
|
||||
// Test 2: Verify the secret file is still intact
|
||||
content, _ := os.ReadFile(secretFile)
|
||||
if string(content) != "SECRET DATA" {
|
||||
t.Errorf("Secret file was modified!")
|
||||
}
|
||||
|
||||
// Test 3: Verify no files were created outside sheets directory
|
||||
err = filepath.Walk(testRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() &&
|
||||
path != configPath &&
|
||||
path != secretFile &&
|
||||
!strings.HasPrefix(path, sheetsDir) {
|
||||
t.Errorf("File created outside allowed directory: %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Walk error: %v", err)
|
||||
}
|
||||
}
|
||||
209
cmd/cheat/search_bench_test.go
Normal file
209
cmd/cheat/search_bench_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// BenchmarkSearchCommand benchmarks the actual cheat search command
|
||||
func BenchmarkSearchCommand(b *testing.B) {
|
||||
// Build the cheat binary in .tmp (using absolute path)
|
||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
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 {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
cheatBin := filepath.Join(tmpDir, "cheat-bench")
|
||||
|
||||
// Clean up the binary when done
|
||||
b.Cleanup(func() {
|
||||
os.Remove(cheatBin)
|
||||
})
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||
cmd.Dir = rootDir
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment in .tmp
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
cheatsheetDir := filepath.Join(configDir, "cheatsheets", "community")
|
||||
|
||||
// Clone community cheatsheets (or reuse if already exists)
|
||||
if _, err := os.Stat(cheatsheetDir); os.IsNotExist(err) {
|
||||
b.Logf("Cloning community cheatsheets to %s...", cheatsheetDir)
|
||||
_, err := git.PlainClone(cheatsheetDir, false, &git.CloneOptions{
|
||||
URL: "https://github.com/cheat/cheatsheets.git",
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
Progress: nil,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a minimal config file
|
||||
configFile := filepath.Join(configDir, "conf.yml")
|
||||
configContent := fmt.Sprintf(`---
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: %s
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
`, cheatsheetDir)
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
b.Fatalf("Failed to create config dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
|
||||
b.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Set environment to use our config
|
||||
env := append(os.Environ(),
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configFile),
|
||||
)
|
||||
|
||||
// Define test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"SimpleSearch", []string{"-s", "echo"}},
|
||||
{"RegexSearch", []string{"-r", "-s", "^#.*example"}},
|
||||
{"ColorizedSearch", []string{"-c", "-s", "grep"}},
|
||||
{"ComplexRegex", []string{"-r", "-s", "(git|hg|svn)\\s+(add|commit|push)"}},
|
||||
{"AllCheatpaths", []string{"-a", "-s", "list"}},
|
||||
}
|
||||
|
||||
// Warm up - run once to ensure everything is loaded
|
||||
warmupCmd := exec.Command(cheatBin, "-l")
|
||||
warmupCmd.Env = env
|
||||
warmupCmd.Run()
|
||||
|
||||
// Run benchmarks
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
// Reset timer to exclude setup
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cmd := exec.Command(cheatBin, tc.args...)
|
||||
cmd.Env = env
|
||||
|
||||
// Capture output to prevent spamming
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
start := time.Now()
|
||||
err := cmd.Run()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
b.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
// Report custom metric
|
||||
b.ReportMetric(float64(elapsed.Nanoseconds())/1e6, "ms/op")
|
||||
|
||||
// Ensure we got some results
|
||||
if stdout.Len() == 0 {
|
||||
b.Fatal("No output from search")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkListCommand benchmarks the list command for comparison
|
||||
func BenchmarkListCommand(b *testing.B) {
|
||||
// Build the cheat binary in .tmp (using absolute path)
|
||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
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 {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
cheatBin := filepath.Join(tmpDir, "cheat-bench")
|
||||
|
||||
// Clean up the binary when done
|
||||
b.Cleanup(func() {
|
||||
os.Remove(cheatBin)
|
||||
})
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||
cmd.Dir = rootDir
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment (simplified - reuse if possible)
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
cheatsheetDir := filepath.Join(configDir, "cheatsheets", "community")
|
||||
|
||||
// Check if we need to clone
|
||||
if _, err := os.Stat(cheatsheetDir); os.IsNotExist(err) {
|
||||
_, err := git.PlainClone(cheatsheetDir, false, &git.CloneOptions{
|
||||
URL: "https://github.com/cheat/cheatsheets.git",
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
Progress: nil,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create config
|
||||
configFile := filepath.Join(configDir, "conf.yml")
|
||||
configContent := fmt.Sprintf(`---
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: %s
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
`, cheatsheetDir)
|
||||
|
||||
os.MkdirAll(configDir, 0755)
|
||||
os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
|
||||
env := append(os.Environ(),
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configFile),
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cmd := exec.Command(cheatBin, "-l")
|
||||
cmd.Env = env
|
||||
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
b.Fatalf("Command failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package main
|
||||
|
||||
// Code generated .* DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func configs() string {
|
||||
return strings.TrimSpace(`---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: EDITOR_PATH
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: false
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
# https://github.com/alecthomas/chroma/tree/master/styles
|
||||
style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal256
|
||||
|
||||
# Through which pager should output be piped?
|
||||
# 'less -FRX' is recommended on Unix systems
|
||||
# 'more' is recommended on Windows
|
||||
pager: PAGER_PATH
|
||||
|
||||
# Cheatpaths are paths at which cheatsheets are available on your local
|
||||
# filesystem.
|
||||
#
|
||||
# It is useful to sort cheatsheets into different cheatpaths for organizational
|
||||
# purposes. For example, you might want one cheatpath for community
|
||||
# cheatsheets, one for personal cheatsheets, one for cheatsheets pertaining to
|
||||
# your day job, one for code snippets, etc.
|
||||
#
|
||||
# Cheatpaths are scoped, such that more "local" cheatpaths take priority over
|
||||
# more "global" cheatpaths. (The most global cheatpath is listed first in this
|
||||
# file; the most local is listed last.) For example, if there is a 'tar'
|
||||
# cheatsheet on both global and local paths, you'll be presented with the local
|
||||
# one by default. ('cheat -p' can be used to view cheatsheets from alternative
|
||||
# cheatpaths.)
|
||||
#
|
||||
# Cheatpaths can also be tagged as "read only". This instructs cheat not to
|
||||
# automatically create cheatsheets on a read-only cheatpath. Instead, when you
|
||||
# would like to edit a read-only cheatsheet using 'cheat -e', cheat will
|
||||
# perform a copy-on-write of that cheatsheet from a read-only cheatpath to a
|
||||
# writeable cheatpath.
|
||||
#
|
||||
# This is very useful when you would like to maintain, for example, a
|
||||
# "pristine" repository of community cheatsheets on one cheatpath, and an
|
||||
# editable personal reponsity of cheatsheets on another cheatpath.
|
||||
#
|
||||
# Cheatpaths can be also configured to automatically apply tags to cheatsheets
|
||||
# on certain paths, which can be useful for querying purposes.
|
||||
# Example: 'cheat -t work jenkins'.
|
||||
#
|
||||
# Community cheatsheets must be installed separately, though you may have
|
||||
# downloaded them automatically when installing 'cheat'. If not, you may
|
||||
# download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
cheatpaths:
|
||||
# Cheatpath properties mean the following:
|
||||
# 'name': the name of the cheatpath (view with 'cheat -d', filter with 'cheat -p')
|
||||
# 'path': the filesystem path of the cheatsheet directory (view with 'cheat -d')
|
||||
# 'tags': tags that should be automatically applied to sheets on this path
|
||||
# 'readonly': shall user-created ('cheat -e') cheatsheets be saved here?
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# If you have personalized cheatsheets, list them last. They will take
|
||||
# precedence over the more global cheatsheets.
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# While it requires no configuration here, it's also worth noting that
|
||||
# cheat will automatically append directories named '.cheat' within the
|
||||
# current working directory to the 'cheatpath'. This can be very useful if
|
||||
# you'd like to closely associate cheatsheets with, for example, a directory
|
||||
# containing source code.
|
||||
#
|
||||
# Such "directory-scoped" cheatsheets will be treated as the most "local"
|
||||
# cheatsheets, and will override less "local" cheatsheets. Similarly,
|
||||
# directory-scoped cheatsheets will always be editable ('readonly: false').
|
||||
`)
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
package main
|
||||
|
||||
// Code generated .* DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// usage returns the usage text for the cheat command
|
||||
func usage() string {
|
||||
return strings.TrimSpace(`Usage:
|
||||
return `Usage:
|
||||
cheat [options] [<cheatsheet>]
|
||||
|
||||
Options:
|
||||
@@ -65,6 +60,5 @@ Examples:
|
||||
cheat --rm foo/bar
|
||||
|
||||
To view the configuration file path:
|
||||
cheat --conf
|
||||
`)
|
||||
cheat --conf`
|
||||
}
|
||||
Reference in New Issue
Block a user