mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
Compare commits
3 Commits
5.1.0
...
chore/hous
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecfb83a3b0 | ||
|
|
9440b4f816 | ||
|
|
971be88150 |
@@ -1,105 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0",
|
|
||||||
"test_command": "go test ./...",
|
|
||||||
"last_updated": "2026-02-15T00:00:00Z",
|
|
||||||
"modules": {
|
|
||||||
"internal/sheet/parse.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/sheet/parse_test.go", "internal/sheet/parse_extended_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 8,
|
|
||||||
"mutations_killed": 8,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "Originally 7/8 (87.5%). Added TestHasMalformedYAML to kill YAML unmarshal error survivor."
|
|
||||||
},
|
|
||||||
"internal/config/validate.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/config/validate_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 8,
|
|
||||||
"mutations_killed": 8,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "Originally 7/8 (87.5%). Added TestInvalidateInvalidCheatpath to kill cheatpath.Validate() delegation survivor."
|
|
||||||
},
|
|
||||||
"internal/sheets/filter.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/sheets/filter_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 7,
|
|
||||||
"mutations_killed": 5,
|
|
||||||
"mutation_score": 71.4,
|
|
||||||
"notes": "Survivors relate to UTF-8 condition ordering and OR→AND on dead code path. Not actionable — logically equivalent mutations."
|
|
||||||
},
|
|
||||||
"internal/config/paths.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/config/paths_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 8,
|
|
||||||
"mutations_killed": 8,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "Perfect score. Excellent existing coverage."
|
|
||||||
},
|
|
||||||
"internal/sheet/colorize.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/sheet/colorize_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 5,
|
|
||||||
"mutations_killed": 5,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "Originally 2/5 (40%). Added TestColorizeDefaultSyntax and TestColorizeExplicitSyntax. All 5 mutations now killed."
|
|
||||||
},
|
|
||||||
"internal/sheets/consolidate.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/sheets/consolidate_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 2,
|
|
||||||
"mutations_killed": 2,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "Override semantics well-tested."
|
|
||||||
},
|
|
||||||
"internal/display/indent.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/display/indent_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 3,
|
|
||||||
"mutations_killed": 3,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "Originally 2/3 (66.7%). Added TestIndentTrimsWhitespace to kill TrimSpace survivor."
|
|
||||||
},
|
|
||||||
"internal/display/faint.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/display/faint_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 3,
|
|
||||||
"mutations_killed": 3,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "Perfect score."
|
|
||||||
},
|
|
||||||
"internal/sheets/tags.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/sheets/tags_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 2,
|
|
||||||
"mutations_killed": 2,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "UTF-8 validation and sort order both tested."
|
|
||||||
},
|
|
||||||
"internal/sheet/validate.go": {
|
|
||||||
"status": "completed",
|
|
||||||
"covering_tests": ["internal/sheet/validate_test.go"],
|
|
||||||
"last_tested": "2026-02-15T00:00:00Z",
|
|
||||||
"mutations_applied": 10,
|
|
||||||
"mutations_killed": 10,
|
|
||||||
"mutation_score": 100.0,
|
|
||||||
"notes": "Perfect score. All security checks well-tested."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"global_statistics": {
|
|
||||||
"total_modules": 10,
|
|
||||||
"completed_modules": 10,
|
|
||||||
"total_mutations": 56,
|
|
||||||
"total_killed": 54,
|
|
||||||
"total_survived": 2,
|
|
||||||
"overall_score": 96.4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
CLAUDE.md
10
CLAUDE.md
@@ -55,13 +55,9 @@ make vendor-update
|
|||||||
The `cheat` command-line tool is organized into several key packages:
|
The `cheat` command-line tool is organized into several key packages:
|
||||||
|
|
||||||
### Command Layer (`cmd/cheat/`)
|
### Command Layer (`cmd/cheat/`)
|
||||||
- `main.go`: Entry point, cobra command definition, flag registration, command routing
|
- `main.go`: Entry point, argument parsing, command routing
|
||||||
- `cmd_*.go`: Individual command implementations (view, edit, list, search, etc.)
|
- `cmd_*.go`: Individual command implementations (view, edit, list, search, etc.)
|
||||||
- Commands are routed via a `switch` block in the cobra `RunE` handler
|
- Commands are selected based on docopt parsed arguments
|
||||||
|
|
||||||
### Completions (`internal/completions/`)
|
|
||||||
- Dynamic shell completion functions for cheatsheet names, tags, and cheatpath names
|
|
||||||
- `generate.go`: Generates completion scripts for bash, zsh, fish, and powershell
|
|
||||||
|
|
||||||
### Core Internal Packages
|
### Core Internal Packages
|
||||||
|
|
||||||
@@ -123,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 `mocks` package for test data
|
- Test files use the `internal/mock` package for test data
|
||||||
12
HACKING.md
12
HACKING.md
@@ -63,7 +63,7 @@ make coverage-text # Terminal output
|
|||||||
|
|
||||||
The `cheat` application follows a clean architecture with well-separated concerns:
|
The `cheat` application follows a clean architecture with well-separated concerns:
|
||||||
|
|
||||||
- **`cmd/cheat/`**: Command layer (cobra-based CLI, flag registration, command routing, shell completions)
|
- **`cmd/cheat/`**: Command layer with argument parsing and command routing
|
||||||
- **`internal/config`**: Configuration management (YAML loading, validation, paths)
|
- **`internal/config`**: Configuration management (YAML loading, validation, paths)
|
||||||
- **`internal/cheatpath`**: Cheatsheet path management (collections, filtering)
|
- **`internal/cheatpath`**: Cheatsheet path management (collections, filtering)
|
||||||
- **`internal/sheet`**: Individual cheatsheet handling (parsing, search, highlighting)
|
- **`internal/sheet`**: Individual cheatsheet handling (parsing, search, highlighting)
|
||||||
@@ -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.Path `yaml:"cheatpaths"`
|
Cheatpaths []cp.Cheatpath `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(confPath, resolve)` - Load config from file
|
- `New(opts, 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 Path struct {
|
type Cheatpath 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 `mocks` package
|
- Mock data in `internal/mock` package
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ The codebase follows consistent error handling patterns:
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
```go
|
```go
|
||||||
s, err := sheet.New(title, cheatpath, path, tags, false)
|
sheet, err := sheet.New(path, tags, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load sheet: %w", err)
|
return fmt.Errorf("failed to load sheet: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ On Unix-like systems, you may simply paste the following snippet into your termi
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd /tmp \
|
cd /tmp \
|
||||||
&& wget https://github.com/cheat/cheat/releases/download/5.0.0/cheat-linux-amd64.gz \
|
&& wget https://github.com/cheat/cheat/releases/download/4.7.0/cheat-linux-amd64.gz \
|
||||||
&& gunzip cheat-linux-amd64.gz \
|
&& gunzip cheat-linux-amd64.gz \
|
||||||
&& chmod +x cheat-linux-amd64 \
|
&& chmod +x cheat-linux-amd64 \
|
||||||
&& sudo mv cheat-linux-amd64 /usr/local/bin/cheat
|
&& sudo mv cheat-linux-amd64 /usr/local/bin/cheat
|
||||||
```
|
```
|
||||||
|
|
||||||
You may need to need to change the version number (`5.0.0`) and the archive
|
You may need to need to change the version number (`4.7.0`) and the archive
|
||||||
(`cheat-linux-amd64.gz`) depending on your platform.
|
(`cheat-linux-amd64.gz`) depending on your platform.
|
||||||
|
|
||||||
See the [releases page][releases] for a list of supported platforms.
|
See the [releases page][releases] for a list of supported platforms.
|
||||||
@@ -24,7 +24,7 @@ On Windows, download the appropriate binary from the [releases page][releases],
|
|||||||
unzip the archive, and place the `cheat.exe` executable on your `PATH`.
|
unzip the archive, and place the `cheat.exe` executable on your `PATH`.
|
||||||
|
|
||||||
## Install via `go install`
|
## Install via `go install`
|
||||||
If you have `go` version `>=1.26` available on your `PATH`, you can install
|
If you have `go` version `>=1.17` available on your `PATH`, you can install
|
||||||
`cheat` via `go install`:
|
`cheat` via `go install`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
21
Makefile
21
Makefile
@@ -109,6 +109,11 @@ $(dist_dir)/cheat-openbsd-amd64:
|
|||||||
GOARCH=amd64 GOOS=openbsd \
|
GOARCH=amd64 GOOS=openbsd \
|
||||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||||
|
|
||||||
|
# cheat-plan9-amd64
|
||||||
|
$(dist_dir)/cheat-plan9-amd64:
|
||||||
|
GOARCH=amd64 GOOS=plan9 \
|
||||||
|
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||||
|
|
||||||
# cheat-solaris-amd64
|
# cheat-solaris-amd64
|
||||||
$(dist_dir)/cheat-solaris-amd64:
|
$(dist_dir)/cheat-solaris-amd64:
|
||||||
GOARCH=amd64 GOOS=solaris \
|
GOARCH=amd64 GOOS=solaris \
|
||||||
@@ -208,12 +213,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:
|
||||||
@./test/fuzz.sh 15s
|
@./build/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:
|
||||||
@./test/fuzz.sh 10m
|
@./build/fuzz.sh 10m
|
||||||
|
|
||||||
## coverage: generate a test coverage report
|
## coverage: generate a test coverage report
|
||||||
.PHONY: coverage
|
.PHONY: coverage
|
||||||
@@ -235,22 +240,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 ./test/integration | tee .tmp/benchmark-latest.txt && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem ./cmd/cheat | tee .tmp/benchmark-latest.txt && \
|
||||||
$(RM) -f integration.test
|
$(RM) -f cheat.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 ./test/integration && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -cpuprofile=.tmp/cpu.prof ./cmd/cheat && \
|
||||||
$(RM) -f integration.test && \
|
$(RM) -f cheat.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 ./test/integration && \
|
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem -memprofile=.tmp/mem.prof ./cmd/cheat && \
|
||||||
$(RM) -f integration.test && \
|
$(RM) -f cheat.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"
|
||||||
|
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -175,38 +175,21 @@ added to the cheatpaths. This means you can place a `.cheat` directory at your
|
|||||||
project root and it will be available from any subdirectory within that project.
|
project root and it will be available from any subdirectory within that project.
|
||||||
|
|
||||||
## Autocompletion
|
## Autocompletion
|
||||||
`cheat` can generate shell completion scripts for `bash`, `zsh`, `fish`, and
|
Shell autocompletion is currently available for `bash`, `fish`, and `zsh`. Copy
|
||||||
`powershell` via the `--completion` flag:
|
the relevant [completion script][completions] into the appropriate directory on
|
||||||
|
your filesystem to enable autocompletion. (This directory will vary depending
|
||||||
|
on operating system and shell specifics.)
|
||||||
|
|
||||||
```sh
|
Additionally, `cheat` supports enhanced autocompletion via integration with
|
||||||
cheat --completion bash
|
[fzf][]. To enable `fzf` integration:
|
||||||
cheat --completion zsh
|
|
||||||
cheat --completion fish
|
|
||||||
cheat --completion powershell
|
|
||||||
```
|
|
||||||
|
|
||||||
Pipe the output to the appropriate location for your shell. For example:
|
1. Ensure that `fzf` is available on your `$PATH`
|
||||||
|
2. Set an envvar: `export CHEAT_USE_FZF=true`
|
||||||
```sh
|
|
||||||
# bash (user-local)
|
|
||||||
mkdir -p ~/.local/share/bash-completion/completions
|
|
||||||
cheat --completion bash > ~/.local/share/bash-completion/completions/cheat
|
|
||||||
|
|
||||||
# bash (system-wide)
|
|
||||||
cheat --completion bash > /etc/bash_completion.d/cheat
|
|
||||||
|
|
||||||
# zsh (ensure the directory is on your fpath)
|
|
||||||
cheat --completion zsh > "${fpath[1]}/_cheat"
|
|
||||||
|
|
||||||
# fish
|
|
||||||
cheat --completion fish > ~/.config/fish/completions/cheat.fish
|
|
||||||
```
|
|
||||||
|
|
||||||
Completions are dynamically generated and include cheatsheet names, tags, and
|
|
||||||
cheatpath names.
|
|
||||||
|
|
||||||
[INSTALLING.md]: INSTALLING.md
|
[INSTALLING.md]: INSTALLING.md
|
||||||
[Releases]: https://github.com/cheat/cheat/releases
|
[Releases]: https://github.com/cheat/cheat/releases
|
||||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||||
|
[completions]: https://github.com/cheat/cheat/tree/master/scripts
|
||||||
[Chroma]: https://github.com/alecthomas/chroma
|
[Chroma]: https://github.com/alecthomas/chroma
|
||||||
[supported languages]: https://github.com/alecthomas/chroma#supported-languages
|
[supported languages]: https://github.com/alecthomas/chroma#supported-languages
|
||||||
|
[fzf]: https://github.com/junegunn/fzf
|
||||||
|
|||||||
@@ -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/sheet/validate.go`:
|
The validation is implemented in `internal/cheatpath/validate.go`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func Validate(name string) error {
|
func ValidateSheetName(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/sheet/validate_test.go`) verify the validation logic
|
1. **Unit tests** (`internal/cheatpath/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
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Accepted
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
In the `EnvVars()` function in `internal/config/env.go`, the code parses environment variables assuming they all contain an equals sign:
|
In `cmd/cheat/main.go` lines 47-52, the code parses environment variables assuming they all contain an equals sign:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
for _, e := range os.Environ() {
|
for _, e := range os.Environ() {
|
||||||
|
|||||||
@@ -100,5 +100,5 @@ The parallelization attempt was valuable as a learning exercise and definitively
|
|||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Benchmark implementation: test/integration/search_bench_test.go
|
- Benchmark implementation: cmd/cheat/search_bench_test.go
|
||||||
- Reverted parallel implementation: see git history (commit 82eb918)
|
- Reverted parallel implementation: see git history (commit 82eb918)
|
||||||
@@ -16,12 +16,14 @@ 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"
|
||||||
"FuzzValidate:./internal/sheet:sheet name validation (path traversal protection)"
|
"FuzzValidateSheetName:./internal/cheatpath: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 integration
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -18,8 +18,7 @@ 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, "./cmd/cheat")
|
build := exec.Command("go", "build", "-o", binPath, ".")
|
||||||
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 integration
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -32,8 +32,7 @@ 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, "./cmd/cheat")
|
build := exec.Command("go", "build", "-o", binPath, ".")
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cmdConf(_ *cobra.Command, _ []string, conf config.Config) {
|
func cmdConf(_ map[string]interface{}, conf config.Config) {
|
||||||
fmt.Println(conf.Path)
|
fmt.Println(conf.Path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
"github.com/cheat/cheat/internal/display"
|
"github.com/cheat/cheat/internal/display"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdDirectories lists the configured cheatpaths.
|
// cmdDirectories lists the configured cheatpaths.
|
||||||
func cmdDirectories(_ *cobra.Command, _ []string, conf config.Config) {
|
func cmdDirectories(_ map[string]interface{}, conf config.Config) {
|
||||||
|
|
||||||
// initialize a tabwriter to produce cleanly columnized output
|
// initialize a tabwriter to produce cleanly columnized output
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
|
|||||||
@@ -7,21 +7,18 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdEdit opens a cheatsheet for editing (or creates it if it doesn't exist).
|
// cmdEdit opens a cheatsheet for editing (or creates it if it doesn't exist).
|
||||||
func cmdEdit(cmd *cobra.Command, _ []string, conf config.Config) {
|
func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
||||||
|
|
||||||
cheatsheet, _ := cmd.Flags().GetString("edit")
|
cheatsheet := opts["--edit"].(string)
|
||||||
|
|
||||||
// validate the cheatsheet name
|
// validate the cheatsheet name
|
||||||
if err := sheet.Validate(cheatsheet); err != nil {
|
if err := cheatpath.ValidateSheetName(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)
|
||||||
}
|
}
|
||||||
@@ -32,11 +29,12 @@ func cmdEdit(cmd *cobra.Command, _ []string, 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)
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("tag") {
|
|
||||||
tagVal, _ := cmd.Flags().GetString("tag")
|
// filter cheatcheats by tag if --tag was provided
|
||||||
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
strings.Split(tagVal, ","),
|
strings.Split(opts["--tag"].(string), ","),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,36 +52,55 @@ func cmdEdit(cmd *cobra.Command, _ []string, 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 {
|
|
||||||
// for read-only or non-existent sheets, resolve a writeable path
|
// if the sheet exists but is read-only, copy it before editing
|
||||||
|
} 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the existing title for read-only copies, the requested name otherwise
|
// compute the new edit path
|
||||||
title := cheatsheet
|
editpath = filepath.Join(writepath.Path, sheet.Title)
|
||||||
if ok {
|
|
||||||
title = sheet.Title
|
|
||||||
}
|
|
||||||
editpath = filepath.Join(writepath.Path, title)
|
|
||||||
|
|
||||||
if ok {
|
// create any necessary subdirectories
|
||||||
// copy the read-only sheet to the writeable path
|
dirs := filepath.Dir(editpath)
|
||||||
// (Copy handles MkdirAll internally)
|
if dirs != "." {
|
||||||
if err := sheet.Copy(editpath); 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)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// create any necessary subdirectories for the new sheet
|
|
||||||
dirs := filepath.Dir(editpath)
|
// copy the sheet to the new edit path
|
||||||
if dirs != "." {
|
err = sheet.Copy(editpath)
|
||||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
if 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,14 +110,14 @@ func cmdEdit(cmd *cobra.Command, _ []string, conf config.Config) {
|
|||||||
// call to `exec.Command` will fail.
|
// call to `exec.Command` will fail.
|
||||||
parts := strings.Fields(conf.Editor)
|
parts := strings.Fields(conf.Editor)
|
||||||
editor := parts[0]
|
editor := parts[0]
|
||||||
editorArgs := append(parts[1:], editpath)
|
args := append(parts[1:], editpath)
|
||||||
|
|
||||||
// edit the cheatsheet
|
// edit the cheatsheet
|
||||||
editorCmd := exec.Command(editor, editorArgs...)
|
cmd := exec.Command(editor, args...)
|
||||||
editorCmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
editorCmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
editorCmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
if err := editorCmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to edit cheatsheet: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to edit cheatsheet: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,27 +3,78 @@ 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(home string, envvars map[string]string) {
|
func cmdInit() {
|
||||||
|
|
||||||
// identify the os-specific paths at which configs may be located
|
// get the user's home directory
|
||||||
|
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)
|
||||||
|
|
||||||
// expand template placeholders and comment out community cheatpath
|
// create paths for community, personal, and work cheatsheets
|
||||||
configs := installer.ExpandTemplate(configs(), confpath)
|
community := filepath.Join(confdir, "cheatsheets", "community")
|
||||||
configs = installer.CommentCommunity(configs, confpath)
|
personal := filepath.Join(confdir, "cheatsheets", "personal")
|
||||||
|
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)
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
"github.com/cheat/cheat/internal/display"
|
"github.com/cheat/cheat/internal/display"
|
||||||
"github.com/cheat/cheat/internal/sheet"
|
"github.com/cheat/cheat/internal/sheet"
|
||||||
@@ -18,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// cmdList lists all available cheatsheets.
|
// cmdList lists all available cheatsheets.
|
||||||
func cmdList(cmd *cobra.Command, args []string, conf config.Config) {
|
func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||||
|
|
||||||
// load the cheatsheets
|
// load the cheatsheets
|
||||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||||
@@ -26,11 +24,12 @@ func cmdList(cmd *cobra.Command, args []string, 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)
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("tag") {
|
|
||||||
tagVal, _ := cmd.Flags().GetString("tag")
|
// filter cheatsheets by tag if --tag was provided
|
||||||
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
strings.Split(tagVal, ","),
|
strings.Split(opts["--tag"].(string), ","),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,13 +49,16 @@ func cmdList(cmd *cobra.Command, args []string, conf config.Config) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// filter if <cheatsheet> was specified
|
// filter if <cheatsheet> was specified
|
||||||
if len(args) > 0 {
|
// NB: our docopt specification is misleading here. When used in conjunction
|
||||||
|
// with `-l`, `<cheatsheet>` is really a pattern against which to filter
|
||||||
|
// sheet titles.
|
||||||
|
if opts["<cheatsheet>"] != nil {
|
||||||
|
|
||||||
// initialize a slice of filtered sheets
|
// initialize a slice of filtered sheets
|
||||||
filtered := []sheet.Sheet{}
|
filtered := []sheet.Sheet{}
|
||||||
|
|
||||||
// initialize our filter pattern
|
// initialize our filter pattern
|
||||||
pattern := "(?i)" + args[0]
|
pattern := "(?i)" + opts["<cheatsheet>"].(string)
|
||||||
|
|
||||||
// compile the regex
|
// compile the regex
|
||||||
reg, err := regexp.Compile(pattern)
|
reg, err := regexp.Compile(pattern)
|
||||||
@@ -86,8 +88,7 @@ func cmdList(cmd *cobra.Command, args []string, conf config.Config) {
|
|||||||
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
|
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
|
||||||
|
|
||||||
// generate sorted, columnized output
|
// generate sorted, columnized output
|
||||||
briefFlag, _ := cmd.Flags().GetBool("brief")
|
if opts["--brief"].(bool) {
|
||||||
if briefFlag {
|
|
||||||
fmt.Fprintln(w, "title:\ttags:")
|
fmt.Fprintln(w, "title:\ttags:")
|
||||||
for _, sheet := range flattened {
|
for _, sheet := range flattened {
|
||||||
fmt.Fprintf(w, "%s\t%s\n", sheet.Title, strings.Join(sheet.Tags, ","))
|
fmt.Fprintf(w, "%s\t%s\n", sheet.Title, strings.Join(sheet.Tags, ","))
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdRemove removes (deletes) a cheatsheet.
|
// cmdRemove removes (deletes) a cheatsheet.
|
||||||
func cmdRemove(cmd *cobra.Command, _ []string, conf config.Config) {
|
func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
||||||
|
|
||||||
cheatsheet, _ := cmd.Flags().GetString("rm")
|
cheatsheet := opts["--rm"].(string)
|
||||||
|
|
||||||
// validate the cheatsheet name
|
// validate the cheatsheet name
|
||||||
if err := sheet.Validate(cheatsheet); err != nil {
|
if err := cheatpath.ValidateSheetName(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,11 +27,12 @@ func cmdRemove(cmd *cobra.Command, _ []string, 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)
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("tag") {
|
|
||||||
tagVal, _ := cmd.Flags().GetString("tag")
|
// filter cheatcheats by tag if --tag was provided
|
||||||
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
strings.Split(tagVal, ","),
|
strings.Split(opts["--tag"].(string), ","),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,15 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
"github.com/cheat/cheat/internal/display"
|
"github.com/cheat/cheat/internal/display"
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
"github.com/cheat/cheat/internal/sheets"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdSearch searches for strings in cheatsheets.
|
// cmdSearch searches for strings in cheatsheets.
|
||||||
func cmdSearch(cmd *cobra.Command, args []string, conf config.Config) {
|
func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||||
|
|
||||||
phrase, _ := cmd.Flags().GetString("search")
|
phrase := opts["--search"].(string)
|
||||||
colorize, _ := cmd.Flags().GetBool("colorize")
|
|
||||||
useRegex, _ := cmd.Flags().GetBool("regex")
|
|
||||||
|
|
||||||
// load the cheatsheets
|
// load the cheatsheets
|
||||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||||
@@ -26,11 +22,12 @@ func cmdSearch(cmd *cobra.Command, args []string, 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)
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("tag") {
|
|
||||||
tagVal, _ := cmd.Flags().GetString("tag")
|
// filter cheatcheats by tag if --tag was provided
|
||||||
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
strings.Split(tagVal, ","),
|
strings.Split(opts["--tag"].(string), ","),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +35,7 @@ func cmdSearch(cmd *cobra.Command, args []string, conf config.Config) {
|
|||||||
pattern := "(?i)" + phrase
|
pattern := "(?i)" + phrase
|
||||||
|
|
||||||
// unless --regex is provided, in which case we pass the regex unaltered
|
// unless --regex is provided, in which case we pass the regex unaltered
|
||||||
if useRegex {
|
if opts["--regex"] == true {
|
||||||
pattern = phrase
|
pattern = phrase
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +55,7 @@ func cmdSearch(cmd *cobra.Command, args []string, conf config.Config) {
|
|||||||
|
|
||||||
// if <cheatsheet> was provided, constrain the search only to
|
// if <cheatsheet> was provided, constrain the search only to
|
||||||
// matching cheatsheets
|
// matching cheatsheets
|
||||||
if len(args) > 0 && sheet.Title != args[0] {
|
if opts["<cheatsheet>"] != nil && sheet.Title != opts["<cheatsheet>"] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +70,7 @@ func cmdSearch(cmd *cobra.Command, args []string, conf config.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if colorization was requested, apply it here
|
// if colorization was requested, apply it here
|
||||||
if conf.Color(colorize) {
|
if conf.Color(opts) {
|
||||||
sheet.Colorize(conf)
|
sheet.Colorize(conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +80,7 @@ func cmdSearch(cmd *cobra.Command, args []string, 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.Color(colorize)),
|
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
||||||
// indent each line of content
|
// indent each line of content
|
||||||
display.Indent(sheet.Text),
|
display.Indent(sheet.Text),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
"github.com/cheat/cheat/internal/display"
|
"github.com/cheat/cheat/internal/display"
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
"github.com/cheat/cheat/internal/sheets"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdTags lists all tags in use.
|
// cmdTags lists all tags in use.
|
||||||
func cmdTags(_ *cobra.Command, _ []string, conf config.Config) {
|
func cmdTags(_ map[string]interface{}, conf config.Config) {
|
||||||
|
|
||||||
// load the cheatsheets
|
// load the cheatsheets
|
||||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
|
||||||
"github.com/cheat/cheat/internal/repo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// cmdUpdate updates git-backed cheatpaths.
|
|
||||||
func cmdUpdate(_ *cobra.Command, _ []string, conf config.Config) {
|
|
||||||
|
|
||||||
hasError := false
|
|
||||||
|
|
||||||
for _, path := range conf.Cheatpaths {
|
|
||||||
err := repo.Pull(path.Path)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
fmt.Printf("%s: ok\n", path.Name)
|
|
||||||
|
|
||||||
case errors.Is(err, git.ErrRepositoryNotExists):
|
|
||||||
fmt.Printf("%s: skipped (not a git repository)\n", path.Name)
|
|
||||||
|
|
||||||
case errors.Is(err, repo.ErrDirtyWorktree):
|
|
||||||
fmt.Printf("%s: skipped (dirty worktree)\n", path.Name)
|
|
||||||
|
|
||||||
default:
|
|
||||||
fmt.Fprintf(os.Stderr, "%s: error (%v)\n", path.Name, err)
|
|
||||||
hasError = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasError {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,19 +5,15 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
"github.com/cheat/cheat/internal/display"
|
"github.com/cheat/cheat/internal/display"
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
"github.com/cheat/cheat/internal/sheets"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdView displays a cheatsheet for viewing.
|
// cmdView displays a cheatsheet for viewing.
|
||||||
func cmdView(cmd *cobra.Command, args []string, conf config.Config) {
|
func cmdView(opts map[string]interface{}, conf config.Config) {
|
||||||
|
|
||||||
cheatsheet := args[0]
|
cheatsheet := opts["<cheatsheet>"].(string)
|
||||||
|
|
||||||
colorize, _ := cmd.Flags().GetBool("colorize")
|
|
||||||
|
|
||||||
// load the cheatsheets
|
// load the cheatsheets
|
||||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||||
@@ -25,17 +21,17 @@ func cmdView(cmd *cobra.Command, args []string, 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)
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("tag") {
|
|
||||||
tagVal, _ := cmd.Flags().GetString("tag")
|
// filter cheatcheats by tag if --tag was provided
|
||||||
|
if opts["--tag"] != nil {
|
||||||
cheatsheets = sheets.Filter(
|
cheatsheets = sheets.Filter(
|
||||||
cheatsheets,
|
cheatsheets,
|
||||||
strings.Split(tagVal, ","),
|
strings.Split(opts["--tag"].(string), ","),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if --all was passed, display cheatsheets from all cheatpaths
|
// if --all was passed, display cheatsheets from all cheatpaths
|
||||||
allFlag, _ := cmd.Flags().GetBool("all")
|
if opts["--all"].(bool) {
|
||||||
if allFlag {
|
|
||||||
// iterate over the cheatpaths
|
// iterate over the cheatpaths
|
||||||
out := ""
|
out := ""
|
||||||
for _, cheatpath := range cheatsheets {
|
for _, cheatpath := range cheatsheets {
|
||||||
@@ -46,11 +42,11 @@ func cmdView(cmd *cobra.Command, args []string, 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.Color(colorize)),
|
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
||||||
)
|
)
|
||||||
|
|
||||||
// apply colorization if requested
|
// apply colorization if requested
|
||||||
if conf.Color(colorize) {
|
if conf.Color(opts) {
|
||||||
sheet.Colorize(conf)
|
sheet.Colorize(conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +73,7 @@ func cmdView(cmd *cobra.Command, args []string, conf config.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply colorization if requested
|
// apply colorization if requested
|
||||||
if conf.Color(colorize) {
|
if conf.Color(opts) {
|
||||||
sheet.Colorize(conf)
|
sheet.Colorize(conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package integration
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -19,8 +19,7 @@ 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, "./cmd/cheat")
|
build := exec.Command("go", "build", "-o", binPath, ".")
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -5,144 +5,34 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docopt/docopt-go"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/cheatpath"
|
"github.com/cheat/cheat/internal/cheatpath"
|
||||||
"github.com/cheat/cheat/internal/completions"
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
"github.com/cheat/cheat/internal/config"
|
||||||
"github.com/cheat/cheat/internal/installer"
|
"github.com/cheat/cheat/internal/installer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "5.1.0"
|
const version = "4.7.0"
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "cheat [cheatsheet]",
|
|
||||||
Short: "Create and view interactive cheatsheets on the command-line",
|
|
||||||
Long: `cheat allows you to create and view interactive cheatsheets on the
|
|
||||||
command-line. It was designed to help remind *nix system administrators of
|
|
||||||
options for commands that they use frequently, but not frequently enough to
|
|
||||||
remember.`,
|
|
||||||
Example: ` 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 briefly list all cheatsheets whose titles match "apt":
|
|
||||||
cheat -b 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 update all git-backed cheatpaths:
|
|
||||||
cheat --update
|
|
||||||
|
|
||||||
To view the configuration file path:
|
|
||||||
cheat --conf
|
|
||||||
|
|
||||||
To generate shell completions (bash, zsh, fish, powershell):
|
|
||||||
cheat --completion bash`,
|
|
||||||
RunE: run,
|
|
||||||
Args: cobra.MaximumNArgs(1),
|
|
||||||
SilenceErrors: true,
|
|
||||||
SilenceUsage: true,
|
|
||||||
ValidArgsFunction: completions.Cheatsheets,
|
|
||||||
CompletionOptions: cobra.CompletionOptions{
|
|
||||||
DisableDefaultCmd: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
f := rootCmd.Flags()
|
|
||||||
|
|
||||||
// bool flags
|
|
||||||
f.BoolP("all", "a", false, "Search among all cheatpaths")
|
|
||||||
f.BoolP("brief", "b", false, "List cheatsheets without file paths")
|
|
||||||
f.BoolP("colorize", "c", false, "Colorize output")
|
|
||||||
f.BoolP("directories", "d", false, "List cheatsheet directories")
|
|
||||||
f.Bool("init", false, "Write a default config file to stdout")
|
|
||||||
f.BoolP("list", "l", false, "List cheatsheets")
|
|
||||||
f.BoolP("regex", "r", false, "Treat search <phrase> as a regex")
|
|
||||||
f.BoolP("tags", "T", false, "List all tags in use")
|
|
||||||
f.BoolP("update", "u", false, "Update git-backed cheatpaths")
|
|
||||||
f.BoolP("version", "v", false, "Print the version number")
|
|
||||||
f.Bool("conf", false, "Display the config file path")
|
|
||||||
|
|
||||||
// string flags
|
|
||||||
f.StringP("edit", "e", "", "Edit `cheatsheet`")
|
|
||||||
f.StringP("path", "p", "", "Return only sheets found on cheatpath `name`")
|
|
||||||
f.StringP("search", "s", "", "Search cheatsheets for `phrase`")
|
|
||||||
f.StringP("tag", "t", "", "Return only sheets matching `tag`")
|
|
||||||
f.String("rm", "", "Remove (delete) `cheatsheet`")
|
|
||||||
f.String("completion", "", "Generate shell completion script (`shell`: bash, zsh, fish, powershell)")
|
|
||||||
|
|
||||||
// register flag completion functions
|
|
||||||
rootCmd.RegisterFlagCompletionFunc("tag", completions.Tags)
|
|
||||||
rootCmd.RegisterFlagCompletionFunc("path", completions.Paths)
|
|
||||||
rootCmd.RegisterFlagCompletionFunc("edit", completions.Cheatsheets)
|
|
||||||
rootCmd.RegisterFlagCompletionFunc("rm", completions.Cheatsheets)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
// initialize options
|
||||||
os.Exit(1)
|
opts, err := docopt.ParseArgs(usage(), nil, version)
|
||||||
|
if err != nil {
|
||||||
|
// panic here, because this should never happen
|
||||||
|
panic(fmt.Errorf("docopt failed to parse: %v", err))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func run(cmd *cobra.Command, args []string) error {
|
// if --init was passed, we don't want to attempt to load a config file.
|
||||||
f := cmd.Flags()
|
// Instead, just execute cmd_init and exit
|
||||||
|
if opts["--init"] != nil && opts["--init"] == true {
|
||||||
// handle --init early (no config needed)
|
cmdInit()
|
||||||
if initFlag, _ := f.GetBool("init"); initFlag {
|
|
||||||
home, err := homedir.Dir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "failed to get user home directory: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
envvars := config.EnvVars()
|
|
||||||
cmdInit(home, envvars)
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle --version early
|
|
||||||
if versionFlag, _ := f.GetBool("version"); versionFlag {
|
|
||||||
fmt.Println(version)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle --completion early (no config needed)
|
|
||||||
if f.Changed("completion") {
|
|
||||||
shell, _ := f.GetString("completion")
|
|
||||||
return completions.Generate(cmd, shell, os.Stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -151,9 +41,17 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// read the envvars into a map of strings
|
// read the envvars into a map of strings
|
||||||
envvars := config.EnvVars()
|
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])
|
||||||
|
}
|
||||||
|
envvars[pair[0]] = pair[1]
|
||||||
|
}
|
||||||
|
|
||||||
// identify the os-specific 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 {
|
||||||
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
|
||||||
@@ -194,7 +92,7 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initialize the configs
|
// initialize the configs
|
||||||
conf, err := config.New(confpath, true)
|
conf, err := config.New(opts, 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)
|
||||||
@@ -207,11 +105,10 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// filter the cheatpaths if --path was passed
|
// filter the cheatpaths if --path was passed
|
||||||
if f.Changed("path") {
|
if opts["--path"] != nil {
|
||||||
pathVal, _ := f.GetString("path")
|
|
||||||
conf.Cheatpaths, err = cheatpath.Filter(
|
conf.Cheatpaths, err = cheatpath.Filter(
|
||||||
conf.Cheatpaths,
|
conf.Cheatpaths,
|
||||||
pathVal,
|
opts["--path"].(string),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "invalid option --path: %v\n", err)
|
fmt.Fprintf(os.Stderr, "invalid option --path: %v\n", err)
|
||||||
@@ -220,48 +117,41 @@ func run(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// determine which command to execute
|
// determine which command to execute
|
||||||
confFlag, _ := f.GetBool("conf")
|
var cmd func(map[string]interface{}, config.Config)
|
||||||
dirFlag, _ := f.GetBool("directories")
|
|
||||||
listFlag, _ := f.GetBool("list")
|
|
||||||
briefFlag, _ := f.GetBool("brief")
|
|
||||||
tagsFlag, _ := f.GetBool("tags")
|
|
||||||
updateFlag, _ := f.GetBool("update")
|
|
||||||
tagVal, _ := f.GetString("tag")
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case confFlag:
|
case opts["--conf"].(bool):
|
||||||
cmdConf(cmd, args, conf)
|
cmd = cmdConf
|
||||||
|
|
||||||
case dirFlag:
|
case opts["--directories"].(bool):
|
||||||
cmdDirectories(cmd, args, conf)
|
cmd = cmdDirectories
|
||||||
|
|
||||||
case f.Changed("edit"):
|
case opts["--edit"] != nil:
|
||||||
cmdEdit(cmd, args, conf)
|
cmd = cmdEdit
|
||||||
|
|
||||||
case listFlag, briefFlag:
|
case opts["--list"].(bool), opts["--brief"].(bool):
|
||||||
cmdList(cmd, args, conf)
|
cmd = cmdList
|
||||||
|
|
||||||
case tagsFlag:
|
case opts["--tags"].(bool):
|
||||||
cmdTags(cmd, args, conf)
|
cmd = cmdTags
|
||||||
|
|
||||||
case updateFlag:
|
case opts["--search"] != nil:
|
||||||
cmdUpdate(cmd, args, conf)
|
cmd = cmdSearch
|
||||||
|
|
||||||
case f.Changed("search"):
|
case opts["--rm"] != nil:
|
||||||
cmdSearch(cmd, args, conf)
|
cmd = cmdRemove
|
||||||
|
|
||||||
case f.Changed("rm"):
|
case opts["<cheatsheet>"] != nil:
|
||||||
cmdRemove(cmd, args, conf)
|
cmd = cmdView
|
||||||
|
|
||||||
case len(args) > 0:
|
case opts["--tag"] != nil && opts["--tag"].(string) != "":
|
||||||
cmdView(cmd, args, conf)
|
cmd = cmdList
|
||||||
|
|
||||||
case tagVal != "":
|
|
||||||
cmdList(cmd, args, conf)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return cmd.Help()
|
fmt.Println(usage())
|
||||||
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// execute the command
|
||||||
|
cmd(opts, conf)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package integration
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -19,9 +19,7 @@ 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")
|
||||||
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,9 +159,7 @@ func TestPathTraversalRealWorld(t *testing.T) {
|
|||||||
|
|
||||||
// Build cheat
|
// Build cheat
|
||||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
build := exec.Command("go", "build", "-o", binPath, "./cmd/cheat")
|
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||||
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 integration
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -16,10 +16,12 @@ 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)
|
||||||
tmpDir := filepath.Join(root, ".tmp", "bench-test")
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
@@ -32,7 +34,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 = root
|
cmd.Dir = rootDir
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -124,10 +126,12 @@ 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)
|
||||||
tmpDir := filepath.Join(root, ".tmp", "bench-test")
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
@@ -140,7 +144,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 = root
|
cmd.Dir = rootDir
|
||||||
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)
|
||||||
}
|
}
|
||||||
65
cmd/cheat/usage.go
Normal file
65
cmd/cheat/usage.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// usage returns the usage text for the cheat command
|
||||||
|
func usage() string {
|
||||||
|
return `Usage:
|
||||||
|
cheat [options] [<cheatsheet>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--init Write a default config file to stdout
|
||||||
|
-a --all Search among all cheatpaths
|
||||||
|
-b --brief List cheatsheets without file paths
|
||||||
|
-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 briefly list all cheatsheets whose titles match "apt":
|
||||||
|
cheat -b 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`
|
||||||
|
}
|
||||||
@@ -77,4 +77,4 @@ multiple cheatpaths can configure them in `conf.yml`.
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- GitHub issue: #602
|
- GitHub issue: #602
|
||||||
- Implementation: `findLocalCheatpath()` in `internal/config/new.go`
|
- Implementation: `findLocalCheatpath()` in `internal/config/config.go`
|
||||||
89
doc/cheat.1
89
doc/cheat.1
@@ -53,19 +53,11 @@ Filter only to sheets tagged with \f[I]TAG\f[R].
|
|||||||
\-T, \[en]tags
|
\-T, \[en]tags
|
||||||
List all tags in use.
|
List all tags in use.
|
||||||
.TP
|
.TP
|
||||||
\-u, \[en]update
|
|
||||||
Update git\-backed cheatpaths by pulling the latest changes.
|
|
||||||
.TP
|
|
||||||
\-v, \[en]version
|
\-v, \[en]version
|
||||||
Print the version number.
|
Print the version number.
|
||||||
.TP
|
.TP
|
||||||
\[en]rm=\f[I]CHEATSHEET\f[R]
|
\[en]rm=\f[I]CHEATSHEET\f[R]
|
||||||
Remove (deletes) \f[I]CHEATSHEET\f[R].
|
Remove (deletes) \f[I]CHEATSHEET\f[R].
|
||||||
.TP
|
|
||||||
\[en]completion=\f[I]SHELL\f[R]
|
|
||||||
Generate a shell completion script.
|
|
||||||
\f[I]SHELL\f[R] must be one of: \f[B]bash\f[R], \f[B]zsh\f[R],
|
|
||||||
\f[B]fish\f[R], \f[B]powershell\f[R].
|
|
||||||
.SH EXAMPLES
|
.SH EXAMPLES
|
||||||
.TP
|
.TP
|
||||||
To view the foo cheatsheet:
|
To view the foo cheatsheet:
|
||||||
@@ -101,31 +93,23 @@ cheat \-c \-r \-s \f[I]`(?:[0\-9]{1,3}.){3}[0\-9]{1,3}'\f[R]
|
|||||||
To remove (delete) the foo/bar cheatsheet:
|
To remove (delete) the foo/bar cheatsheet:
|
||||||
cheat \[en]rm \f[I]foo/bar\f[R]
|
cheat \[en]rm \f[I]foo/bar\f[R]
|
||||||
.TP
|
.TP
|
||||||
To update all git\-backed cheatpaths:
|
|
||||||
cheat \[en]update
|
|
||||||
.TP
|
|
||||||
To update only the `community' cheatpath:
|
|
||||||
cheat \-u \-p \f[I]community\f[R]
|
|
||||||
.TP
|
|
||||||
To view the configuration file path:
|
To view the configuration file path:
|
||||||
cheat \[en]conf
|
cheat \[en]conf
|
||||||
.SH FILES
|
.SH FILES
|
||||||
.SS Configuration
|
.SS Configuration
|
||||||
\f[B]cheat\f[R] is configured via a YAML file that is conventionally
|
\f[B]cheat\f[R] is configured via a YAML file that is conventionally
|
||||||
named \f[I]conf.yml\f[R].
|
named \f[I]conf.yaml\f[R].
|
||||||
\f[B]cheat\f[R] will search for \f[I]conf.yml\f[R] in varying locations,
|
\f[B]cheat\f[R] will search for \f[I]conf.yaml\f[R] in varying
|
||||||
depending upon your platform:
|
locations, depending upon your platform:
|
||||||
.SS Linux, OSX, and other Unixes
|
.SS Linux, OSX, and other Unixes
|
||||||
.IP "1." 3
|
.IP "1." 3
|
||||||
\f[B]CHEAT_CONFIG_PATH\f[R]
|
\f[B]CHEAT_CONFIG_PATH\f[R]
|
||||||
.IP "2." 3
|
.IP "2." 3
|
||||||
\f[B]XDG_CONFIG_HOME\f[R]/cheat/conf.yml
|
\f[B]XDG_CONFIG_HOME\f[R]/cheat/conf.yaml
|
||||||
.IP "3." 3
|
.IP "3." 3
|
||||||
\f[B]$HOME\f[R]/.config/cheat/conf.yml
|
\f[B]$HOME\f[R]/.config/cheat/conf.yml
|
||||||
.IP "4." 3
|
.IP "4." 3
|
||||||
\f[B]$HOME\f[R]/.cheat/conf.yml
|
\f[B]$HOME\f[R]/.cheat/conf.yml
|
||||||
.IP "5." 3
|
|
||||||
/etc/cheat/conf.yml
|
|
||||||
.SS Windows
|
.SS Windows
|
||||||
.IP "1." 3
|
.IP "1." 3
|
||||||
\f[B]CHEAT_CONFIG_PATH\f[R]
|
\f[B]CHEAT_CONFIG_PATH\f[R]
|
||||||
@@ -135,7 +119,7 @@ depending upon your platform:
|
|||||||
\f[B]PROGRAMDATA\f[R]/cheat/conf.yml
|
\f[B]PROGRAMDATA\f[R]/cheat/conf.yml
|
||||||
.PP
|
.PP
|
||||||
\f[B]cheat\f[R] will search in the order specified above.
|
\f[B]cheat\f[R] will search in the order specified above.
|
||||||
The first \f[I]conf.yml\f[R] encountered will be respected.
|
The first \f[I]conf.yaml\f[R] encountered will be respected.
|
||||||
.PP
|
.PP
|
||||||
If \f[B]cheat\f[R] cannot locate a config file, it will ask if you\[cq]d
|
If \f[B]cheat\f[R] cannot locate a config file, it will ask if you\[cq]d
|
||||||
like to generate one automatically.
|
like to generate one automatically.
|
||||||
@@ -145,58 +129,43 @@ location for your platform.
|
|||||||
.SS Cheatpaths
|
.SS Cheatpaths
|
||||||
\f[B]cheat\f[R] reads its cheatsheets from \[lq]cheatpaths\[rq], which
|
\f[B]cheat\f[R] reads its cheatsheets from \[lq]cheatpaths\[rq], which
|
||||||
are the directories in which cheatsheets are stored.
|
are the directories in which cheatsheets are stored.
|
||||||
Cheatpaths may be configured in \f[I]conf.yml\f[R], and viewed via
|
Cheatpaths may be configured in \f[I]conf.yaml\f[R], and viewed via
|
||||||
\f[B]cheat \-d\f[R].
|
\f[B]cheat \-d\f[R].
|
||||||
.PP
|
.PP
|
||||||
For detailed instructions on how to configure cheatpaths, please refer
|
For detailed instructions on how to configure cheatpaths, please refer
|
||||||
to the comments in conf.yml.
|
to the comments in conf.yml.
|
||||||
.SS Autocompletion
|
.SS Autocompletion
|
||||||
\f[B]cheat\f[R] can generate shell completion scripts for
|
Autocompletion scripts for \f[B]bash\f[R], \f[B]zsh\f[R], and
|
||||||
\f[B]bash\f[R], \f[B]zsh\f[R], \f[B]fish\f[R], and \f[B]powershell\f[R]
|
\f[B]fish\f[R] are available for download:
|
||||||
via the \f[B]\[en]completion\f[R] flag:
|
.IP \[bu] 2
|
||||||
.IP
|
\c
|
||||||
.EX
|
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.bash
|
||||||
cheat \-\-completion bash
|
.UE \c
|
||||||
cheat \-\-completion zsh
|
.IP \[bu] 2
|
||||||
cheat \-\-completion fish
|
\c
|
||||||
cheat \-\-completion powershell
|
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.fish
|
||||||
.EE
|
.UE \c
|
||||||
|
.IP \[bu] 2
|
||||||
|
\c
|
||||||
|
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.zsh
|
||||||
|
.UE \c
|
||||||
.PP
|
.PP
|
||||||
Completions are dynamically generated and include cheatsheet names,
|
The \f[B]bash\f[R] and \f[B]zsh\f[R] scripts provide optional
|
||||||
tags, and cheatpath names.
|
integration with \f[B]fzf\f[R], if the latter is available on your
|
||||||
|
\f[B]PATH\f[R].
|
||||||
.PP
|
.PP
|
||||||
To install completions, pipe the output to the appropriate location for
|
The installation process will vary per system and shell configuration,
|
||||||
your shell.
|
and thus will not be discussed here.
|
||||||
For example, on \f[B]bash\f[R]:
|
|
||||||
.IP
|
|
||||||
.EX
|
|
||||||
cheat \-\-completion bash > /etc/bash_completion.d/cheat
|
|
||||||
.EE
|
|
||||||
.PP
|
|
||||||
Or for the current user only:
|
|
||||||
.IP
|
|
||||||
.EX
|
|
||||||
cheat \-\-completion bash > \[ti]/.local/share/bash\-completion/completions/cheat
|
|
||||||
.EE
|
|
||||||
.PP
|
|
||||||
For \f[B]zsh\f[R], you may need to add the completions directory to your
|
|
||||||
\f[B]fpath\f[R]:
|
|
||||||
.IP
|
|
||||||
.EX
|
|
||||||
cheat \-\-completion zsh > \[dq]${fpath[1]}/_cheat\[dq]
|
|
||||||
.EE
|
|
||||||
.PP
|
|
||||||
For \f[B]fish\f[R]:
|
|
||||||
.IP
|
|
||||||
.EX
|
|
||||||
cheat \-\-completion fish > \[ti]/.config/fish/completions/cheat.fish
|
|
||||||
.EE
|
|
||||||
.SH ENVIRONMENT
|
.SH ENVIRONMENT
|
||||||
.TP
|
.TP
|
||||||
\f[B]CHEAT_CONFIG_PATH\f[R]
|
\f[B]CHEAT_CONFIG_PATH\f[R]
|
||||||
The path at which the config file is available.
|
The path at which the config file is available.
|
||||||
If \f[B]CHEAT_CONFIG_PATH\f[R] is set, all other config paths will be
|
If \f[B]CHEAT_CONFIG_PATH\f[R] is set, all other config paths will be
|
||||||
ignored.
|
ignored.
|
||||||
|
.TP
|
||||||
|
\f[B]CHEAT_USE_FZF\f[R]
|
||||||
|
If set, autocompletion scripts will attempt to integrate with
|
||||||
|
\f[B]fzf\f[R].
|
||||||
.SH RETURN VALUES
|
.SH RETURN VALUES
|
||||||
.IP "0." 3
|
.IP "0." 3
|
||||||
Successful termination
|
Successful termination
|
||||||
|
|||||||
@@ -59,19 +59,12 @@ OPTIONS
|
|||||||
-T, --tags
|
-T, --tags
|
||||||
: List all tags in use.
|
: List all tags in use.
|
||||||
|
|
||||||
-u, --update
|
|
||||||
: Update git-backed cheatpaths by pulling the latest changes.
|
|
||||||
|
|
||||||
-v, --version
|
-v, --version
|
||||||
: Print the version number.
|
: Print the version number.
|
||||||
|
|
||||||
--rm=_CHEATSHEET_
|
--rm=_CHEATSHEET_
|
||||||
: Remove (deletes) _CHEATSHEET_.
|
: Remove (deletes) _CHEATSHEET_.
|
||||||
|
|
||||||
--completion=_SHELL_
|
|
||||||
: Generate a shell completion script. _SHELL_ must be one of: **bash**,
|
|
||||||
**zsh**, **fish**, **powershell**.
|
|
||||||
|
|
||||||
|
|
||||||
EXAMPLES
|
EXAMPLES
|
||||||
========
|
========
|
||||||
@@ -109,12 +102,6 @@ To search (by regex) for cheatsheets that contain an IP address:
|
|||||||
To remove (delete) the foo/bar cheatsheet:
|
To remove (delete) the foo/bar cheatsheet:
|
||||||
: cheat --rm _foo/bar_
|
: cheat --rm _foo/bar_
|
||||||
|
|
||||||
To update all git-backed cheatpaths:
|
|
||||||
: cheat --update
|
|
||||||
|
|
||||||
To update only the 'community' cheatpath:
|
|
||||||
: cheat -u -p _community_
|
|
||||||
|
|
||||||
To view the configuration file path:
|
To view the configuration file path:
|
||||||
: cheat --conf
|
: cheat --conf
|
||||||
|
|
||||||
@@ -125,16 +112,15 @@ FILES
|
|||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
**cheat** is configured via a YAML file that is conventionally named
|
**cheat** is configured via a YAML file that is conventionally named
|
||||||
_conf.yml_. **cheat** will search for _conf.yml_ in varying locations,
|
_conf.yaml_. **cheat** will search for _conf.yaml_ in varying locations,
|
||||||
depending upon your platform:
|
depending upon your platform:
|
||||||
|
|
||||||
### Linux, OSX, and other Unixes ###
|
### Linux, OSX, and other Unixes ###
|
||||||
|
|
||||||
1. **CHEAT_CONFIG_PATH**
|
1. **CHEAT_CONFIG_PATH**
|
||||||
2. **XDG_CONFIG_HOME**/cheat/conf.yml
|
2. **XDG_CONFIG_HOME**/cheat/conf.yaml
|
||||||
3. **$HOME**/.config/cheat/conf.yml
|
3. **$HOME**/.config/cheat/conf.yml
|
||||||
4. **$HOME**/.cheat/conf.yml
|
4. **$HOME**/.cheat/conf.yml
|
||||||
5. /etc/cheat/conf.yml
|
|
||||||
|
|
||||||
### Windows ###
|
### Windows ###
|
||||||
|
|
||||||
@@ -142,7 +128,7 @@ depending upon your platform:
|
|||||||
2. **APPDATA**/cheat/conf.yml
|
2. **APPDATA**/cheat/conf.yml
|
||||||
3. **PROGRAMDATA**/cheat/conf.yml
|
3. **PROGRAMDATA**/cheat/conf.yml
|
||||||
|
|
||||||
**cheat** will search in the order specified above. The first _conf.yml_
|
**cheat** will search in the order specified above. The first _conf.yaml_
|
||||||
encountered will be respected.
|
encountered will be respected.
|
||||||
|
|
||||||
If **cheat** cannot locate a config file, it will ask if you'd like to generate
|
If **cheat** cannot locate a config file, it will ask if you'd like to generate
|
||||||
@@ -154,7 +140,7 @@ for your platform.
|
|||||||
Cheatpaths
|
Cheatpaths
|
||||||
----------
|
----------
|
||||||
**cheat** reads its cheatsheets from "cheatpaths", which are the directories in
|
**cheat** reads its cheatsheets from "cheatpaths", which are the directories in
|
||||||
which cheatsheets are stored. Cheatpaths may be configured in _conf.yml_, and
|
which cheatsheets are stored. Cheatpaths may be configured in _conf.yaml_, and
|
||||||
viewed via **cheat -d**.
|
viewed via **cheat -d**.
|
||||||
|
|
||||||
For detailed instructions on how to configure cheatpaths, please refer to the
|
For detailed instructions on how to configure cheatpaths, please refer to the
|
||||||
@@ -163,33 +149,18 @@ comments in conf.yml.
|
|||||||
|
|
||||||
Autocompletion
|
Autocompletion
|
||||||
--------------
|
--------------
|
||||||
**cheat** can generate shell completion scripts for **bash**, **zsh**,
|
Autocompletion scripts for **bash**, **zsh**, and **fish** are available for
|
||||||
**fish**, and **powershell** via the **--completion** flag:
|
download:
|
||||||
|
|
||||||
cheat --completion bash
|
- <https://github.com/cheat/cheat/blob/master/scripts/cheat.bash>
|
||||||
cheat --completion zsh
|
- <https://github.com/cheat/cheat/blob/master/scripts/cheat.fish>
|
||||||
cheat --completion fish
|
- <https://github.com/cheat/cheat/blob/master/scripts/cheat.zsh>
|
||||||
cheat --completion powershell
|
|
||||||
|
|
||||||
Completions are dynamically generated and include cheatsheet names, tags, and
|
The **bash** and **zsh** scripts provide optional integration with **fzf**, if
|
||||||
cheatpath names.
|
the latter is available on your **PATH**.
|
||||||
|
|
||||||
To install completions, pipe the output to the appropriate location for your
|
The installation process will vary per system and shell configuration, and thus
|
||||||
shell. For example, on **bash**:
|
will not be discussed here.
|
||||||
|
|
||||||
cheat --completion bash > /etc/bash_completion.d/cheat
|
|
||||||
|
|
||||||
Or for the current user only:
|
|
||||||
|
|
||||||
cheat --completion bash > ~/.local/share/bash-completion/completions/cheat
|
|
||||||
|
|
||||||
For **zsh**, you may need to add the completions directory to your **fpath**:
|
|
||||||
|
|
||||||
cheat --completion zsh > "${fpath[1]}/_cheat"
|
|
||||||
|
|
||||||
For **fish**:
|
|
||||||
|
|
||||||
cheat --completion fish > ~/.config/fish/completions/cheat.fish
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT
|
ENVIRONMENT
|
||||||
@@ -200,6 +171,10 @@ ENVIRONMENT
|
|||||||
: The path at which the config file is available. If **CHEAT_CONFIG_PATH** is
|
: The path at which the config file is available. If **CHEAT_CONFIG_PATH** is
|
||||||
set, all other config paths will be ignored.
|
set, all other config paths will be ignored.
|
||||||
|
|
||||||
|
**CHEAT_USE_FZF**
|
||||||
|
|
||||||
|
: If set, autocompletion scripts will attempt to integrate with **fzf**.
|
||||||
|
|
||||||
RETURN VALUES
|
RETURN VALUES
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -5,10 +5,10 @@ go 1.26
|
|||||||
require (
|
require (
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
github.com/go-git/go-git/v5 v5.16.5
|
github.com/go-git/go-git/v5 v5.16.5
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/spf13/cobra v1.10.2
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,14 +23,12 @@ require (
|
|||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.5.0 // indirect
|
github.com/kevinburke/ssh_config v1.5.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
|
|||||||
11
go.sum
11
go.sum
@@ -17,7 +17,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
|||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -25,6 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||||
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
@@ -45,8 +46,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+PRLSPpA=
|
github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+PRLSPpA=
|
||||||
@@ -74,16 +73,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
|
||||||
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
@@ -91,7 +85,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
|||||||
@@ -2,10 +2,27 @@
|
|||||||
// management.
|
// management.
|
||||||
package cheatpath
|
package cheatpath
|
||||||
|
|
||||||
// Path encapsulates cheatsheet path information
|
import "fmt"
|
||||||
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 Path
|
cheatpath Cheatpath
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errMsg string
|
errMsg string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid cheatpath",
|
name: "valid cheatpath",
|
||||||
cheatpath: Path{
|
cheatpath: Cheatpath{
|
||||||
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: Path{
|
cheatpath: Cheatpath{
|
||||||
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: Path{
|
cheatpath: Cheatpath{
|
||||||
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: Path{
|
cheatpath: Cheatpath{
|
||||||
Name: "",
|
Name: "",
|
||||||
Path: "",
|
Path: "",
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
@@ -57,7 +57,7 @@ func TestCheatpathValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "minimal valid",
|
name: "minimal valid",
|
||||||
cheatpath: Path{
|
cheatpath: Cheatpath{
|
||||||
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: Path{
|
cheatpath: Cheatpath{
|
||||||
Name: "community",
|
Name: "community",
|
||||||
Path: "/usr/share/cheat",
|
Path: "/usr/share/cheat",
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
|
|||||||
64
internal/cheatpath/doc.go
Normal file
64
internal/cheatpath/doc.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// 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 []Path, name string) ([]Path, error) {
|
func Filter(paths []Cheatpath, name string) ([]Cheatpath, 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 []Path{path}, nil
|
return []Cheatpath{path}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, return an error
|
// otherwise, return an error
|
||||||
return []Path{}, fmt.Errorf("cheatpath does not exist: %s", name)
|
return []Cheatpath{}, 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 := []Path{
|
paths := []Cheatpath{
|
||||||
Path{Name: "foo"},
|
Cheatpath{Name: "foo"},
|
||||||
Path{Name: "bar"},
|
Cheatpath{Name: "bar"},
|
||||||
Path{Name: "baz"},
|
Cheatpath{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 := []Path{
|
paths := []Cheatpath{
|
||||||
Path{Name: "foo"},
|
Cheatpath{Name: "foo"},
|
||||||
Path{Name: "bar"},
|
Cheatpath{Name: "bar"},
|
||||||
Path{Name: "baz"},
|
Cheatpath{Name: "baz"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter the paths
|
// filter the paths
|
||||||
|
|||||||
@@ -2,15 +2,39 @@ package cheatpath
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validate ensures that the Path is valid
|
// ValidateSheetName ensures that a cheatsheet name does not contain
|
||||||
func (c Path) Validate() error {
|
// directory traversal sequences or other potentially dangerous patterns.
|
||||||
if c.Name == "" {
|
func ValidateSheetName(name string) error {
|
||||||
return fmt.Errorf("cheatpath name cannot be empty")
|
// Reject empty names
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("cheatsheet name cannot be empty")
|
||||||
}
|
}
|
||||||
if c.Path == "" {
|
|
||||||
return fmt.Errorf("cheatpath path 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package sheet
|
package cheatpath
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FuzzValidate tests the Validate function with fuzzing
|
// FuzzValidateSheetName tests the ValidateSheetName 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 FuzzValidate(f *testing.F) {
|
func FuzzValidateSheetName(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 FuzzValidate(f *testing.F) {
|
|||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("Validate panicked with input %q: %v", input, r)
|
t.Errorf("ValidateSheetName panicked with input %q: %v", input, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := Validate(input)
|
err := ValidateSheetName(input)
|
||||||
|
|
||||||
// Security invariants that must always hold
|
// Security invariants that must always hold
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -129,8 +129,8 @@ func FuzzValidate(f *testing.F) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FuzzValidatePathTraversal specifically targets path traversal bypasses
|
// FuzzValidateSheetNamePathTraversal specifically targets path traversal bypasses
|
||||||
func FuzzValidatePathTraversal(f *testing.F) {
|
func FuzzValidateSheetNamePathTraversal(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 FuzzValidatePathTraversal(f *testing.F) {
|
|||||||
func() {
|
func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
t.Errorf("Validate panicked with constructed input %q: %v", input, r)
|
t.Errorf("ValidateSheetName panicked with constructed input %q: %v", input, r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := Validate(input)
|
err := ValidateSheetName(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 sheet
|
package cheatpath
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidate(t *testing.T) {
|
func TestValidateSheetName(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
@@ -98,14 +98,14 @@ func TestValidate(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 := Validate(tt.input)
|
err := ValidateSheetName(tt.input)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("Validate(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
t.Errorf("ValidateName(%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("Validate(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
t.Errorf("ValidateName(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Writeable returns a writeable Path
|
// Writeable returns a writeable Cheatpath
|
||||||
func Writeable(cheatpaths []Path) (Path, error) {
|
func Writeable(cheatpaths []Cheatpath) (Cheatpath, 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 []Path) (Path, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, return an error
|
// otherwise, return an error
|
||||||
return Path{}, fmt.Errorf("no writeable cheatpaths found")
|
return Cheatpath{}, 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 := []Path{
|
cheatpaths := []Cheatpath{
|
||||||
Path{Path: "/foo", ReadOnly: true},
|
Cheatpath{Path: "/foo", ReadOnly: true},
|
||||||
Path{Path: "/bar", ReadOnly: false},
|
Cheatpath{Path: "/bar", ReadOnly: false},
|
||||||
Path{Path: "/baz", ReadOnly: true},
|
Cheatpath{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 := []Path{
|
cheatpaths := []Cheatpath{
|
||||||
Path{Path: "/foo", ReadOnly: true},
|
Cheatpath{Path: "/foo", ReadOnly: true},
|
||||||
Path{Path: "/bar", ReadOnly: true},
|
Cheatpath{Path: "/bar", ReadOnly: true},
|
||||||
Path{Path: "/baz", ReadOnly: true},
|
Cheatpath{Path: "/baz", ReadOnly: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the writeable cheatpath
|
// get the writeable cheatpath
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
// Package completions provides dynamic shell completion functions and
|
|
||||||
// completion script generation for the cheat CLI.
|
|
||||||
package completions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cheatsheets provides completion for cheatsheet names.
|
|
||||||
func Cheatsheets(
|
|
||||||
_ *cobra.Command,
|
|
||||||
args []string,
|
|
||||||
_ string,
|
|
||||||
) ([]string, cobra.ShellCompDirective) {
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
|
|
||||||
conf, err := loadConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
|
|
||||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
|
|
||||||
consolidated := sheets.Consolidate(cheatsheets)
|
|
||||||
|
|
||||||
names := make([]string, 0, len(consolidated))
|
|
||||||
for name := range consolidated {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
sort.Strings(names)
|
|
||||||
|
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package completions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loadConfig loads the cheat configuration for use in completion functions.
|
|
||||||
// It returns an error rather than exiting, since completions should degrade
|
|
||||||
// gracefully.
|
|
||||||
func loadConfig() (config.Config, error) {
|
|
||||||
home, err := homedir.Dir()
|
|
||||||
if err != nil {
|
|
||||||
return config.Config{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
envvars := config.EnvVars()
|
|
||||||
|
|
||||||
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
|
||||||
if err != nil {
|
|
||||||
return config.Config{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
confpath, err := config.Path(confpaths)
|
|
||||||
if err != nil {
|
|
||||||
return config.Config{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf, err := config.New(confpath, true)
|
|
||||||
if err != nil {
|
|
||||||
return config.Config{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conf, nil
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package completions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate writes a shell completion script to the given writer.
|
|
||||||
func Generate(cmd *cobra.Command, shell string, w io.Writer) error {
|
|
||||||
switch shell {
|
|
||||||
case "bash":
|
|
||||||
return cmd.Root().GenBashCompletionV2(w, true)
|
|
||||||
case "zsh":
|
|
||||||
return cmd.Root().GenZshCompletion(w)
|
|
||||||
case "fish":
|
|
||||||
return cmd.Root().GenFishCompletion(w, true)
|
|
||||||
case "powershell":
|
|
||||||
return cmd.Root().GenPowerShellCompletionWithDesc(w)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported shell: %s (valid: bash, zsh, fish, powershell)", shell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package completions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Paths provides completion for the --path flag.
|
|
||||||
func Paths(
|
|
||||||
_ *cobra.Command,
|
|
||||||
_ []string,
|
|
||||||
_ string,
|
|
||||||
) ([]string, cobra.ShellCompDirective) {
|
|
||||||
|
|
||||||
conf, err := loadConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
|
|
||||||
names := make([]string, 0, len(conf.Cheatpaths))
|
|
||||||
for _, cp := range conf.Cheatpaths {
|
|
||||||
names = append(names, cp.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return names, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package completions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/cheat/cheat/internal/sheets"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tags provides completion for the --tag flag.
|
|
||||||
func Tags(
|
|
||||||
_ *cobra.Command,
|
|
||||||
_ []string,
|
|
||||||
_ string,
|
|
||||||
) ([]string, cobra.ShellCompDirective) {
|
|
||||||
|
|
||||||
conf, err := loadConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
|
|
||||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
|
|
||||||
return sheets.Tags(cheatsheets), cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Color indicates whether colorization should be applied to the output
|
// Color indicates whether colorization should be applied to the output
|
||||||
func (c *Config) Color(forceColorize bool) bool {
|
func (c *Config) Color(opts map[string]interface{}) bool {
|
||||||
|
|
||||||
// default to the colorization specified in the configs...
|
// default to the colorization specified in the configs...
|
||||||
colorize := c.Colorize
|
colorize := c.Colorize
|
||||||
@@ -18,7 +18,7 @@ func (c *Config) Color(forceColorize bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ... *unless* the --colorize flag was passed
|
// ... *unless* the --colorize flag was passed
|
||||||
if forceColorize {
|
if opts["--colorize"] == true {
|
||||||
colorize = true
|
colorize = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ func TestColor(t *testing.T) {
|
|||||||
// mock a config
|
// mock a config
|
||||||
conf := Config{}
|
conf := Config{}
|
||||||
|
|
||||||
if conf.Color(false) {
|
opts := map[string]interface{}{"--colorize": false}
|
||||||
t.Errorf("failed to respect forceColorize (false)")
|
if conf.Color(opts) {
|
||||||
|
t.Errorf("failed to respect --colorize (false)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !conf.Color(true) {
|
opts = map[string]interface{}{"--colorize": true}
|
||||||
t.Errorf("failed to respect forceColorize (true)")
|
if !conf.Color(opts) {
|
||||||
|
t.Errorf("failed to respect --colorize (true)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,158 @@
|
|||||||
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.Path `yaml:"cheatpaths"`
|
Cheatpaths []cp.Cheatpath `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/mocks"
|
"github.com/cheat/cheat/internal/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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(invalidYAML, false)
|
_, err = New(map[string]interface{}{}, 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(mocks.Path("conf/empty.yml"), false)
|
conf, err := New(map[string]interface{}{}, mock.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(configFile, true)
|
conf, err := New(map[string]interface{}{}, 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(configFile, true)
|
conf, err := New(map[string]interface{}{}, 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/mocks"
|
"github.com/cheat/cheat/internal/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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(mocks.Path("conf/conf.yml"), false)
|
conf, err := New(map[string]interface{}{}, mock.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.Path{
|
want := []cheatpath.Cheatpath{
|
||||||
cheatpath.Path{
|
cheatpath.Cheatpath{
|
||||||
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.Path{
|
cheatpath.Cheatpath{
|
||||||
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.Path{
|
cheatpath.Cheatpath{
|
||||||
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("/does-not-exit", false)
|
_, err := New(map[string]interface{}{}, "/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(mocks.Path("conf/conf.yml"), false)
|
conf, err := New(map[string]interface{}{}, mock.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(mocks.Path("conf/conf.yml"), false)
|
conf, err = New(map[string]interface{}{}, mock.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(mocks.Path("conf/conf.yml"), false)
|
conf, err = New(map[string]interface{}{}, mock.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(mocks.Path("conf/empty.yml"), false)
|
conf, err := New(map[string]interface{}{}, mock.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(mocks.Path("conf/empty.yml"), false)
|
conf, err = New(map[string]interface{}{}, mock.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)
|
||||||
}
|
}
|
||||||
|
|||||||
52
internal/config/doc.go
Normal file
52
internal/config/doc.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// 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
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnvVars reads environment variables into a map of strings.
|
|
||||||
func EnvVars() map[string]string {
|
|
||||||
envvars := map[string]string{}
|
|
||||||
for _, e := range os.Environ() {
|
|
||||||
pair := strings.SplitN(e, "=", 2)
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
pair[0] = strings.ToUpper(pair[0])
|
|
||||||
}
|
|
||||||
envvars[pair[0]] = pair[1]
|
|
||||||
}
|
|
||||||
return envvars
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
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(configPath, false)
|
conf, err := New(map[string]interface{}{}, 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(configPath, false)
|
conf, err := New(map[string]interface{}{}, 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(configPath, false)
|
conf, err := New(map[string]interface{}{}, 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.Path{
|
Cheatpaths: []cheatpath.Cheatpath{
|
||||||
cheatpath.Path{
|
cheatpath.Cheatpath{
|
||||||
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.Path{
|
Cheatpaths: []cheatpath.Cheatpath{
|
||||||
cheatpath.Path{
|
cheatpath.Cheatpath{
|
||||||
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.Path{
|
Cheatpaths: []cheatpath.Cheatpath{
|
||||||
cheatpath.Path{
|
cheatpath.Cheatpath{
|
||||||
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.Path{
|
Cheatpaths: []cheatpath.Cheatpath{
|
||||||
cheatpath.Path{
|
cheatpath.Cheatpath{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
},
|
},
|
||||||
cheatpath.Path{
|
cheatpath.Cheatpath{
|
||||||
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.Path{
|
Cheatpaths: []cheatpath.Cheatpath{
|
||||||
cheatpath.Path{
|
cheatpath.Cheatpath{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
},
|
},
|
||||||
cheatpath.Path{
|
cheatpath.Cheatpath{
|
||||||
Name: "bar",
|
Name: "bar",
|
||||||
Path: "/foo",
|
Path: "/foo",
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
@@ -157,28 +157,3 @@ func TestInvalidateDuplicateCheatpathPaths(t *testing.T) {
|
|||||||
t.Errorf("failed to invalidate config with cheatpaths with duplicate paths")
|
t.Errorf("failed to invalidate config with cheatpaths with duplicate paths")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestInvalidateInvalidCheatpath asserts that configs containing a cheatpath
|
|
||||||
// with an empty name are invalidated
|
|
||||||
func TestInvalidateInvalidCheatpath(t *testing.T) {
|
|
||||||
|
|
||||||
// mock a config with a cheatpath that has an empty name
|
|
||||||
conf := Config{
|
|
||||||
Colorize: true,
|
|
||||||
Editor: "vim",
|
|
||||||
Formatter: "terminal16m",
|
|
||||||
Cheatpaths: []cheatpath.Path{
|
|
||||||
cheatpath.Path{
|
|
||||||
Name: "",
|
|
||||||
Path: "/foo",
|
|
||||||
ReadOnly: false,
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert that an error is returned
|
|
||||||
if err := conf.Validate(); err == nil {
|
|
||||||
t.Errorf("failed to invalidate config with invalid cheatpath (empty name)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
45
internal/display/doc.go
Normal file
45
internal/display/doc.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 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,13 +2,17 @@
|
|||||||
// cheatsheet content to stdout, or alternatively the system pager.
|
// cheatsheet content to stdout, or alternatively the system pager.
|
||||||
package display
|
package display
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"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, colorize bool) string {
|
func Faint(str string, conf config.Config) string {
|
||||||
// make `str` faint only if colorization has been requested
|
// make `str` faint only if colorization has been requested
|
||||||
if colorize {
|
if conf.Colorize {
|
||||||
return fmt.Sprintf("\033[2m%s\033[0m", str)
|
return fmt.Sprintf("\033[2m%s\033[0m", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
package display
|
package display
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"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", true)
|
got := Faint("foo", conf)
|
||||||
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", false)
|
got = Faint("foo", conf)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,3 @@ func TestIndent(t *testing.T) {
|
|||||||
t.Errorf("failed to indent: want: %s, got: %s", want, got)
|
t.Errorf("failed to indent: want: %s, got: %s", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestIndentTrimsWhitespace asserts that Indent trims leading and trailing
|
|
||||||
// whitespace before indenting
|
|
||||||
func TestIndentTrimsWhitespace(t *testing.T) {
|
|
||||||
got := Indent(" foo\nbar\nbaz \n")
|
|
||||||
want := "\tfoo\n\tbar\n\tbaz\n"
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("failed to trim and indent: want: %q, got: %q", 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.Fields(conf.Pager)
|
parts := strings.Split(conf.Pager, " ")
|
||||||
pager := parts[0]
|
pager := parts[0]
|
||||||
args := parts[1:]
|
args := parts[1:]
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ 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"
|
||||||
@@ -11,11 +13,27 @@ import (
|
|||||||
// Run runs the installer
|
// Run runs the installer
|
||||||
func Run(configs string, confpath string) error {
|
func Run(configs string, confpath string) error {
|
||||||
|
|
||||||
// expand template placeholders with platform-appropriate paths
|
// determine the appropriate paths for config data and (optional) community
|
||||||
configs = ExpandTemplate(configs, confpath)
|
// cheatsheets based on the user's platform
|
||||||
|
confdir := filepath.Dir(confpath)
|
||||||
|
|
||||||
// determine cheatsheet directory paths
|
// create paths for community, personal, and work cheatsheets
|
||||||
community, personal, work := cheatsheetDirs(confpath)
|
community := filepath.Join(confdir, "cheatsheets", "community")
|
||||||
|
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(
|
||||||
@@ -33,7 +51,19 @@ 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 {
|
||||||
configs = CommentCommunity(configs, confpath)
|
// comment out the community cheatpath in the config since
|
||||||
|
// 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
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
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",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
29
internal/mock/path.go
Normal file
29
internal/mock/path.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// 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,130 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
|
||||||
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrDirtyWorktree indicates that the worktree has uncommitted changes.
|
|
||||||
var ErrDirtyWorktree = errors.New("dirty worktree")
|
|
||||||
|
|
||||||
// Pull performs a git pull on the repository at path. It returns
|
|
||||||
// ErrDirtyWorktree if the worktree has uncommitted changes, and
|
|
||||||
// git.ErrRepositoryNotExists if path is not a git repository.
|
|
||||||
func Pull(path string) error {
|
|
||||||
|
|
||||||
// open the repository
|
|
||||||
r, err := git.PlainOpen(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the worktree
|
|
||||||
wt, err := r.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the worktree is clean
|
|
||||||
status, err := wt.Status()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !status.IsClean() {
|
|
||||||
return ErrDirtyWorktree
|
|
||||||
}
|
|
||||||
|
|
||||||
// build pull options, using SSH auth when the remote is SSH
|
|
||||||
opts := &git.PullOptions{}
|
|
||||||
if auth, err := sshAuth(r); err == nil && auth != nil {
|
|
||||||
opts.Auth = auth
|
|
||||||
}
|
|
||||||
|
|
||||||
// pull
|
|
||||||
err = wt.Pull(opts)
|
|
||||||
if errors.Is(err, git.NoErrAlreadyUpToDate) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultKeyFiles are the SSH key filenames tried in order, matching the
|
|
||||||
// default behavior of OpenSSH.
|
|
||||||
var defaultKeyFiles = []string{
|
|
||||||
"id_rsa",
|
|
||||||
"id_ecdsa",
|
|
||||||
"id_ecdsa_sk",
|
|
||||||
"id_ed25519",
|
|
||||||
"id_ed25519_sk",
|
|
||||||
"id_dsa",
|
|
||||||
}
|
|
||||||
|
|
||||||
// sshAuth returns an appropriate SSH auth method if the origin remote uses
|
|
||||||
// the SSH protocol, or nil if it does not. It tries the SSH agent first, then
|
|
||||||
// falls back to default key files in ~/.ssh/.
|
|
||||||
func sshAuth(r *git.Repository) (transport.AuthMethod, error) {
|
|
||||||
remote, err := r.Remote("origin")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
urls := remote.Config().URLs
|
|
||||||
if len(urls) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ep, err := transport.NewEndpoint(urls[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ep.Protocol != "ssh" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ep.User
|
|
||||||
if user == "" {
|
|
||||||
user = "git"
|
|
||||||
}
|
|
||||||
|
|
||||||
// try default key files first — this is more reliable than the SSH
|
|
||||||
// agent, which may report success even when no keys are loaded
|
|
||||||
home, err := homedir.Dir()
|
|
||||||
if err == nil {
|
|
||||||
if auth := findKeyFile(filepath.Join(home, ".ssh"), user); auth != nil {
|
|
||||||
return auth, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fall back to SSH agent
|
|
||||||
if auth, err := gitssh.NewSSHAgentAuth(user); err == nil {
|
|
||||||
return auth, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// findKeyFile looks for a usable SSH private key in sshDir, trying the
|
|
||||||
// standard OpenSSH default filenames in order. Returns nil if no usable key
|
|
||||||
// is found.
|
|
||||||
func findKeyFile(sshDir, user string) transport.AuthMethod {
|
|
||||||
for _, name := range defaultKeyFiles {
|
|
||||||
keyPath := filepath.Join(sshDir, name)
|
|
||||||
if _, err := os.Stat(keyPath); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
auth, err := gitssh.NewPublicKeysFromFile(user, keyPath, "")
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return auth
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
package repo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
gitconfig "github.com/go-git/go-git/v5/config"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// testCommitOpts returns a CommitOptions suitable for test commits.
|
|
||||||
func testCommitOpts() *git.CommitOptions {
|
|
||||||
return &git.CommitOptions{
|
|
||||||
Author: &object.Signature{
|
|
||||||
Name: "test",
|
|
||||||
Email: "test@test",
|
|
||||||
When: time.Now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initBareRepoWithCommit creates a bare git repository at dir with an initial
|
|
||||||
// commit, suitable for use as a remote.
|
|
||||||
func initBareRepoWithCommit(t *testing.T, dir string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
// init a non-bare repo to make the commit, then we'll clone it as bare
|
|
||||||
tmpWork := t.TempDir()
|
|
||||||
r, err := git.PlainInit(tmpWork, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to init repo: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f := filepath.Join(tmpWork, "README")
|
|
||||||
if err := os.WriteFile(f, []byte("hello\n"), 0644); err != nil {
|
|
||||||
t.Fatalf("failed to write file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wt, err := r.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get worktree: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := wt.Add("README"); err != nil {
|
|
||||||
t.Fatalf("failed to stage file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = wt.Commit("initial commit", testCommitOpts()); err != nil {
|
|
||||||
t.Fatalf("failed to commit: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// clone as bare into the target dir
|
|
||||||
if _, err = git.PlainClone(dir, true, &git.CloneOptions{URL: tmpWork}); err != nil {
|
|
||||||
t.Fatalf("failed to create bare clone: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cloneLocal clones the bare repo at bareDir into a new working directory and
|
|
||||||
// returns the path.
|
|
||||||
func cloneLocal(t *testing.T, bareDir string) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
|
||||||
URL: bareDir,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to clone: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// pushNewCommit clones bareDir into a temporary working copy, commits a new
|
|
||||||
// file, and pushes back to the bare repo.
|
|
||||||
func pushNewCommit(t *testing.T, bareDir string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
tmpWork := t.TempDir()
|
|
||||||
r, err := git.PlainClone(tmpWork, false, &git.CloneOptions{URL: bareDir})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to clone for push: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(tmpWork, "new.txt"), []byte("new\n"), 0644); err != nil {
|
|
||||||
t.Fatalf("failed to write file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wt, err := r.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get worktree: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := wt.Add("new.txt"); err != nil {
|
|
||||||
t.Fatalf("failed to stage file: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := wt.Commit("add new file", testCommitOpts()); err != nil {
|
|
||||||
t.Fatalf("failed to commit: %v", err)
|
|
||||||
}
|
|
||||||
if err := r.Push(&git.PushOptions{}); err != nil {
|
|
||||||
t.Fatalf("failed to push: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateTestKey creates an unencrypted ed25519 PEM private key file at path.
|
|
||||||
func generateTestKey(t *testing.T, path string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
_, priv, err := ed25519.GenerateKey(nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to generate key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
der, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to marshal key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
|
|
||||||
if err := os.WriteFile(path, pemBytes, 0600); err != nil {
|
|
||||||
t.Fatalf("failed to write key file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Pull tests ---
|
|
||||||
|
|
||||||
func TestPull_NotARepo(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
|
|
||||||
err := Pull(dir)
|
|
||||||
if err != git.ErrRepositoryNotExists {
|
|
||||||
t.Fatalf("expected ErrRepositoryNotExists, got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPull_CleanAlreadyUpToDate(t *testing.T) {
|
|
||||||
bare := t.TempDir()
|
|
||||||
initBareRepoWithCommit(t, bare)
|
|
||||||
clone := cloneLocal(t, bare)
|
|
||||||
|
|
||||||
err := Pull(clone)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected nil (already up-to-date), got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPull_NewUpstreamChanges(t *testing.T) {
|
|
||||||
bare := t.TempDir()
|
|
||||||
initBareRepoWithCommit(t, bare)
|
|
||||||
clone := cloneLocal(t, bare)
|
|
||||||
|
|
||||||
// push a new commit to the bare repo after the clone
|
|
||||||
pushNewCommit(t, bare)
|
|
||||||
|
|
||||||
err := Pull(clone)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected nil (successful pull), got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify the new file was pulled
|
|
||||||
if _, err := os.Stat(filepath.Join(clone, "new.txt")); err != nil {
|
|
||||||
t.Fatalf("expected new.txt to exist after pull: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPull_DirtyWorktree(t *testing.T) {
|
|
||||||
bare := t.TempDir()
|
|
||||||
initBareRepoWithCommit(t, bare)
|
|
||||||
clone := cloneLocal(t, bare)
|
|
||||||
|
|
||||||
// make the worktree dirty with a modified tracked file
|
|
||||||
if err := os.WriteFile(filepath.Join(clone, "README"), []byte("changed\n"), 0644); err != nil {
|
|
||||||
t.Fatalf("failed to modify file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Pull(clone)
|
|
||||||
if err != ErrDirtyWorktree {
|
|
||||||
t.Fatalf("expected ErrDirtyWorktree, got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPull_DirtyWorktreeUntracked(t *testing.T) {
|
|
||||||
bare := t.TempDir()
|
|
||||||
initBareRepoWithCommit(t, bare)
|
|
||||||
clone := cloneLocal(t, bare)
|
|
||||||
|
|
||||||
// make the worktree dirty with an untracked file
|
|
||||||
if err := os.WriteFile(filepath.Join(clone, "untracked.txt"), []byte("new\n"), 0644); err != nil {
|
|
||||||
t.Fatalf("failed to create file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Pull(clone)
|
|
||||||
if err != ErrDirtyWorktree {
|
|
||||||
t.Fatalf("expected ErrDirtyWorktree, got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- sshAuth tests ---
|
|
||||||
|
|
||||||
func TestSshAuth_NonSSHRemote(t *testing.T) {
|
|
||||||
bare := t.TempDir()
|
|
||||||
initBareRepoWithCommit(t, bare)
|
|
||||||
clone := cloneLocal(t, bare)
|
|
||||||
|
|
||||||
r, err := git.PlainOpen(clone)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to open repo: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// the clone's origin is a local file:// path, not SSH
|
|
||||||
auth, err := sshAuth(r)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected nil error, got: %v", err)
|
|
||||||
}
|
|
||||||
if auth != nil {
|
|
||||||
t.Fatalf("expected nil auth for non-SSH remote, got: %v", auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSshAuth_NoRemote(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
r, err := git.PlainInit(dir, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to init repo: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// repo has no remotes
|
|
||||||
auth, err := sshAuth(r)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error for missing remote, got auth: %v", auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSshAuth_SSHRemote(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
r, err := git.PlainInit(dir, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to init repo: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add an SSH remote
|
|
||||||
_, err = r.CreateRemote(&gitconfig.RemoteConfig{
|
|
||||||
Name: "origin",
|
|
||||||
URLs: []string{"git@github.com:example/repo.git"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create remote: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sshAuth should not return an error — even if no key is found, it
|
|
||||||
// returns (nil, nil) rather than an error
|
|
||||||
auth, err := sshAuth(r)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected nil error, got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we can't predict whether auth is nil or non-nil here because it
|
|
||||||
// depends on whether the test runner has SSH keys or an agent; just
|
|
||||||
// verify it didn't error
|
|
||||||
_ = auth
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- findKeyFile tests ---
|
|
||||||
|
|
||||||
func TestFindKeyFile_ValidKey(t *testing.T) {
|
|
||||||
sshDir := t.TempDir()
|
|
||||||
generateTestKey(t, filepath.Join(sshDir, "id_ed25519"))
|
|
||||||
|
|
||||||
auth := findKeyFile(sshDir, "git")
|
|
||||||
if auth == nil {
|
|
||||||
t.Fatal("expected non-nil auth for valid key file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindKeyFile_NoKeys(t *testing.T) {
|
|
||||||
sshDir := t.TempDir()
|
|
||||||
|
|
||||||
auth := findKeyFile(sshDir, "git")
|
|
||||||
if auth != nil {
|
|
||||||
t.Fatalf("expected nil auth for empty directory, got: %v", auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindKeyFile_InvalidKey(t *testing.T) {
|
|
||||||
sshDir := t.TempDir()
|
|
||||||
// write garbage into a file named like a key
|
|
||||||
if err := os.WriteFile(filepath.Join(sshDir, "id_ed25519"), []byte("not a key"), 0600); err != nil {
|
|
||||||
t.Fatalf("failed to write file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
auth := findKeyFile(sshDir, "git")
|
|
||||||
if auth != nil {
|
|
||||||
t.Fatalf("expected nil auth for invalid key file, got: %v", auth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindKeyFile_SkipsInvalidFindsValid(t *testing.T) {
|
|
||||||
sshDir := t.TempDir()
|
|
||||||
|
|
||||||
// put garbage in id_rsa (tried first), valid key in id_ed25519 (tried later)
|
|
||||||
if err := os.WriteFile(filepath.Join(sshDir, "id_rsa"), []byte("not a key"), 0600); err != nil {
|
|
||||||
t.Fatalf("failed to write file: %v", err)
|
|
||||||
}
|
|
||||||
generateTestKey(t, filepath.Join(sshDir, "id_ed25519"))
|
|
||||||
|
|
||||||
auth := findKeyFile(sshDir, "git")
|
|
||||||
if auth == nil {
|
|
||||||
t.Fatal("expected non-nil auth; should skip invalid id_rsa and find id_ed25519")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
internal/repo/update.go
Normal file
1
internal/repo/update.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package repo
|
||||||
@@ -40,55 +40,3 @@ func TestColorize(t *testing.T) {
|
|||||||
t.Errorf("colorized text lost original content: %q", s.Text)
|
t.Errorf("colorized text lost original content: %q", s.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestColorizeDefaultSyntax asserts that when no syntax is specified, the
|
|
||||||
// default ("bash") is used and produces the same output as an explicit "bash"
|
|
||||||
func TestColorizeDefaultSyntax(t *testing.T) {
|
|
||||||
|
|
||||||
conf := config.Config{
|
|
||||||
Formatter: "terminal16m",
|
|
||||||
Style: "monokai",
|
|
||||||
}
|
|
||||||
|
|
||||||
// use bash-specific content that tokenizes differently across lexers
|
|
||||||
code := "if [[ -f /etc/passwd ]]; then\n echo \"found\" | grep -o found\nfi"
|
|
||||||
|
|
||||||
// colorize with empty syntax (should default to "bash")
|
|
||||||
noSyntax := Sheet{Text: code}
|
|
||||||
noSyntax.Colorize(conf)
|
|
||||||
|
|
||||||
// colorize with explicit "bash" syntax
|
|
||||||
bashSyntax := Sheet{Text: code, Syntax: "bash"}
|
|
||||||
bashSyntax.Colorize(conf)
|
|
||||||
|
|
||||||
// both should produce the same output
|
|
||||||
if noSyntax.Text != bashSyntax.Text {
|
|
||||||
t.Errorf(
|
|
||||||
"default syntax does not match explicit bash:\ndefault: %q\nexplicit: %q",
|
|
||||||
noSyntax.Text,
|
|
||||||
bashSyntax.Text,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestColorizeExplicitSyntax asserts that a specified syntax is used
|
|
||||||
func TestColorizeExplicitSyntax(t *testing.T) {
|
|
||||||
|
|
||||||
conf := config.Config{
|
|
||||||
Formatter: "terminal16m",
|
|
||||||
Style: "monokai",
|
|
||||||
}
|
|
||||||
|
|
||||||
// colorize as bash
|
|
||||||
bashSheet := Sheet{Text: "def hello():\n pass", Syntax: "bash"}
|
|
||||||
bashSheet.Colorize(conf)
|
|
||||||
|
|
||||||
// colorize as python
|
|
||||||
pySheet := Sheet{Text: "def hello():\n pass", Syntax: "python"}
|
|
||||||
pySheet.Colorize(conf)
|
|
||||||
|
|
||||||
// different lexers should produce different output for Python code
|
|
||||||
if bashSheet.Text == pySheet.Text {
|
|
||||||
t.Error("bash and python syntax produced identical output")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
65
internal/sheet/doc.go
Normal file
65
internal/sheet/doc.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// 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
|
||||||
@@ -93,20 +93,3 @@ To foo the bar: baz`
|
|||||||
t.Errorf("failed to parse text: want: %s, got: %s", markdown, text)
|
t.Errorf("failed to parse text: want: %s, got: %s", markdown, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHasMalformedYAML asserts that an error is returned when the frontmatter
|
|
||||||
// contains invalid YAML that cannot be unmarshalled
|
|
||||||
func TestHasMalformedYAML(t *testing.T) {
|
|
||||||
|
|
||||||
// stub cheatsheet content with syntactically invalid YAML between the
|
|
||||||
// delimiters (a bare tab character followed by unquoted colon)
|
|
||||||
markdown := "---\n\t:\t:\n---\nBody text here"
|
|
||||||
|
|
||||||
// parse the frontmatter
|
|
||||||
_, _, err := parse(markdown)
|
|
||||||
|
|
||||||
// assert that an error was returned due to YAML unmarshal failure
|
|
||||||
if err == nil {
|
|
||||||
t.Error("failed to error on malformed YAML frontmatter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cheat/cheat/mocks"
|
"github.com/cheat/cheat/internal/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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",
|
||||||
mocks.Path("sheet/foo"),
|
mock.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 != mocks.Path("sheet/foo") {
|
if sheet.Path != mock.Path("sheet/foo") {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"failed to init path: want: %s, got: %s",
|
"failed to init path: want: %s, got: %s",
|
||||||
mocks.Path("sheet/foo"),
|
mock.Path("sheet/foo"),
|
||||||
sheet.Path,
|
sheet.Path,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ func TestSheetFailure(t *testing.T) {
|
|||||||
_, err := New(
|
_, err := New(
|
||||||
"foo",
|
"foo",
|
||||||
"community",
|
"community",
|
||||||
mocks.Path("/does-not-exist"),
|
mock.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",
|
||||||
mocks.Path("sheet/bad-fm"),
|
mock.Path("sheet/bad-fm"),
|
||||||
[]string{"alpha", "bravo"},
|
[]string{"alpha", "bravo"},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
65
internal/sheets/doc.go
Normal file
65
internal/sheets/doc.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// 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,11 +8,12 @@ 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.Path) ([]map[string]sheet.Sheet, error) {
|
func Load(cheatpaths []cp.Cheatpath) ([]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.
|
||||||
@@ -26,10 +27,10 @@ func Load(cheatpaths []cp.Path) ([]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.WalkDir(
|
err := filepath.Walk(
|
||||||
cheatpath.Path, func(
|
cheatpath.Path, func(
|
||||||
path string,
|
path string,
|
||||||
d fs.DirEntry,
|
info os.FileInfo,
|
||||||
err error) error {
|
err error) error {
|
||||||
|
|
||||||
// fail if an error occurred while walking the directory
|
// fail if an error occurred while walking the directory
|
||||||
@@ -37,12 +38,8 @@ func Load(cheatpaths []cp.Path) ([]map[string]sheet.Sheet, error) {
|
|||||||
return fmt.Errorf("failed to walk path: %v", err)
|
return fmt.Errorf("failed to walk path: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.IsDir() {
|
// don't register directories as cheatsheets
|
||||||
// skip .git directories to avoid hundreds/thousands of
|
if info.IsDir() {
|
||||||
// needless syscalls (see repo.GitDir for full history)
|
|
||||||
if filepath.Base(path) == ".git" {
|
|
||||||
return fs.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +63,17 @@ func Load(cheatpaths []cp.Path) ([]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/mocks"
|
"github.com/cheat/cheat/internal/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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.Path{
|
cheatpaths := []cheatpath.Cheatpath{
|
||||||
{
|
{
|
||||||
Name: "community",
|
Name: "community",
|
||||||
Path: path.Join(mocks.Path("cheatsheets"), "community"),
|
Path: path.Join(mock.Path("cheatsheets"), "community"),
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "personal",
|
Name: "personal",
|
||||||
Path: path.Join(mocks.Path("cheatsheets"), "personal"),
|
Path: path.Join(mock.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.Path{
|
cheatpaths := []cheatpath.Cheatpath{
|
||||||
{
|
{
|
||||||
Name: "badpath",
|
Name: "badpath",
|
||||||
Path: "/cheat/test/path/does/not/exist",
|
Path: "/cheat/test/path/does/not/exist",
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ func Tags(cheatpaths []map[string]sheet.Sheet) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sort the slice
|
// sort the slice
|
||||||
sort.Strings(sorted)
|
sort.Slice(sorted, func(i, j int) bool {
|
||||||
|
return sorted[i] < sorted[j]
|
||||||
|
})
|
||||||
|
|
||||||
return sorted
|
return sorted
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
74
scripts/cheat.bash
Executable file
74
scripts/cheat.bash
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
# cheat(1) completion -*- shell-script -*-
|
||||||
|
|
||||||
|
# generate cheatsheet completions, optionally using `fzf`
|
||||||
|
_cheat_complete_cheatsheets()
|
||||||
|
{
|
||||||
|
if [[ "$CHEAT_USE_FZF" = true ]]; then
|
||||||
|
FZF_COMPLETION_TRIGGER='' _fzf_complete "--no-multi" "$@" < <(
|
||||||
|
cheat -l | tail -n +2 | cut -d' ' -f1
|
||||||
|
)
|
||||||
|
else
|
||||||
|
COMPREPLY=( $(compgen -W "$(cheat -l | tail -n +2 | cut -d' ' -f1)" -- "$cur") )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# generate tag completions, optionally using `fzf`
|
||||||
|
_cheat_complete_tags()
|
||||||
|
{
|
||||||
|
if [ "$CHEAT_USE_FZF" = true ]; then
|
||||||
|
FZF_COMPLETION_TRIGGER='' _fzf_complete "--no-multi" "$@" < <(cheat -T)
|
||||||
|
else
|
||||||
|
COMPREPLY=( $(compgen -W "$(cheat -T)" -- "$cur") )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# implement the `cheat` autocompletions
|
||||||
|
_cheat()
|
||||||
|
{
|
||||||
|
local cur prev words cword split
|
||||||
|
_init_completion -s || return
|
||||||
|
|
||||||
|
# complete options that are currently being typed: `--col` => `--colorize`
|
||||||
|
if [[ $cur == -* ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W '$(_parse_help "$1" | sed "s/=//g")' -- "$cur") )
|
||||||
|
[[ $COMPREPLY == *= ]] && compopt -o nospace
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# implement completions
|
||||||
|
case $prev in
|
||||||
|
--colorize|-c|\
|
||||||
|
--directories|-d|\
|
||||||
|
--init|\
|
||||||
|
--regex|-r|\
|
||||||
|
--search|-s|\
|
||||||
|
--tags|-T|\
|
||||||
|
--version|-v)
|
||||||
|
# noop the above, which should implement no completions
|
||||||
|
;;
|
||||||
|
--edit|-e)
|
||||||
|
_cheat_complete_cheatsheets
|
||||||
|
;;
|
||||||
|
--list|-l)
|
||||||
|
_cheat_complete_cheatsheets
|
||||||
|
;;
|
||||||
|
--path|-p)
|
||||||
|
COMPREPLY=( $(compgen -W "$(cheat -d | cut -d':' -f1)" -- "$cur") )
|
||||||
|
;;
|
||||||
|
--rm)
|
||||||
|
_cheat_complete_cheatsheets
|
||||||
|
;;
|
||||||
|
--tag|-t)
|
||||||
|
_cheat_complete_tags
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
_cheat_complete_cheatsheets
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
$split && return
|
||||||
|
|
||||||
|
} &&
|
||||||
|
complete -F _cheat cheat
|
||||||
|
|
||||||
|
# ex: filetype=sh
|
||||||
13
scripts/cheat.fish
Executable file
13
scripts/cheat.fish
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
complete -c cheat -f -a "(cheat -l | tail -n +2 | cut -d ' ' -f 1)"
|
||||||
|
complete -c cheat -l init -d "Write a default config file to stdout"
|
||||||
|
complete -c cheat -s c -l colorize -d "Colorize output"
|
||||||
|
complete -c cheat -s d -l directories -d "List cheatsheet directories"
|
||||||
|
complete -c cheat -s e -l edit -x -a "(cheat -l | tail -n +2 | cut -d ' ' -f 1)" -d "Edit cheatsheet"
|
||||||
|
complete -c cheat -s l -l list -d "List cheatsheets"
|
||||||
|
complete -c cheat -s p -l path -x -a "(cheat -d | cut -d ':' -f 1)" -d "Return only sheets found on given path"
|
||||||
|
complete -c cheat -s r -l regex -d "Treat search phrase as a regex"
|
||||||
|
complete -c cheat -s s -l search -x -d "Search cheatsheets for given phrase"
|
||||||
|
complete -c cheat -s t -l tag -x -a "(cheat -T)" -d "Return only sheets matching the given tag"
|
||||||
|
complete -c cheat -s T -l tags -d "List all tags in use"
|
||||||
|
complete -c cheat -s v -l version -d "Print the version number"
|
||||||
|
complete -c cheat -l rm -x -a "(cheat -l | tail -n +2 | cut -d ' ' -f 1)" -d "Remove (delete) cheatsheet"
|
||||||
65
scripts/cheat.zsh
Executable file
65
scripts/cheat.zsh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#compdef cheat
|
||||||
|
|
||||||
|
local cheats taglist pathlist
|
||||||
|
|
||||||
|
_cheat_complete_personal_cheatsheets()
|
||||||
|
{
|
||||||
|
cheats=("${(f)$(cheat -l -t personal | tail -n +2 | cut -d' ' -f1)}")
|
||||||
|
_describe -t cheats 'cheats' cheats
|
||||||
|
}
|
||||||
|
|
||||||
|
_cheat_complete_full_cheatsheets()
|
||||||
|
{
|
||||||
|
cheats=("${(f)$(cheat -l | tail -n +2 | cut -d' ' -f1)}")
|
||||||
|
_describe -t cheats 'cheats' cheats
|
||||||
|
}
|
||||||
|
|
||||||
|
_cheat_complete_tags()
|
||||||
|
{
|
||||||
|
taglist=("${(f)$(cheat -T)}")
|
||||||
|
_describe -t taglist 'taglist' taglist
|
||||||
|
}
|
||||||
|
|
||||||
|
_cheat_complete_paths()
|
||||||
|
{
|
||||||
|
pathlist=("${(f)$(cheat -d | cut -d':' -f1)}")
|
||||||
|
_describe -t pathlist 'pathlist' pathlist
|
||||||
|
}
|
||||||
|
|
||||||
|
_cheat() {
|
||||||
|
|
||||||
|
_arguments -C \
|
||||||
|
'(--init)--init[Write a default config file to stdout]: :->none' \
|
||||||
|
'(-c --colorize)'{-c,--colorize}'[Colorize output]: :->none' \
|
||||||
|
'(-d --directories)'{-d,--directories}'[List cheatsheet directories]: :->none' \
|
||||||
|
'(-e --edit)'{-e,--edit}'[Edit <sheet>]: :->personal' \
|
||||||
|
'(-l --list)'{-l,--list}'[List cheatsheets]: :->full' \
|
||||||
|
'(-p --path)'{-p,--path}'[Return only sheets found on path <name>]: :->pathlist' \
|
||||||
|
'(-r --regex)'{-r,--regex}'[Treat search <phrase> as a regex]: :->none' \
|
||||||
|
'(-s --search)'{-s,--search}'[Search cheatsheets for <phrase>]: :->none' \
|
||||||
|
'(-t --tag)'{-t,--tag}'[Return only sheets matching <tag>]: :->taglist' \
|
||||||
|
'(-T --tags)'{-T,--tags}'[List all tags in use]: :->none' \
|
||||||
|
'(-v --version)'{-v,--version}'[Print the version number]: :->none' \
|
||||||
|
'(--rm)--rm[Remove (delete) <sheet>]: :->personal'
|
||||||
|
|
||||||
|
case $state in
|
||||||
|
(none)
|
||||||
|
;;
|
||||||
|
(full)
|
||||||
|
_cheat_complete_full_cheatsheets
|
||||||
|
;;
|
||||||
|
(personal)
|
||||||
|
_cheat_complete_personal_cheatsheets
|
||||||
|
;;
|
||||||
|
(taglist)
|
||||||
|
_cheat_complete_tags
|
||||||
|
;;
|
||||||
|
(pathlist)
|
||||||
|
_cheat_complete_paths
|
||||||
|
;;
|
||||||
|
(*)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
compdef _cheat cheat
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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)))
|
|
||||||
}
|
|
||||||
18
vendor/github.com/spf13/cobra/.gitignore → vendor/github.com/docopt/docopt-go/.gitignore
generated
vendored
18
vendor/github.com/spf13/cobra/.gitignore → vendor/github.com/docopt/docopt-go/.gitignore
generated
vendored
@@ -19,21 +19,7 @@ _cgo_export.*
|
|||||||
|
|
||||||
_testmain.go
|
_testmain.go
|
||||||
|
|
||||||
# Vim files https://github.com/github/gitignore/blob/master/Global/Vim.gitignore
|
|
||||||
# swap
|
|
||||||
[._]*.s[a-w][a-z]
|
|
||||||
[._]s[a-w][a-z]
|
|
||||||
# session
|
|
||||||
Session.vim
|
|
||||||
# temporary
|
|
||||||
.netrwhist
|
|
||||||
*~
|
|
||||||
# auto-generated tag files
|
|
||||||
tags
|
|
||||||
|
|
||||||
*.exe
|
*.exe
|
||||||
cobra.test
|
|
||||||
bin
|
|
||||||
|
|
||||||
.idea/
|
# coverage droppings
|
||||||
*.iml
|
profile.cov
|
||||||
32
vendor/github.com/docopt/docopt-go/.travis.yml
generated
vendored
Normal file
32
vendor/github.com/docopt/docopt-go/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Travis CI (http://travis-ci.org/) is a continuous integration
|
||||||
|
# service for open source projects. This file configures it
|
||||||
|
# to run unit tests for docopt-go.
|
||||||
|
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.4
|
||||||
|
- 1.5
|
||||||
|
- 1.6
|
||||||
|
- 1.7
|
||||||
|
- 1.8
|
||||||
|
- 1.9
|
||||||
|
- tip
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
fast_finish: true
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get -d -v ./... && go build -v ./...
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go vet -x ./...
|
||||||
|
- go test -v ./...
|
||||||
|
- go test -covermode=count -coverprofile=profile.cov .
|
||||||
|
|
||||||
|
after_script:
|
||||||
|
- $HOME/gopath/bin/goveralls -coverprofile=profile.cov -service=travis-ci
|
||||||
21
vendor/github.com/docopt/docopt-go/LICENSE
generated
vendored
Normal file
21
vendor/github.com/docopt/docopt-go/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 Keith Batten
|
||||||
|
Copyright (c) 2016 David Irvine
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
116
vendor/github.com/docopt/docopt-go/README.md
generated
vendored
Normal file
116
vendor/github.com/docopt/docopt-go/README.md
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
docopt-go
|
||||||
|
=========
|
||||||
|
|
||||||
|
[](https://travis-ci.org/docopt/docopt.go)
|
||||||
|
[](https://coveralls.io/github/docopt/docopt.go)
|
||||||
|
[](https://godoc.org/github.com/docopt/docopt.go)
|
||||||
|
|
||||||
|
An implementation of [docopt](http://docopt.org/) in the [Go](http://golang.org/) programming language.
|
||||||
|
|
||||||
|
**docopt** helps you create *beautiful* command-line interfaces easily:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/docopt/docopt-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
usage := `Naval Fate.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
naval_fate ship new <name>...
|
||||||
|
naval_fate ship <name> move <x> <y> [--speed=<kn>]
|
||||||
|
naval_fate ship shoot <x> <y>
|
||||||
|
naval_fate mine (set|remove) <x> <y> [--moored|--drifting]
|
||||||
|
naval_fate -h | --help
|
||||||
|
naval_fate --version
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h --help Show this screen.
|
||||||
|
--version Show version.
|
||||||
|
--speed=<kn> Speed in knots [default: 10].
|
||||||
|
--moored Moored (anchored) mine.
|
||||||
|
--drifting Drifting mine.`
|
||||||
|
|
||||||
|
arguments, _ := docopt.ParseDoc(usage)
|
||||||
|
fmt.Println(arguments)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**docopt** parses command-line arguments based on a help message. Don't write parser code: a good help message already has all the necessary information in it.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
⚠ Use the alias "docopt-go". To use docopt in your Go code:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/docopt/docopt-go"
|
||||||
|
```
|
||||||
|
|
||||||
|
To install docopt in your `$GOPATH`:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ go get github.com/docopt/docopt-go
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Given a conventional command-line help message, docopt processes the arguments. See https://github.com/docopt/docopt#help-message-format for a description of the help message format.
|
||||||
|
|
||||||
|
This package exposes three different APIs, depending on the level of control required. The first, simplest way to parse your docopt usage is to just call:
|
||||||
|
|
||||||
|
```go
|
||||||
|
docopt.ParseDoc(usage)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will use `os.Args[1:]` as the argv slice, and use the default parser options. If you want to provide your own version string and args, then use:
|
||||||
|
|
||||||
|
```go
|
||||||
|
docopt.ParseArgs(usage, argv, "1.2.3")
|
||||||
|
```
|
||||||
|
|
||||||
|
If the last parameter (version) is a non-empty string, it will be printed when `--version` is given in the argv slice. Finally, we can instantiate our own `docopt.Parser` which gives us control over how things like help messages are printed and whether to exit after displaying usage messages, etc.
|
||||||
|
|
||||||
|
```go
|
||||||
|
parser := &docopt.Parser{
|
||||||
|
HelpHandler: docopt.PrintHelpOnly,
|
||||||
|
OptionsFirst: true,
|
||||||
|
}
|
||||||
|
opts, err := parser.ParseArgs(usage, argv, "")
|
||||||
|
```
|
||||||
|
|
||||||
|
In particular, setting your own custom `HelpHandler` function makes unit testing your own docs with example command line invocations much more enjoyable.
|
||||||
|
|
||||||
|
All three of these return a map of option names to the values parsed from argv, and an error or nil. You can get the values using the helpers, or just treat it as a regular map:
|
||||||
|
|
||||||
|
```go
|
||||||
|
flag, _ := opts.Bool("--flag")
|
||||||
|
secs, _ := opts.Int("<seconds>")
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, you can `Bind` these to a struct, assigning option values to the
|
||||||
|
exported fields of that struct, all at once.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var config struct {
|
||||||
|
Command string `docopt:"<cmd>"`
|
||||||
|
Tries int `docopt:"-n"`
|
||||||
|
Force bool // Gets the value of --force
|
||||||
|
}
|
||||||
|
opts.Bind(&config)
|
||||||
|
```
|
||||||
|
|
||||||
|
More documentation is available at [godoc.org](https://godoc.org/github.com/docopt/docopt-go).
|
||||||
|
|
||||||
|
## Unit Testing
|
||||||
|
|
||||||
|
Unit testing your own usage docs is recommended, so you can be sure that for a given command line invocation, the expected options are set. An example of how to do this is [in the examples folder](examples/unit_test/unit_test.go).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
All tests from the Python version are implemented and passing at [Travis CI](https://travis-ci.org/docopt/docopt-go). New language-agnostic tests have been added to [test_golang.docopt](test_golang.docopt).
|
||||||
|
|
||||||
|
To run tests for docopt-go, use `go test`.
|
||||||
49
vendor/github.com/docopt/docopt-go/doc.go
generated
vendored
Normal file
49
vendor/github.com/docopt/docopt-go/doc.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
Package docopt parses command-line arguments based on a help message.
|
||||||
|
|
||||||
|
Given a conventional command-line help message, docopt processes the arguments.
|
||||||
|
See https://github.com/docopt/docopt#help-message-format for a description of
|
||||||
|
the help message format.
|
||||||
|
|
||||||
|
This package exposes three different APIs, depending on the level of control
|
||||||
|
required. The first, simplest way to parse your docopt usage is to just call:
|
||||||
|
|
||||||
|
docopt.ParseDoc(usage)
|
||||||
|
|
||||||
|
This will use os.Args[1:] as the argv slice, and use the default parser
|
||||||
|
options. If you want to provide your own version string and args, then use:
|
||||||
|
|
||||||
|
docopt.ParseArgs(usage, argv, "1.2.3")
|
||||||
|
|
||||||
|
If the last parameter (version) is a non-empty string, it will be printed when
|
||||||
|
--version is given in the argv slice. Finally, we can instantiate our own
|
||||||
|
docopt.Parser which gives us control over how things like help messages are
|
||||||
|
printed and whether to exit after displaying usage messages, etc.
|
||||||
|
|
||||||
|
parser := &docopt.Parser{
|
||||||
|
HelpHandler: docopt.PrintHelpOnly,
|
||||||
|
OptionsFirst: true,
|
||||||
|
}
|
||||||
|
opts, err := parser.ParseArgs(usage, argv, "")
|
||||||
|
|
||||||
|
In particular, setting your own custom HelpHandler function makes unit testing
|
||||||
|
your own docs with example command line invocations much more enjoyable.
|
||||||
|
|
||||||
|
All three of these return a map of option names to the values parsed from argv,
|
||||||
|
and an error or nil. You can get the values using the helpers, or just treat it
|
||||||
|
as a regular map:
|
||||||
|
|
||||||
|
flag, _ := opts.Bool("--flag")
|
||||||
|
secs, _ := opts.Int("<seconds>")
|
||||||
|
|
||||||
|
Additionally, you can `Bind` these to a struct, assigning option values to the
|
||||||
|
exported fields of that struct, all at once.
|
||||||
|
|
||||||
|
var config struct {
|
||||||
|
Command string `docopt:"<cmd>"`
|
||||||
|
Tries int `docopt:"-n"`
|
||||||
|
Force bool // Gets the value of --force
|
||||||
|
}
|
||||||
|
opts.Bind(&config)
|
||||||
|
*/
|
||||||
|
package docopt
|
||||||
575
vendor/github.com/docopt/docopt-go/docopt.go
generated
vendored
Normal file
575
vendor/github.com/docopt/docopt-go/docopt.go
generated
vendored
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
// Licensed under terms of MIT license (see LICENSE-MIT)
|
||||||
|
// Copyright (c) 2013 Keith Batten, kbatten@gmail.com
|
||||||
|
// Copyright (c) 2016 David Irvine
|
||||||
|
|
||||||
|
package docopt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
// HelpHandler is called when we encounter bad user input, or when the user
|
||||||
|
// asks for help.
|
||||||
|
// By default, this calls os.Exit(0) if it handled a built-in option such
|
||||||
|
// as -h, --help or --version. If the user errored with a wrong command or
|
||||||
|
// options, we exit with a return code of 1.
|
||||||
|
HelpHandler func(err error, usage string)
|
||||||
|
// OptionsFirst requires that option flags always come before positional
|
||||||
|
// arguments; otherwise they can overlap.
|
||||||
|
OptionsFirst bool
|
||||||
|
// SkipHelpFlags tells the parser not to look for -h and --help flags and
|
||||||
|
// call the HelpHandler.
|
||||||
|
SkipHelpFlags bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var PrintHelpAndExit = func(err error, usage string) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, usage)
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
fmt.Println(usage)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var PrintHelpOnly = func(err error, usage string) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, usage)
|
||||||
|
} else {
|
||||||
|
fmt.Println(usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var NoHelpHandler = func(err error, usage string) {}
|
||||||
|
|
||||||
|
var DefaultParser = &Parser{
|
||||||
|
HelpHandler: PrintHelpAndExit,
|
||||||
|
OptionsFirst: false,
|
||||||
|
SkipHelpFlags: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDoc parses os.Args[1:] based on the interface described in doc, using the default parser options.
|
||||||
|
func ParseDoc(doc string) (Opts, error) {
|
||||||
|
return ParseArgs(doc, nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseArgs parses custom arguments based on the interface described in doc. If you provide a non-empty version
|
||||||
|
// string, then this will be displayed when the --version flag is found. This method uses the default parser options.
|
||||||
|
func ParseArgs(doc string, argv []string, version string) (Opts, error) {
|
||||||
|
return DefaultParser.ParseArgs(doc, argv, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseArgs parses custom arguments based on the interface described in doc. If you provide a non-empty version
|
||||||
|
// string, then this will be displayed when the --version flag is found.
|
||||||
|
func (p *Parser) ParseArgs(doc string, argv []string, version string) (Opts, error) {
|
||||||
|
return p.parse(doc, argv, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Parse is provided for backward compatibility with the original docopt.go package.
|
||||||
|
// Please rather make use of ParseDoc, ParseArgs, or use your own custom Parser.
|
||||||
|
func Parse(doc string, argv []string, help bool, version string, optionsFirst bool, exit ...bool) (map[string]interface{}, error) {
|
||||||
|
exitOk := true
|
||||||
|
if len(exit) > 0 {
|
||||||
|
exitOk = exit[0]
|
||||||
|
}
|
||||||
|
p := &Parser{
|
||||||
|
OptionsFirst: optionsFirst,
|
||||||
|
SkipHelpFlags: !help,
|
||||||
|
}
|
||||||
|
if exitOk {
|
||||||
|
p.HelpHandler = PrintHelpAndExit
|
||||||
|
} else {
|
||||||
|
p.HelpHandler = PrintHelpOnly
|
||||||
|
}
|
||||||
|
return p.parse(doc, argv, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parse(doc string, argv []string, version string) (map[string]interface{}, error) {
|
||||||
|
if argv == nil {
|
||||||
|
argv = os.Args[1:]
|
||||||
|
}
|
||||||
|
if p.HelpHandler == nil {
|
||||||
|
p.HelpHandler = DefaultParser.HelpHandler
|
||||||
|
}
|
||||||
|
args, output, err := parse(doc, argv, !p.SkipHelpFlags, version, p.OptionsFirst)
|
||||||
|
if _, ok := err.(*UserError); ok {
|
||||||
|
// the user gave us bad input
|
||||||
|
p.HelpHandler(err, output)
|
||||||
|
} else if len(output) > 0 && err == nil {
|
||||||
|
// the user asked for help or --version
|
||||||
|
p.HelpHandler(err, output)
|
||||||
|
}
|
||||||
|
return args, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// parse and return a map of args, output and all errors
|
||||||
|
func parse(doc string, argv []string, help bool, version string, optionsFirst bool) (args map[string]interface{}, output string, err error) {
|
||||||
|
if argv == nil && len(os.Args) > 1 {
|
||||||
|
argv = os.Args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
usageSections := parseSection("usage:", doc)
|
||||||
|
|
||||||
|
if len(usageSections) == 0 {
|
||||||
|
err = newLanguageError("\"usage:\" (case-insensitive) not found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(usageSections) > 1 {
|
||||||
|
err = newLanguageError("More than one \"usage:\" (case-insensitive).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
usage := usageSections[0]
|
||||||
|
|
||||||
|
options := parseDefaults(doc)
|
||||||
|
formal, err := formalUsage(usage)
|
||||||
|
if err != nil {
|
||||||
|
output = handleError(err, usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pat, err := parsePattern(formal, &options)
|
||||||
|
if err != nil {
|
||||||
|
output = handleError(err, usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
patternArgv, err := parseArgv(newTokenList(argv, errorUser), &options, optionsFirst)
|
||||||
|
if err != nil {
|
||||||
|
output = handleError(err, usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
patFlat, err := pat.flat(patternOption)
|
||||||
|
if err != nil {
|
||||||
|
output = handleError(err, usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
patternOptions := patFlat.unique()
|
||||||
|
|
||||||
|
patFlat, err = pat.flat(patternOptionSSHORTCUT)
|
||||||
|
if err != nil {
|
||||||
|
output = handleError(err, usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, optionsShortcut := range patFlat {
|
||||||
|
docOptions := parseDefaults(doc)
|
||||||
|
optionsShortcut.children = docOptions.unique().diff(patternOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if output = extras(help, version, patternArgv, doc); len(output) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pat.fix()
|
||||||
|
if err != nil {
|
||||||
|
output = handleError(err, usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
matched, left, collected := pat.match(&patternArgv, nil)
|
||||||
|
if matched && len(*left) == 0 {
|
||||||
|
patFlat, err = pat.flat(patternDefault)
|
||||||
|
if err != nil {
|
||||||
|
output = handleError(err, usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
args = append(patFlat, *collected...).dictionary()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = newUserError("")
|
||||||
|
output = handleError(err, usage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(err error, usage string) string {
|
||||||
|
if _, ok := err.(*UserError); ok {
|
||||||
|
return strings.TrimSpace(fmt.Sprintf("%s\n%s", err, usage))
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSection(name, source string) []string {
|
||||||
|
p := regexp.MustCompile(`(?im)^([^\n]*` + name + `[^\n]*\n?(?:[ \t].*?(?:\n|$))*)`)
|
||||||
|
s := p.FindAllString(source, -1)
|
||||||
|
if s == nil {
|
||||||
|
s = []string{}
|
||||||
|
}
|
||||||
|
for i, v := range s {
|
||||||
|
s[i] = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDefaults(doc string) patternList {
|
||||||
|
defaults := patternList{}
|
||||||
|
p := regexp.MustCompile(`\n[ \t]*(-\S+?)`)
|
||||||
|
for _, s := range parseSection("options:", doc) {
|
||||||
|
// FIXME corner case "bla: options: --foo"
|
||||||
|
_, _, s = stringPartition(s, ":") // get rid of "options:"
|
||||||
|
split := p.Split("\n"+s, -1)[1:]
|
||||||
|
match := p.FindAllStringSubmatch("\n"+s, -1)
|
||||||
|
for i := range split {
|
||||||
|
optionDescription := match[i][1] + split[i]
|
||||||
|
if strings.HasPrefix(optionDescription, "-") {
|
||||||
|
defaults = append(defaults, parseOption(optionDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePattern(source string, options *patternList) (*pattern, error) {
|
||||||
|
tokens := tokenListFromPattern(source)
|
||||||
|
result, err := parseExpr(tokens, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tokens.current() != nil {
|
||||||
|
return nil, tokens.errorFunc("unexpected ending: %s" + strings.Join(tokens.tokens, " "))
|
||||||
|
}
|
||||||
|
return newRequired(result...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgv(tokens *tokenList, options *patternList, optionsFirst bool) (patternList, error) {
|
||||||
|
/*
|
||||||
|
Parse command-line argument vector.
|
||||||
|
|
||||||
|
If options_first:
|
||||||
|
argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
|
||||||
|
else:
|
||||||
|
argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
|
||||||
|
*/
|
||||||
|
parsed := patternList{}
|
||||||
|
for tokens.current() != nil {
|
||||||
|
if tokens.current().eq("--") {
|
||||||
|
for _, v := range tokens.tokens {
|
||||||
|
parsed = append(parsed, newArgument("", v))
|
||||||
|
}
|
||||||
|
return parsed, nil
|
||||||
|
} else if tokens.current().hasPrefix("--") {
|
||||||
|
pl, err := parseLong(tokens, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsed = append(parsed, pl...)
|
||||||
|
} else if tokens.current().hasPrefix("-") && !tokens.current().eq("-") {
|
||||||
|
ps, err := parseShorts(tokens, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsed = append(parsed, ps...)
|
||||||
|
} else if optionsFirst {
|
||||||
|
for _, v := range tokens.tokens {
|
||||||
|
parsed = append(parsed, newArgument("", v))
|
||||||
|
}
|
||||||
|
return parsed, nil
|
||||||
|
} else {
|
||||||
|
parsed = append(parsed, newArgument("", tokens.move().String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOption(optionDescription string) *pattern {
|
||||||
|
optionDescription = strings.TrimSpace(optionDescription)
|
||||||
|
options, _, description := stringPartition(optionDescription, " ")
|
||||||
|
options = strings.Replace(options, ",", " ", -1)
|
||||||
|
options = strings.Replace(options, "=", " ", -1)
|
||||||
|
|
||||||
|
short := ""
|
||||||
|
long := ""
|
||||||
|
argcount := 0
|
||||||
|
var value interface{}
|
||||||
|
value = false
|
||||||
|
|
||||||
|
reDefault := regexp.MustCompile(`(?i)\[default: (.*)\]`)
|
||||||
|
for _, s := range strings.Fields(options) {
|
||||||
|
if strings.HasPrefix(s, "--") {
|
||||||
|
long = s
|
||||||
|
} else if strings.HasPrefix(s, "-") {
|
||||||
|
short = s
|
||||||
|
} else {
|
||||||
|
argcount = 1
|
||||||
|
}
|
||||||
|
if argcount > 0 {
|
||||||
|
matched := reDefault.FindAllStringSubmatch(description, -1)
|
||||||
|
if len(matched) > 0 {
|
||||||
|
value = matched[0][1]
|
||||||
|
} else {
|
||||||
|
value = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newOption(short, long, argcount, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExpr(tokens *tokenList, options *patternList) (patternList, error) {
|
||||||
|
// expr ::= seq ( '|' seq )* ;
|
||||||
|
seq, err := parseSeq(tokens, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !tokens.current().eq("|") {
|
||||||
|
return seq, nil
|
||||||
|
}
|
||||||
|
var result patternList
|
||||||
|
if len(seq) > 1 {
|
||||||
|
result = patternList{newRequired(seq...)}
|
||||||
|
} else {
|
||||||
|
result = seq
|
||||||
|
}
|
||||||
|
for tokens.current().eq("|") {
|
||||||
|
tokens.move()
|
||||||
|
seq, err = parseSeq(tokens, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(seq) > 1 {
|
||||||
|
result = append(result, newRequired(seq...))
|
||||||
|
} else {
|
||||||
|
result = append(result, seq...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) > 1 {
|
||||||
|
return patternList{newEither(result...)}, nil
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSeq(tokens *tokenList, options *patternList) (patternList, error) {
|
||||||
|
// seq ::= ( atom [ '...' ] )* ;
|
||||||
|
result := patternList{}
|
||||||
|
for !tokens.current().match(true, "]", ")", "|") {
|
||||||
|
atom, err := parseAtom(tokens, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tokens.current().eq("...") {
|
||||||
|
atom = patternList{newOneOrMore(atom...)}
|
||||||
|
tokens.move()
|
||||||
|
}
|
||||||
|
result = append(result, atom...)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAtom(tokens *tokenList, options *patternList) (patternList, error) {
|
||||||
|
// atom ::= '(' expr ')' | '[' expr ']' | 'options' | long | shorts | argument | command ;
|
||||||
|
tok := tokens.current()
|
||||||
|
result := patternList{}
|
||||||
|
if tokens.current().match(false, "(", "[") {
|
||||||
|
tokens.move()
|
||||||
|
var matching string
|
||||||
|
pl, err := parseExpr(tokens, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tok.eq("(") {
|
||||||
|
matching = ")"
|
||||||
|
result = patternList{newRequired(pl...)}
|
||||||
|
} else if tok.eq("[") {
|
||||||
|
matching = "]"
|
||||||
|
result = patternList{newOptional(pl...)}
|
||||||
|
}
|
||||||
|
moved := tokens.move()
|
||||||
|
if !moved.eq(matching) {
|
||||||
|
return nil, tokens.errorFunc("unmatched '%s', expected: '%s' got: '%s'", tok, matching, moved)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
} else if tok.eq("options") {
|
||||||
|
tokens.move()
|
||||||
|
return patternList{newOptionsShortcut()}, nil
|
||||||
|
} else if tok.hasPrefix("--") && !tok.eq("--") {
|
||||||
|
return parseLong(tokens, options)
|
||||||
|
} else if tok.hasPrefix("-") && !tok.eq("-") && !tok.eq("--") {
|
||||||
|
return parseShorts(tokens, options)
|
||||||
|
} else if tok.hasPrefix("<") && tok.hasSuffix(">") || tok.isUpper() {
|
||||||
|
return patternList{newArgument(tokens.move().String(), nil)}, nil
|
||||||
|
}
|
||||||
|
return patternList{newCommand(tokens.move().String(), false)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLong(tokens *tokenList, options *patternList) (patternList, error) {
|
||||||
|
// long ::= '--' chars [ ( ' ' | '=' ) chars ] ;
|
||||||
|
long, eq, v := stringPartition(tokens.move().String(), "=")
|
||||||
|
var value interface{}
|
||||||
|
var opt *pattern
|
||||||
|
if eq == "" && v == "" {
|
||||||
|
value = nil
|
||||||
|
} else {
|
||||||
|
value = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(long, "--") {
|
||||||
|
return nil, newError("long option '%s' doesn't start with --", long)
|
||||||
|
}
|
||||||
|
similar := patternList{}
|
||||||
|
for _, o := range *options {
|
||||||
|
if o.long == long {
|
||||||
|
similar = append(similar, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokens.err == errorUser && len(similar) == 0 { // if no exact match
|
||||||
|
similar = patternList{}
|
||||||
|
for _, o := range *options {
|
||||||
|
if strings.HasPrefix(o.long, long) {
|
||||||
|
similar = append(similar, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(similar) > 1 { // might be simply specified ambiguously 2+ times?
|
||||||
|
similarLong := make([]string, len(similar))
|
||||||
|
for i, s := range similar {
|
||||||
|
similarLong[i] = s.long
|
||||||
|
}
|
||||||
|
return nil, tokens.errorFunc("%s is not a unique prefix: %s?", long, strings.Join(similarLong, ", "))
|
||||||
|
} else if len(similar) < 1 {
|
||||||
|
argcount := 0
|
||||||
|
if eq == "=" {
|
||||||
|
argcount = 1
|
||||||
|
}
|
||||||
|
opt = newOption("", long, argcount, false)
|
||||||
|
*options = append(*options, opt)
|
||||||
|
if tokens.err == errorUser {
|
||||||
|
var val interface{}
|
||||||
|
if argcount > 0 {
|
||||||
|
val = value
|
||||||
|
} else {
|
||||||
|
val = true
|
||||||
|
}
|
||||||
|
opt = newOption("", long, argcount, val)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opt = newOption(similar[0].short, similar[0].long, similar[0].argcount, similar[0].value)
|
||||||
|
if opt.argcount == 0 {
|
||||||
|
if value != nil {
|
||||||
|
return nil, tokens.errorFunc("%s must not have an argument", opt.long)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if value == nil {
|
||||||
|
if tokens.current().match(true, "--") {
|
||||||
|
return nil, tokens.errorFunc("%s requires argument", opt.long)
|
||||||
|
}
|
||||||
|
moved := tokens.move()
|
||||||
|
if moved != nil {
|
||||||
|
value = moved.String() // only set as string if not nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokens.err == errorUser {
|
||||||
|
if value != nil {
|
||||||
|
opt.value = value
|
||||||
|
} else {
|
||||||
|
opt.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return patternList{opt}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseShorts(tokens *tokenList, options *patternList) (patternList, error) {
|
||||||
|
// shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;
|
||||||
|
tok := tokens.move()
|
||||||
|
if !tok.hasPrefix("-") || tok.hasPrefix("--") {
|
||||||
|
return nil, newError("short option '%s' doesn't start with -", tok)
|
||||||
|
}
|
||||||
|
left := strings.TrimLeft(tok.String(), "-")
|
||||||
|
parsed := patternList{}
|
||||||
|
for left != "" {
|
||||||
|
var opt *pattern
|
||||||
|
short := "-" + left[0:1]
|
||||||
|
left = left[1:]
|
||||||
|
similar := patternList{}
|
||||||
|
for _, o := range *options {
|
||||||
|
if o.short == short {
|
||||||
|
similar = append(similar, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(similar) > 1 {
|
||||||
|
return nil, tokens.errorFunc("%s is specified ambiguously %d times", short, len(similar))
|
||||||
|
} else if len(similar) < 1 {
|
||||||
|
opt = newOption(short, "", 0, false)
|
||||||
|
*options = append(*options, opt)
|
||||||
|
if tokens.err == errorUser {
|
||||||
|
opt = newOption(short, "", 0, true)
|
||||||
|
}
|
||||||
|
} else { // why copying is necessary here?
|
||||||
|
opt = newOption(short, similar[0].long, similar[0].argcount, similar[0].value)
|
||||||
|
var value interface{}
|
||||||
|
if opt.argcount > 0 {
|
||||||
|
if left == "" {
|
||||||
|
if tokens.current().match(true, "--") {
|
||||||
|
return nil, tokens.errorFunc("%s requires argument", short)
|
||||||
|
}
|
||||||
|
value = tokens.move().String()
|
||||||
|
} else {
|
||||||
|
value = left
|
||||||
|
left = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokens.err == errorUser {
|
||||||
|
if value != nil {
|
||||||
|
opt.value = value
|
||||||
|
} else {
|
||||||
|
opt.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsed = append(parsed, opt)
|
||||||
|
}
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formalUsage(section string) (string, error) {
|
||||||
|
_, _, section = stringPartition(section, ":") // drop "usage:"
|
||||||
|
pu := strings.Fields(section)
|
||||||
|
|
||||||
|
if len(pu) == 0 {
|
||||||
|
return "", newLanguageError("no fields found in usage (perhaps a spacing error).")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := "( "
|
||||||
|
for _, s := range pu[1:] {
|
||||||
|
if s == pu[0] {
|
||||||
|
result += ") | ( "
|
||||||
|
} else {
|
||||||
|
result += s + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += ")"
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extras(help bool, version string, options patternList, doc string) string {
|
||||||
|
if help {
|
||||||
|
for _, o := range options {
|
||||||
|
if (o.name == "-h" || o.name == "--help") && o.value == true {
|
||||||
|
return strings.Trim(doc, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if version != "" {
|
||||||
|
for _, o := range options {
|
||||||
|
if (o.name == "--version") && o.value == true {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringPartition(s, sep string) (string, string, string) {
|
||||||
|
sepPos := strings.Index(s, sep)
|
||||||
|
if sepPos == -1 { // no seperator found
|
||||||
|
return s, "", ""
|
||||||
|
}
|
||||||
|
split := strings.SplitN(s, sep, 2)
|
||||||
|
return split[0], sep, split[1]
|
||||||
|
}
|
||||||
49
vendor/github.com/docopt/docopt-go/error.go
generated
vendored
Normal file
49
vendor/github.com/docopt/docopt-go/error.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package docopt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errorType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
errorUser errorType = iota
|
||||||
|
errorLanguage
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e errorType) String() string {
|
||||||
|
switch e {
|
||||||
|
case errorUser:
|
||||||
|
return "errorUser"
|
||||||
|
case errorLanguage:
|
||||||
|
return "errorLanguage"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserError records an error with program arguments.
|
||||||
|
type UserError struct {
|
||||||
|
msg string
|
||||||
|
Usage string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UserError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
func newUserError(msg string, f ...interface{}) error {
|
||||||
|
return &UserError{fmt.Sprintf(msg, f...), ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LanguageError records an error with the doc string.
|
||||||
|
type LanguageError struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e LanguageError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
func newLanguageError(msg string, f ...interface{}) error {
|
||||||
|
return &LanguageError{fmt.Sprintf(msg, f...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newError = fmt.Errorf
|
||||||
264
vendor/github.com/docopt/docopt-go/opts.go
generated
vendored
Normal file
264
vendor/github.com/docopt/docopt-go/opts.go
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package docopt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func errKey(key string) error {
|
||||||
|
return fmt.Errorf("no such key: %q", key)
|
||||||
|
}
|
||||||
|
func errType(key string) error {
|
||||||
|
return fmt.Errorf("key: %q failed type conversion", key)
|
||||||
|
}
|
||||||
|
func errStrconv(key string, convErr error) error {
|
||||||
|
return fmt.Errorf("key: %q failed type conversion: %s", key, convErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opts is a map of command line options to their values, with some convenience
|
||||||
|
// methods for value type conversion (bool, float64, int, string). For example,
|
||||||
|
// to get an option value as an int:
|
||||||
|
//
|
||||||
|
// opts, _ := docopt.ParseDoc("Usage: sleep <seconds>")
|
||||||
|
// secs, _ := opts.Int("<seconds>")
|
||||||
|
//
|
||||||
|
// Additionally, Opts.Bind allows you easily populate a struct's fields with the
|
||||||
|
// values of each option value. See below for examples.
|
||||||
|
//
|
||||||
|
// Lastly, you can still treat Opts as a regular map, and do any type checking
|
||||||
|
// and conversion that you want to yourself. For example:
|
||||||
|
//
|
||||||
|
// if s, ok := opts["<binary>"].(string); ok {
|
||||||
|
// if val, err := strconv.ParseUint(s, 2, 64); err != nil { ... }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note that any non-boolean option / flag will have a string value in the
|
||||||
|
// underlying map.
|
||||||
|
type Opts map[string]interface{}
|
||||||
|
|
||||||
|
func (o Opts) String(key string) (s string, err error) {
|
||||||
|
v, ok := o[key]
|
||||||
|
if !ok {
|
||||||
|
err = errKey(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s, ok = v.(string)
|
||||||
|
if !ok {
|
||||||
|
err = errType(key)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Opts) Bool(key string) (b bool, err error) {
|
||||||
|
v, ok := o[key]
|
||||||
|
if !ok {
|
||||||
|
err = errKey(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, ok = v.(bool)
|
||||||
|
if !ok {
|
||||||
|
err = errType(key)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Opts) Int(key string) (i int, err error) {
|
||||||
|
s, err := o.String(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i, err = strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
err = errStrconv(key, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Opts) Float64(key string) (f float64, err error) {
|
||||||
|
s, err := o.String(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err = strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = errStrconv(key, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind populates the fields of a given struct with matching option values.
|
||||||
|
// Each key in Opts will be mapped to an exported field of the struct pointed
|
||||||
|
// to by `v`, as follows:
|
||||||
|
//
|
||||||
|
// abc int // Unexported field, ignored
|
||||||
|
// Abc string // Mapped from `--abc`, `<abc>`, or `abc`
|
||||||
|
// // (case insensitive)
|
||||||
|
// A string // Mapped from `-a`, `<a>` or `a`
|
||||||
|
// // (case insensitive)
|
||||||
|
// Abc int `docopt:"XYZ"` // Mapped from `XYZ`
|
||||||
|
// Abc bool `docopt:"-"` // Mapped from `-`
|
||||||
|
// Abc bool `docopt:"-x,--xyz"` // Mapped from `-x` or `--xyz`
|
||||||
|
// // (first non-zero value found)
|
||||||
|
//
|
||||||
|
// Tagged (annotated) fields will always be mapped first. If no field is tagged
|
||||||
|
// with an option's key, Bind will try to map the option to an appropriately
|
||||||
|
// named field (as above).
|
||||||
|
//
|
||||||
|
// Bind also handles conversion to bool, float, int or string types.
|
||||||
|
func (o Opts) Bind(v interface{}) error {
|
||||||
|
structVal := reflect.ValueOf(v)
|
||||||
|
if structVal.Kind() != reflect.Ptr {
|
||||||
|
return newError("'v' argument is not pointer to struct type")
|
||||||
|
}
|
||||||
|
for structVal.Kind() == reflect.Ptr {
|
||||||
|
structVal = structVal.Elem()
|
||||||
|
}
|
||||||
|
if structVal.Kind() != reflect.Struct {
|
||||||
|
return newError("'v' argument is not pointer to struct type")
|
||||||
|
}
|
||||||
|
structType := structVal.Type()
|
||||||
|
|
||||||
|
tagged := make(map[string]int) // Tagged field tags
|
||||||
|
untagged := make(map[string]int) // Untagged field names
|
||||||
|
|
||||||
|
for i := 0; i < structType.NumField(); i++ {
|
||||||
|
field := structType.Field(i)
|
||||||
|
if isUnexportedField(field) || field.Anonymous {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tag := field.Tag.Get("docopt")
|
||||||
|
if tag == "" {
|
||||||
|
untagged[field.Name] = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, t := range strings.Split(tag, ",") {
|
||||||
|
tagged[t] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the index of the struct field to use, based on the option key.
|
||||||
|
// Second argument is true/false on whether something was matched.
|
||||||
|
getFieldIndex := func(key string) (int, bool) {
|
||||||
|
if i, ok := tagged[key]; ok {
|
||||||
|
return i, true
|
||||||
|
}
|
||||||
|
if i, ok := untagged[guessUntaggedField(key)]; ok {
|
||||||
|
return i, true
|
||||||
|
}
|
||||||
|
return -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
indexMap := make(map[string]int) // Option keys to field index
|
||||||
|
|
||||||
|
// Pre-check that option keys are mapped to fields and fields are zero valued, before populating them.
|
||||||
|
for k := range o {
|
||||||
|
i, ok := getFieldIndex(k)
|
||||||
|
if !ok {
|
||||||
|
if k == "--help" || k == "--version" { // Don't require these to be mapped.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return newError("mapping of %q is not found in given struct, or is an unexported field", k)
|
||||||
|
}
|
||||||
|
fieldVal := structVal.Field(i)
|
||||||
|
zeroVal := reflect.Zero(fieldVal.Type())
|
||||||
|
if !reflect.DeepEqual(fieldVal.Interface(), zeroVal.Interface()) {
|
||||||
|
return newError("%q field is non-zero, will be overwritten by value of %q", structType.Field(i).Name, k)
|
||||||
|
}
|
||||||
|
indexMap[k] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate fields with option values.
|
||||||
|
for k, v := range o {
|
||||||
|
i, ok := indexMap[k]
|
||||||
|
if !ok {
|
||||||
|
continue // Not mapped.
|
||||||
|
}
|
||||||
|
field := structVal.Field(i)
|
||||||
|
if !reflect.DeepEqual(field.Interface(), reflect.Zero(field.Type()).Interface()) {
|
||||||
|
// The struct's field is already non-zero (by our doing), so don't change it.
|
||||||
|
// This happens with comma separated tags, e.g. `docopt:"-h,--help"` which is a
|
||||||
|
// convenient way of checking if one of multiple boolean flags are set.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
optVal := reflect.ValueOf(v)
|
||||||
|
// Option value is the zero Value, so we can't get its .Type(). No need to assign anyway, so move along.
|
||||||
|
if !optVal.IsValid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !field.CanSet() {
|
||||||
|
return newError("%q field cannot be set", structType.Field(i).Name)
|
||||||
|
}
|
||||||
|
// Try to assign now if able. bool and string values should be assignable already.
|
||||||
|
if optVal.Type().AssignableTo(field.Type()) {
|
||||||
|
field.Set(optVal)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Try to convert the value and assign if able.
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
if x, err := o.Int(k); err == nil {
|
||||||
|
field.SetInt(int64(x))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
if x, err := o.Float64(k); err == nil {
|
||||||
|
field.SetFloat(x)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Something clever (recursive?) with non-string slices.
|
||||||
|
// case reflect.Slice:
|
||||||
|
// if optVal.Kind() == reflect.Slice {
|
||||||
|
// for i := 0; i < optVal.Len(); i++ {
|
||||||
|
// sliceVal := optVal.Index(i)
|
||||||
|
// fmt.Printf("%v", sliceVal)
|
||||||
|
// }
|
||||||
|
// fmt.Printf("\n")
|
||||||
|
// }
|
||||||
|
return newError("value of %q is not assignable to %q field", k, structType.Field(i).Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUnexportedField returns whether the field is unexported.
|
||||||
|
// isUnexportedField is to avoid the bug in versions older than Go1.3.
|
||||||
|
// See following links:
|
||||||
|
// https://code.google.com/p/go/issues/detail?id=7247
|
||||||
|
// http://golang.org/ref/spec#Exported_identifiers
|
||||||
|
func isUnexportedField(field reflect.StructField) bool {
|
||||||
|
return !(field.PkgPath == "" && unicode.IsUpper(rune(field.Name[0])))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a string like "--my-special-flag" to "MySpecialFlag".
|
||||||
|
func titleCaseDashes(key string) string {
|
||||||
|
nextToUpper := true
|
||||||
|
mapFn := func(r rune) rune {
|
||||||
|
if r == '-' {
|
||||||
|
nextToUpper = true
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if nextToUpper {
|
||||||
|
nextToUpper = false
|
||||||
|
return unicode.ToUpper(r)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return strings.Map(mapFn, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best guess which field.Name in a struct to assign for an option key.
|
||||||
|
func guessUntaggedField(key string) string {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(key, "--") && len(key[2:]) > 1:
|
||||||
|
return titleCaseDashes(key[2:])
|
||||||
|
case strings.HasPrefix(key, "-") && len(key[1:]) == 1:
|
||||||
|
return titleCaseDashes(key[1:])
|
||||||
|
case strings.HasPrefix(key, "<") && strings.HasSuffix(key, ">"):
|
||||||
|
key = key[1 : len(key)-1]
|
||||||
|
}
|
||||||
|
return strings.Title(strings.ToLower(key))
|
||||||
|
}
|
||||||
550
vendor/github.com/docopt/docopt-go/pattern.go
generated
vendored
Normal file
550
vendor/github.com/docopt/docopt-go/pattern.go
generated
vendored
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
package docopt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type patternType uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// leaf
|
||||||
|
patternArgument patternType = 1 << iota
|
||||||
|
patternCommand
|
||||||
|
patternOption
|
||||||
|
|
||||||
|
// branch
|
||||||
|
patternRequired
|
||||||
|
patternOptionAL
|
||||||
|
patternOptionSSHORTCUT // Marker/placeholder for [options] shortcut.
|
||||||
|
patternOneOrMore
|
||||||
|
patternEither
|
||||||
|
|
||||||
|
patternLeaf = patternArgument +
|
||||||
|
patternCommand +
|
||||||
|
patternOption
|
||||||
|
patternBranch = patternRequired +
|
||||||
|
patternOptionAL +
|
||||||
|
patternOptionSSHORTCUT +
|
||||||
|
patternOneOrMore +
|
||||||
|
patternEither
|
||||||
|
patternAll = patternLeaf + patternBranch
|
||||||
|
patternDefault = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pt patternType) String() string {
|
||||||
|
switch pt {
|
||||||
|
case patternArgument:
|
||||||
|
return "argument"
|
||||||
|
case patternCommand:
|
||||||
|
return "command"
|
||||||
|
case patternOption:
|
||||||
|
return "option"
|
||||||
|
case patternRequired:
|
||||||
|
return "required"
|
||||||
|
case patternOptionAL:
|
||||||
|
return "optional"
|
||||||
|
case patternOptionSSHORTCUT:
|
||||||
|
return "optionsshortcut"
|
||||||
|
case patternOneOrMore:
|
||||||
|
return "oneormore"
|
||||||
|
case patternEither:
|
||||||
|
return "either"
|
||||||
|
case patternLeaf:
|
||||||
|
return "leaf"
|
||||||
|
case patternBranch:
|
||||||
|
return "branch"
|
||||||
|
case patternAll:
|
||||||
|
return "all"
|
||||||
|
case patternDefault:
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type pattern struct {
|
||||||
|
t patternType
|
||||||
|
|
||||||
|
children patternList
|
||||||
|
|
||||||
|
name string
|
||||||
|
value interface{}
|
||||||
|
|
||||||
|
short string
|
||||||
|
long string
|
||||||
|
argcount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type patternList []*pattern
|
||||||
|
|
||||||
|
func newBranchPattern(t patternType, pl ...*pattern) *pattern {
|
||||||
|
var p pattern
|
||||||
|
p.t = t
|
||||||
|
p.children = make(patternList, len(pl))
|
||||||
|
copy(p.children, pl)
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequired(pl ...*pattern) *pattern {
|
||||||
|
return newBranchPattern(patternRequired, pl...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEither(pl ...*pattern) *pattern {
|
||||||
|
return newBranchPattern(patternEither, pl...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOneOrMore(pl ...*pattern) *pattern {
|
||||||
|
return newBranchPattern(patternOneOrMore, pl...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOptional(pl ...*pattern) *pattern {
|
||||||
|
return newBranchPattern(patternOptionAL, pl...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOptionsShortcut() *pattern {
|
||||||
|
var p pattern
|
||||||
|
p.t = patternOptionSSHORTCUT
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLeafPattern(t patternType, name string, value interface{}) *pattern {
|
||||||
|
// default: value=nil
|
||||||
|
var p pattern
|
||||||
|
p.t = t
|
||||||
|
p.name = name
|
||||||
|
p.value = value
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
|
||||||
|
func newArgument(name string, value interface{}) *pattern {
|
||||||
|
// default: value=nil
|
||||||
|
return newLeafPattern(patternArgument, name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommand(name string, value interface{}) *pattern {
|
||||||
|
// default: value=false
|
||||||
|
var p pattern
|
||||||
|
p.t = patternCommand
|
||||||
|
p.name = name
|
||||||
|
p.value = value
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOption(short, long string, argcount int, value interface{}) *pattern {
|
||||||
|
// default: "", "", 0, false
|
||||||
|
var p pattern
|
||||||
|
p.t = patternOption
|
||||||
|
p.short = short
|
||||||
|
p.long = long
|
||||||
|
if long != "" {
|
||||||
|
p.name = long
|
||||||
|
} else {
|
||||||
|
p.name = short
|
||||||
|
}
|
||||||
|
p.argcount = argcount
|
||||||
|
if value == false && argcount > 0 {
|
||||||
|
p.value = nil
|
||||||
|
} else {
|
||||||
|
p.value = value
|
||||||
|
}
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) flat(types patternType) (patternList, error) {
|
||||||
|
if p.t&patternLeaf != 0 {
|
||||||
|
if types == patternDefault {
|
||||||
|
types = patternAll
|
||||||
|
}
|
||||||
|
if p.t&types != 0 {
|
||||||
|
return patternList{p}, nil
|
||||||
|
}
|
||||||
|
return patternList{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.t&patternBranch != 0 {
|
||||||
|
if p.t&types != 0 {
|
||||||
|
return patternList{p}, nil
|
||||||
|
}
|
||||||
|
result := patternList{}
|
||||||
|
for _, child := range p.children {
|
||||||
|
childFlat, err := child.flat(types)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = append(result, childFlat...)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
return nil, newError("unknown pattern type: %d, %d", p.t, types)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) fix() error {
|
||||||
|
err := p.fixIdentities(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.fixRepeatingArguments()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) fixIdentities(uniq patternList) error {
|
||||||
|
// Make pattern-tree tips point to same object if they are equal.
|
||||||
|
if p.t&patternBranch == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if uniq == nil {
|
||||||
|
pFlat, err := p.flat(patternDefault)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
uniq = pFlat.unique()
|
||||||
|
}
|
||||||
|
for i, child := range p.children {
|
||||||
|
if child.t&patternBranch == 0 {
|
||||||
|
ind, err := uniq.index(child)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.children[i] = uniq[ind]
|
||||||
|
} else {
|
||||||
|
err := child.fixIdentities(uniq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) fixRepeatingArguments() {
|
||||||
|
// Fix elements that should accumulate/increment values.
|
||||||
|
var either []patternList
|
||||||
|
|
||||||
|
for _, child := range p.transform().children {
|
||||||
|
either = append(either, child.children)
|
||||||
|
}
|
||||||
|
for _, cas := range either {
|
||||||
|
casMultiple := patternList{}
|
||||||
|
for _, e := range cas {
|
||||||
|
if cas.count(e) > 1 {
|
||||||
|
casMultiple = append(casMultiple, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range casMultiple {
|
||||||
|
if e.t == patternArgument || e.t == patternOption && e.argcount > 0 {
|
||||||
|
switch e.value.(type) {
|
||||||
|
case string:
|
||||||
|
e.value = strings.Fields(e.value.(string))
|
||||||
|
case []string:
|
||||||
|
default:
|
||||||
|
e.value = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.t == patternCommand || e.t == patternOption && e.argcount == 0 {
|
||||||
|
e.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) match(left *patternList, collected *patternList) (bool, *patternList, *patternList) {
|
||||||
|
if collected == nil {
|
||||||
|
collected = &patternList{}
|
||||||
|
}
|
||||||
|
if p.t&patternRequired != 0 {
|
||||||
|
l := left
|
||||||
|
c := collected
|
||||||
|
for _, p := range p.children {
|
||||||
|
var matched bool
|
||||||
|
matched, l, c = p.match(l, c)
|
||||||
|
if !matched {
|
||||||
|
return false, left, collected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, l, c
|
||||||
|
} else if p.t&patternOptionAL != 0 || p.t&patternOptionSSHORTCUT != 0 {
|
||||||
|
for _, p := range p.children {
|
||||||
|
_, left, collected = p.match(left, collected)
|
||||||
|
}
|
||||||
|
return true, left, collected
|
||||||
|
} else if p.t&patternOneOrMore != 0 {
|
||||||
|
if len(p.children) != 1 {
|
||||||
|
panic("OneOrMore.match(): assert len(p.children) == 1")
|
||||||
|
}
|
||||||
|
l := left
|
||||||
|
c := collected
|
||||||
|
var lAlt *patternList
|
||||||
|
matched := true
|
||||||
|
times := 0
|
||||||
|
for matched {
|
||||||
|
// could it be that something didn't match but changed l or c?
|
||||||
|
matched, l, c = p.children[0].match(l, c)
|
||||||
|
if matched {
|
||||||
|
times++
|
||||||
|
}
|
||||||
|
if lAlt == l {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lAlt = l
|
||||||
|
}
|
||||||
|
if times >= 1 {
|
||||||
|
return true, l, c
|
||||||
|
}
|
||||||
|
return false, left, collected
|
||||||
|
} else if p.t&patternEither != 0 {
|
||||||
|
type outcomeStruct struct {
|
||||||
|
matched bool
|
||||||
|
left *patternList
|
||||||
|
collected *patternList
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
outcomes := []outcomeStruct{}
|
||||||
|
for _, p := range p.children {
|
||||||
|
matched, l, c := p.match(left, collected)
|
||||||
|
outcome := outcomeStruct{matched, l, c, len(*l)}
|
||||||
|
if matched {
|
||||||
|
outcomes = append(outcomes, outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(outcomes) > 0 {
|
||||||
|
minLen := outcomes[0].length
|
||||||
|
minIndex := 0
|
||||||
|
for i, v := range outcomes {
|
||||||
|
if v.length < minLen {
|
||||||
|
minIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outcomes[minIndex].matched, outcomes[minIndex].left, outcomes[minIndex].collected
|
||||||
|
}
|
||||||
|
return false, left, collected
|
||||||
|
} else if p.t&patternLeaf != 0 {
|
||||||
|
pos, match := p.singleMatch(left)
|
||||||
|
var increment interface{}
|
||||||
|
if match == nil {
|
||||||
|
return false, left, collected
|
||||||
|
}
|
||||||
|
leftAlt := make(patternList, len((*left)[:pos]), len((*left)[:pos])+len((*left)[pos+1:]))
|
||||||
|
copy(leftAlt, (*left)[:pos])
|
||||||
|
leftAlt = append(leftAlt, (*left)[pos+1:]...)
|
||||||
|
sameName := patternList{}
|
||||||
|
for _, a := range *collected {
|
||||||
|
if a.name == p.name {
|
||||||
|
sameName = append(sameName, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.value.(type) {
|
||||||
|
case int, []string:
|
||||||
|
switch p.value.(type) {
|
||||||
|
case int:
|
||||||
|
increment = 1
|
||||||
|
case []string:
|
||||||
|
switch match.value.(type) {
|
||||||
|
case string:
|
||||||
|
increment = []string{match.value.(string)}
|
||||||
|
default:
|
||||||
|
increment = match.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(sameName) == 0 {
|
||||||
|
match.value = increment
|
||||||
|
collectedMatch := make(patternList, len(*collected), len(*collected)+1)
|
||||||
|
copy(collectedMatch, *collected)
|
||||||
|
collectedMatch = append(collectedMatch, match)
|
||||||
|
return true, &leftAlt, &collectedMatch
|
||||||
|
}
|
||||||
|
switch sameName[0].value.(type) {
|
||||||
|
case int:
|
||||||
|
sameName[0].value = sameName[0].value.(int) + increment.(int)
|
||||||
|
case []string:
|
||||||
|
sameName[0].value = append(sameName[0].value.([]string), increment.([]string)...)
|
||||||
|
}
|
||||||
|
return true, &leftAlt, collected
|
||||||
|
}
|
||||||
|
collectedMatch := make(patternList, len(*collected), len(*collected)+1)
|
||||||
|
copy(collectedMatch, *collected)
|
||||||
|
collectedMatch = append(collectedMatch, match)
|
||||||
|
return true, &leftAlt, &collectedMatch
|
||||||
|
}
|
||||||
|
panic("unmatched type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) singleMatch(left *patternList) (int, *pattern) {
|
||||||
|
if p.t&patternArgument != 0 {
|
||||||
|
for n, pat := range *left {
|
||||||
|
if pat.t&patternArgument != 0 {
|
||||||
|
return n, newArgument(p.name, pat.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
} else if p.t&patternCommand != 0 {
|
||||||
|
for n, pat := range *left {
|
||||||
|
if pat.t&patternArgument != 0 {
|
||||||
|
if pat.value == p.name {
|
||||||
|
return n, newCommand(p.name, true)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
} else if p.t&patternOption != 0 {
|
||||||
|
for n, pat := range *left {
|
||||||
|
if p.name == pat.name {
|
||||||
|
return n, pat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
panic("unmatched type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) String() string {
|
||||||
|
if p.t&patternOption != 0 {
|
||||||
|
return fmt.Sprintf("%s(%s, %s, %d, %+v)", p.t, p.short, p.long, p.argcount, p.value)
|
||||||
|
} else if p.t&patternLeaf != 0 {
|
||||||
|
return fmt.Sprintf("%s(%s, %+v)", p.t, p.name, p.value)
|
||||||
|
} else if p.t&patternBranch != 0 {
|
||||||
|
result := ""
|
||||||
|
for i, child := range p.children {
|
||||||
|
if i > 0 {
|
||||||
|
result += ", "
|
||||||
|
}
|
||||||
|
result += child.String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s(%s)", p.t, result)
|
||||||
|
}
|
||||||
|
panic("unmatched type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) transform() *pattern {
|
||||||
|
/*
|
||||||
|
Expand pattern into an (almost) equivalent one, but with single Either.
|
||||||
|
|
||||||
|
Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
|
||||||
|
Quirks: [-a] => (-a), (-a...) => (-a -a)
|
||||||
|
*/
|
||||||
|
result := []patternList{}
|
||||||
|
groups := []patternList{patternList{p}}
|
||||||
|
parents := patternRequired +
|
||||||
|
patternOptionAL +
|
||||||
|
patternOptionSSHORTCUT +
|
||||||
|
patternEither +
|
||||||
|
patternOneOrMore
|
||||||
|
for len(groups) > 0 {
|
||||||
|
children := groups[0]
|
||||||
|
groups = groups[1:]
|
||||||
|
var child *pattern
|
||||||
|
for _, c := range children {
|
||||||
|
if c.t&parents != 0 {
|
||||||
|
child = c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if child != nil {
|
||||||
|
children.remove(child)
|
||||||
|
if child.t&patternEither != 0 {
|
||||||
|
for _, c := range child.children {
|
||||||
|
r := patternList{}
|
||||||
|
r = append(r, c)
|
||||||
|
r = append(r, children...)
|
||||||
|
groups = append(groups, r)
|
||||||
|
}
|
||||||
|
} else if child.t&patternOneOrMore != 0 {
|
||||||
|
r := patternList{}
|
||||||
|
r = append(r, child.children.double()...)
|
||||||
|
r = append(r, children...)
|
||||||
|
groups = append(groups, r)
|
||||||
|
} else {
|
||||||
|
r := patternList{}
|
||||||
|
r = append(r, child.children...)
|
||||||
|
r = append(r, children...)
|
||||||
|
groups = append(groups, r)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = append(result, children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
either := patternList{}
|
||||||
|
for _, e := range result {
|
||||||
|
either = append(either, newRequired(e...))
|
||||||
|
}
|
||||||
|
return newEither(either...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) eq(other *pattern) bool {
|
||||||
|
return reflect.DeepEqual(p, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl patternList) unique() patternList {
|
||||||
|
table := make(map[string]bool)
|
||||||
|
result := patternList{}
|
||||||
|
for _, v := range pl {
|
||||||
|
if !table[v.String()] {
|
||||||
|
table[v.String()] = true
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl patternList) index(p *pattern) (int, error) {
|
||||||
|
for i, c := range pl {
|
||||||
|
if c.eq(p) {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, newError("%s not in list", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl patternList) count(p *pattern) int {
|
||||||
|
count := 0
|
||||||
|
for _, c := range pl {
|
||||||
|
if c.eq(p) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl patternList) diff(l patternList) patternList {
|
||||||
|
lAlt := make(patternList, len(l))
|
||||||
|
copy(lAlt, l)
|
||||||
|
result := make(patternList, 0, len(pl))
|
||||||
|
for _, v := range pl {
|
||||||
|
if v != nil {
|
||||||
|
match := false
|
||||||
|
for i, w := range lAlt {
|
||||||
|
if w.eq(v) {
|
||||||
|
match = true
|
||||||
|
lAlt[i] = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match == false {
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl patternList) double() patternList {
|
||||||
|
l := len(pl)
|
||||||
|
result := make(patternList, l*2)
|
||||||
|
copy(result, pl)
|
||||||
|
copy(result[l:2*l], pl)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl *patternList) remove(p *pattern) {
|
||||||
|
(*pl) = pl.diff(patternList{p})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pl patternList) dictionary() map[string]interface{} {
|
||||||
|
dict := make(map[string]interface{})
|
||||||
|
for _, a := range pl {
|
||||||
|
dict[a.name] = a.value
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}
|
||||||
9
vendor/github.com/docopt/docopt-go/test_golang.docopt
generated
vendored
Normal file
9
vendor/github.com/docopt/docopt-go/test_golang.docopt
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
r"""usage: prog [NAME_-2]..."""
|
||||||
|
$ prog 10 20
|
||||||
|
{"NAME_-2": ["10", "20"]}
|
||||||
|
|
||||||
|
$ prog 10
|
||||||
|
{"NAME_-2": ["10"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"NAME_-2": []}
|
||||||
957
vendor/github.com/docopt/docopt-go/testcases.docopt
generated
vendored
Normal file
957
vendor/github.com/docopt/docopt-go/testcases.docopt
generated
vendored
Normal file
@@ -0,0 +1,957 @@
|
|||||||
|
r"""Usage: prog
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{}
|
||||||
|
|
||||||
|
$ prog --xxx
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options: -a All.
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{"-a": false}
|
||||||
|
|
||||||
|
$ prog -a
|
||||||
|
{"-a": true}
|
||||||
|
|
||||||
|
$ prog -x
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options: --all All.
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{"--all": false}
|
||||||
|
|
||||||
|
$ prog --all
|
||||||
|
{"--all": true}
|
||||||
|
|
||||||
|
$ prog --xxx
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options: -v, --verbose Verbose.
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog --verbose
|
||||||
|
{"--verbose": true}
|
||||||
|
|
||||||
|
$ prog --ver
|
||||||
|
{"--verbose": true}
|
||||||
|
|
||||||
|
$ prog -v
|
||||||
|
{"--verbose": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options: -p PATH
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -p home/
|
||||||
|
{"-p": "home/"}
|
||||||
|
|
||||||
|
$ prog -phome/
|
||||||
|
{"-p": "home/"}
|
||||||
|
|
||||||
|
$ prog -p
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options: --path <path>
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog --path home/
|
||||||
|
{"--path": "home/"}
|
||||||
|
|
||||||
|
$ prog --path=home/
|
||||||
|
{"--path": "home/"}
|
||||||
|
|
||||||
|
$ prog --pa home/
|
||||||
|
{"--path": "home/"}
|
||||||
|
|
||||||
|
$ prog --pa=home/
|
||||||
|
{"--path": "home/"}
|
||||||
|
|
||||||
|
$ prog --path
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options: -p PATH, --path=<path> Path to files.
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -proot
|
||||||
|
{"--path": "root"}
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options: -p --path PATH Path to files.
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -p root
|
||||||
|
{"--path": "root"}
|
||||||
|
|
||||||
|
$ prog --path root
|
||||||
|
{"--path": "root"}
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-p PATH Path to files [default: ./]
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{"-p": "./"}
|
||||||
|
|
||||||
|
$ prog -phome
|
||||||
|
{"-p": "home"}
|
||||||
|
|
||||||
|
|
||||||
|
r"""UsAgE: prog [options]
|
||||||
|
|
||||||
|
OpTiOnS: --path=<files> Path to files
|
||||||
|
[dEfAuLt: /root]
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{"--path": "/root"}
|
||||||
|
|
||||||
|
$ prog --path=home
|
||||||
|
{"--path": "home"}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [options]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-a Add
|
||||||
|
-r Remote
|
||||||
|
-m <msg> Message
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a -r -m Hello
|
||||||
|
{"-a": true,
|
||||||
|
"-r": true,
|
||||||
|
"-m": "Hello"}
|
||||||
|
|
||||||
|
$ prog -armyourass
|
||||||
|
{"-a": true,
|
||||||
|
"-r": true,
|
||||||
|
"-m": "yourass"}
|
||||||
|
|
||||||
|
$ prog -a -r
|
||||||
|
{"-a": true,
|
||||||
|
"-r": true,
|
||||||
|
"-m": null}
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
Options: --version
|
||||||
|
--verbose
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog --version
|
||||||
|
{"--version": true,
|
||||||
|
"--verbose": false}
|
||||||
|
|
||||||
|
$ prog --verbose
|
||||||
|
{"--version": false,
|
||||||
|
"--verbose": true}
|
||||||
|
|
||||||
|
$ prog --ver
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog --verb
|
||||||
|
{"--version": false,
|
||||||
|
"--verbose": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [-a -r -m <msg>]
|
||||||
|
|
||||||
|
options:
|
||||||
|
-a Add
|
||||||
|
-r Remote
|
||||||
|
-m <msg> Message
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -armyourass
|
||||||
|
{"-a": true,
|
||||||
|
"-r": true,
|
||||||
|
"-m": "yourass"}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [-armmsg]
|
||||||
|
|
||||||
|
options: -a Add
|
||||||
|
-r Remote
|
||||||
|
-m <msg> Message
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a -r -m Hello
|
||||||
|
{"-a": true,
|
||||||
|
"-r": true,
|
||||||
|
"-m": "Hello"}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog -a -b
|
||||||
|
|
||||||
|
options:
|
||||||
|
-a
|
||||||
|
-b
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a -b
|
||||||
|
{"-a": true, "-b": true}
|
||||||
|
|
||||||
|
$ prog -b -a
|
||||||
|
{"-a": true, "-b": true}
|
||||||
|
|
||||||
|
$ prog -a
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog (-a -b)
|
||||||
|
|
||||||
|
options: -a
|
||||||
|
-b
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a -b
|
||||||
|
{"-a": true, "-b": true}
|
||||||
|
|
||||||
|
$ prog -b -a
|
||||||
|
{"-a": true, "-b": true}
|
||||||
|
|
||||||
|
$ prog -a
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [-a] -b
|
||||||
|
|
||||||
|
options: -a
|
||||||
|
-b
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a -b
|
||||||
|
{"-a": true, "-b": true}
|
||||||
|
|
||||||
|
$ prog -b -a
|
||||||
|
{"-a": true, "-b": true}
|
||||||
|
|
||||||
|
$ prog -a
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog -b
|
||||||
|
{"-a": false, "-b": true}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [(-a -b)]
|
||||||
|
|
||||||
|
options: -a
|
||||||
|
-b
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a -b
|
||||||
|
{"-a": true, "-b": true}
|
||||||
|
|
||||||
|
$ prog -b -a
|
||||||
|
{"-a": true, "-b": true}
|
||||||
|
|
||||||
|
$ prog -a
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog -b
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"-a": false, "-b": false}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog (-a|-b)
|
||||||
|
|
||||||
|
options: -a
|
||||||
|
-b
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a -b
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog -a
|
||||||
|
{"-a": true, "-b": false}
|
||||||
|
|
||||||
|
$ prog -b
|
||||||
|
{"-a": false, "-b": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [ -a | -b ]
|
||||||
|
|
||||||
|
options: -a
|
||||||
|
-b
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a -b
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"-a": false, "-b": false}
|
||||||
|
|
||||||
|
$ prog -a
|
||||||
|
{"-a": true, "-b": false}
|
||||||
|
|
||||||
|
$ prog -b
|
||||||
|
{"-a": false, "-b": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog <arg>"""
|
||||||
|
$ prog 10
|
||||||
|
{"<arg>": "10"}
|
||||||
|
|
||||||
|
$ prog 10 20
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [<arg>]"""
|
||||||
|
$ prog 10
|
||||||
|
{"<arg>": "10"}
|
||||||
|
|
||||||
|
$ prog 10 20
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"<arg>": null}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog <kind> <name> <type>"""
|
||||||
|
$ prog 10 20 40
|
||||||
|
{"<kind>": "10", "<name>": "20", "<type>": "40"}
|
||||||
|
|
||||||
|
$ prog 10 20
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog <kind> [<name> <type>]"""
|
||||||
|
$ prog 10 20 40
|
||||||
|
{"<kind>": "10", "<name>": "20", "<type>": "40"}
|
||||||
|
|
||||||
|
$ prog 10 20
|
||||||
|
{"<kind>": "10", "<name>": "20", "<type>": null}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [<kind> | <name> <type>]"""
|
||||||
|
$ prog 10 20 40
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog 20 40
|
||||||
|
{"<kind>": null, "<name>": "20", "<type>": "40"}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"<kind>": null, "<name>": null, "<type>": null}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog (<kind> --all | <name>)
|
||||||
|
|
||||||
|
options:
|
||||||
|
--all
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog 10 --all
|
||||||
|
{"<kind>": "10", "--all": true, "<name>": null}
|
||||||
|
|
||||||
|
$ prog 10
|
||||||
|
{"<kind>": null, "--all": false, "<name>": "10"}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [<name> <name>]"""
|
||||||
|
$ prog 10 20
|
||||||
|
{"<name>": ["10", "20"]}
|
||||||
|
|
||||||
|
$ prog 10
|
||||||
|
{"<name>": ["10"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"<name>": []}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [(<name> <name>)]"""
|
||||||
|
$ prog 10 20
|
||||||
|
{"<name>": ["10", "20"]}
|
||||||
|
|
||||||
|
$ prog 10
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"<name>": []}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog NAME..."""
|
||||||
|
$ prog 10 20
|
||||||
|
{"NAME": ["10", "20"]}
|
||||||
|
|
||||||
|
$ prog 10
|
||||||
|
{"NAME": ["10"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [NAME]..."""
|
||||||
|
$ prog 10 20
|
||||||
|
{"NAME": ["10", "20"]}
|
||||||
|
|
||||||
|
$ prog 10
|
||||||
|
{"NAME": ["10"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"NAME": []}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [NAME...]"""
|
||||||
|
$ prog 10 20
|
||||||
|
{"NAME": ["10", "20"]}
|
||||||
|
|
||||||
|
$ prog 10
|
||||||
|
{"NAME": ["10"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"NAME": []}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [NAME [NAME ...]]"""
|
||||||
|
$ prog 10 20
|
||||||
|
{"NAME": ["10", "20"]}
|
||||||
|
|
||||||
|
$ prog 10
|
||||||
|
{"NAME": ["10"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"NAME": []}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog (NAME | --foo NAME)
|
||||||
|
|
||||||
|
options: --foo
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog 10
|
||||||
|
{"NAME": "10", "--foo": false}
|
||||||
|
|
||||||
|
$ prog --foo 10
|
||||||
|
{"NAME": "10", "--foo": true}
|
||||||
|
|
||||||
|
$ prog --foo=10
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog (NAME | --foo) [--bar | NAME]
|
||||||
|
|
||||||
|
options: --foo
|
||||||
|
options: --bar
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog 10
|
||||||
|
{"NAME": ["10"], "--foo": false, "--bar": false}
|
||||||
|
|
||||||
|
$ prog 10 20
|
||||||
|
{"NAME": ["10", "20"], "--foo": false, "--bar": false}
|
||||||
|
|
||||||
|
$ prog --foo --bar
|
||||||
|
{"NAME": [], "--foo": true, "--bar": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""Naval Fate.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
prog ship new <name>...
|
||||||
|
prog ship [<name>] move <x> <y> [--speed=<kn>]
|
||||||
|
prog ship shoot <x> <y>
|
||||||
|
prog mine (set|remove) <x> <y> [--moored|--drifting]
|
||||||
|
prog -h | --help
|
||||||
|
prog --version
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h --help Show this screen.
|
||||||
|
--version Show version.
|
||||||
|
--speed=<kn> Speed in knots [default: 10].
|
||||||
|
--moored Mored (anchored) mine.
|
||||||
|
--drifting Drifting mine.
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog ship Guardian move 150 300 --speed=20
|
||||||
|
{"--drifting": false,
|
||||||
|
"--help": false,
|
||||||
|
"--moored": false,
|
||||||
|
"--speed": "20",
|
||||||
|
"--version": false,
|
||||||
|
"<name>": ["Guardian"],
|
||||||
|
"<x>": "150",
|
||||||
|
"<y>": "300",
|
||||||
|
"mine": false,
|
||||||
|
"move": true,
|
||||||
|
"new": false,
|
||||||
|
"remove": false,
|
||||||
|
"set": false,
|
||||||
|
"ship": true,
|
||||||
|
"shoot": false}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog --hello"""
|
||||||
|
$ prog --hello
|
||||||
|
{"--hello": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [--hello=<world>]"""
|
||||||
|
$ prog
|
||||||
|
{"--hello": null}
|
||||||
|
|
||||||
|
$ prog --hello wrld
|
||||||
|
{"--hello": "wrld"}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [-o]"""
|
||||||
|
$ prog
|
||||||
|
{"-o": false}
|
||||||
|
|
||||||
|
$ prog -o
|
||||||
|
{"-o": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [-opr]"""
|
||||||
|
$ prog -op
|
||||||
|
{"-o": true, "-p": true, "-r": false}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog --aabb | --aa"""
|
||||||
|
$ prog --aa
|
||||||
|
{"--aabb": false, "--aa": true}
|
||||||
|
|
||||||
|
$ prog --a
|
||||||
|
"user-error" # not a unique prefix
|
||||||
|
|
||||||
|
#
|
||||||
|
# Counting number of flags
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""Usage: prog -v"""
|
||||||
|
$ prog -v
|
||||||
|
{"-v": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [-v -v]"""
|
||||||
|
$ prog
|
||||||
|
{"-v": 0}
|
||||||
|
|
||||||
|
$ prog -v
|
||||||
|
{"-v": 1}
|
||||||
|
|
||||||
|
$ prog -vv
|
||||||
|
{"-v": 2}
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog -v ..."""
|
||||||
|
$ prog
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
$ prog -v
|
||||||
|
{"-v": 1}
|
||||||
|
|
||||||
|
$ prog -vv
|
||||||
|
{"-v": 2}
|
||||||
|
|
||||||
|
$ prog -vvvvvv
|
||||||
|
{"-v": 6}
|
||||||
|
|
||||||
|
|
||||||
|
r"""Usage: prog [-v | -vv | -vvv]
|
||||||
|
|
||||||
|
This one is probably most readable user-friednly variant.
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{"-v": 0}
|
||||||
|
|
||||||
|
$ prog -v
|
||||||
|
{"-v": 1}
|
||||||
|
|
||||||
|
$ prog -vv
|
||||||
|
{"-v": 2}
|
||||||
|
|
||||||
|
$ prog -vvvv
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [--ver --ver]"""
|
||||||
|
$ prog --ver --ver
|
||||||
|
{"--ver": 2}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Counting commands
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog [go]"""
|
||||||
|
$ prog go
|
||||||
|
{"go": true}
|
||||||
|
|
||||||
|
|
||||||
|
r"""usage: prog [go go]"""
|
||||||
|
$ prog
|
||||||
|
{"go": 0}
|
||||||
|
|
||||||
|
$ prog go
|
||||||
|
{"go": 1}
|
||||||
|
|
||||||
|
$ prog go go
|
||||||
|
{"go": 2}
|
||||||
|
|
||||||
|
$ prog go go go
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
r"""usage: prog go..."""
|
||||||
|
$ prog go go go go go
|
||||||
|
{"go": 5}
|
||||||
|
|
||||||
|
#
|
||||||
|
# [options] does not include options from usage-pattern
|
||||||
|
#
|
||||||
|
r"""usage: prog [options] [-a]
|
||||||
|
|
||||||
|
options: -a
|
||||||
|
-b
|
||||||
|
"""
|
||||||
|
$ prog -a
|
||||||
|
{"-a": true, "-b": false}
|
||||||
|
|
||||||
|
$ prog -aa
|
||||||
|
"user-error"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test [options] shourtcut
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""Usage: prog [options] A
|
||||||
|
Options:
|
||||||
|
-q Be quiet
|
||||||
|
-v Be verbose.
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog arg
|
||||||
|
{"A": "arg", "-v": false, "-q": false}
|
||||||
|
|
||||||
|
$ prog -v arg
|
||||||
|
{"A": "arg", "-v": true, "-q": false}
|
||||||
|
|
||||||
|
$ prog -q arg
|
||||||
|
{"A": "arg", "-v": false, "-q": true}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test single dash
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog [-]"""
|
||||||
|
|
||||||
|
$ prog -
|
||||||
|
{"-": true}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"-": false}
|
||||||
|
|
||||||
|
#
|
||||||
|
# If argument is repeated, its value should always be a list
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog [NAME [NAME ...]]"""
|
||||||
|
|
||||||
|
$ prog a b
|
||||||
|
{"NAME": ["a", "b"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"NAME": []}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Option's argument defaults to null/None
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog [options]
|
||||||
|
options:
|
||||||
|
-a Add
|
||||||
|
-m <msg> Message
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a
|
||||||
|
{"-m": null, "-a": true}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test options without description
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog --hello"""
|
||||||
|
$ prog --hello
|
||||||
|
{"--hello": true}
|
||||||
|
|
||||||
|
r"""usage: prog [--hello=<world>]"""
|
||||||
|
$ prog
|
||||||
|
{"--hello": null}
|
||||||
|
|
||||||
|
$ prog --hello wrld
|
||||||
|
{"--hello": "wrld"}
|
||||||
|
|
||||||
|
r"""usage: prog [-o]"""
|
||||||
|
$ prog
|
||||||
|
{"-o": false}
|
||||||
|
|
||||||
|
$ prog -o
|
||||||
|
{"-o": true}
|
||||||
|
|
||||||
|
r"""usage: prog [-opr]"""
|
||||||
|
$ prog -op
|
||||||
|
{"-o": true, "-p": true, "-r": false}
|
||||||
|
|
||||||
|
r"""usage: git [-v | --verbose]"""
|
||||||
|
$ prog -v
|
||||||
|
{"-v": true, "--verbose": false}
|
||||||
|
|
||||||
|
r"""usage: git remote [-v | --verbose]"""
|
||||||
|
$ prog remote -v
|
||||||
|
{"remote": true, "-v": true, "--verbose": false}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test empty usage pattern
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog"""
|
||||||
|
$ prog
|
||||||
|
{}
|
||||||
|
|
||||||
|
r"""usage: prog
|
||||||
|
prog <a> <b>
|
||||||
|
"""
|
||||||
|
$ prog 1 2
|
||||||
|
{"<a>": "1", "<b>": "2"}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"<a>": null, "<b>": null}
|
||||||
|
|
||||||
|
r"""usage: prog <a> <b>
|
||||||
|
prog
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{"<a>": null, "<b>": null}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Option's argument should not capture default value from usage pattern
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog [--file=<f>]"""
|
||||||
|
$ prog
|
||||||
|
{"--file": null}
|
||||||
|
|
||||||
|
r"""usage: prog [--file=<f>]
|
||||||
|
|
||||||
|
options: --file <a>
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{"--file": null}
|
||||||
|
|
||||||
|
r"""Usage: prog [-a <host:port>]
|
||||||
|
|
||||||
|
Options: -a, --address <host:port> TCP address [default: localhost:6283].
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog
|
||||||
|
{"--address": "localhost:6283"}
|
||||||
|
|
||||||
|
#
|
||||||
|
# If option with argument could be repeated,
|
||||||
|
# its arguments should be accumulated into a list
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog --long=<arg> ..."""
|
||||||
|
|
||||||
|
$ prog --long one
|
||||||
|
{"--long": ["one"]}
|
||||||
|
|
||||||
|
$ prog --long one --long two
|
||||||
|
{"--long": ["one", "two"]}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test multiple elements repeated at once
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog (go <direction> --speed=<km/h>)..."""
|
||||||
|
$ prog go left --speed=5 go right --speed=9
|
||||||
|
{"go": 2, "<direction>": ["left", "right"], "--speed": ["5", "9"]}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Required options should work with option shortcut
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog [options] -a
|
||||||
|
|
||||||
|
options: -a
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -a
|
||||||
|
{"-a": true}
|
||||||
|
|
||||||
|
#
|
||||||
|
# If option could be repeated its defaults should be split into a list
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog [-o <o>]...
|
||||||
|
|
||||||
|
options: -o <o> [default: x]
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -o this -o that
|
||||||
|
{"-o": ["this", "that"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"-o": ["x"]}
|
||||||
|
|
||||||
|
r"""usage: prog [-o <o>]...
|
||||||
|
|
||||||
|
options: -o <o> [default: x y]
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -o this
|
||||||
|
{"-o": ["this"]}
|
||||||
|
|
||||||
|
$ prog
|
||||||
|
{"-o": ["x", "y"]}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test stacked option's argument
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog -pPATH
|
||||||
|
|
||||||
|
options: -p PATH
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog -pHOME
|
||||||
|
{"-p": "HOME"}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Issue 56: Repeated mutually exclusive args give nested lists sometimes
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""Usage: foo (--xx=x|--yy=y)..."""
|
||||||
|
$ prog --xx=1 --yy=2
|
||||||
|
{"--xx": ["1"], "--yy": ["2"]}
|
||||||
|
|
||||||
|
#
|
||||||
|
# POSIXly correct tokenization
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog [<input file>]"""
|
||||||
|
$ prog f.txt
|
||||||
|
{"<input file>": "f.txt"}
|
||||||
|
|
||||||
|
r"""usage: prog [--input=<file name>]..."""
|
||||||
|
$ prog --input a.txt --input=b.txt
|
||||||
|
{"--input": ["a.txt", "b.txt"]}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Issue 85: `[options]` shourtcut with multiple subcommands
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage: prog good [options]
|
||||||
|
prog fail [options]
|
||||||
|
|
||||||
|
options: --loglevel=N
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog fail --loglevel 5
|
||||||
|
{"--loglevel": "5", "fail": true, "good": false}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Usage-section syntax
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""usage:prog --foo"""
|
||||||
|
$ prog --foo
|
||||||
|
{"--foo": true}
|
||||||
|
|
||||||
|
r"""PROGRAM USAGE: prog --foo"""
|
||||||
|
$ prog --foo
|
||||||
|
{"--foo": true}
|
||||||
|
|
||||||
|
r"""Usage: prog --foo
|
||||||
|
prog --bar
|
||||||
|
NOT PART OF SECTION"""
|
||||||
|
$ prog --foo
|
||||||
|
{"--foo": true, "--bar": false}
|
||||||
|
|
||||||
|
r"""Usage:
|
||||||
|
prog --foo
|
||||||
|
prog --bar
|
||||||
|
|
||||||
|
NOT PART OF SECTION"""
|
||||||
|
$ prog --foo
|
||||||
|
{"--foo": true, "--bar": false}
|
||||||
|
|
||||||
|
r"""Usage:
|
||||||
|
prog --foo
|
||||||
|
prog --bar
|
||||||
|
NOT PART OF SECTION"""
|
||||||
|
$ prog --foo
|
||||||
|
{"--foo": true, "--bar": false}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Options-section syntax
|
||||||
|
#
|
||||||
|
|
||||||
|
r"""Usage: prog [options]
|
||||||
|
|
||||||
|
global options: --foo
|
||||||
|
local options: --baz
|
||||||
|
--bar
|
||||||
|
other options:
|
||||||
|
--egg
|
||||||
|
--spam
|
||||||
|
-not-an-option-
|
||||||
|
|
||||||
|
"""
|
||||||
|
$ prog --baz --egg
|
||||||
|
{"--foo": false, "--baz": true, "--bar": false, "--egg": true, "--spam": false}
|
||||||
126
vendor/github.com/docopt/docopt-go/token.go
generated
vendored
Normal file
126
vendor/github.com/docopt/docopt-go/token.go
generated
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package docopt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tokenList struct {
|
||||||
|
tokens []string
|
||||||
|
errorFunc func(string, ...interface{}) error
|
||||||
|
err errorType
|
||||||
|
}
|
||||||
|
type token string
|
||||||
|
|
||||||
|
func newTokenList(source []string, err errorType) *tokenList {
|
||||||
|
errorFunc := newError
|
||||||
|
if err == errorUser {
|
||||||
|
errorFunc = newUserError
|
||||||
|
} else if err == errorLanguage {
|
||||||
|
errorFunc = newLanguageError
|
||||||
|
}
|
||||||
|
return &tokenList{source, errorFunc, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenListFromString(source string) *tokenList {
|
||||||
|
return newTokenList(strings.Fields(source), errorUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenListFromPattern(source string) *tokenList {
|
||||||
|
p := regexp.MustCompile(`([\[\]\(\)\|]|\.\.\.)`)
|
||||||
|
source = p.ReplaceAllString(source, ` $1 `)
|
||||||
|
p = regexp.MustCompile(`\s+|(\S*<.*?>)`)
|
||||||
|
split := p.Split(source, -1)
|
||||||
|
match := p.FindAllStringSubmatch(source, -1)
|
||||||
|
var result []string
|
||||||
|
l := len(split)
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
if len(split[i]) > 0 {
|
||||||
|
result = append(result, split[i])
|
||||||
|
}
|
||||||
|
if i < l-1 && len(match[i][1]) > 0 {
|
||||||
|
result = append(result, match[i][1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newTokenList(result, errorLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *token) eq(s string) bool {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return string(*t) == s
|
||||||
|
}
|
||||||
|
func (t *token) match(matchNil bool, tokenStrings ...string) bool {
|
||||||
|
if t == nil && matchNil {
|
||||||
|
return true
|
||||||
|
} else if t == nil && !matchNil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tok := range tokenStrings {
|
||||||
|
if tok == string(*t) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func (t *token) hasPrefix(prefix string) bool {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(string(*t), prefix)
|
||||||
|
}
|
||||||
|
func (t *token) hasSuffix(suffix string) bool {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasSuffix(string(*t), suffix)
|
||||||
|
}
|
||||||
|
func (t *token) isUpper() bool {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isStringUppercase(string(*t))
|
||||||
|
}
|
||||||
|
func (t *token) String() string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(*t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *tokenList) current() *token {
|
||||||
|
if len(tl.tokens) > 0 {
|
||||||
|
return (*token)(&(tl.tokens[0]))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *tokenList) length() int {
|
||||||
|
return len(tl.tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *tokenList) move() *token {
|
||||||
|
if len(tl.tokens) > 0 {
|
||||||
|
t := tl.tokens[0]
|
||||||
|
tl.tokens = tl.tokens[1:]
|
||||||
|
return (*token)(&t)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if all cased characters in the string are uppercase
|
||||||
|
// and there are there is at least one cased charcter
|
||||||
|
func isStringUppercase(s string) bool {
|
||||||
|
if strings.ToUpper(s) != s {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range []rune(s) {
|
||||||
|
if unicode.IsUpper(c) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
201
vendor/github.com/inconshreveable/mousetrap/LICENSE
generated
vendored
201
vendor/github.com/inconshreveable/mousetrap/LICENSE
generated
vendored
@@ -1,201 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2022 Alan Shreve (@inconshreveable)
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
23
vendor/github.com/inconshreveable/mousetrap/README.md
generated
vendored
23
vendor/github.com/inconshreveable/mousetrap/README.md
generated
vendored
@@ -1,23 +0,0 @@
|
|||||||
# mousetrap
|
|
||||||
|
|
||||||
mousetrap is a tiny library that answers a single question.
|
|
||||||
|
|
||||||
On a Windows machine, was the process invoked by someone double clicking on
|
|
||||||
the executable file while browsing in explorer?
|
|
||||||
|
|
||||||
### Motivation
|
|
||||||
|
|
||||||
Windows developers unfamiliar with command line tools will often "double-click"
|
|
||||||
the executable for a tool. Because most CLI tools print the help and then exit
|
|
||||||
when invoked without arguments, this is often very frustrating for those users.
|
|
||||||
|
|
||||||
mousetrap provides a way to detect these invocations so that you can provide
|
|
||||||
more helpful behavior and instructions on how to run the CLI tool. To see what
|
|
||||||
this looks like, both from an organizational and a technical perspective, see
|
|
||||||
https://inconshreveable.com/09-09-2014/sweat-the-small-stuff/
|
|
||||||
|
|
||||||
### The interface
|
|
||||||
|
|
||||||
The library exposes a single interface:
|
|
||||||
|
|
||||||
func StartedByExplorer() (bool)
|
|
||||||
16
vendor/github.com/inconshreveable/mousetrap/trap_others.go
generated
vendored
16
vendor/github.com/inconshreveable/mousetrap/trap_others.go
generated
vendored
@@ -1,16 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package mousetrap
|
|
||||||
|
|
||||||
// StartedByExplorer returns true if the program was invoked by the user
|
|
||||||
// double-clicking on the executable from explorer.exe
|
|
||||||
//
|
|
||||||
// It is conservative and returns false if any of the internal calls fail.
|
|
||||||
// It does not guarantee that the program was run from a terminal. It only can tell you
|
|
||||||
// whether it was launched from explorer.exe
|
|
||||||
//
|
|
||||||
// On non-Windows platforms, it always returns false.
|
|
||||||
func StartedByExplorer() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
42
vendor/github.com/inconshreveable/mousetrap/trap_windows.go
generated
vendored
42
vendor/github.com/inconshreveable/mousetrap/trap_windows.go
generated
vendored
@@ -1,42 +0,0 @@
|
|||||||
package mousetrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) {
|
|
||||||
snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer syscall.CloseHandle(snapshot)
|
|
||||||
var procEntry syscall.ProcessEntry32
|
|
||||||
procEntry.Size = uint32(unsafe.Sizeof(procEntry))
|
|
||||||
if err = syscall.Process32First(snapshot, &procEntry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
if procEntry.ProcessID == uint32(pid) {
|
|
||||||
return &procEntry, nil
|
|
||||||
}
|
|
||||||
err = syscall.Process32Next(snapshot, &procEntry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartedByExplorer returns true if the program was invoked by the user double-clicking
|
|
||||||
// on the executable from explorer.exe
|
|
||||||
//
|
|
||||||
// It is conservative and returns false if any of the internal calls fail.
|
|
||||||
// It does not guarantee that the program was run from a terminal. It only can tell you
|
|
||||||
// whether it was launched from explorer.exe
|
|
||||||
func StartedByExplorer() bool {
|
|
||||||
pe, err := getProcessEntry(syscall.Getppid())
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return "explorer.exe" == syscall.UTF16ToString(pe.ExeFile[:])
|
|
||||||
}
|
|
||||||
66
vendor/github.com/spf13/cobra/.golangci.yml
generated
vendored
66
vendor/github.com/spf13/cobra/.golangci.yml
generated
vendored
@@ -1,66 +0,0 @@
|
|||||||
# Copyright 2013-2023 The Cobra Authors
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
version: "2"
|
|
||||||
|
|
||||||
run:
|
|
||||||
timeout: 5m
|
|
||||||
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gofmt
|
|
||||||
- goimports
|
|
||||||
|
|
||||||
linters:
|
|
||||||
default: none
|
|
||||||
enable:
|
|
||||||
#- bodyclose
|
|
||||||
#- depguard
|
|
||||||
#- dogsled
|
|
||||||
#- dupl
|
|
||||||
- errcheck
|
|
||||||
#- exhaustive
|
|
||||||
#- funlen
|
|
||||||
#- gochecknoinits
|
|
||||||
- goconst
|
|
||||||
- gocritic
|
|
||||||
#- gocyclo
|
|
||||||
#- goprintffuncname
|
|
||||||
- gosec
|
|
||||||
- govet
|
|
||||||
- ineffassign
|
|
||||||
#- lll
|
|
||||||
- misspell
|
|
||||||
#- mnd
|
|
||||||
#- nakedret
|
|
||||||
#- noctx
|
|
||||||
- nolintlint
|
|
||||||
#- rowserrcheck
|
|
||||||
- staticcheck
|
|
||||||
- unconvert
|
|
||||||
#- unparam
|
|
||||||
- unused
|
|
||||||
#- whitespace
|
|
||||||
exclusions:
|
|
||||||
presets:
|
|
||||||
- common-false-positives
|
|
||||||
- legacy
|
|
||||||
- std-error-handling
|
|
||||||
settings:
|
|
||||||
govet:
|
|
||||||
# Disable buildtag check to allow dual build tag syntax (both //go:build and // +build).
|
|
||||||
# This is necessary for Go 1.15 compatibility since //go:build was introduced in Go 1.17.
|
|
||||||
# This can be removed once Cobra requires Go 1.17 or higher.
|
|
||||||
disable:
|
|
||||||
- buildtag
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user