mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4250b854c9 |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: github.com/alecthomas/chroma
|
||||
versions:
|
||||
- 0.9.1
|
||||
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
@@ -1,46 +0,0 @@
|
||||
---
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
# TODO: is it possible to DRY out these jobs? Aside from `runs-on`, they are
|
||||
# identical.
|
||||
# See: https://github.com/actions/runner/issues/1182
|
||||
build-linux:
|
||||
runs-on: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Set up Revive (linter)
|
||||
run: go get -u github.com/boyter/scc github.com/mgechev/revive
|
||||
env:
|
||||
GO111MODULE: "off"
|
||||
- name: Build
|
||||
run: make build
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
build-osx:
|
||||
runs-on: [macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Set up Revive (linter)
|
||||
run: go get -u github.com/boyter/scc github.com/mgechev/revive
|
||||
env:
|
||||
GO111MODULE: "off"
|
||||
- name: Build
|
||||
run: make build
|
||||
- name: Test
|
||||
run: make test
|
||||
30
.github/workflows/codeql-analysis.yml
vendored
30
.github/workflows/codeql-analysis.yml
vendored
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '45 23 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [go]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
4
.github/workflows/homebrew.yml
vendored
4
.github/workflows/homebrew.yml
vendored
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: homebrew
|
||||
|
||||
on:
|
||||
@@ -12,8 +11,7 @@ jobs:
|
||||
steps:
|
||||
- uses: mislav/bump-homebrew-formula-action@v1
|
||||
with:
|
||||
# A PR will be sent to github.com/Homebrew/homebrew-core to update
|
||||
# this formula:
|
||||
# A PR will be sent to github.com/Homebrew/homebrew-core to update this formula:
|
||||
formula-name: cheat
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,2 @@
|
||||
dist
|
||||
tags
|
||||
.tmp
|
||||
*.test
|
||||
.claude
|
||||
|
||||
15
.travis.yml
Normal file
15
.travis.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.14.x
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
install: true
|
||||
|
||||
script: make ci
|
||||
117
CLAUDE.md
117
CLAUDE.md
@@ -1,117 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Building
|
||||
```bash
|
||||
# Build for your architecture
|
||||
make build
|
||||
|
||||
# Build release binaries for all platforms
|
||||
make build-release
|
||||
|
||||
# Install cheat to your PATH
|
||||
make install
|
||||
```
|
||||
|
||||
### Testing and Quality Checks
|
||||
```bash
|
||||
# Run all tests
|
||||
make test
|
||||
go test ./...
|
||||
|
||||
# Run a single test
|
||||
go test -run TestFunctionName ./internal/package_name
|
||||
|
||||
# Generate test coverage report
|
||||
make coverage
|
||||
|
||||
# Run linter (revive)
|
||||
make lint
|
||||
|
||||
# Run go vet
|
||||
make vet
|
||||
|
||||
# Format code
|
||||
make fmt
|
||||
|
||||
# Run all checks (vendor, fmt, lint, vet, test)
|
||||
make check
|
||||
```
|
||||
|
||||
### Development Setup
|
||||
```bash
|
||||
# Install development dependencies (revive linter, scc)
|
||||
make setup
|
||||
|
||||
# Update and verify vendored dependencies
|
||||
make vendor-update
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The `cheat` command-line tool is organized into several key packages:
|
||||
|
||||
### Command Layer (`cmd/cheat/`)
|
||||
- `main.go`: Entry point, argument parsing, command routing
|
||||
- `cmd_*.go`: Individual command implementations (view, edit, list, search, etc.)
|
||||
- Commands are selected based on docopt parsed arguments
|
||||
|
||||
### Core Internal Packages
|
||||
|
||||
1. **`internal/config`**: Configuration management
|
||||
- Loads YAML config from platform-specific paths
|
||||
- Manages editor, pager, colorization settings
|
||||
- Validates and expands cheatpath configurations
|
||||
|
||||
2. **`internal/cheatpath`**: Cheatsheet path management
|
||||
- Represents collections of cheatsheets on filesystem
|
||||
- Handles read-only vs writable paths
|
||||
- Supports filtering and validation
|
||||
|
||||
3. **`internal/sheet`**: Individual cheatsheet handling
|
||||
- Parses YAML frontmatter for tags and syntax
|
||||
- Implements syntax highlighting via Chroma
|
||||
- Provides search functionality within sheets
|
||||
|
||||
4. **`internal/sheets`**: Collection operations
|
||||
- Loads sheets from multiple cheatpaths
|
||||
- Consolidates duplicates (local overrides global)
|
||||
- Filters by tags and sorts results
|
||||
|
||||
5. **`internal/display`**: Output formatting
|
||||
- Writes to stdout or pager
|
||||
- Handles text formatting and indentation
|
||||
|
||||
6. **`internal/repo`**: Git repository management
|
||||
- Clones community cheatsheet repositories
|
||||
- Updates existing repositories
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
- **Filesystem-based storage**: Cheatsheets are plain text files
|
||||
- **Override mechanism**: Local sheets override community sheets with same name
|
||||
- **Tag system**: Sheets can be categorized with tags in frontmatter
|
||||
- **Multiple cheatpaths**: Supports personal, community, and directory-scoped sheets
|
||||
|
||||
### Sheet Format
|
||||
|
||||
Cheatsheets are plain text files optionally prefixed with YAML frontmatter:
|
||||
```
|
||||
---
|
||||
syntax: bash
|
||||
tags: [ networking, ssh ]
|
||||
---
|
||||
# SSH tunneling example
|
||||
ssh -L 8080:localhost:80 user@remote
|
||||
```
|
||||
|
||||
### Working with the Codebase
|
||||
|
||||
- Always check for `.git` directories and skip them during filesystem walks
|
||||
- Use `go-git` for repository operations, not exec'ing git commands
|
||||
- Platform-specific paths are handled in `internal/config/paths.go`
|
||||
- Color output uses ANSI codes via the Chroma library
|
||||
- Test files use the `internal/mock` package for test data
|
||||
@@ -1,14 +1,43 @@
|
||||
Contributing
|
||||
CONTRIBUTING
|
||||
============
|
||||
Do you want to contribute to `cheat`? There are a few ways to help:
|
||||
|
||||
Thank you for your interest in `cheat`.
|
||||
#### Submit a cheatsheet ####
|
||||
Do you have a witty bash one-liner to share? [Open a pull-request][pr] against
|
||||
the [cheatsheets][] repository. (The `cheat` executable source code lives in
|
||||
[cheat/cheat][cheat]. Cheatsheet content lives in
|
||||
[cheat/cheatsheets][cheatsheets].)
|
||||
|
||||
Pull requests are no longer being accepted, and have been disabled on this
|
||||
repository. The maintainer is not currently reviewing or merging external code
|
||||
contributions.
|
||||
#### Report a bug ####
|
||||
Did you find a bug? Report it in the [issue tracker][issues]. (But before you
|
||||
do, please look through the open issues to make sure that it hasn't already
|
||||
been reported.)
|
||||
|
||||
Bug reports are still welcome. If you've found a bug, please open an issue in
|
||||
the [issue tracker][issues]. Before doing so, please search through the
|
||||
existing open issues to make sure it hasn't already been reported.
|
||||
#### Add a feature ####
|
||||
Do you have a feature that you'd like to contribute? Propose it in the [issue
|
||||
tracker][issues] to discuss with the maintainer whether it would be considered
|
||||
for merging.
|
||||
|
||||
[issues]: https://github.com/cheat/cheat/issues
|
||||
`cheat` is mostly mature and feature-complete, but may still have some room for
|
||||
new features.
|
||||
|
||||
#### Add documentation ####
|
||||
Did you encounter features, bugs, edge-cases, use-cases, or environment
|
||||
considerations that were undocumented or under-documented? Add them to the
|
||||
[wiki][]. (You may also open a pull-request against the `README`, if
|
||||
appropriate.)
|
||||
|
||||
Do you enjoy technical writing or proofreading? Help keep the documentation
|
||||
error-free and well-organized.
|
||||
|
||||
#### Spread the word ####
|
||||
Are you unable to do the above, but still want to contribute? You can help
|
||||
`cheat` simply by telling others about it. Share it with friends and coworkers
|
||||
that might benefit from using it.
|
||||
|
||||
|
||||
[cheat]: https://github.com/cheat/cheat
|
||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||
[issues]: https://github.com/cheat/cheat/issues
|
||||
[pr]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork
|
||||
[wiki]: https://github.com/cheat/cheat/wiki
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# NB: this image isn't used anywhere in the build pipeline. It exists to
|
||||
# conveniently facilitate ad-hoc experimentation in a sandboxed environment
|
||||
# during development.
|
||||
FROM golang:1.15-alpine
|
||||
|
||||
RUN apk add git less make
|
||||
|
||||
WORKDIR /app
|
||||
241
HACKING.md
241
HACKING.md
@@ -1,241 +0,0 @@
|
||||
# Hacking Guide
|
||||
|
||||
This document provides a comprehensive guide for developing `cheat`, including setup, architecture overview, and code patterns.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install system dependencies
|
||||
|
||||
The following are required and must be available on your `PATH`:
|
||||
- `git`
|
||||
- `go` (>= 1.19 is recommended)
|
||||
- `make`
|
||||
|
||||
Optional dependencies:
|
||||
- `docker`
|
||||
- `pandoc` (necessary to generate a `man` page)
|
||||
|
||||
### 2. Install utility applications
|
||||
Run `make setup` to install `scc` and `revive`, which are used by various `make` targets.
|
||||
|
||||
### 3. Development workflow
|
||||
|
||||
1. Make changes to the `cheat` source code
|
||||
2. Run `make test` to run unit-tests
|
||||
3. Fix compiler errors and failing tests as necessary
|
||||
4. Run `make build`. A `cheat` executable will be written to the `dist` directory
|
||||
5. Use the new executable by running `dist/cheat <command>`
|
||||
6. Run `make install` to install `cheat` to your `PATH`
|
||||
7. Run `make build-release` to build cross-platform binaries in `dist`
|
||||
8. Run `make clean` to clean the `dist` directory when desired
|
||||
|
||||
You may run `make help` to see a list of available `make` commands.
|
||||
|
||||
### 4. Testing
|
||||
|
||||
#### Unit Tests
|
||||
Run unit tests with:
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
#### Integration Tests
|
||||
Integration tests that require network access are separated using build tags. Run them with:
|
||||
```bash
|
||||
make test-integration
|
||||
```
|
||||
|
||||
To run all tests (unit and integration):
|
||||
```bash
|
||||
make test-all
|
||||
```
|
||||
|
||||
#### Test Coverage
|
||||
Generate a coverage report with:
|
||||
```bash
|
||||
make coverage # HTML report
|
||||
make coverage-text # Terminal output
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Package Structure
|
||||
|
||||
The `cheat` application follows a clean architecture with well-separated concerns:
|
||||
|
||||
- **`cmd/cheat/`**: Command layer with argument parsing and command routing
|
||||
- **`internal/config`**: Configuration management (YAML loading, validation, paths)
|
||||
- **`internal/cheatpath`**: Cheatsheet path management (collections, filtering)
|
||||
- **`internal/sheet`**: Individual cheatsheet handling (parsing, search, highlighting)
|
||||
- **`internal/sheets`**: Collection operations (loading, consolidation, filtering)
|
||||
- **`internal/display`**: Output formatting (pager integration, colorization)
|
||||
- **`internal/repo`**: Git repository management for community sheets
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
- **Filesystem-based storage**: Cheatsheets are plain text files
|
||||
- **Override mechanism**: Local sheets override community sheets with same name
|
||||
- **Tag system**: Sheets can be categorized with tags in frontmatter
|
||||
- **Multiple cheatpaths**: Supports personal, community, and directory-scoped sheets
|
||||
|
||||
## Core Types and Functions
|
||||
|
||||
### Config (`internal/config`)
|
||||
|
||||
The main configuration structure:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Colorize bool `yaml:"colorize"`
|
||||
Editor string `yaml:"editor"`
|
||||
Cheatpaths []cp.Cheatpath `yaml:"cheatpaths"`
|
||||
Style string `yaml:"style"`
|
||||
Formatter string `yaml:"formatter"`
|
||||
Pager string `yaml:"pager"`
|
||||
Path string
|
||||
}
|
||||
```
|
||||
|
||||
Key functions:
|
||||
- `New(opts, confPath, resolve)` - Load config from file
|
||||
- `Validate()` - Validate configuration values
|
||||
- `Editor()` - Get editor from environment or defaults (package-level function)
|
||||
- `Pager()` - Get pager from environment or defaults (package-level function)
|
||||
|
||||
### Cheatpath (`internal/cheatpath`)
|
||||
|
||||
Represents a directory containing cheatsheets:
|
||||
|
||||
```go
|
||||
type Cheatpath struct {
|
||||
Name string // Friendly name (e.g., "personal")
|
||||
Path string // Filesystem path
|
||||
Tags []string // Tags applied to all sheets in this path
|
||||
ReadOnly bool // Whether sheets can be modified
|
||||
}
|
||||
```
|
||||
|
||||
### Sheet (`internal/sheet`)
|
||||
|
||||
Represents an individual cheatsheet:
|
||||
|
||||
```go
|
||||
type Sheet struct {
|
||||
Title string // Sheet name (from filename)
|
||||
CheatPath string // Name of the cheatpath this sheet belongs to
|
||||
Path string // Full filesystem path
|
||||
Text string // Content (without frontmatter)
|
||||
Tags []string // Combined tags (from frontmatter + cheatpath)
|
||||
Syntax string // Syntax for highlighting
|
||||
ReadOnly bool // Whether sheet can be edited
|
||||
}
|
||||
```
|
||||
|
||||
Key methods:
|
||||
- `New(title, cheatpath, path, tags, readOnly)` - Load from file
|
||||
- `Search(reg)` - Search content with a compiled regexp
|
||||
- `Colorize(conf)` - Apply syntax highlighting (modifies sheet in place)
|
||||
- `Tagged(needle)` - Check if sheet has the given tag
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Loading and Displaying a Sheet
|
||||
|
||||
```go
|
||||
// Load sheet
|
||||
s, err := sheet.New("tar", "personal", "/path/to/tar", []string{"personal"}, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Apply syntax highlighting (modifies sheet in place)
|
||||
s.Colorize(conf)
|
||||
|
||||
// Display with pager
|
||||
display.Write(s.Text, conf)
|
||||
```
|
||||
|
||||
### Working with Sheet Collections
|
||||
|
||||
```go
|
||||
// Load all sheets from cheatpaths (returns a slice of maps, one per cheatpath)
|
||||
allSheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Consolidate to handle duplicates (later cheatpaths take precedence)
|
||||
consolidated := sheets.Consolidate(allSheets)
|
||||
|
||||
// Filter by tag (operates on the slice of maps)
|
||||
filtered := sheets.Filter(allSheets, []string{"networking"})
|
||||
|
||||
// Sort alphabetically (returns a sorted slice)
|
||||
sorted := sheets.Sort(consolidated)
|
||||
```
|
||||
|
||||
### Sheet Format
|
||||
|
||||
Cheatsheets are plain text files that may begin with YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
syntax: bash
|
||||
tags: [networking, linux, ssh]
|
||||
---
|
||||
# Connect to remote server
|
||||
ssh user@hostname
|
||||
|
||||
# Copy files over SSH
|
||||
scp local_file user@hostname:/remote/path
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with:
|
||||
```bash
|
||||
make test # Run all tests
|
||||
make coverage # Generate coverage report
|
||||
go test ./... # Go test directly
|
||||
```
|
||||
|
||||
Test files follow Go conventions:
|
||||
- `*_test.go` files in same package
|
||||
- Table-driven tests for multiple scenarios
|
||||
- Mock data in `internal/mock` package
|
||||
|
||||
## Error Handling
|
||||
|
||||
The codebase follows consistent error handling patterns:
|
||||
- Functions return explicit errors
|
||||
- Errors are wrapped with context using `fmt.Errorf`
|
||||
- User-facing errors are written to stderr
|
||||
|
||||
Example:
|
||||
```go
|
||||
sheet, err := sheet.New(path, tags, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load sheet: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
## Developing with Docker
|
||||
|
||||
It may be useful to test your changes within a pristine environment. An Alpine-based docker container has been provided for that purpose.
|
||||
|
||||
Build the docker container:
|
||||
```bash
|
||||
make docker-setup
|
||||
```
|
||||
|
||||
Shell into the container:
|
||||
```bash
|
||||
make docker-sh
|
||||
```
|
||||
|
||||
The `cheat` source code will be mounted at `/app` within the container.
|
||||
|
||||
To destroy the container:
|
||||
```bash
|
||||
make distclean
|
||||
```
|
||||
@@ -1,79 +0,0 @@
|
||||
Installing
|
||||
==========
|
||||
`cheat` has no runtime dependencies. As such, installing it is generally
|
||||
straightforward. There are a few methods available:
|
||||
|
||||
### Install manually
|
||||
#### Unix-like
|
||||
On Unix-like systems, you may simply paste the following snippet into your terminal:
|
||||
|
||||
```sh
|
||||
cd /tmp \
|
||||
&& wget https://github.com/cheat/cheat/releases/download/4.5.0/cheat-linux-amd64.gz \
|
||||
&& gunzip cheat-linux-amd64.gz \
|
||||
&& chmod +x cheat-linux-amd64 \
|
||||
&& sudo mv cheat-linux-amd64 /usr/local/bin/cheat
|
||||
```
|
||||
|
||||
You may need to need to change the version number (`4.5.0`) and the archive
|
||||
(`cheat-linux-amd64.gz`) depending on your platform.
|
||||
|
||||
See the [releases page][releases] for a list of supported platforms.
|
||||
|
||||
#### Windows
|
||||
On Windows, download the appropriate binary from the [releases page][releases],
|
||||
unzip the archive, and place the `cheat.exe` executable on your `PATH`.
|
||||
|
||||
### Install via `go install`
|
||||
If you have `go` version `>=1.17` available on your `PATH`, you can install
|
||||
`cheat` via `go install`:
|
||||
|
||||
```sh
|
||||
go install github.com/cheat/cheat/cmd/cheat@latest
|
||||
```
|
||||
|
||||
### Install via package manager
|
||||
Several community-maintained packages are also available:
|
||||
|
||||
Package manager | Package(s)
|
||||
---------------- | -----------
|
||||
aur | [cheat][pkg-aur-cheat], [cheat-bin][pkg-aur-cheat-bin]
|
||||
brew | [cheat][pkg-brew]
|
||||
docker | [docker-cheat][pkg-docker]
|
||||
nix | [nixos.cheat][pkg-nix]
|
||||
snap | [cheat][pkg-snap]
|
||||
|
||||
<!--[pacman][] |-->
|
||||
|
||||
## Configuring
|
||||
Three things must be done before you can use `cheat`:
|
||||
1. A config file must be generated
|
||||
2. [`cheatpaths`][cheatpaths] must be configured
|
||||
3. [Community cheatsheets][community] must be downloaded
|
||||
|
||||
On first run, `cheat` will run an installer that will do all of the above
|
||||
automatically. After the installer is complete, it is strongly advised that you
|
||||
view the configuration file that was generated, as you may want to change some
|
||||
of its default values (to enable colorization, change the paginator, etc).
|
||||
|
||||
### conf.yml ###
|
||||
`cheat` is configured by a YAML file that will be auto-generated on first run.
|
||||
|
||||
By default, the config file is assumed to exist on an XDG-compliant
|
||||
configuration path like `~/.config/cheat/conf.yml`. If you would like to store
|
||||
it elsewhere, you may export a `CHEAT_CONFIG_PATH` environment variable that
|
||||
specifies its path:
|
||||
|
||||
```sh
|
||||
export CHEAT_CONFIG_PATH="~/.dotfiles/cheat/conf.yml"
|
||||
```
|
||||
|
||||
[cheatpaths]: README.md#cheatpaths
|
||||
[community]: https://github.com/cheat/cheatsheets/
|
||||
[pkg-aur-cheat-bin]: https://aur.archlinux.org/packages/cheat-bin
|
||||
[pkg-aur-cheat]: https://aur.archlinux.org/packages/cheat
|
||||
[pkg-brew]: https://formulae.brew.sh/formula/cheat
|
||||
[pkg-docker]: https://github.com/bannmann/docker-cheat
|
||||
[pkg-nix]: https://search.nixos.org/packages?channel=unstable&show=cheat&from=0&size=50&sort=relevance&type=packages&query=cheat
|
||||
[pkg-snap]: https://snapcraft.io/cheat
|
||||
[releases]: https://github.com/cheat/cheat/releases
|
||||
156
Makefile
156
Makefile
@@ -3,14 +3,10 @@ makefile := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
cmd_dir := ./cmd/cheat
|
||||
dist_dir := ./dist
|
||||
|
||||
# parallel jobs for build-release (can be overridden)
|
||||
JOBS ?= 8
|
||||
|
||||
# executables
|
||||
CAT := cat
|
||||
COLUMN := column
|
||||
CTAGS := ctags
|
||||
DOCKER := docker
|
||||
GO := go
|
||||
GREP := grep
|
||||
GZIP := gzip --best
|
||||
@@ -24,8 +20,6 @@ SED := sed
|
||||
SORT := sort
|
||||
ZIP := zip -m
|
||||
|
||||
docker_image := cheat-devel:latest
|
||||
|
||||
# build flags
|
||||
BUILD_FLAGS := -ldflags="-s -w" -mod vendor -trimpath
|
||||
GOBIN :=
|
||||
@@ -34,102 +28,68 @@ TMPDIR := /tmp
|
||||
# release binaries
|
||||
releases := \
|
||||
$(dist_dir)/cheat-darwin-amd64 \
|
||||
$(dist_dir)/cheat-darwin-arm64 \
|
||||
$(dist_dir)/cheat-linux-386 \
|
||||
$(dist_dir)/cheat-linux-amd64 \
|
||||
$(dist_dir)/cheat-linux-arm5 \
|
||||
$(dist_dir)/cheat-linux-arm6 \
|
||||
$(dist_dir)/cheat-linux-arm64 \
|
||||
$(dist_dir)/cheat-linux-arm7 \
|
||||
$(dist_dir)/cheat-netbsd-amd64 \
|
||||
$(dist_dir)/cheat-openbsd-amd64 \
|
||||
$(dist_dir)/cheat-solaris-amd64 \
|
||||
$(dist_dir)/cheat-windows-amd64.exe
|
||||
|
||||
## build: build an executable for your architecture
|
||||
.PHONY: build
|
||||
build: | clean $(dist_dir) fmt lint vet vendor man
|
||||
build: $(dist_dir) clean vendor generate man
|
||||
$(GO) build $(BUILD_FLAGS) -o $(dist_dir)/cheat $(cmd_dir)
|
||||
|
||||
## build-release: build release executables
|
||||
# Runs prepare once, then builds all binaries in parallel
|
||||
# Override jobs with: make build-release JOBS=16
|
||||
.PHONY: build-release
|
||||
build-release: prepare
|
||||
$(MAKE) -j$(JOBS) $(releases)
|
||||
build-release: $(releases)
|
||||
|
||||
## ci: build a "release" executable for the current architecture (used in ci)
|
||||
.PHONY: ci
|
||||
ci: | setup prepare build
|
||||
|
||||
# cheat-darwin-amd64
|
||||
$(dist_dir)/cheat-darwin-amd64:
|
||||
$(dist_dir)/cheat-darwin-amd64: prepare
|
||||
GOARCH=amd64 GOOS=darwin \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-darwin-arm64
|
||||
$(dist_dir)/cheat-darwin-arm64:
|
||||
GOARCH=arm64 GOOS=darwin \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-386
|
||||
$(dist_dir)/cheat-linux-386:
|
||||
$(dist_dir)/cheat-linux-386: prepare
|
||||
GOARCH=386 GOOS=linux \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-amd64
|
||||
$(dist_dir)/cheat-linux-amd64:
|
||||
$(dist_dir)/cheat-linux-amd64: prepare
|
||||
GOARCH=amd64 GOOS=linux \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-arm5
|
||||
$(dist_dir)/cheat-linux-arm5:
|
||||
$(dist_dir)/cheat-linux-arm5: prepare
|
||||
GOARCH=arm GOOS=linux GOARM=5 \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-arm6
|
||||
$(dist_dir)/cheat-linux-arm6:
|
||||
$(dist_dir)/cheat-linux-arm6: prepare
|
||||
GOARCH=arm GOOS=linux GOARM=6 \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-arm7
|
||||
$(dist_dir)/cheat-linux-arm7:
|
||||
$(dist_dir)/cheat-linux-arm7: prepare
|
||||
GOARCH=arm GOOS=linux GOARM=7 \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-linux-arm64
|
||||
$(dist_dir)/cheat-linux-arm64:
|
||||
GOARCH=arm64 GOOS=linux \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-netbsd-amd64
|
||||
$(dist_dir)/cheat-netbsd-amd64:
|
||||
GOARCH=amd64 GOOS=netbsd \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-openbsd-amd64
|
||||
$(dist_dir)/cheat-openbsd-amd64:
|
||||
GOARCH=amd64 GOOS=openbsd \
|
||||
$(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
|
||||
$(dist_dir)/cheat-solaris-amd64:
|
||||
GOARCH=amd64 GOOS=solaris \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
|
||||
|
||||
# cheat-windows-amd64
|
||||
$(dist_dir)/cheat-windows-amd64.exe:
|
||||
$(dist_dir)/cheat-windows-amd64.exe: prepare
|
||||
GOARCH=amd64 GOOS=windows \
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(ZIP) $@.zip $@ -j
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(ZIP) $@.zip $@
|
||||
|
||||
# ./dist
|
||||
$(dist_dir):
|
||||
$(MKDIR) $(dist_dir)
|
||||
|
||||
# .tmp
|
||||
.tmp:
|
||||
$(MKDIR) .tmp
|
||||
.PHONY: generate
|
||||
generate:
|
||||
$(GO) generate $(cmd_dir)
|
||||
|
||||
## install: build and install cheat on your PATH
|
||||
.PHONY: install
|
||||
@@ -138,21 +98,18 @@ install: build
|
||||
|
||||
## clean: remove compiled executables
|
||||
.PHONY: clean
|
||||
clean:
|
||||
clean: $(dist_dir)
|
||||
$(RM) -f $(dist_dir)/*
|
||||
$(RM) -rf .tmp
|
||||
|
||||
## distclean: remove the tags file
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
$(RM) -f tags
|
||||
@$(DOCKER) image rm -f $(docker_image)
|
||||
|
||||
## setup: install revive (linter) and scc (sloc tool)
|
||||
.PHONY: setup
|
||||
setup:
|
||||
$(GO) install github.com/boyter/scc@latest
|
||||
$(GO) install github.com/mgechev/revive@latest
|
||||
GO111MODULE=off $(GO) get -u github.com/boyter/scc github.com/mgechev/revive
|
||||
|
||||
## sloc: count "semantic lines of code"
|
||||
.PHONY: sloc
|
||||
@@ -175,11 +132,6 @@ man:
|
||||
vendor:
|
||||
$(GO) mod vendor && $(GO) mod tidy && $(GO) mod verify
|
||||
|
||||
## vendor-update: update vendored dependencies
|
||||
.PHONY: vendor-update
|
||||
vendor-update:
|
||||
$(GO) get -t -u ./... && $(GO) mod vendor && $(GO) mod tidy && $(GO) mod verify
|
||||
|
||||
## fmt: run go fmt
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@@ -200,80 +152,18 @@ vet:
|
||||
test:
|
||||
$(GO) test ./...
|
||||
|
||||
## test-integration: run integration tests (requires network)
|
||||
.PHONY: test-integration
|
||||
test-integration:
|
||||
$(GO) test -tags=integration -count=1 ./...
|
||||
|
||||
## test-all: run all tests (unit and integration)
|
||||
.PHONY: test-all
|
||||
test-all: test test-integration
|
||||
|
||||
## test-fuzz: run quick fuzz tests for security-critical functions
|
||||
.PHONY: test-fuzz
|
||||
test-fuzz:
|
||||
@./build/fuzz.sh 15s
|
||||
|
||||
## test-fuzz-long: run extended fuzz tests (10 minutes each)
|
||||
.PHONY: test-fuzz-long
|
||||
test-fuzz-long:
|
||||
@./build/fuzz.sh 10m
|
||||
|
||||
## coverage: generate a test coverage report
|
||||
.PHONY: coverage
|
||||
coverage: .tmp
|
||||
$(GO) test ./... -coverprofile=.tmp/cheat-coverage.out && \
|
||||
$(GO) tool cover -html=.tmp/cheat-coverage.out -o .tmp/cheat-coverage.html && \
|
||||
echo "Coverage report generated: .tmp/cheat-coverage.html" && \
|
||||
(sensible-browser .tmp/cheat-coverage.html 2>/dev/null || \
|
||||
xdg-open .tmp/cheat-coverage.html 2>/dev/null || \
|
||||
open .tmp/cheat-coverage.html 2>/dev/null || \
|
||||
echo "Please open .tmp/cheat-coverage.html in your browser")
|
||||
|
||||
## coverage-text: show test coverage by function in terminal
|
||||
.PHONY: coverage-text
|
||||
coverage-text: .tmp
|
||||
$(GO) test ./... -coverprofile=.tmp/cheat-coverage.out && \
|
||||
$(GO) tool cover -func=.tmp/cheat-coverage.out | $(SORT) -k3 -n
|
||||
|
||||
## benchmark: run performance benchmarks
|
||||
.PHONY: benchmark
|
||||
benchmark: .tmp
|
||||
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem ./cmd/cheat | tee .tmp/benchmark-latest.txt && \
|
||||
$(RM) -f cheat.test
|
||||
|
||||
## benchmark-cpu: run benchmarks with CPU profiling
|
||||
.PHONY: benchmark-cpu
|
||||
benchmark-cpu: .tmp
|
||||
$(GO) test -tags=integration -bench=. -benchtime=10s -cpuprofile=.tmp/cpu.prof ./cmd/cheat && \
|
||||
$(RM) -f cheat.test && \
|
||||
echo "CPU profile saved to .tmp/cpu.prof" && \
|
||||
echo "View with: go tool pprof -http=:8080 .tmp/cpu.prof"
|
||||
|
||||
## benchmark-mem: run benchmarks with memory profiling
|
||||
.PHONY: benchmark-mem
|
||||
benchmark-mem: .tmp
|
||||
$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem -memprofile=.tmp/mem.prof ./cmd/cheat && \
|
||||
$(RM) -f cheat.test && \
|
||||
echo "Memory profile saved to .tmp/mem.prof" && \
|
||||
echo "View with: go tool pprof -http=:8080 .tmp/mem.prof"
|
||||
coverage:
|
||||
$(GO) test ./... -coverprofile=$(TMPDIR)/cheat-coverage.out && \
|
||||
$(GO) tool cover -html=$(TMPDIR)/cheat-coverage.out
|
||||
|
||||
## check: format, lint, vet, vendor, and run unit-tests
|
||||
.PHONY: check
|
||||
check: | vendor fmt lint vet test
|
||||
|
||||
.PHONY: prepare
|
||||
prepare: | clean $(dist_dir) vendor fmt lint vet test
|
||||
|
||||
## docker-setup: create a docker image for use during development
|
||||
.PHONY: docker-setup
|
||||
docker-setup:
|
||||
$(DOCKER) build -t $(docker_image) -f Dockerfile .
|
||||
|
||||
## docker-sh: shell into the docker development container
|
||||
.PHONY: docker-sh
|
||||
docker-sh:
|
||||
$(DOCKER) run -v $(shell pwd):/app -ti $(docker_image) /bin/ash
|
||||
prepare: | $(dist_dir) clean generate vendor fmt lint vet test
|
||||
|
||||
## help: display this help text
|
||||
.PHONY: help
|
||||
|
||||
180
README.md
180
README.md
@@ -1,9 +1,8 @@
|
||||

|
||||
|
||||
|
||||
cheat
|
||||
=====
|
||||
|
||||
[](https://travis-ci.com/cheat/cheat)
|
||||
|
||||
`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
|
||||
@@ -42,6 +41,99 @@ tar -xjvf '/path/to/foo.tgz'
|
||||
tar -cjvf '/path/to/foo.tgz' '/path/to/foo/'
|
||||
```
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
`cheat` has no dependencies. To install it, download the executable from the
|
||||
[releases][] page and place it on your `PATH`.
|
||||
|
||||
|
||||
Configuring
|
||||
-----------
|
||||
### conf.yml ###
|
||||
`cheat` is configured by a YAML file that will be auto-generated on first run.
|
||||
Should you need to create a config file manually, you can do
|
||||
so via:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.config/cheat && cheat --init > ~/.config/cheat/conf.yml
|
||||
```
|
||||
|
||||
By default, the config file is assumed to exist on an XDG-compliant
|
||||
configuration path like `~/.config/cheat/conf.yml`. If you would like to store
|
||||
it elsewhere, you may export a `CHEAT_CONFIG_PATH` environment variable that
|
||||
specifies its path:
|
||||
|
||||
```sh
|
||||
export CHEAT_CONFIG_PATH="~/.dotfiles/cheat/conf.yml"
|
||||
```
|
||||
|
||||
Cheatsheets
|
||||
-----------
|
||||
Cheatsheets are plain-text files with no file extension, and are named
|
||||
according to the command used to view them:
|
||||
|
||||
```sh
|
||||
cheat tar # file is named "tar"
|
||||
cheat foo/bar # file is named "bar", in a "foo" subdirectory
|
||||
```
|
||||
|
||||
Cheatsheet text may optionally be preceeded by a YAML frontmatter header that
|
||||
assigns tags and specifies syntax:
|
||||
|
||||
```
|
||||
---
|
||||
syntax: javascript
|
||||
tags: [ array, map ]
|
||||
---
|
||||
// To map over an array:
|
||||
const squares = [1, 2, 3, 4].map(x => x * x);
|
||||
```
|
||||
|
||||
The `cheat` executable includes no cheatsheets, but [community-sourced
|
||||
cheatsheets are available][cheatsheets]. You will be asked if you would like to
|
||||
install the community-sourced cheatsheets the first time you run `cheat`.
|
||||
|
||||
|
||||
Cheatpaths
|
||||
----------
|
||||
Cheatsheets are stored on "cheatpaths", which are directories that contain
|
||||
cheatsheets. Cheatpaths are specified in the `conf.yml` file.
|
||||
|
||||
It can be useful to configure `cheat` against multiple cheatpaths. A common
|
||||
pattern is to store cheatsheets from multiple repositories on individual
|
||||
cheatpaths:
|
||||
|
||||
```yaml
|
||||
# conf.yml:
|
||||
# ...
|
||||
cheatpaths:
|
||||
- name: community # a name for the cheatpath
|
||||
path: ~/documents/cheat/community # the path's location on the filesystem
|
||||
tags: [ community ] # these tags will be applied to all sheets on the path
|
||||
readonly: true # if true, `cheat` will not create new cheatsheets here
|
||||
|
||||
- name: personal
|
||||
path: ~/documents/cheat/personal # this is a separate directory and repository than above
|
||||
tags: [ personal ]
|
||||
readonly: false # new sheets may be written here
|
||||
# ...
|
||||
```
|
||||
|
||||
The `readonly` option instructs `cheat` not to edit (or create) any cheatsheets
|
||||
on the path. This is useful to prevent merge-conflicts from arising on upstream
|
||||
cheatsheet repositories.
|
||||
|
||||
If a user attempts to edit a cheatsheet on a read-only cheatpath, `cheat` will
|
||||
transparently copy that sheet to a writeable directory before opening it for
|
||||
editing.
|
||||
|
||||
### Directory-scoped Cheatpaths ###
|
||||
At times, it can be useful to closely associate cheatsheets with a directory on
|
||||
your filesystem. `cheat` facilitates this by searching for a `.cheat` folder in
|
||||
the current working directory. If found, the `.cheat` directory will
|
||||
(temporarily) be added to the cheatpaths.
|
||||
|
||||
Usage
|
||||
-----
|
||||
To view a cheatsheet:
|
||||
@@ -102,77 +194,7 @@ cheat -p personal -t networking --regex -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
|
||||
```
|
||||
|
||||
|
||||
|
||||
Installing
|
||||
----------
|
||||
For installation and configuration instructions, see [INSTALLING.md][].
|
||||
|
||||
Cheatsheets
|
||||
-----------
|
||||
Cheatsheets are plain-text files with no file extension, and are named
|
||||
according to the command used to view them:
|
||||
|
||||
```sh
|
||||
cheat tar # file is named "tar"
|
||||
cheat foo/bar # file is named "bar", in a "foo" subdirectory
|
||||
```
|
||||
|
||||
Cheatsheet text may optionally be preceded by a YAML frontmatter header that
|
||||
assigns tags and specifies syntax:
|
||||
|
||||
```
|
||||
---
|
||||
syntax: javascript
|
||||
tags: [ array, map ]
|
||||
---
|
||||
// To map over an array:
|
||||
const squares = [1, 2, 3, 4].map(x => x * x);
|
||||
```
|
||||
|
||||
The `cheat` executable includes no cheatsheets, but [community-sourced
|
||||
cheatsheets are available][cheatsheets]. You will be asked if you would like to
|
||||
install the community-sourced cheatsheets the first time you run `cheat`.
|
||||
|
||||
Cheatpaths
|
||||
----------
|
||||
Cheatsheets are stored on "cheatpaths", which are directories that contain
|
||||
cheatsheets. Cheatpaths are specified in the `conf.yml` file.
|
||||
|
||||
It can be useful to configure `cheat` against multiple cheatpaths. A common
|
||||
pattern is to store cheatsheets from multiple repositories on individual
|
||||
cheatpaths:
|
||||
|
||||
```yaml
|
||||
# conf.yml:
|
||||
# ...
|
||||
cheatpaths:
|
||||
- name: community # a name for the cheatpath
|
||||
path: ~/documents/cheat/community # the path's location on the filesystem
|
||||
tags: [ community ] # these tags will be applied to all sheets on the path
|
||||
readonly: true # if true, `cheat` will not create new cheatsheets here
|
||||
|
||||
- name: personal
|
||||
path: ~/documents/cheat/personal # this is a separate directory and repository than above
|
||||
tags: [ personal ]
|
||||
readonly: false # new sheets may be written here
|
||||
# ...
|
||||
```
|
||||
|
||||
The `readonly` option instructs `cheat` not to edit (or create) any cheatsheets
|
||||
on the path. This is useful to prevent merge-conflicts from arising on upstream
|
||||
cheatsheet repositories.
|
||||
|
||||
If a user attempts to edit a cheatsheet on a read-only cheatpath, `cheat` will
|
||||
transparently copy that sheet to a writeable directory before opening it for
|
||||
editing.
|
||||
|
||||
### Directory-scoped Cheatpaths ###
|
||||
At times, it can be useful to closely associate cheatsheets with a directory on
|
||||
your filesystem. `cheat` facilitates this by searching for a `.cheat` folder in
|
||||
the current working directory. If found, the `.cheat` directory will
|
||||
(temporarily) be added to the cheatpaths.
|
||||
|
||||
Autocompletion
|
||||
Advanced Usage
|
||||
--------------
|
||||
Shell autocompletion is currently available for `bash`, `fish`, and `zsh`. Copy
|
||||
the relevant [completion script][completions] into the appropriate directory on
|
||||
@@ -185,9 +207,7 @@ Additionally, `cheat` supports enhanced autocompletion via integration with
|
||||
1. Ensure that `fzf` is available on your `$PATH`
|
||||
2. Set an envvar: `export CHEAT_USE_FZF=true`
|
||||
|
||||
[INSTALLING.md]: INSTALLING.md
|
||||
[Releases]: https://github.com/cheat/cheat/releases
|
||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||
[completions]: https://github.com/cheat/cheat/tree/master/scripts
|
||||
[fzf]: https://github.com/junegunn/fzf
|
||||
[go]: https://golang.org
|
||||
[Releases]: https://github.com/cheat/cheat/releases
|
||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||
[completions]: https://github.com/cheat/cheat/tree/master/scripts
|
||||
[fzf]: https://github.com/junegunn/fzf
|
||||
|
||||
93
build/embed.go
Normal file
93
build/embed.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// +build ignore
|
||||
|
||||
// This script embeds `docopt.txt and `conf.yml` into the binary during at
|
||||
// build time.
|
||||
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// get the cwd
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// get the project root
|
||||
root, err := filepath.Abs(cwd + "../../../")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// specify template file information
|
||||
type file struct {
|
||||
In string
|
||||
Out string
|
||||
Method string
|
||||
}
|
||||
|
||||
// enumerate the template files to process
|
||||
files := []file{
|
||||
file{
|
||||
In: "cmd/cheat/docopt.txt",
|
||||
Out: "cmd/cheat/str_usage.go",
|
||||
Method: "usage"},
|
||||
file{
|
||||
In: "configs/conf.yml",
|
||||
Out: "cmd/cheat/str_config.go",
|
||||
Method: "configs"},
|
||||
}
|
||||
|
||||
// iterate over each static file
|
||||
for _, file := range files {
|
||||
|
||||
// delete the outfile
|
||||
os.Remove(path.Join(root, file.Out))
|
||||
|
||||
// read the static template
|
||||
bytes, err := ioutil.ReadFile(path.Join(root, file.In))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// render the template
|
||||
data := template(file.Method, string(bytes))
|
||||
|
||||
// write the file to the specified outpath
|
||||
spath := path.Join(root, file.Out)
|
||||
err = ioutil.WriteFile(spath, []byte(data), 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// template packages the
|
||||
func template(method string, body string) string {
|
||||
|
||||
// specify the template string
|
||||
t := `package main
|
||||
|
||||
// Code generated .* DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func %s() string {
|
||||
return strings.TrimSpace(%s)
|
||||
}
|
||||
`
|
||||
|
||||
return fmt.Sprintf(t, method, "`"+body+"`")
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Run fuzz tests for cheat
|
||||
# Usage: ./scripts/fuzz.sh [duration]
|
||||
#
|
||||
# Note: Go's fuzzer will fail immediately if it finds a known failing input
|
||||
# in the corpus (testdata/fuzz/*). This is by design - it ensures you fix
|
||||
# known bugs before searching for new ones. To see failing inputs:
|
||||
# ls internal/*/testdata/fuzz/*/
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
DURATION="${1:-15s}"
|
||||
|
||||
# Define fuzz tests: "TestName:Package:Description"
|
||||
TESTS=(
|
||||
"FuzzParse:./internal/sheet:YAML frontmatter parsing"
|
||||
"FuzzValidateSheetName:./internal/cheatpath:sheet name validation (path traversal protection)"
|
||||
"FuzzSearchRegex:./internal/sheet:regex search operations"
|
||||
"FuzzSearchCatastrophicBacktracking:./internal/sheet:catastrophic backtracking"
|
||||
"FuzzTagged:./internal/sheet:tag matching with malicious input"
|
||||
"FuzzFilter:./internal/sheets:tag filtering operations"
|
||||
"FuzzTags:./internal/sheets:tag aggregation and sorting"
|
||||
)
|
||||
|
||||
echo "Running fuzz tests ($DURATION each)..."
|
||||
echo
|
||||
|
||||
for i in "${!TESTS[@]}"; do
|
||||
IFS=':' read -r test_name package description <<< "${TESTS[$i]}"
|
||||
echo "$((i+1)). Testing $description..."
|
||||
go test -fuzz="^${test_name}$" -fuzztime="$DURATION" "$package"
|
||||
echo
|
||||
done
|
||||
|
||||
echo "All fuzz tests passed!"
|
||||
@@ -1,11 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
func cmdConf(_ map[string]interface{}, conf config.Config) {
|
||||
fmt.Println(conf.Path)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// cmdDirectories lists the configured cheatpaths.
|
||||
func cmdDirectories(_ map[string]interface{}, conf config.Config) {
|
||||
func cmdDirectories(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
// initialize a tabwriter to produce cleanly columnized output
|
||||
var out bytes.Buffer
|
||||
@@ -18,10 +18,14 @@ func cmdDirectories(_ map[string]interface{}, conf config.Config) {
|
||||
|
||||
// generate sorted, columnized output
|
||||
for _, path := range conf.Cheatpaths {
|
||||
fmt.Fprintf(w, "%s:\t%s\n", path.Name, path.Path)
|
||||
fmt.Fprintln(w, fmt.Sprintf(
|
||||
"%s:\t%s",
|
||||
path.Name,
|
||||
path.Path,
|
||||
))
|
||||
}
|
||||
|
||||
// write columnized output to stdout
|
||||
w.Flush()
|
||||
display.Write(out.String(), conf)
|
||||
display.Display(out.String(), conf)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/cheat/cheat/internal/cheatpath"
|
||||
@@ -17,16 +17,10 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
cheatsheet := opts["--edit"].(string)
|
||||
|
||||
// validate the cheatsheet name
|
||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -64,10 +58,10 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
||||
}
|
||||
|
||||
// compute the new edit path
|
||||
editpath = filepath.Join(writepath.Path, sheet.Title)
|
||||
editpath = path.Join(writepath.Path, sheet.Title)
|
||||
|
||||
// create any necessary subdirectories
|
||||
dirs := filepath.Dir(editpath)
|
||||
dirs := path.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)
|
||||
@@ -93,10 +87,10 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
||||
}
|
||||
|
||||
// compute the new edit path
|
||||
editpath = filepath.Join(writepath.Path, cheatsheet)
|
||||
editpath = path.Join(writepath.Path, cheatsheet)
|
||||
|
||||
// create any necessary subdirectories
|
||||
dirs := filepath.Dir(editpath)
|
||||
dirs := path.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)
|
||||
|
||||
@@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -42,11 +42,11 @@ func cmdInit() {
|
||||
// determine the appropriate paths for config data and (optional) community
|
||||
// cheatsheets based on the user's platform
|
||||
confpath := confpaths[0]
|
||||
confdir := filepath.Dir(confpath)
|
||||
confdir := path.Dir(confpath)
|
||||
|
||||
// create paths for community and personal cheatsheets
|
||||
community := filepath.Join(confdir, "cheatsheets", "community")
|
||||
personal := filepath.Join(confdir, "cheatsheets", "personal")
|
||||
community := path.Join(confdir, "/cheatsheets/community")
|
||||
personal := path.Join(confdir, "/cheatsheets/personal")
|
||||
|
||||
// template the above paths into the default configs
|
||||
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
|
||||
|
||||
@@ -21,11 +21,11 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// filter cheatsheets by tag if --tag was provided
|
||||
// filter cheatcheats by tag if --tag was provided
|
||||
if opts["--tag"] != nil {
|
||||
cheatsheets = sheets.Filter(
|
||||
cheatsheets,
|
||||
@@ -37,8 +37,8 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||
// sheets with local sheets), here we simply want to create a slice
|
||||
// containing all sheets.
|
||||
flattened := []sheet.Sheet{}
|
||||
for _, pathsheets := range cheatsheets {
|
||||
for _, s := range pathsheets {
|
||||
for _, pathSheets := range cheatsheets {
|
||||
for _, s := range pathSheets {
|
||||
flattened = append(flattened, s)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,10 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||
// compile the regex
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
|
||||
fmt.Fprintln(
|
||||
os.Stderr,
|
||||
fmt.Sprintf("failed to compile regexp: %s, %v", pattern, err),
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -92,10 +95,15 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
// generate sorted, columnized output
|
||||
for _, sheet := range flattened {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", sheet.Title, sheet.Path, strings.Join(sheet.Tags, ","))
|
||||
fmt.Fprintln(w, fmt.Sprintf(
|
||||
"%s\t%s\t%s",
|
||||
sheet.Title,
|
||||
sheet.Path,
|
||||
strings.Join(sheet.Tags, ","),
|
||||
))
|
||||
}
|
||||
|
||||
// write columnized output to stdout
|
||||
w.Flush()
|
||||
display.Write(out.String(), conf)
|
||||
display.Display(out.String(), conf)
|
||||
}
|
||||
|
||||
@@ -5,26 +5,19 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cheat/cheat/internal/cheatpath"
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
// cmdRemove removes (deletes) a cheatsheet.
|
||||
// cmdRemove opens a cheatsheet for editing (or creates it if it doesn't exist).
|
||||
func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
cheatsheet := opts["--rm"].(string)
|
||||
|
||||
// validate the cheatsheet name
|
||||
if err := cheatpath.ValidateSheetName(cheatsheet); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid cheatsheet name: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -44,19 +37,19 @@ func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
||||
// fail early if the requested cheatsheet does not exist
|
||||
sheet, ok := consolidated[cheatsheet]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "No cheatsheet found for '%s'.\n", cheatsheet)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("No cheatsheet found for '%s'.\n", cheatsheet))
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// fail early if the sheet is read-only
|
||||
if sheet.ReadOnly {
|
||||
fmt.Fprintf(os.Stderr, "cheatsheet '%s' is read-only.\n", cheatsheet)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("cheatsheet '%s' is read-only.", cheatsheet))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// otherwise, attempt to delete the sheet
|
||||
if err := os.Remove(sheet.Path); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to delete sheet: %s, %v\n", sheet.Title, err)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to delete sheet: %s, %v", sheet.Title, err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
"github.com/cheat/cheat/internal/display"
|
||||
"github.com/cheat/cheat/internal/sheet"
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
@@ -19,7 +20,7 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -31,67 +32,71 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||
)
|
||||
}
|
||||
|
||||
// prepare the search pattern
|
||||
pattern := "(?i)" + phrase
|
||||
// consolidate the cheatsheets found on all paths into a single map of
|
||||
// `title` => `sheet` (ie, allow more local cheatsheets to override less
|
||||
// local cheatsheets)
|
||||
consolidated := sheets.Consolidate(cheatsheets)
|
||||
|
||||
// unless --regex is provided, in which case we pass the regex unaltered
|
||||
if opts["--regex"] == true {
|
||||
pattern = phrase
|
||||
}
|
||||
// if <cheatsheet> was provided, search that single sheet only
|
||||
if opts["<cheatsheet>"] != nil {
|
||||
|
||||
// compile the regex once, outside the loop
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cheatsheet := opts["<cheatsheet>"].(string)
|
||||
|
||||
// iterate over each cheatpath
|
||||
out := ""
|
||||
for _, pathcheats := range cheatsheets {
|
||||
// assert that the cheatsheet exists
|
||||
s, ok := consolidated[cheatsheet]
|
||||
if !ok {
|
||||
fmt.Printf("No cheatsheet found for '%s'.\n", cheatsheet)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// sort the cheatsheets alphabetically, and search for matches
|
||||
for _, sheet := range sheets.Sort(pathcheats) {
|
||||
|
||||
// if <cheatsheet> was provided, constrain the search only to
|
||||
// matching cheatsheets
|
||||
if opts["<cheatsheet>"] != nil && sheet.Title != opts["<cheatsheet>"] {
|
||||
continue
|
||||
}
|
||||
|
||||
// `Search` will return text entries that match the search terms.
|
||||
// We're using it here to overwrite the prior cheatsheet Text,
|
||||
// filtering it to only what is relevant.
|
||||
sheet.Text = sheet.Search(reg)
|
||||
|
||||
// if the sheet did not match the search, ignore it and move on
|
||||
if sheet.Text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// if colorization was requested, apply it here
|
||||
if conf.Color(opts) {
|
||||
sheet.Colorize(conf)
|
||||
}
|
||||
|
||||
// display the cheatsheet body
|
||||
out += fmt.Sprintf(
|
||||
"%s %s\n%s\n",
|
||||
// append the cheatsheet title
|
||||
sheet.Title,
|
||||
// append the cheatsheet path
|
||||
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
||||
// indent each line of content
|
||||
display.Indent(sheet.Text),
|
||||
)
|
||||
consolidated = map[string]sheet.Sheet{
|
||||
cheatsheet: s,
|
||||
}
|
||||
}
|
||||
|
||||
// trim superfluous newlines
|
||||
out = strings.TrimSpace(out)
|
||||
// sort the cheatsheets alphabetically, and search for matches
|
||||
out := ""
|
||||
for _, sheet := range sheets.Sort(consolidated) {
|
||||
|
||||
// assume that we want to perform a case-insensitive search for <phrase>
|
||||
pattern := "(?i)" + phrase
|
||||
|
||||
// unless --regex is provided, in which case we pass the regex unaltered
|
||||
if opts["--regex"] == true {
|
||||
pattern = phrase
|
||||
}
|
||||
|
||||
// compile the regex
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to compile regexp: %s, %v", pattern, err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// `Search` will return text entries that match the search terms. We're
|
||||
// using it here to overwrite the prior cheatsheet Text, filtering it to
|
||||
// only what is relevant
|
||||
sheet.Text = sheet.Search(reg)
|
||||
|
||||
// if the sheet did not match the search, ignore it and move on
|
||||
if sheet.Text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// if colorization was requested, apply it here
|
||||
if conf.Color(opts) {
|
||||
sheet.Colorize(conf)
|
||||
}
|
||||
|
||||
// output the cheatsheet title
|
||||
out += fmt.Sprintf("%s:\n", sheet.Title)
|
||||
|
||||
// indent each line of content with two spaces
|
||||
for _, line := range strings.Split(sheet.Text, "\n") {
|
||||
out += fmt.Sprintf(" %s\n", line)
|
||||
}
|
||||
}
|
||||
|
||||
// display the output
|
||||
// NB: resist the temptation to call `display.Write` multiple times in the
|
||||
// loop above. That will not play nicely with the paginator.
|
||||
display.Write(out, conf)
|
||||
display.Display(out, conf)
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ import (
|
||||
)
|
||||
|
||||
// cmdTags lists all tags in use.
|
||||
func cmdTags(_ map[string]interface{}, conf config.Config) {
|
||||
func cmdTags(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -26,5 +26,5 @@ func cmdTags(_ map[string]interface{}, conf config.Config) {
|
||||
}
|
||||
|
||||
// display the output
|
||||
display.Write(out, conf)
|
||||
display.Display(out, conf)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -30,39 +30,9 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
||||
)
|
||||
}
|
||||
|
||||
// if --all was passed, display cheatsheets from all cheatpaths
|
||||
if opts["--all"].(bool) {
|
||||
// iterate over the cheatpaths
|
||||
out := ""
|
||||
for _, cheatpath := range cheatsheets {
|
||||
|
||||
// if the cheatpath contains the specified cheatsheet, display it
|
||||
if sheet, ok := cheatpath[cheatsheet]; ok {
|
||||
|
||||
// identify the matching cheatsheet
|
||||
out += fmt.Sprintf("%s %s\n",
|
||||
sheet.Title,
|
||||
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
||||
)
|
||||
|
||||
// apply colorization if requested
|
||||
if conf.Color(opts) {
|
||||
sheet.Colorize(conf)
|
||||
}
|
||||
|
||||
// display the cheatsheet
|
||||
out += display.Indent(sheet.Text) + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// display and exit
|
||||
display.Write(strings.TrimSuffix(out, "\n"), conf)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// otherwise, consolidate the cheatsheets found on all paths into a single
|
||||
// map of `title` => `sheet` (ie, allow more local cheatsheets to override
|
||||
// less local cheatsheets)
|
||||
// consolidate the cheatsheets found on all paths into a single map of
|
||||
// `title` => `sheet` (ie, allow more local cheatsheets to override less
|
||||
// local cheatsheets)
|
||||
consolidated := sheets.Consolidate(cheatsheets)
|
||||
|
||||
// fail early if the requested cheatsheet does not exist
|
||||
@@ -78,5 +48,5 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
||||
}
|
||||
|
||||
// display the cheatsheet
|
||||
display.Write(sheet.Text, conf)
|
||||
display.Display(sheet.Text, conf)
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package main
|
||||
|
||||
// configs returns the default configuration template
|
||||
func configs() string {
|
||||
return `---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: EDITOR_PATH
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: false
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
# https://github.com/alecthomas/chroma/tree/master/styles
|
||||
style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal256
|
||||
|
||||
# Through which pager should output be piped?
|
||||
# 'less -FRX' is recommended on Unix systems
|
||||
# 'more' is recommended on Windows
|
||||
pager: PAGER_PATH
|
||||
|
||||
# The paths at which cheatsheets are available. Tags associated with a cheatpath
|
||||
# are automatically attached to all cheatsheets residing on that path.
|
||||
#
|
||||
# Whenever cheatsheets share the same title (like 'tar'), the most local
|
||||
# cheatsheets (those which come later in this file) take precedence over the
|
||||
# less local sheets. This allows you to create your own "overides" for
|
||||
# "upstream" cheatsheets.
|
||||
#
|
||||
# But what if you want to view the "upstream" cheatsheets instead of your own?
|
||||
# Cheatsheets may be filtered by 'tags' in combination with the '--tag' flag.
|
||||
#
|
||||
# Example: 'cheat tar --tag=community' will display the 'tar' cheatsheet that
|
||||
# is tagged as 'community' rather than your own.
|
||||
#
|
||||
# Paths that come earlier are considered to be the most "global", and paths
|
||||
# that come later are considered to be the most "local". The most "local" paths
|
||||
# take precedence.
|
||||
#
|
||||
# See: https://github.com/cheat/cheat/blob/master/doc/cheat.1.md#cheatpaths
|
||||
cheatpaths:
|
||||
|
||||
# Cheatsheets that are tagged "personal" are stored here by default:
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# Cheatsheets that are tagged "work" are stored here by default:
|
||||
- name: work
|
||||
path: WORK_PATH
|
||||
tags: [ work ]
|
||||
readonly: false
|
||||
|
||||
# Community cheatsheets are stored here by default:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# You can also use glob patterns to automatically load cheatsheets from all
|
||||
# directories that match.
|
||||
#
|
||||
# Example: overload cheatsheets for projects under ~/src/github.com/example/*/
|
||||
#- name: example-projects
|
||||
# path: ~/src/github.com/example/**/.cheat
|
||||
# tags: [ example ]
|
||||
# readonly: true`
|
||||
}
|
||||
54
cmd/cheat/docopt.txt
Normal file
54
cmd/cheat/docopt.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
Usage:
|
||||
cheat [options] [<cheatsheet>]
|
||||
|
||||
Options:
|
||||
--init Write a default config file to stdout
|
||||
-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 path <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>
|
||||
|
||||
Examples:
|
||||
|
||||
To initialize a config file:
|
||||
mkdir -p ~/.config/cheat && cheat --init > ~/.config/cheat/conf.yml
|
||||
|
||||
To view the tar cheatsheet:
|
||||
cheat tar
|
||||
|
||||
To edit (or create) the foo cheatsheet:
|
||||
cheat -e foo
|
||||
|
||||
To edit (or create) the foo/bar cheatsheet on the "work" cheatpath:
|
||||
cheat -p work -e foo/bar
|
||||
|
||||
To view all cheatsheet directories:
|
||||
cheat -d
|
||||
|
||||
To list all available cheatsheets:
|
||||
cheat -l
|
||||
|
||||
To list all cheatsheets whose titles match "apt":
|
||||
cheat -l apt
|
||||
|
||||
To list all tags in use:
|
||||
cheat -T
|
||||
|
||||
To list available cheatsheets that are tagged as "personal":
|
||||
cheat -l -t personal
|
||||
|
||||
To search for "ssh" among all cheatsheets, and colorize matches:
|
||||
cheat -c -s ssh
|
||||
|
||||
To search (by regex) for cheatsheets that contain an IP address:
|
||||
cheat -c -r -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
|
||||
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
cheat --rm foo/bar
|
||||
@@ -1,9 +1,11 @@
|
||||
// Package main serves as the executable entrypoint.
|
||||
package main
|
||||
|
||||
//go:generate go run ../../build/embed.go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -15,12 +17,12 @@ import (
|
||||
"github.com/cheat/cheat/internal/installer"
|
||||
)
|
||||
|
||||
const version = "4.5.0"
|
||||
const version = "4.0.4"
|
||||
|
||||
func main() {
|
||||
|
||||
// initialize options
|
||||
opts, err := docopt.ParseArgs(usage(), nil, version)
|
||||
opts, err := docopt.Parse(usage(), nil, true, version, false)
|
||||
if err != nil {
|
||||
// panic here, because this should never happen
|
||||
panic(fmt.Errorf("docopt failed to parse: %v", err))
|
||||
@@ -43,11 +45,7 @@ func main() {
|
||||
// read the envvars into a map of strings
|
||||
envvars := map[string]string{}
|
||||
for _, e := range os.Environ() {
|
||||
// os.Environ() guarantees "key=value" format (see ADR-002)
|
||||
pair := strings.SplitN(e, "=", 2)
|
||||
if runtime.GOOS == "windows" {
|
||||
pair[0] = strings.ToUpper(pair[0])
|
||||
}
|
||||
envvars[pair[0]] = pair[1]
|
||||
}
|
||||
|
||||
@@ -76,16 +74,62 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// choose a confpath
|
||||
confpath = confpaths[0]
|
||||
// read the config template
|
||||
configs := configs()
|
||||
|
||||
// run the installer
|
||||
if err := installer.Run(configs(), confpath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to run installer: %v\n", err)
|
||||
// determine the appropriate paths for config data and (optional) community
|
||||
// cheatsheets based on the user's platform
|
||||
confpath = confpaths[0]
|
||||
confdir := path.Dir(confpath)
|
||||
|
||||
// create paths for community and personal cheatsheets
|
||||
community := path.Join(confdir, "/cheatsheets/community")
|
||||
personal := path.Join(confdir, "/cheatsheets/personal")
|
||||
|
||||
// template the above paths into the default configs
|
||||
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
|
||||
configs = strings.Replace(configs, "PERSONAL_PATH", personal, -1)
|
||||
|
||||
// prompt the user to download the community cheatsheets
|
||||
yes, err = installer.Prompt(
|
||||
"Would you like to download the community cheatsheets? [Y/n]",
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// clone the community cheatsheets if so instructed
|
||||
if yes {
|
||||
// clone the community cheatsheets
|
||||
if err := installer.Clone(community); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// also create a directory for personal cheatsheets
|
||||
if err := os.MkdirAll(personal, os.ModePerm); err != nil {
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"failed to create config: failed to create directory: %s: %v\n",
|
||||
personal,
|
||||
err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// the config file does not exist, so we'll try to create one
|
||||
if err = config.Init(confpath, configs); err != nil {
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"failed to create config file: %s: %v\n",
|
||||
confpath,
|
||||
err,
|
||||
)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// notify the user and exit
|
||||
fmt.Printf("Created config file: %s\n", confpath)
|
||||
fmt.Println("Please read this file for advanced configuration information.")
|
||||
os.Exit(0)
|
||||
@@ -120,9 +164,6 @@ func main() {
|
||||
var cmd func(map[string]interface{}, config.Config)
|
||||
|
||||
switch {
|
||||
case opts["--conf"].(bool):
|
||||
cmd = cmdConf
|
||||
|
||||
case opts["--directories"].(bool):
|
||||
cmd = cmdDirectories
|
||||
|
||||
@@ -144,9 +185,6 @@ func main() {
|
||||
case opts["<cheatsheet>"] != nil:
|
||||
cmd = cmdView
|
||||
|
||||
case opts["--tag"] != nil && opts["--tag"].(string) != "":
|
||||
cmd = cmdList
|
||||
|
||||
default:
|
||||
fmt.Println(usage())
|
||||
os.Exit(0)
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPathTraversalIntegration tests that the cheat binary properly blocks
|
||||
// path traversal attempts when invoked as a subprocess.
|
||||
func TestPathTraversalIntegration(t *testing.T) {
|
||||
// Build the cheat binary
|
||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment
|
||||
testDir := t.TempDir()
|
||||
sheetsDir := filepath.Join(testDir, "sheets")
|
||||
os.MkdirAll(sheetsDir, 0755)
|
||||
|
||||
// Create config
|
||||
config := fmt.Sprintf(`---
|
||||
editor: echo
|
||||
colorize: false
|
||||
pager: cat
|
||||
cheatpaths:
|
||||
- name: test
|
||||
path: %s
|
||||
readonly: false
|
||||
`, sheetsDir)
|
||||
|
||||
configPath := filepath.Join(testDir, "config.yml")
|
||||
if err := os.WriteFile(configPath, []byte(config), 0644); err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Test table
|
||||
tests := []struct {
|
||||
name string
|
||||
command []string
|
||||
wantFail bool
|
||||
wantMsg string
|
||||
}{
|
||||
// Blocked patterns
|
||||
{
|
||||
name: "block parent traversal edit",
|
||||
command: []string{"--edit", "../evil"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block absolute path edit",
|
||||
command: []string{"--edit", "/etc/passwd"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot be an absolute path",
|
||||
},
|
||||
{
|
||||
name: "block home dir edit",
|
||||
command: []string{"--edit", "~/.ssh/config"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot start with '~'",
|
||||
},
|
||||
{
|
||||
name: "block parent traversal remove",
|
||||
command: []string{"--rm", "../evil"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block complex traversal",
|
||||
command: []string{"--edit", "foo/../../bar"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block just dots",
|
||||
command: []string{"--edit", ".."},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot contain '..'",
|
||||
},
|
||||
{
|
||||
name: "block empty name",
|
||||
command: []string{"--edit", ""},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot be empty",
|
||||
},
|
||||
// Allowed patterns
|
||||
{
|
||||
name: "allow simple name",
|
||||
command: []string{"--edit", "docker"},
|
||||
wantFail: false,
|
||||
},
|
||||
{
|
||||
name: "allow nested name",
|
||||
command: []string{"--edit", "lang/go"},
|
||||
wantFail: false,
|
||||
},
|
||||
{
|
||||
name: "block hidden file",
|
||||
command: []string{"--edit", ".gitignore"},
|
||||
wantFail: true,
|
||||
wantMsg: "cannot start with '.'",
|
||||
},
|
||||
{
|
||||
name: "allow current dir",
|
||||
command: []string{"--edit", "./local"},
|
||||
wantFail: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := exec.Command(binPath, tc.command...)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configPath),
|
||||
fmt.Sprintf("HOME=%s", testDir),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if tc.wantFail {
|
||||
if err == nil {
|
||||
t.Errorf("Expected failure but command succeeded. Output: %s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Expected 'invalid cheatsheet name' error, got: %s", output)
|
||||
}
|
||||
if tc.wantMsg != "" && !strings.Contains(string(output), tc.wantMsg) {
|
||||
t.Errorf("Expected message %q in output, got: %s", tc.wantMsg, output)
|
||||
}
|
||||
} else {
|
||||
// Command might fail for other reasons (e.g., editor not found)
|
||||
// but should NOT fail with "invalid cheatsheet name"
|
||||
if strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Command incorrectly blocked. Output: %s", output)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPathTraversalRealWorld tests with more realistic scenarios
|
||||
func TestPathTraversalRealWorld(t *testing.T) {
|
||||
// This test ensures our protection works with actual file operations
|
||||
|
||||
// Build cheat
|
||||
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("Failed to build: %v\n%s", err, output)
|
||||
}
|
||||
|
||||
// Create test structure
|
||||
testRoot := t.TempDir()
|
||||
sheetsDir := filepath.Join(testRoot, "cheatsheets")
|
||||
secretDir := filepath.Join(testRoot, "secrets")
|
||||
os.MkdirAll(sheetsDir, 0755)
|
||||
os.MkdirAll(secretDir, 0755)
|
||||
|
||||
// Create a "secret" file that should not be accessible
|
||||
secretFile := filepath.Join(secretDir, "secret.txt")
|
||||
os.WriteFile(secretFile, []byte("SECRET DATA"), 0644)
|
||||
|
||||
// Create config using vim in non-interactive mode
|
||||
config := fmt.Sprintf(`---
|
||||
editor: vim -u NONE -n --cmd "set noswapfile" --cmd "wq"
|
||||
colorize: false
|
||||
pager: cat
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: %s
|
||||
readonly: false
|
||||
`, sheetsDir)
|
||||
|
||||
configPath := filepath.Join(testRoot, "config.yml")
|
||||
os.WriteFile(configPath, []byte(config), 0644)
|
||||
|
||||
// Test 1: Try to edit a file outside cheatsheets using traversal
|
||||
cmd := exec.Command(binPath, "--edit", "../secrets/secret")
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configPath),
|
||||
fmt.Sprintf("HOME=%s", testRoot),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if err == nil || !strings.Contains(string(output), "invalid cheatsheet name") {
|
||||
t.Errorf("Path traversal was not blocked! Output: %s", output)
|
||||
}
|
||||
|
||||
// Test 2: Verify the secret file is still intact
|
||||
content, _ := os.ReadFile(secretFile)
|
||||
if string(content) != "SECRET DATA" {
|
||||
t.Errorf("Secret file was modified!")
|
||||
}
|
||||
|
||||
// Test 3: Verify no files were created outside sheets directory
|
||||
err = filepath.Walk(testRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() &&
|
||||
path != configPath &&
|
||||
path != secretFile &&
|
||||
!strings.HasPrefix(path, sheetsDir) {
|
||||
t.Errorf("File created outside allowed directory: %s", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Walk error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// BenchmarkSearchCommand benchmarks the actual cheat search command
|
||||
func BenchmarkSearchCommand(b *testing.B) {
|
||||
// Build the cheat binary in .tmp (using absolute path)
|
||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to get root dir: %v", err)
|
||||
}
|
||||
tmpDir := filepath.Join(rootDir, ".tmp", "bench-test")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
cheatBin := filepath.Join(tmpDir, "cheat-bench")
|
||||
|
||||
// Clean up the binary when done
|
||||
b.Cleanup(func() {
|
||||
os.Remove(cheatBin)
|
||||
})
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||
cmd.Dir = rootDir
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment in .tmp
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
cheatsheetDir := filepath.Join(configDir, "cheatsheets", "community")
|
||||
|
||||
// Clone community cheatsheets (or reuse if already exists)
|
||||
if _, err := os.Stat(cheatsheetDir); os.IsNotExist(err) {
|
||||
b.Logf("Cloning community cheatsheets to %s...", cheatsheetDir)
|
||||
_, err := git.PlainClone(cheatsheetDir, false, &git.CloneOptions{
|
||||
URL: "https://github.com/cheat/cheatsheets.git",
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
Progress: nil,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a minimal config file
|
||||
configFile := filepath.Join(configDir, "conf.yml")
|
||||
configContent := fmt.Sprintf(`---
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: %s
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
`, cheatsheetDir)
|
||||
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
b.Fatalf("Failed to create config dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
|
||||
b.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Set environment to use our config
|
||||
env := append(os.Environ(),
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configFile),
|
||||
)
|
||||
|
||||
// Define test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"SimpleSearch", []string{"-s", "echo"}},
|
||||
{"RegexSearch", []string{"-r", "-s", "^#.*example"}},
|
||||
{"ColorizedSearch", []string{"-c", "-s", "grep"}},
|
||||
{"ComplexRegex", []string{"-r", "-s", "(git|hg|svn)\\s+(add|commit|push)"}},
|
||||
{"AllCheatpaths", []string{"-a", "-s", "list"}},
|
||||
}
|
||||
|
||||
// Warm up - run once to ensure everything is loaded
|
||||
warmupCmd := exec.Command(cheatBin, "-l")
|
||||
warmupCmd.Env = env
|
||||
warmupCmd.Run()
|
||||
|
||||
// Run benchmarks
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
// Reset timer to exclude setup
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cmd := exec.Command(cheatBin, tc.args...)
|
||||
cmd.Env = env
|
||||
|
||||
// Capture output to prevent spamming
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
start := time.Now()
|
||||
err := cmd.Run()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
b.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
// Report custom metric
|
||||
b.ReportMetric(float64(elapsed.Nanoseconds())/1e6, "ms/op")
|
||||
|
||||
// Ensure we got some results
|
||||
if stdout.Len() == 0 {
|
||||
b.Fatal("No output from search")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkListCommand benchmarks the list command for comparison
|
||||
func BenchmarkListCommand(b *testing.B) {
|
||||
// Build the cheat binary in .tmp (using absolute path)
|
||||
rootDir, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to get root dir: %v", err)
|
||||
}
|
||||
tmpDir := filepath.Join(rootDir, ".tmp", "bench-test")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
b.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
cheatBin := filepath.Join(tmpDir, "cheat-bench")
|
||||
|
||||
// Clean up the binary when done
|
||||
b.Cleanup(func() {
|
||||
os.Remove(cheatBin)
|
||||
})
|
||||
|
||||
cmd := exec.Command("go", "build", "-o", cheatBin, "./cmd/cheat")
|
||||
cmd.Dir = rootDir
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
b.Fatalf("Failed to build cheat: %v\nOutput: %s", err, output)
|
||||
}
|
||||
|
||||
// Set up test environment (simplified - reuse if possible)
|
||||
configDir := filepath.Join(tmpDir, "config")
|
||||
cheatsheetDir := filepath.Join(configDir, "cheatsheets", "community")
|
||||
|
||||
// Check if we need to clone
|
||||
if _, err := os.Stat(cheatsheetDir); os.IsNotExist(err) {
|
||||
_, err := git.PlainClone(cheatsheetDir, false, &git.CloneOptions{
|
||||
URL: "https://github.com/cheat/cheatsheets.git",
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
Progress: nil,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create config
|
||||
configFile := filepath.Join(configDir, "conf.yml")
|
||||
configContent := fmt.Sprintf(`---
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: %s
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
`, cheatsheetDir)
|
||||
|
||||
os.MkdirAll(configDir, 0755)
|
||||
os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
|
||||
env := append(os.Environ(),
|
||||
fmt.Sprintf("CHEAT_CONFIG_PATH=%s", configFile),
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cmd := exec.Command(cheatBin, "-l")
|
||||
cmd.Env = env
|
||||
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
b.Fatalf("Command failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
80
cmd/cheat/str_config.go
Normal file
80
cmd/cheat/str_config.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
// Code generated .* DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func configs() string {
|
||||
return strings.TrimSpace(`---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: vim
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: true
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
# https://github.com/alecthomas/chroma/tree/master/styles
|
||||
style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal16m
|
||||
|
||||
# Through which pager should output be piped? (Unset this key for no pager.)
|
||||
pager: less -FRX
|
||||
|
||||
# The paths at which cheatsheets are available. Tags associated with a cheatpath
|
||||
# are automatically attached to all cheatsheets residing on that path.
|
||||
#
|
||||
# Whenever cheatsheets share the same title (like 'tar'), the most local
|
||||
# cheatsheets (those which come later in this file) take precedent over the
|
||||
# less local sheets. This allows you to create your own "overides" for
|
||||
# "upstream" cheatsheets.
|
||||
#
|
||||
# But what if you want to view the "upstream" cheatsheets instead of your own?
|
||||
# Cheatsheets may be filtered via 'cheat -t <tag>' in combination with other
|
||||
# commands. So, if you want to view the 'tar' cheatsheet that is tagged as
|
||||
# 'community' rather than your own, you can use: cheat tar -t community
|
||||
cheatpaths:
|
||||
|
||||
# Paths that come earlier are considered to be the most "global", and will
|
||||
# thus be overridden by more local cheatsheets. That being the case, you
|
||||
# should probably list community cheatsheets first.
|
||||
#
|
||||
# Note that the paths and tags listed below are placeholders. You may freely
|
||||
# change them to suit your needs.
|
||||
#
|
||||
# Community cheatsheets must be installed separately, though you may have
|
||||
# downloaded them automatically when installing 'cheat'. If not, you may
|
||||
# download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
#
|
||||
# Once downloaded, ensure that 'path' below points to the location at which
|
||||
# you downloaded the community cheatsheets.
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# If you have personalized cheatsheets, list them last. They will take
|
||||
# precedence over the more global cheatsheets.
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# While it requires no configuration here, it's also worth noting that
|
||||
# 'cheat' will automatically append directories named '.cheat' within the
|
||||
# current working directory to the 'cheatpath'. This can be very useful if
|
||||
# you'd like to closely associate cheatsheets with, for example, a directory
|
||||
# containing source code.
|
||||
#
|
||||
# Such "directory-scoped" cheatsheets will be treated as the most "local"
|
||||
# cheatsheets, and will override less "local" cheatsheets. Likewise,
|
||||
# directory-scoped cheatsheets will always be editable ('readonly: false').
|
||||
`)
|
||||
}
|
||||
@@ -1,25 +1,28 @@
|
||||
package main
|
||||
|
||||
// usage returns the usage text for the cheat command
|
||||
// Code generated .* DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func usage() string {
|
||||
return `Usage:
|
||||
return strings.TrimSpace(`Usage:
|
||||
cheat [options] [<cheatsheet>]
|
||||
|
||||
Options:
|
||||
--init Write a default config file to stdout
|
||||
-a --all Search among all cheatpaths
|
||||
-c --colorize Colorize output
|
||||
-d --directories List cheatsheet directories
|
||||
-e --edit=<cheatsheet> Edit <cheatsheet>
|
||||
-l --list List cheatsheets
|
||||
-p --path=<name> Return only sheets found on cheatpath <name>
|
||||
-p --path=<name> Return only sheets found on path <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:
|
||||
|
||||
@@ -58,7 +61,5 @@ Examples:
|
||||
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
cheat --rm foo/bar
|
||||
|
||||
To view the configuration file path:
|
||||
cheat --conf`
|
||||
`)
|
||||
}
|
||||
69
configs/conf.yml
Normal file
69
configs/conf.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: vim
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: true
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
# https://github.com/alecthomas/chroma/tree/master/styles
|
||||
style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal16m
|
||||
|
||||
# Through which pager should output be piped? (Unset this key for no pager.)
|
||||
pager: less -FRX
|
||||
|
||||
# The paths at which cheatsheets are available. Tags associated with a cheatpath
|
||||
# are automatically attached to all cheatsheets residing on that path.
|
||||
#
|
||||
# Whenever cheatsheets share the same title (like 'tar'), the most local
|
||||
# cheatsheets (those which come later in this file) take precedent over the
|
||||
# less local sheets. This allows you to create your own "overides" for
|
||||
# "upstream" cheatsheets.
|
||||
#
|
||||
# But what if you want to view the "upstream" cheatsheets instead of your own?
|
||||
# Cheatsheets may be filtered via 'cheat -t <tag>' in combination with other
|
||||
# commands. So, if you want to view the 'tar' cheatsheet that is tagged as
|
||||
# 'community' rather than your own, you can use: cheat tar -t community
|
||||
cheatpaths:
|
||||
|
||||
# Paths that come earlier are considered to be the most "global", and will
|
||||
# thus be overridden by more local cheatsheets. That being the case, you
|
||||
# should probably list community cheatsheets first.
|
||||
#
|
||||
# Note that the paths and tags listed below are placeholders. You may freely
|
||||
# change them to suit your needs.
|
||||
#
|
||||
# Community cheatsheets must be installed separately, though you may have
|
||||
# downloaded them automatically when installing 'cheat'. If not, you may
|
||||
# download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
#
|
||||
# Once downloaded, ensure that 'path' below points to the location at which
|
||||
# you downloaded the community cheatsheets.
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# If you have personalized cheatsheets, list them last. They will take
|
||||
# precedence over the more global cheatsheets.
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# While it requires no configuration here, it's also worth noting that
|
||||
# 'cheat' will automatically append directories named '.cheat' within the
|
||||
# current working directory to the 'cheatpath'. This can be very useful if
|
||||
# you'd like to closely associate cheatsheets with, for example, a directory
|
||||
# containing source code.
|
||||
#
|
||||
# Such "directory-scoped" cheatsheets will be treated as the most "local"
|
||||
# cheatsheets, and will override less "local" cheatsheets. Likewise,
|
||||
# directory-scoped cheatsheets will always be editable ('readonly: false').
|
||||
@@ -1,169 +0,0 @@
|
||||
# ADR-001: Path Traversal Protection for Cheatsheet Names
|
||||
|
||||
Date: 2025-01-21
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The `cheat` tool allows users to create, edit, and remove cheatsheets using commands like:
|
||||
- `cheat --edit <name>`
|
||||
- `cheat --rm <name>`
|
||||
|
||||
Without validation, a user could potentially provide malicious names like:
|
||||
- `../../../etc/passwd` (directory traversal)
|
||||
- `/etc/passwd` (absolute path)
|
||||
- `~/.ssh/authorized_keys` (home directory expansion)
|
||||
|
||||
While `cheat` is a local tool run by the user themselves (not a network service), path traversal could still lead to:
|
||||
1. Accidental file overwrites outside cheatsheet directories
|
||||
2. Confusion about where files are being created
|
||||
3. Potential security issues in shared environments
|
||||
|
||||
## Decision
|
||||
|
||||
We implemented input validation for cheatsheet names to prevent directory traversal attacks. The validation rejects names that:
|
||||
|
||||
1. Contain `..` (parent directory references)
|
||||
2. Are absolute paths (start with `/` on Unix)
|
||||
3. Start with `~` (home directory expansion)
|
||||
4. Are empty
|
||||
5. Start with `.` (hidden files - these are not displayed by cheat)
|
||||
|
||||
The validation is performed at the application layer before any file operations occur.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Validation Function
|
||||
|
||||
The validation is implemented in `internal/cheatpath/validate.go`:
|
||||
|
||||
```go
|
||||
func ValidateSheetName(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)
|
||||
filename := filepath.Base(name)
|
||||
if strings.HasPrefix(filename, ".") {
|
||||
return fmt.Errorf("cheatsheet name cannot start with '.' (hidden files are not supported)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Points
|
||||
|
||||
The validation is called in:
|
||||
- `cmd/cheat/cmd_edit.go` - before creating or editing a cheatsheet
|
||||
- `cmd/cheat/cmd_remove.go` - before removing a cheatsheet
|
||||
|
||||
### Allowed Patterns
|
||||
|
||||
The following patterns are explicitly allowed:
|
||||
- Simple names: `docker`, `git`
|
||||
- Nested paths: `docker/compose`, `lang/go/slice`
|
||||
- Current directory references: `./mysheet`
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Safety**: Prevents accidental or intentional file operations outside cheatsheet directories
|
||||
2. **Simplicity**: Validation happens early, before any file operations
|
||||
3. **User-friendly**: Clear error messages explain why a name was rejected
|
||||
4. **Performance**: Minimal overhead - simple string checks
|
||||
5. **Compatibility**: Doesn't break existing valid cheatsheet names
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Limitation**: Users cannot use `..` in cheatsheet names even if legitimate
|
||||
2. **No symlink support**: Cannot create cheatsheets through symlinks outside the cheatpath
|
||||
|
||||
### Neutral
|
||||
|
||||
1. Uses Go's `filepath.IsAbs()` which handles platform differences (Windows vs Unix)
|
||||
2. No attempt to resolve or canonicalize paths - validation is purely syntactic
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Threat Model
|
||||
|
||||
`cheat` is a local command-line tool, not a network service. The primary threats are:
|
||||
- User error (accidentally overwriting important files)
|
||||
- Malicious scripts that invoke `cheat` with crafted arguments
|
||||
- Shared system scenarios where cheatsheets might be shared
|
||||
|
||||
### What This Protects Against
|
||||
|
||||
- Directory traversal using `../`
|
||||
- Absolute path access to system files
|
||||
- Shell expansion of `~` to home directory
|
||||
- Empty names that might cause unexpected behavior
|
||||
- Hidden files that wouldn't be displayed anyway
|
||||
|
||||
### What This Does NOT Protect Against
|
||||
|
||||
- Users with filesystem permissions can still directly edit any file
|
||||
- Symbolic links within the cheatpath pointing outside
|
||||
- Race conditions (TOCTOU) - though minimal risk for a local tool
|
||||
- Malicious content within cheatsheets themselves
|
||||
|
||||
## Testing
|
||||
|
||||
Comprehensive tests ensure the validation works correctly:
|
||||
|
||||
1. **Unit tests** (`internal/cheatpath/validate_test.go`) verify the validation logic
|
||||
2. **Integration tests** verify the actual binary blocks malicious inputs
|
||||
3. **No system files are accessed** during testing - all tests use isolated directories
|
||||
|
||||
Example test cases:
|
||||
```bash
|
||||
# These are blocked:
|
||||
cheat --edit "../../../etc/passwd"
|
||||
cheat --edit "/etc/passwd"
|
||||
cheat --edit "~/.ssh/config"
|
||||
cheat --rm ".."
|
||||
|
||||
# These are allowed:
|
||||
cheat --edit "docker"
|
||||
cheat --edit "docker/compose"
|
||||
cheat --edit "./local"
|
||||
```
|
||||
|
||||
## Alternative Approaches Considered
|
||||
|
||||
1. **Path resolution and verification**: Resolve the final path and check if it's within the cheatpath
|
||||
- Rejected: More complex, potential race conditions, platform-specific edge cases
|
||||
|
||||
2. **Chroot/sandbox**: Run file operations in a restricted environment
|
||||
- Rejected: Overkill for a local tool, platform compatibility issues
|
||||
|
||||
3. **Filename allowlist**: Only allow alphanumeric characters and specific symbols
|
||||
- Rejected: Too restrictive, would break existing cheatsheets with valid special characters
|
||||
|
||||
## References
|
||||
|
||||
- OWASP Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal
|
||||
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
|
||||
- Go filepath package documentation: https://pkg.go.dev/path/filepath
|
||||
@@ -1,100 +0,0 @@
|
||||
# ADR-002: No Defensive Checks for Environment Variable Parsing
|
||||
|
||||
Date: 2025-01-21
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
In `cmd/cheat/main.go` lines 47-52, the code parses environment variables assuming they all contain an equals sign:
|
||||
|
||||
```go
|
||||
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] // Could panic if pair has < 2 elements
|
||||
}
|
||||
```
|
||||
|
||||
If `os.Environ()` returned a string without an equals sign, `strings.SplitN` would return a slice with only one element, causing a panic when accessing `pair[1]`.
|
||||
|
||||
## Decision
|
||||
|
||||
We will **not** add defensive checks for this condition. The current code that assumes all environment strings contain "=" will remain unchanged.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Go Runtime Guarantees
|
||||
|
||||
Go's official documentation guarantees that `os.Environ()` returns environment variables in the form "key=value". This is a documented contract of the Go runtime that has been stable since Go 1.0.
|
||||
|
||||
### Empirical Evidence
|
||||
|
||||
Testing across platforms confirms:
|
||||
- All environment variables returned by `os.Environ()` contain at least one "="
|
||||
- Empty environment variables appear as "KEY=" (with an empty value)
|
||||
- Even Windows special variables like "=C:=C:\path" maintain the format
|
||||
|
||||
### Cost-Benefit Analysis
|
||||
|
||||
Adding defensive code would:
|
||||
- **Cost**: Add complexity and cognitive overhead
|
||||
- **Cost**: Suggest uncertainty about Go's documented behavior
|
||||
- **Cost**: Create dead code that can never execute under normal conditions
|
||||
- **Benefit**: Protect against a theoretical scenario that violates Go's guarantees
|
||||
|
||||
The only scenarios where this could panic are:
|
||||
1. A bug in Go's runtime (extremely unlikely, would affect all Go programs)
|
||||
2. Corrupted OS-level environment (would cause broader system issues)
|
||||
3. Breaking change in future Go version (would break many programs, unlikely)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Simpler, more readable code
|
||||
- Trust in platform guarantees reduces unnecessary defensive programming
|
||||
- No performance overhead from unnecessary checks
|
||||
|
||||
### Negative
|
||||
- Theoretical panic if Go's guarantees are violated
|
||||
|
||||
### Neutral
|
||||
- Follows Go community standards of trusting standard library contracts
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Add Defensive Check
|
||||
```go
|
||||
if len(pair) < 2 {
|
||||
continue // or pair[1] = ""
|
||||
}
|
||||
```
|
||||
**Rejected**: Adds complexity for a condition that should never occur.
|
||||
|
||||
### 2. Add Panic with Clear Message
|
||||
```go
|
||||
if len(pair) < 2 {
|
||||
panic("os.Environ() contract violation: " + e)
|
||||
}
|
||||
```
|
||||
**Rejected**: Would crash the program for the same theoretical issue.
|
||||
|
||||
### 3. Add Comment Documenting Assumption
|
||||
```go
|
||||
// os.Environ() guarantees "key=value" format, so pair[1] is safe
|
||||
envvars[pair[0]] = pair[1]
|
||||
```
|
||||
**Rejected**: While documentation is good, this particular guarantee is fundamental to Go.
|
||||
|
||||
## Notes
|
||||
|
||||
If Go ever changes this behavior (extremely unlikely as it would break compatibility), it would be caught immediately in testing as the program would panic on startup. This would be a clear signal to revisit this decision.
|
||||
|
||||
## References
|
||||
|
||||
- Go os.Environ() documentation: https://pkg.go.dev/os#Environ
|
||||
- Go os.Environ() source code and tests
|
||||
@@ -1,104 +0,0 @@
|
||||
# ADR-003: No Parallelization for Search Operations
|
||||
|
||||
Date: 2025-01-22
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
We investigated optimizing cheat's search performance through parallelization. Initial assumptions suggested that I/O operations (reading multiple cheatsheet files) would be the primary bottleneck, making parallel processing beneficial.
|
||||
|
||||
Performance benchmarks were implemented to measure search operations, and a parallel search implementation using goroutines was created and tested.
|
||||
|
||||
## Decision
|
||||
|
||||
We will **not** implement parallel search. The sequential implementation will remain unchanged.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Performance Profile Analysis
|
||||
|
||||
CPU profiling revealed that search performance is dominated by:
|
||||
- **Process creation overhead** (~30% in `os/exec.(*Cmd).Run`)
|
||||
- **System calls** (~30% in `syscall.Syscall6`)
|
||||
- **Process management** (fork, exec, pipe setup)
|
||||
|
||||
The actual search logic (regex matching, file reading) was negligible in the profile, indicating our optimization efforts were targeting the wrong bottleneck.
|
||||
|
||||
### Benchmark Results
|
||||
|
||||
Parallel implementation showed minimal improvements:
|
||||
- Simple search: 17ms → 15.3ms (10% improvement)
|
||||
- Regex search: 15ms → 14.9ms (minimal improvement)
|
||||
- Colorized search: 19.5ms → 16.8ms (14% improvement)
|
||||
- Complex regex: 20ms → 15.3ms (24% improvement)
|
||||
|
||||
The best case saved only ~5ms in absolute terms.
|
||||
|
||||
### Cost-Benefit Analysis
|
||||
|
||||
**Costs of parallelization:**
|
||||
- Added complexity with goroutines, channels, and synchronization
|
||||
- Increased maintenance burden
|
||||
- More difficult debugging and testing
|
||||
- Potential race conditions
|
||||
|
||||
**Benefits:**
|
||||
- 5-15% performance improvement (5ms in real terms)
|
||||
- Imperceptible to users in interactive use
|
||||
|
||||
### User Experience Perspective
|
||||
|
||||
For a command-line tool:
|
||||
- Current 15-20ms response time is excellent
|
||||
- Users cannot perceive 5ms differences
|
||||
- Sub-50ms is considered "instant" in HCI research
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Simpler, more maintainable codebase
|
||||
- Easier to debug and reason about
|
||||
- No synchronization bugs or race conditions
|
||||
- Focus remains on code clarity
|
||||
|
||||
### Negative
|
||||
- Missed opportunity for ~5ms performance gain
|
||||
- Search remains single-threaded
|
||||
|
||||
### Neutral
|
||||
- Performance remains excellent for intended use case
|
||||
- Follows Go philosophy of preferring simplicity
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Keep Parallel Implementation
|
||||
**Rejected**: Complexity outweighs negligible performance gains.
|
||||
|
||||
### 2. Optimize Process Startup
|
||||
**Rejected**: Process creation overhead is inherent to CLI tools and cannot be avoided without fundamental architecture changes.
|
||||
|
||||
### 3. Future Optimizations
|
||||
If performance becomes critical, consider:
|
||||
- **Long-running daemon**: Eliminate process startup overhead entirely
|
||||
- **Shell function**: Reduce fork/exec overhead
|
||||
- **Compiled-in cheatsheets**: Eliminate file I/O
|
||||
|
||||
However, these would fundamentally change the tool's architecture and usage model.
|
||||
|
||||
## Notes
|
||||
|
||||
This decision reinforces important principles:
|
||||
1. Always profile before optimizing
|
||||
2. Consider the full execution context
|
||||
3. Measure what matters to users
|
||||
4. Complexity has a real cost
|
||||
|
||||
The parallelization attempt was valuable as a learning exercise and definitively answered whether this optimization path was worthwhile.
|
||||
|
||||
## References
|
||||
|
||||
- Benchmark implementation: cmd/cheat/search_bench_test.go
|
||||
- Reverted parallel implementation: see git history (commit 82eb918)
|
||||
@@ -1,38 +0,0 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
This directory contains Architecture Decision Records (ADRs) for the cheat project.
|
||||
|
||||
## What is an ADR?
|
||||
|
||||
An Architecture Decision Record captures an important architectural decision made along with its context and consequences. ADRs help us:
|
||||
|
||||
- Document why decisions were made
|
||||
- Understand the context and trade-offs
|
||||
- Review decisions when requirements change
|
||||
- Onboard new contributors
|
||||
|
||||
## ADR Format
|
||||
|
||||
Each ADR follows this template:
|
||||
|
||||
1. **Title**: ADR-NNN: Brief description
|
||||
2. **Date**: When the decision was made
|
||||
3. **Status**: Proposed, Accepted, Deprecated, Superseded
|
||||
4. **Context**: What prompted this decision?
|
||||
5. **Decision**: What did we decide to do?
|
||||
6. **Consequences**: What are the positive, negative, and neutral outcomes?
|
||||
|
||||
## Index of ADRs
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [001](001-path-traversal-protection.md) | Path Traversal Protection for Cheatsheet Names | Accepted | 2025-01-21 |
|
||||
| [002](002-environment-variable-parsing.md) | No Defensive Checks for Environment Variable Parsing | Accepted | 2025-01-21 |
|
||||
| [003](003-search-parallelization.md) | No Parallelization for Search Operations | Accepted | 2025-01-22 |
|
||||
|
||||
## Creating a New ADR
|
||||
|
||||
1. Copy the template from an existing ADR
|
||||
2. Use the next sequential number
|
||||
3. Fill in all sections
|
||||
4. Include the ADR alongside the commit implementing the decision
|
||||
233
doc/cheat.1
233
doc/cheat.1
@@ -1,168 +1,208 @@
|
||||
.\" Automatically generated by Pandoc 3.1.11.1
|
||||
.\" Automatically generated by Pandoc 1.17.2
|
||||
.\"
|
||||
.TH "CHEAT" "1" "" "" "General Commands Manual"
|
||||
.hy
|
||||
.SH NAME
|
||||
\f[B]cheat\f[R] \[em] create and view command\-line cheatsheets
|
||||
.PP
|
||||
\f[B]cheat\f[] \[em] create and view command\-line cheatsheets
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\f[B]cheat\f[R] [options] [\f[I]CHEATSHEET\f[R]]
|
||||
\f[B]cheat\f[] [options] [\f[I]CHEATSHEET\f[]]
|
||||
.SH DESCRIPTION
|
||||
\f[B]cheat\f[R] allows you to create and view interactive cheatsheets on
|
||||
.PP
|
||||
\f[B]cheat\f[] 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.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\[en]init
|
||||
.B \-\-init
|
||||
Print a config file to stdout.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\[en]conf
|
||||
Display the config file path.
|
||||
.TP
|
||||
\-a, \[en]all
|
||||
Search among all cheatpaths.
|
||||
.TP
|
||||
\-c, \[en]colorize
|
||||
.B \-c, \-\-colorize
|
||||
Colorize output.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-d, \[en]directories
|
||||
.B \-d, \-\-directories
|
||||
List cheatsheet directories.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-e, \[en]edit=\f[I]CHEATSHEET\f[R]
|
||||
Open \f[I]CHEATSHEET\f[R] for editing.
|
||||
.B \-e, \-\-edit=\f[I]CHEATSHEET\f[]
|
||||
Open \f[I]CHEATSHEET\f[] for editing.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-l, \[en]list
|
||||
.B \-l, \-\-list
|
||||
List available cheatsheets.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-p, \[en]path=\f[I]PATH\f[R]
|
||||
Filter only to sheets found on path \f[I]PATH\f[R].
|
||||
.B \-p, \-\-path=\f[I]PATH\f[]
|
||||
Filter only to sheets found on path \f[I]PATH\f[].
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-r, \[en]regex
|
||||
Treat search \f[I]PHRASE\f[R] as a regular expression.
|
||||
.B \-r, \-\-regex
|
||||
Treat search \f[I]PHRASE\f[] as a regular expression.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-s, \[en]search=\f[I]PHRASE\f[R]
|
||||
Search cheatsheets for \f[I]PHRASE\f[R].
|
||||
.B \-s, \-\-search=\f[I]PHRASE\f[]
|
||||
Search cheatsheets for \f[I]PHRASE\f[].
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-t, \[en]tag=\f[I]TAG\f[R]
|
||||
Filter only to sheets tagged with \f[I]TAG\f[R].
|
||||
.B \-t, \-\-tag=\f[I]TAG\f[]
|
||||
Filter only to sheets tagged with \f[I]TAG\f[].
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-T, \[en]tags
|
||||
.B \-T, \-\-tags
|
||||
List all tags in use.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\-v, \[en]version
|
||||
.B \-v, \-\-version
|
||||
Print the version number.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\[en]rm=\f[I]CHEATSHEET\f[R]
|
||||
Remove (deletes) \f[I]CHEATSHEET\f[R].
|
||||
.B \-\-rm=\f[I]CHEATSHEET\f[]
|
||||
Remove (deletes) \f[I]CHEATSHEET\f[].
|
||||
.RS
|
||||
.RE
|
||||
.SH EXAMPLES
|
||||
.TP
|
||||
To view the foo cheatsheet:
|
||||
cheat \f[I]foo\f[R]
|
||||
.B To view the foo cheatsheet:
|
||||
cheat \f[I]foo\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To edit (or create) the foo cheatsheet:
|
||||
cheat \-e \f[I]foo\f[R]
|
||||
.B To edit (or create) the foo cheatsheet:
|
||||
cheat \-e \f[I]foo\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To edit (or create) the foo/bar cheatsheet on the `work' cheatpath:
|
||||
cheat \-p \f[I]work\f[R] \-e \f[I]foo/bar\f[R]
|
||||
.B To edit (or create) the foo/bar cheatsheet on the \[aq]work\[aq] cheatpath:
|
||||
cheat \-p \f[I]work\f[] \-e \f[I]foo/bar\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To view all cheatsheet directories:
|
||||
.B To view all cheatsheet directories:
|
||||
cheat \-d
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To list all available cheatsheets:
|
||||
.B To list all available cheatsheets:
|
||||
cheat \-l
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To list all cheatsheets whose titles match `apt':
|
||||
cheat \-l \f[I]apt\f[R]
|
||||
.B To list all cheatsheets whose titles match \[aq]apt\[aq]:
|
||||
cheat \-l \f[I]apt\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To list all tags in use:
|
||||
.B To list all tags in use:
|
||||
cheat \-T
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To list available cheatsheets that are tagged as `personal':
|
||||
cheat \-l \-t \f[I]personal\f[R]
|
||||
.B To list available cheatsheets that are tagged as \[aq]personal\[aq]:
|
||||
cheat \-l \-t \f[I]personal\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To search for `ssh' among all cheatsheets, and colorize matches:
|
||||
cheat \-c \-s \f[I]ssh\f[R]
|
||||
.B To search for \[aq]ssh\[aq] among all cheatsheets, and colorize matches:
|
||||
cheat \-c \-s \f[I]ssh\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To search (by regex) for cheatsheets that contain an IP address:
|
||||
cheat \-c \-r \-s \f[I]`(?:[0\-9]{1,3}.){3}[0\-9]{1,3}'\f[R]
|
||||
.B To search (by regex) for cheatsheets that contain an IP address:
|
||||
cheat \-c \-r \-s \f[I]\[aq](?:[0\-9]{1,3}.){3}[0\-9]{1,3}\[aq]\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
cheat \[en]rm \f[I]foo/bar\f[R]
|
||||
.TP
|
||||
To view the configuration file path:
|
||||
cheat \[en]conf
|
||||
.B To remove (delete) the foo/bar cheatsheet:
|
||||
cheat \-\-rm \f[I]foo/bar\f[]
|
||||
.RS
|
||||
.RE
|
||||
.SH FILES
|
||||
.SS Configuration
|
||||
\f[B]cheat\f[R] is configured via a YAML file that is conventionally
|
||||
named \f[I]conf.yaml\f[R].
|
||||
\f[B]cheat\f[R] will search for \f[I]conf.yaml\f[R] in varying
|
||||
locations, depending upon your platform:
|
||||
.PP
|
||||
\f[B]cheat\f[] is configured via a YAML file that is conventionally
|
||||
named \f[I]conf.yaml\f[].
|
||||
\f[B]cheat\f[] will search for \f[I]conf.yaml\f[] in varying locations,
|
||||
depending upon your platform:
|
||||
.SS Linux, OSX, and other Unixes
|
||||
.IP "1." 3
|
||||
\f[B]CHEAT_CONFIG_PATH\f[R]
|
||||
\f[B]CHEAT_CONFIG_PATH\f[]
|
||||
.IP "2." 3
|
||||
\f[B]XDG_CONFIG_HOME\f[R]/cheat/conf.yaml
|
||||
\f[B]XDG_CONFIG_HOME\f[]/cheat/conf.yaml
|
||||
.IP "3." 3
|
||||
\f[B]$HOME\f[R]/.config/cheat/conf.yml
|
||||
\f[B]$HOME\f[]/.config/cheat/conf.yml
|
||||
.IP "4." 3
|
||||
\f[B]$HOME\f[R]/.cheat/conf.yml
|
||||
\f[B]$HOME\f[]/.cheat/conf.yml
|
||||
.SS Windows
|
||||
.IP "1." 3
|
||||
\f[B]CHEAT_CONFIG_PATH\f[R]
|
||||
\f[B]CHEAT_CONFIG_PATH\f[]
|
||||
.IP "2." 3
|
||||
\f[B]APPDATA\f[R]/cheat/conf.yml
|
||||
\f[B]APPDATA\f[]/cheat/conf.yml
|
||||
.IP "3." 3
|
||||
\f[B]PROGRAMDATA\f[R]/cheat/conf.yml
|
||||
\f[B]PROGRAMDATA\f[]/cheat/conf.yml
|
||||
.PP
|
||||
\f[B]cheat\f[R] will search in the order specified above.
|
||||
The first \f[I]conf.yaml\f[R] encountered will be respected.
|
||||
\f[B]cheat\f[] will search in the order specified above.
|
||||
The first \f[I]conf.yaml\f[] encountered will be respected.
|
||||
.PP
|
||||
If \f[B]cheat\f[R] cannot locate a config file, it will ask if you\[cq]d
|
||||
If \f[B]cheat\f[] cannot locate a config file, it will ask if you\[aq]d
|
||||
like to generate one automatically.
|
||||
Alternatively, you may also generate a config file manually by running
|
||||
\f[B]cheat \[en]init\f[R] and saving its output to the appropriate
|
||||
\f[B]cheat \-\-init\f[] and saving its output to the appropriate
|
||||
location for your platform.
|
||||
.SS Cheatpaths
|
||||
\f[B]cheat\f[R] reads its cheatsheets from \[lq]cheatpaths\[rq], which
|
||||
are the directories in which cheatsheets are stored.
|
||||
Cheatpaths may be configured in \f[I]conf.yaml\f[R], and viewed via
|
||||
\f[B]cheat \-d\f[R].
|
||||
.PP
|
||||
\f[B]cheat\f[] reads its cheatsheets from "cheatpaths", which are the
|
||||
directories in which cheatsheets are stored.
|
||||
Cheatpaths may be configured in \f[I]conf.yaml\f[], and viewed via
|
||||
\f[B]cheat \-d\f[].
|
||||
.PP
|
||||
For detailed instructions on how to configure cheatpaths, please refer
|
||||
to the comments in conf.yml.
|
||||
.SS Autocompletion
|
||||
Autocompletion scripts for \f[B]bash\f[R], \f[B]zsh\f[R], and
|
||||
\f[B]fish\f[R] are available for download:
|
||||
.IP \[bu] 2
|
||||
\c
|
||||
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.bash
|
||||
.UE \c
|
||||
.IP \[bu] 2
|
||||
\c
|
||||
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.fish
|
||||
.UE \c
|
||||
.IP \[bu] 2
|
||||
\c
|
||||
.UR https://github.com/cheat/cheat/blob/master/scripts/cheat.zsh
|
||||
.UE \c
|
||||
.PP
|
||||
The \f[B]bash\f[R] and \f[B]zsh\f[R] scripts provide optional
|
||||
integration with \f[B]fzf\f[R], if the latter is available on your
|
||||
\f[B]PATH\f[R].
|
||||
Autocompletion scripts for \f[B]bash\f[], \f[B]zsh\f[], and
|
||||
\f[B]fish\f[] are available for download:
|
||||
.IP \[bu] 2
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.bash>
|
||||
.IP \[bu] 2
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.fish>
|
||||
.IP \[bu] 2
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.zsh>
|
||||
.PP
|
||||
The \f[B]bash\f[] and \f[B]zsh\f[] scripts provide optional integration
|
||||
with \f[B]fzf\f[], if the latter is available on your \f[B]PATH\f[].
|
||||
.PP
|
||||
The installation process will vary per system and shell configuration,
|
||||
and thus will not be discussed here.
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
\f[B]CHEAT_CONFIG_PATH\f[R]
|
||||
.B \f[B]CHEAT_CONFIG_PATH\f[]
|
||||
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[] is set, all other config paths will be
|
||||
ignored.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
\f[B]CHEAT_USE_FZF\f[R]
|
||||
.B \f[B]CHEAT_USE_FZF\f[]
|
||||
If set, autocompletion scripts will attempt to integrate with
|
||||
\f[B]fzf\f[R].
|
||||
\f[B]fzf\f[].
|
||||
.RS
|
||||
.RE
|
||||
.SH RETURN VALUES
|
||||
.IP "0." 3
|
||||
Successful termination
|
||||
@@ -171,12 +211,11 @@ Application error
|
||||
.IP "2." 3
|
||||
Cheatsheet(s) not found
|
||||
.SH BUGS
|
||||
See GitHub issues: \c
|
||||
.UR https://github.com/cheat/cheat/issues
|
||||
.UE \c
|
||||
.PP
|
||||
See GitHub issues: <https://github.com/cheat/cheat/issues>
|
||||
.SH AUTHOR
|
||||
Christopher Allen Lane \c
|
||||
.MT chris@chris-allen-lane.com
|
||||
.ME \c
|
||||
.PP
|
||||
Christopher Allen Lane <chris@chris-allen-lane.com>
|
||||
.SH SEE ALSO
|
||||
\f[B]fzf(1)\f[R]
|
||||
.PP
|
||||
\f[B]fzf(1)\f[]
|
||||
|
||||
@@ -23,12 +23,6 @@ OPTIONS
|
||||
--init
|
||||
: Print a config file to stdout.
|
||||
|
||||
--conf
|
||||
: Display the config file path.
|
||||
|
||||
-a, --all
|
||||
: Search among all cheatpaths.
|
||||
|
||||
-c, --colorize
|
||||
: Colorize output.
|
||||
|
||||
@@ -99,9 +93,6 @@ To search (by regex) for cheatsheets that contain an IP address:
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
: cheat --rm _foo/bar_
|
||||
|
||||
To view the configuration file path:
|
||||
: cheat --conf
|
||||
|
||||
|
||||
FILES
|
||||
=====
|
||||
|
||||
39
go.mod
39
go.mod
@@ -1,38 +1,17 @@
|
||||
module github.com/cheat/cheat
|
||||
|
||||
go 1.19
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.12.0
|
||||
github.com/alecthomas/chroma v0.8.0
|
||||
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.11.0
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.1 // indirect
|
||||
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
)
|
||||
|
||||
167
go.sum
167
go.sum
@@ -1,143 +1,64 @@
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
|
||||
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
|
||||
github.com/alecthomas/chroma v0.8.0 h1:HS+HE97sgcqjQGu5uVr8jIE55Mmh5UeQ7kckAhHg2pY=
|
||||
github.com/alecthomas/chroma v0.8.0/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
|
||||
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
||||
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
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 v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
|
||||
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
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/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
||||
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
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.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
// Package cheatpath implements functions pertaining to cheatsheet file path
|
||||
// management.
|
||||
package cheatpath
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Cheatpath encapsulates cheatsheet path information
|
||||
type Cheatpath struct {
|
||||
Name string `yaml:"name"`
|
||||
@@ -11,18 +7,3 @@ type Cheatpath struct {
|
||||
ReadOnly bool `yaml:"readonly"`
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
package cheatpath
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheatpathValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cheatpath Cheatpath
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid cheatpath",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "personal",
|
||||
Path: "/home/user/.config/cheat/personal",
|
||||
ReadOnly: false,
|
||||
Tags: []string{"personal"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "",
|
||||
Path: "/home/user/.config/cheat/personal",
|
||||
ReadOnly: false,
|
||||
Tags: []string{"personal"},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "cheatpath name cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "personal",
|
||||
Path: "",
|
||||
ReadOnly: false,
|
||||
Tags: []string{"personal"},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "cheatpath path cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "both empty",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "",
|
||||
Path: "",
|
||||
ReadOnly: true,
|
||||
Tags: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "cheatpath name cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "minimal valid",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "x",
|
||||
Path: "/",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "with readonly and tags",
|
||||
cheatpath: Cheatpath{
|
||||
Name: "community",
|
||||
Path: "/usr/share/cheat",
|
||||
ReadOnly: true,
|
||||
Tags: []string{"community", "shared", "readonly"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cheatpath.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("Validate() error = %v, want error containing %q", err, tt.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheatpathStruct(t *testing.T) {
|
||||
// Test that the struct fields work as expected
|
||||
cp := Cheatpath{
|
||||
Name: "test",
|
||||
Path: "/test/path",
|
||||
ReadOnly: true,
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
}
|
||||
|
||||
if cp.Name != "test" {
|
||||
t.Errorf("expected Name to be 'test', got %q", cp.Name)
|
||||
}
|
||||
if cp.Path != "/test/path" {
|
||||
t.Errorf("expected Path to be '/test/path', got %q", cp.Path)
|
||||
}
|
||||
if !cp.ReadOnly {
|
||||
t.Error("expected ReadOnly to be true")
|
||||
}
|
||||
if len(cp.Tags) != 2 || cp.Tags[0] != "tag1" || cp.Tags[1] != "tag2" {
|
||||
t.Errorf("expected Tags to be [tag1 tag2], got %v", cp.Tags)
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Package cheatpath manages collections of cheat sheets organized in filesystem directories.
|
||||
//
|
||||
// A Cheatpath represents a directory containing cheat sheets, with associated
|
||||
// metadata such as tags and read-only status. Multiple cheatpaths can be
|
||||
// configured to organize sheets from different sources (personal, community, work, etc.).
|
||||
//
|
||||
// # Cheatpath Structure
|
||||
//
|
||||
// Each cheatpath has:
|
||||
// - Name: A friendly identifier (e.g., "personal", "community")
|
||||
// - Path: The filesystem path to the directory
|
||||
// - Tags: Tags automatically applied to all sheets in this path
|
||||
// - ReadOnly: Whether sheets in this path can be modified
|
||||
//
|
||||
// Example configuration:
|
||||
//
|
||||
// cheatpaths:
|
||||
// - name: personal
|
||||
// path: ~/cheat
|
||||
// tags: []
|
||||
// readonly: false
|
||||
// - name: community
|
||||
// path: ~/cheat/community
|
||||
// tags: [community]
|
||||
// readonly: true
|
||||
//
|
||||
// # Directory-Scoped Cheatpaths
|
||||
//
|
||||
// The package supports directory-scoped cheatpaths via `.cheat` directories.
|
||||
// When running cheat from a directory containing a `.cheat` subdirectory,
|
||||
// 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
|
||||
@@ -46,7 +46,7 @@ func TestFilterFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
// filter the paths
|
||||
_, err := Filter(paths, "qux")
|
||||
paths, err := Filter(paths, "qux")
|
||||
if err == nil {
|
||||
t.Errorf("failed to return an error on non-existent cheatpath")
|
||||
}
|
||||
|
||||
@@ -2,38 +2,16 @@ package cheatpath
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidateSheetName ensures that a cheatsheet name does not contain
|
||||
// directory traversal sequences or other potentially dangerous patterns.
|
||||
func ValidateSheetName(name string) error {
|
||||
// Reject empty names
|
||||
if name == "" {
|
||||
return fmt.Errorf("cheatsheet name cannot be empty")
|
||||
}
|
||||
// Validate returns an error if the cheatpath is invalid
|
||||
func (c *Cheatpath) Validate() error {
|
||||
|
||||
// Reject names containing directory traversal
|
||||
if strings.Contains(name, "..") {
|
||||
return fmt.Errorf("cheatsheet name cannot contain '..'")
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("invalid cheatpath: name must be specified")
|
||||
}
|
||||
|
||||
// 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)")
|
||||
if c.Path == "" {
|
||||
return fmt.Errorf("invalid cheatpath: path must be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
package cheatpath
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// FuzzValidateSheetName tests the ValidateSheetName function with fuzzing
|
||||
// to ensure it properly prevents path traversal and other security issues
|
||||
func FuzzValidateSheetName(f *testing.F) {
|
||||
// Add seed corpus with various valid and malicious inputs
|
||||
// Valid names
|
||||
f.Add("docker")
|
||||
f.Add("docker/compose")
|
||||
f.Add("lang/go/slice")
|
||||
f.Add("my-cheat_sheet")
|
||||
f.Add("file.txt")
|
||||
f.Add("a")
|
||||
f.Add("123")
|
||||
|
||||
// Path traversal attempts
|
||||
f.Add("..")
|
||||
f.Add("../etc/passwd")
|
||||
f.Add("foo/../bar")
|
||||
f.Add("foo/../../etc/passwd")
|
||||
f.Add("..\\windows\\system32")
|
||||
f.Add("foo\\..\\..\\windows")
|
||||
|
||||
// Encoded traversal attempts
|
||||
f.Add("%2e%2e")
|
||||
f.Add("%2e%2e%2f")
|
||||
f.Add("..%2f")
|
||||
f.Add("%2e.")
|
||||
f.Add(".%2e")
|
||||
f.Add("\x2e\x2e")
|
||||
f.Add("\\x2e\\x2e")
|
||||
|
||||
// Unicode and special characters
|
||||
f.Add("€test")
|
||||
f.Add("test€")
|
||||
f.Add("中文")
|
||||
f.Add("🎉emoji")
|
||||
f.Add("\x00null")
|
||||
f.Add("test\x00null")
|
||||
f.Add("\nnewline")
|
||||
f.Add("test\ttab")
|
||||
|
||||
// Absolute paths
|
||||
f.Add("/etc/passwd")
|
||||
f.Add("C:\\Windows\\System32")
|
||||
f.Add("\\\\server\\share")
|
||||
f.Add("//server/share")
|
||||
|
||||
// Home directory
|
||||
f.Add("~")
|
||||
f.Add("~/config")
|
||||
f.Add("~user/file")
|
||||
|
||||
// Hidden files
|
||||
f.Add(".hidden")
|
||||
f.Add("dir/.hidden")
|
||||
f.Add(".git/config")
|
||||
|
||||
// Edge cases
|
||||
f.Add("")
|
||||
f.Add(" ")
|
||||
f.Add(" ")
|
||||
f.Add("\t")
|
||||
f.Add(".")
|
||||
f.Add("./")
|
||||
f.Add("./file")
|
||||
f.Add(".../")
|
||||
f.Add("...")
|
||||
f.Add("....")
|
||||
|
||||
// Very long names
|
||||
f.Add(strings.Repeat("a", 255))
|
||||
f.Add(strings.Repeat("a/", 100) + "file")
|
||||
f.Add(strings.Repeat("../", 50) + "etc/passwd")
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
// The function should never panic
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ValidateSheetName panicked with input %q: %v", input, r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := ValidateSheetName(input)
|
||||
|
||||
// Security invariants that must always hold
|
||||
if err == nil {
|
||||
// If validation passed, verify security properties
|
||||
|
||||
// Should not contain ".." for path traversal
|
||||
if strings.Contains(input, "..") {
|
||||
t.Errorf("validation passed but input contains '..': %q", input)
|
||||
}
|
||||
|
||||
// Should not be empty
|
||||
if input == "" {
|
||||
t.Error("validation passed for empty input")
|
||||
}
|
||||
|
||||
// Should not start with ~ (home directory)
|
||||
if strings.HasPrefix(input, "~") {
|
||||
t.Errorf("validation passed but input starts with '~': %q", input)
|
||||
}
|
||||
|
||||
// Base filename should not start with .
|
||||
parts := strings.Split(input, "/")
|
||||
if len(parts) > 0 {
|
||||
lastPart := parts[len(parts)-1]
|
||||
if strings.HasPrefix(lastPart, ".") && lastPart != "." {
|
||||
t.Errorf("validation passed but filename starts with '.': %q", input)
|
||||
}
|
||||
}
|
||||
|
||||
// Additional check: result should be valid UTF-8
|
||||
if !utf8.ValidString(input) {
|
||||
// While the function doesn't explicitly check this,
|
||||
// we want to ensure it handles invalid UTF-8 gracefully
|
||||
t.Logf("validation passed for invalid UTF-8: %q", input)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzValidateSheetNamePathTraversal specifically targets path traversal bypasses
|
||||
func FuzzValidateSheetNamePathTraversal(f *testing.F) {
|
||||
// Seed corpus focusing on path traversal variations
|
||||
f.Add("..", "/", "")
|
||||
f.Add("", "..", "/")
|
||||
f.Add("a", "b", "c")
|
||||
|
||||
f.Fuzz(func(t *testing.T, prefix string, middle string, suffix string) {
|
||||
// Construct various path traversal attempts
|
||||
inputs := []string{
|
||||
prefix + ".." + suffix,
|
||||
prefix + "/.." + suffix,
|
||||
prefix + "\\.." + suffix,
|
||||
prefix + middle + ".." + suffix,
|
||||
prefix + "../" + middle + suffix,
|
||||
prefix + "..%2f" + suffix,
|
||||
prefix + "%2e%2e" + suffix,
|
||||
prefix + "%2e%2e%2f" + suffix,
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ValidateSheetName panicked with constructed input %q: %v", input, r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := ValidateSheetName(input)
|
||||
|
||||
// If the input contains literal "..", it must be rejected
|
||||
if strings.Contains(input, "..") && err == nil {
|
||||
t.Errorf("validation incorrectly passed for input containing '..': %q", input)
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,106 +1,56 @@
|
||||
package cheatpath
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateSheetName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// Valid names
|
||||
{
|
||||
name: "simple name",
|
||||
input: "docker",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with slash",
|
||||
input: "docker/compose",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with multiple slashes",
|
||||
input: "lang/go/slice",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name with dash and underscore",
|
||||
input: "my-cheat_sheet",
|
||||
wantErr: false,
|
||||
},
|
||||
// Invalid names
|
||||
{
|
||||
name: "empty name",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
errMsg: "empty",
|
||||
},
|
||||
{
|
||||
name: "parent directory traversal",
|
||||
input: "../etc/passwd",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "complex traversal",
|
||||
input: "foo/../../etc/passwd",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "absolute path",
|
||||
input: "/etc/passwd",
|
||||
wantErr: true,
|
||||
errMsg: "absolute",
|
||||
},
|
||||
{
|
||||
name: "home directory",
|
||||
input: "~/secrets",
|
||||
wantErr: true,
|
||||
errMsg: "'~'",
|
||||
},
|
||||
{
|
||||
name: "just dots",
|
||||
input: "..",
|
||||
wantErr: true,
|
||||
errMsg: "'..'",
|
||||
},
|
||||
{
|
||||
name: "hidden file not allowed",
|
||||
input: ".hidden",
|
||||
wantErr: true,
|
||||
errMsg: "cannot start with '.'",
|
||||
},
|
||||
{
|
||||
name: "current dir is ok",
|
||||
input: "./current",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nested hidden file not allowed",
|
||||
input: "config/.gitignore",
|
||||
wantErr: true,
|
||||
errMsg: "cannot start with '.'",
|
||||
},
|
||||
// TestValidateValid asserts that valid cheatpaths validate successfully
|
||||
func TestValidateValid(t *testing.T) {
|
||||
|
||||
// initialize a valid cheatpath
|
||||
cheatpath := Cheatpath{
|
||||
Name: "foo",
|
||||
Path: "/foo",
|
||||
ReadOnly: false,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateSheetName(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" {
|
||||
if !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("ValidateName(%q) error = %v, want error containing %q", tt.input, err, tt.errMsg)
|
||||
}
|
||||
}
|
||||
})
|
||||
// assert that no errors are returned
|
||||
if err := cheatpath.Validate(); err != nil {
|
||||
t.Errorf("failed to validate valid cheatpath: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMissingName asserts that paths that are missing a name fail to
|
||||
// validate
|
||||
func TestValidateMissingName(t *testing.T) {
|
||||
|
||||
// initialize a valid cheatpath
|
||||
cheatpath := Cheatpath{
|
||||
Path: "/foo",
|
||||
ReadOnly: false,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
// assert that no errors are returned
|
||||
if err := cheatpath.Validate(); err == nil {
|
||||
t.Errorf("failed to invalidate cheatpath without name")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMissingPath asserts that paths that are missing a path fail to
|
||||
// validate
|
||||
func TestValidateMissingPath(t *testing.T) {
|
||||
|
||||
// initialize a valid cheatpath
|
||||
cheatpath := Cheatpath{
|
||||
Name: "foo",
|
||||
ReadOnly: false,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
// assert that no errors are returned
|
||||
if err := cheatpath.Validate(); err == nil {
|
||||
t.Errorf("failed to invalidate cheatpath without path")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ func Writeable(cheatpaths []Cheatpath) (Cheatpath, error) {
|
||||
// NB: we're going backwards because we assume that the most "local"
|
||||
// cheatpath will be specified last in the configs
|
||||
for i := len(cheatpaths) - 1; i >= 0; i-- {
|
||||
|
||||
// if the cheatpath is not read-only, it is writeable, and thus returned
|
||||
if !cheatpaths[i].ReadOnly {
|
||||
if cheatpaths[i].ReadOnly == false {
|
||||
return cheatpaths[i], nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// otherwise, return an error
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestColor asserts that colorization rules are properly respected
|
||||
func TestColor(t *testing.T) {
|
||||
|
||||
// mock a config
|
||||
conf := Config{}
|
||||
|
||||
opts := map[string]interface{}{"--colorize": false}
|
||||
if conf.Color(opts) {
|
||||
t.Errorf("failed to respect --colorize (false)")
|
||||
}
|
||||
|
||||
opts = map[string]interface{}{"--colorize": true}
|
||||
if !conf.Color(opts) {
|
||||
t.Errorf("failed to respect --colorize (true)")
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// Package config implements functions pertaining to configuration management.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
cp "github.com/cheat/cheat/internal/cheatpath"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Config encapsulates configuration parameters
|
||||
@@ -21,14 +21,13 @@ type Config struct {
|
||||
Style string `yaml:"style"`
|
||||
Formatter string `yaml:"formatter"`
|
||||
Pager string `yaml:"pager"`
|
||||
Path string
|
||||
}
|
||||
|
||||
// New returns a new Config struct
|
||||
func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error) {
|
||||
func New(opts map[string]interface{}, confPath string, resolve bool) (Config, error) {
|
||||
|
||||
// read the config file
|
||||
buf, err := os.ReadFile(confPath)
|
||||
buf, err := ioutil.ReadFile(confPath)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("could not read config file: %v", err)
|
||||
}
|
||||
@@ -36,11 +35,8 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
|
||||
// initialize a config object
|
||||
conf := Config{}
|
||||
|
||||
// store the config path
|
||||
conf.Path = confPath
|
||||
|
||||
// unmarshal the yaml
|
||||
err = yaml.Unmarshal(buf, &conf)
|
||||
err = yaml.UnmarshalStrict(buf, &conf)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("could not unmarshal yaml: %v", err)
|
||||
}
|
||||
@@ -96,14 +92,14 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
|
||||
conf.Cheatpaths[i].Path = expanded
|
||||
}
|
||||
|
||||
// trim editor whitespace
|
||||
conf.Editor = strings.TrimSpace(conf.Editor)
|
||||
|
||||
// if an editor was not provided in the configs, attempt to choose one
|
||||
// that's appropriate for the environment
|
||||
// if an editor was not provided in the configs, look to envvars
|
||||
if conf.Editor == "" {
|
||||
if conf.Editor, err = Editor(); err != nil {
|
||||
return Config{}, err
|
||||
if os.Getenv("VISUAL") != "" {
|
||||
conf.Editor = os.Getenv("VISUAL")
|
||||
} else if os.Getenv("EDITOR") != "" {
|
||||
conf.Editor = os.Getenv("EDITOR")
|
||||
} else {
|
||||
return Config{}, fmt.Errorf("no editor set")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,11 +110,13 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
|
||||
|
||||
// if a chroma formatter was not provided, set a default
|
||||
if conf.Formatter == "" {
|
||||
conf.Formatter = "terminal"
|
||||
conf.Formatter = "terminal16m"
|
||||
}
|
||||
|
||||
// load the pager
|
||||
conf.Pager = strings.TrimSpace(conf.Pager)
|
||||
// if a pager was not provided, set a default
|
||||
if strings.TrimSpace(conf.Pager) == "" {
|
||||
conf.Pager = ""
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/mock"
|
||||
)
|
||||
|
||||
// TestConfigYAMLErrors tests YAML parsing errors
|
||||
func TestConfigYAMLErrors(t *testing.T) {
|
||||
// Create a temporary file with invalid YAML
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
invalidYAML := filepath.Join(tempDir, "invalid.yml")
|
||||
err = os.WriteFile(invalidYAML, []byte("invalid: yaml: content:\n - no closing"), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write invalid yaml: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to load invalid YAML
|
||||
_, err = New(map[string]interface{}{}, invalidYAML, false)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid YAML, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigLocalCheatpath tests local .cheat directory detection
|
||||
func TestConfigLocalCheatpath(t *testing.T) {
|
||||
// Create a temporary directory to act as working directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Save current working directory
|
||||
oldCwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get cwd: %v", err)
|
||||
}
|
||||
defer os.Chdir(oldCwd)
|
||||
|
||||
// Change to temp directory
|
||||
err = os.Chdir(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to change dir: %v", err)
|
||||
}
|
||||
|
||||
// Create .cheat directory
|
||||
localCheat := filepath.Join(tempDir, ".cheat")
|
||||
err = os.Mkdir(localCheat, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
// Load config
|
||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Check that local cheatpath was added
|
||||
found := false
|
||||
for _, cp := range conf.Cheatpaths {
|
||||
if cp.Name == "cwd" && cp.Path == localCheat {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("local .cheat directory was not added to cheatpaths")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigDefaults tests default values
|
||||
func TestConfigDefaults(t *testing.T) {
|
||||
// Load empty config
|
||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Check defaults
|
||||
if conf.Style != "bw" {
|
||||
t.Errorf("expected default style 'bw', got %s", conf.Style)
|
||||
}
|
||||
|
||||
if conf.Formatter != "terminal" {
|
||||
t.Errorf("expected default formatter 'terminal', got %s", conf.Formatter)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigSymlinkResolution tests symlink resolution
|
||||
func TestConfigSymlinkResolution(t *testing.T) {
|
||||
// Create temp directory structure
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create target directory
|
||||
targetDir := filepath.Join(tempDir, "target")
|
||||
err = os.Mkdir(targetDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create target dir: %v", err)
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
linkPath := filepath.Join(tempDir, "link")
|
||||
err = os.Symlink(targetDir, linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create symlink: %v", err)
|
||||
}
|
||||
|
||||
// Create config with symlink path
|
||||
configContent := `---
|
||||
editor: vim
|
||||
cheatpaths:
|
||||
- name: test
|
||||
path: ` + linkPath + `
|
||||
readonly: true
|
||||
`
|
||||
configFile := filepath.Join(tempDir, "config.yml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Load config with symlink resolution
|
||||
conf, err := New(map[string]interface{}{}, configFile, true)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Verify symlink was resolved
|
||||
if len(conf.Cheatpaths) > 0 && conf.Cheatpaths[0].Path != targetDir {
|
||||
t.Errorf("expected symlink to be resolved to %s, got %s", targetDir, conf.Cheatpaths[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigBrokenSymlink tests broken symlink handling
|
||||
func TestConfigBrokenSymlink(t *testing.T) {
|
||||
// Create temp directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create broken symlink
|
||||
linkPath := filepath.Join(tempDir, "broken-link")
|
||||
err = os.Symlink("/nonexistent/path", linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create symlink: %v", err)
|
||||
}
|
||||
|
||||
// Create config with broken symlink
|
||||
configContent := `---
|
||||
editor: vim
|
||||
cheatpaths:
|
||||
- name: test
|
||||
path: ` + linkPath + `
|
||||
readonly: true
|
||||
`
|
||||
configFile := filepath.Join(tempDir, "config.yml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Load config with symlink resolution should fail
|
||||
_, err = New(map[string]interface{}{}, configFile, true)
|
||||
if err == nil {
|
||||
t.Error("expected error for broken symlink, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigTildeExpansionError tests tilde expansion error handling
|
||||
func TestConfigTildeExpansionError(t *testing.T) {
|
||||
// This is tricky to test without mocking homedir.Expand
|
||||
// We'll create a config with an invalid home reference
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create config with user that likely doesn't exist
|
||||
configContent := `---
|
||||
editor: vim
|
||||
cheatpaths:
|
||||
- name: test
|
||||
path: ~nonexistentuser12345/cheat
|
||||
readonly: true
|
||||
`
|
||||
configFile := filepath.Join(tempDir, "config.yml")
|
||||
err = os.WriteFile(configFile, []byte(configContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
// Load config - this may or may not fail depending on the system
|
||||
// but we're testing that it doesn't panic
|
||||
_, _ = New(map[string]interface{}{}, configFile, false)
|
||||
}
|
||||
|
||||
// TestConfigGetCwdError tests error handling when os.Getwd fails
|
||||
func TestConfigGetCwdError(t *testing.T) {
|
||||
// This is difficult to test without being able to break os.Getwd
|
||||
// We'll create a scenario where the current directory is removed
|
||||
|
||||
// Create and enter a temp directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
oldCwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get cwd: %v", err)
|
||||
}
|
||||
defer os.Chdir(oldCwd)
|
||||
|
||||
err = os.Chdir(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to change dir: %v", err)
|
||||
}
|
||||
|
||||
// Remove the directory we're in
|
||||
err = os.RemoveAll(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to remove temp dir: %v", err)
|
||||
}
|
||||
|
||||
// Now os.Getwd should fail
|
||||
_, err = New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
||||
// This might not fail on all systems, so we just ensure no panic
|
||||
_ = err
|
||||
}
|
||||
@@ -39,17 +39,17 @@ func TestConfigSuccessful(t *testing.T) {
|
||||
// assert that the cheatpaths are correct
|
||||
want := []cheatpath.Cheatpath{
|
||||
cheatpath.Cheatpath{
|
||||
Path: filepath.Join(home, ".dotfiles", "cheat", "community"),
|
||||
Path: filepath.Join(home, ".dotfiles/cheat/community"),
|
||||
ReadOnly: true,
|
||||
Tags: []string{"community"},
|
||||
},
|
||||
cheatpath.Cheatpath{
|
||||
Path: filepath.Join(home, ".dotfiles", "cheat", "work"),
|
||||
Path: filepath.Join(home, ".dotfiles/cheat/work"),
|
||||
ReadOnly: false,
|
||||
Tags: []string{"work"},
|
||||
},
|
||||
cheatpath.Cheatpath{
|
||||
Path: filepath.Join(home, ".dotfiles", "cheat", "personal"),
|
||||
Path: filepath.Join(home, ".dotfiles/cheat/personal"),
|
||||
ReadOnly: false,
|
||||
Tags: []string{"personal"},
|
||||
},
|
||||
@@ -85,8 +85,8 @@ func TestEmptyEditor(t *testing.T) {
|
||||
|
||||
// initialize a config
|
||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialize test: %v", err)
|
||||
if err == nil {
|
||||
t.Errorf("failed to return an error on empty editor")
|
||||
}
|
||||
|
||||
// set editor, and assert that it is respected
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Package config manages application configuration and settings.
|
||||
//
|
||||
// The config package provides functionality to:
|
||||
// - Load configuration from YAML files
|
||||
// - Validate configuration values
|
||||
// - Manage platform-specific configuration paths
|
||||
// - Handle editor and pager settings
|
||||
// - Configure colorization and formatting options
|
||||
//
|
||||
// # Configuration Structure
|
||||
//
|
||||
// The main configuration file (conf.yml) contains:
|
||||
// - Editor preferences
|
||||
// - Pager settings
|
||||
// - Colorization options
|
||||
// - Cheatpath definitions
|
||||
// - Formatting preferences
|
||||
//
|
||||
// Example configuration:
|
||||
//
|
||||
// ---
|
||||
// editor: vim
|
||||
// colorize: true
|
||||
// style: monokai
|
||||
// formatter: terminal256
|
||||
// pager: less -FRX
|
||||
// cheatpaths:
|
||||
// - name: personal
|
||||
// path: ~/cheat
|
||||
// tags: []
|
||||
// readonly: false
|
||||
// - name: community
|
||||
// path: ~/cheat/.cheat
|
||||
// tags: [community]
|
||||
// readonly: true
|
||||
//
|
||||
// # Platform-Specific Paths
|
||||
//
|
||||
// The package automatically detects configuration paths based on the operating system:
|
||||
// - Linux/Unix: $XDG_CONFIG_HOME/cheat/conf.yml or ~/.config/cheat/conf.yml
|
||||
// - macOS: ~/Library/Application Support/cheat/conf.yml
|
||||
// - Windows: %APPDATA%\cheat\conf.yml
|
||||
//
|
||||
// # Environment Variables
|
||||
//
|
||||
// The following environment variables are respected:
|
||||
// - CHEAT_CONFIG_PATH: Override the configuration file location
|
||||
// - CHEAT_USE_FZF: Enable fzf integration when set to "true"
|
||||
// - EDITOR: Default editor if not specified in config
|
||||
// - VISUAL: Fallback editor if EDITOR is not set
|
||||
// - PAGER: Default pager if not specified in config
|
||||
package config
|
||||
@@ -1,41 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Editor attempts to locate an editor that's appropriate for the environment.
|
||||
func Editor() (string, error) {
|
||||
|
||||
// default to `notepad.exe` on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
return "notepad", nil
|
||||
}
|
||||
|
||||
// look for `nano` and `vim` on the `PATH`
|
||||
def, _ := exec.LookPath("editor") // default `editor` wrapper
|
||||
nano, _ := exec.LookPath("nano")
|
||||
vim, _ := exec.LookPath("vim")
|
||||
|
||||
// set editor priority
|
||||
editors := []string{
|
||||
os.Getenv("VISUAL"),
|
||||
os.Getenv("EDITOR"),
|
||||
def,
|
||||
nano,
|
||||
vim,
|
||||
}
|
||||
|
||||
// return the first editor that was found per the priority above
|
||||
for _, editor := range editors {
|
||||
if editor != "" {
|
||||
return editor, nil
|
||||
}
|
||||
}
|
||||
|
||||
// return an error if no path is found
|
||||
return "", fmt.Errorf("no editor set")
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEditor tests the Editor function
|
||||
func TestEditor(t *testing.T) {
|
||||
// Save original env vars
|
||||
oldVisual := os.Getenv("VISUAL")
|
||||
oldEditor := os.Getenv("EDITOR")
|
||||
defer func() {
|
||||
os.Setenv("VISUAL", oldVisual)
|
||||
os.Setenv("EDITOR", oldEditor)
|
||||
}()
|
||||
|
||||
t.Run("windows default", func(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("skipping windows test on non-windows platform")
|
||||
}
|
||||
|
||||
// Clear env vars
|
||||
os.Setenv("VISUAL", "")
|
||||
os.Setenv("EDITOR", "")
|
||||
|
||||
editor, err := Editor()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if editor != "notepad" {
|
||||
t.Errorf("expected 'notepad' on windows, got %s", editor)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("VISUAL takes precedence", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("VISUAL", "emacs")
|
||||
os.Setenv("EDITOR", "nano")
|
||||
|
||||
editor, err := Editor()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if editor != "emacs" {
|
||||
t.Errorf("expected VISUAL to take precedence, got %s", editor)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EDITOR when no VISUAL", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("VISUAL", "")
|
||||
os.Setenv("EDITOR", "vim")
|
||||
|
||||
editor, err := Editor()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if editor != "vim" {
|
||||
t.Errorf("expected EDITOR value, got %s", editor)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no editor found error", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
// Clear all environment variables
|
||||
os.Setenv("VISUAL", "")
|
||||
os.Setenv("EDITOR", "")
|
||||
|
||||
// Create a custom PATH that doesn't include common editors
|
||||
oldPath := os.Getenv("PATH")
|
||||
defer os.Setenv("PATH", oldPath)
|
||||
|
||||
// Set a very limited PATH that won't have editors
|
||||
os.Setenv("PATH", "/nonexistent")
|
||||
|
||||
editor, err := Editor()
|
||||
|
||||
// If we found an editor, it's likely in the system
|
||||
// This test might not always produce an error on systems with editors
|
||||
if editor == "" && err == nil {
|
||||
t.Error("expected error when no editor found")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
@@ -15,7 +16,7 @@ func Init(confpath string, configs string) error {
|
||||
}
|
||||
|
||||
// write the config file
|
||||
if err := os.WriteFile(confpath, []byte(configs), 0644); err != nil {
|
||||
if err := ioutil.WriteFile(confpath, []byte(configs), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestInit asserts that configs are properly initialized
|
||||
func TestInit(t *testing.T) {
|
||||
|
||||
// initialize a temporary config file
|
||||
confFile, err := os.CreateTemp("", "cheat-test")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
// clean up the temp file
|
||||
defer os.Remove(confFile.Name())
|
||||
|
||||
// initialize the config file
|
||||
conf := "mock config data"
|
||||
if err = Init(confFile.Name(), conf); err != nil {
|
||||
t.Errorf("failed to init config file: %v", err)
|
||||
}
|
||||
|
||||
// read back the config file contents
|
||||
bytes, err := os.ReadFile(confFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
// assert that the contents were written correctly
|
||||
got := string(bytes)
|
||||
if got != conf {
|
||||
t.Errorf("failed to write configs: want: %s, got: %s", conf, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitCreateDirectory tests that Init creates the directory if it doesn't exist
|
||||
func TestInitCreateDirectory(t *testing.T) {
|
||||
// Create a temp directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-init-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Path to a config file in a non-existent subdirectory
|
||||
confPath := filepath.Join(tempDir, "subdir", "conf.yml")
|
||||
|
||||
// Initialize the config file
|
||||
conf := "test config"
|
||||
if err = Init(confPath, conf); err != nil {
|
||||
t.Errorf("failed to init config file: %v", err)
|
||||
}
|
||||
|
||||
// Verify the directory was created
|
||||
if _, err := os.Stat(filepath.Dir(confPath)); os.IsNotExist(err) {
|
||||
t.Error("Init did not create the directory")
|
||||
}
|
||||
|
||||
// Verify the file was created with correct content
|
||||
bytes, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
if string(bytes) != conf {
|
||||
t.Errorf("config content mismatch: got %q, want %q", string(bytes), conf)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitWriteError tests error handling when file write fails
|
||||
func TestInitWriteError(t *testing.T) {
|
||||
// Skip this test if running as root (can write anywhere)
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("Cannot test write errors as root")
|
||||
}
|
||||
|
||||
// Try to write to a read-only directory
|
||||
err := Init("/dev/null/impossible/path/conf.yml", "test")
|
||||
if err == nil {
|
||||
t.Error("expected error when writing to invalid path, got nil")
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), "failed to create") {
|
||||
t.Errorf("expected 'failed to create' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInitExistingFile tests that Init overwrites existing files
|
||||
func TestInitExistingFile(t *testing.T) {
|
||||
// Create a temp file
|
||||
tempFile, err := os.CreateTemp("", "cheat-init-existing-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
// Write initial content
|
||||
initialContent := "initial content"
|
||||
if err := os.WriteFile(tempFile.Name(), []byte(initialContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write initial content: %v", err)
|
||||
}
|
||||
|
||||
// Initialize with new content
|
||||
newContent := "new config content"
|
||||
if err = Init(tempFile.Name(), newContent); err != nil {
|
||||
t.Errorf("failed to init over existing file: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was overwritten
|
||||
bytes, err := os.ReadFile(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
if string(bytes) != newContent {
|
||||
t.Errorf("config not overwritten: got %q, want %q", string(bytes), newContent)
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewTrimsWhitespace(t *testing.T) {
|
||||
// Create a temporary config file with whitespace in editor and pager
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
configContent := `---
|
||||
editor: " vim -c 'set number' "
|
||||
pager: " less -R "
|
||||
style: monokai
|
||||
formatter: terminal
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: ~/cheat
|
||||
tags: []
|
||||
readonly: false
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Load the config
|
||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Verify editor is trimmed
|
||||
expectedEditor := "vim -c 'set number'"
|
||||
if conf.Editor != expectedEditor {
|
||||
t.Errorf("editor not properly trimmed: got %q, want %q", conf.Editor, expectedEditor)
|
||||
}
|
||||
|
||||
// Verify pager is trimmed
|
||||
expectedPager := "less -R"
|
||||
if conf.Pager != expectedPager {
|
||||
t.Errorf("pager not properly trimmed: got %q, want %q", conf.Pager, expectedPager)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEmptyEditorFallback(t *testing.T) {
|
||||
// Skip if required environment variables would interfere
|
||||
oldVisual := os.Getenv("VISUAL")
|
||||
oldEditor := os.Getenv("EDITOR")
|
||||
os.Unsetenv("VISUAL")
|
||||
os.Unsetenv("EDITOR")
|
||||
defer func() {
|
||||
os.Setenv("VISUAL", oldVisual)
|
||||
os.Setenv("EDITOR", oldEditor)
|
||||
}()
|
||||
|
||||
// Create a config with whitespace-only editor
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
configContent := `---
|
||||
editor: " "
|
||||
pager: less
|
||||
style: monokai
|
||||
formatter: terminal
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: ~/cheat
|
||||
tags: []
|
||||
readonly: false
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Load the config
|
||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
||||
if err != nil {
|
||||
// It's OK if this fails due to no editor being found
|
||||
// The important thing is it doesn't panic
|
||||
return
|
||||
}
|
||||
|
||||
// If it succeeded, editor should not be empty (fallback was used)
|
||||
if conf.Editor == "" {
|
||||
t.Error("editor should not be empty after fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWhitespaceOnlyPager(t *testing.T) {
|
||||
// Create a config with whitespace-only pager
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yml")
|
||||
|
||||
configContent := `---
|
||||
editor: vim
|
||||
pager: " "
|
||||
style: monokai
|
||||
formatter: terminal
|
||||
cheatpaths:
|
||||
- name: personal
|
||||
path: ~/cheat
|
||||
tags: []
|
||||
readonly: false
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatalf("failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
// Load the config
|
||||
conf, err := New(map[string]interface{}{}, configPath, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Pager should be empty after trimming
|
||||
if conf.Pager != "" {
|
||||
t.Errorf("pager should be empty after trimming whitespace: got %q", conf.Pager)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Pager attempts to locate a pager that's appropriate for the environment.
|
||||
func Pager() string {
|
||||
|
||||
// default to `more` on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
return "more"
|
||||
}
|
||||
|
||||
// if $PAGER is set, return the corresponding pager
|
||||
if os.Getenv("PAGER") != "" {
|
||||
return os.Getenv("PAGER")
|
||||
}
|
||||
|
||||
// Otherwise, search for `pager`, `less`, and `more` on the `$PATH`. If
|
||||
// none are found, return an empty pager.
|
||||
for _, pager := range []string{"pager", "less", "more"} {
|
||||
if path, err := exec.LookPath(pager); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// default to no pager
|
||||
return ""
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPager tests the Pager function
|
||||
func TestPager(t *testing.T) {
|
||||
// Save original env var
|
||||
oldPager := os.Getenv("PAGER")
|
||||
defer os.Setenv("PAGER", oldPager)
|
||||
|
||||
t.Run("windows default", func(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("skipping windows test on non-windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("PAGER", "")
|
||||
pager := Pager()
|
||||
if pager != "more" {
|
||||
t.Errorf("expected 'more' on windows, got %s", pager)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("PAGER env var", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("PAGER", "bat")
|
||||
pager := Pager()
|
||||
if pager != "bat" {
|
||||
t.Errorf("expected PAGER env var value, got %s", pager)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback to system pager", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("PAGER", "")
|
||||
pager := Pager()
|
||||
|
||||
// Should find one of the fallback pagers or return empty string
|
||||
validPagers := map[string]bool{
|
||||
"": true, // no pager found
|
||||
"pager": true,
|
||||
"less": true,
|
||||
"more": true,
|
||||
}
|
||||
|
||||
// Check if it's a path to one of these
|
||||
found := false
|
||||
for p := range validPagers {
|
||||
if p == "" && pager == "" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if p != "" && (pager == p || len(pager) >= len(p) && pager[len(pager)-len(p):] == p) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("unexpected pager value: %s", pager)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no pager available", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping non-windows test on windows platform")
|
||||
}
|
||||
|
||||
os.Setenv("PAGER", "")
|
||||
|
||||
// Save and modify PATH to ensure no pagers are found
|
||||
oldPath := os.Getenv("PATH")
|
||||
defer os.Setenv("PATH", oldPath)
|
||||
os.Setenv("PATH", "/nonexistent")
|
||||
|
||||
pager := Pager()
|
||||
if pager != "" {
|
||||
t.Errorf("expected empty string when no pager found, got %s", pager)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPathConfigNotExists asserts that `Path` identifies non-existent config
|
||||
// files
|
||||
func TestPathConfigNotExists(t *testing.T) {
|
||||
|
||||
// package (invalid) cheatpaths
|
||||
paths := []string{"/cheat-test-conf-does-not-exist"}
|
||||
|
||||
// assert
|
||||
if _, err := Path(paths); err == nil {
|
||||
t.Errorf("failed to identify non-existent config file")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestPathConfigExists asserts that `Path` identifies existent config files
|
||||
func TestPathConfigExists(t *testing.T) {
|
||||
|
||||
// initialize a temporary config file
|
||||
confFile, err := os.CreateTemp("", "cheat-test")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
// clean up the temp file
|
||||
defer os.Remove(confFile.Name())
|
||||
|
||||
// package cheatpaths
|
||||
paths := []string{
|
||||
"/cheat-test-conf-does-not-exist",
|
||||
confFile.Name(),
|
||||
}
|
||||
|
||||
// assert
|
||||
got, err := Path(paths)
|
||||
if err != nil {
|
||||
t.Errorf("failed to identify config file: %v", err)
|
||||
}
|
||||
if got != confFile.Name() {
|
||||
t.Errorf(
|
||||
"failed to return config path: want: %s, got: %s",
|
||||
confFile.Name(),
|
||||
got,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"path"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
@@ -28,30 +28,25 @@ func Paths(
|
||||
}
|
||||
|
||||
switch sys {
|
||||
|
||||
// darwin/linux/unix
|
||||
case "aix", "android", "darwin", "dragonfly", "freebsd", "illumos", "ios",
|
||||
"linux", "netbsd", "openbsd", "plan9", "solaris":
|
||||
case "darwin", "linux", "freebsd":
|
||||
paths := []string{}
|
||||
|
||||
// don't include the `XDG_CONFIG_HOME` path if that envvar is not set
|
||||
if xdgpath, ok := envvars["XDG_CONFIG_HOME"]; ok {
|
||||
paths = append(paths, filepath.Join(xdgpath, "cheat", "conf.yml"))
|
||||
paths = append(paths, path.Join(xdgpath, "/cheat/conf.yml"))
|
||||
}
|
||||
|
||||
paths = append(paths, []string{
|
||||
filepath.Join(home, ".config", "cheat", "conf.yml"),
|
||||
filepath.Join(home, ".cheat", "conf.yml"),
|
||||
path.Join(home, ".config/cheat/conf.yml"),
|
||||
path.Join(home, ".cheat/conf.yml"),
|
||||
"/etc/cheat/conf.yml",
|
||||
}...)
|
||||
|
||||
return paths, nil
|
||||
|
||||
// windows
|
||||
case "windows":
|
||||
return []string{
|
||||
filepath.Join(envvars["APPDATA"], "cheat", "conf.yml"),
|
||||
filepath.Join(envvars["PROGRAMDATA"], "cheat", "conf.yml"),
|
||||
path.Join(envvars["APPDATA"], "/cheat/conf.yml"),
|
||||
path.Join(envvars["PROGRAMDATA"], "/cheat/conf.yml"),
|
||||
}, nil
|
||||
default:
|
||||
return []string{}, fmt.Errorf("unsupported os: %s", sys)
|
||||
|
||||
@@ -21,7 +21,6 @@ func TestValidatePathsNix(t *testing.T) {
|
||||
|
||||
// specify the platforms to test
|
||||
oses := []string{
|
||||
"android",
|
||||
"darwin",
|
||||
"freebsd",
|
||||
"linux",
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// Write writes output either directly to stdout, or through a pager,
|
||||
// Display writes output either directly to stdout, or through a pager,
|
||||
// depending upon configuration.
|
||||
func Write(out string, conf config.Config) {
|
||||
func Display(out string, conf config.Config) {
|
||||
// if no pager was configured, print the output to stdout and exit
|
||||
if conf.Pager == "" {
|
||||
fmt.Print(out)
|
||||
@@ -19,23 +19,19 @@ func Write(out string, conf config.Config) {
|
||||
}
|
||||
|
||||
// otherwise, pipe output through the pager
|
||||
writeToPager(out, conf)
|
||||
}
|
||||
|
||||
// writeToPager writes output through a pager command
|
||||
func writeToPager(out string, conf config.Config) {
|
||||
parts := strings.Split(conf.Pager, " ")
|
||||
pager := parts[0]
|
||||
args := parts[1:]
|
||||
|
||||
// configure the pager
|
||||
// run the pager
|
||||
cmd := exec.Command(pager, args...)
|
||||
cmd.Stdin = strings.NewReader(out)
|
||||
cmd.Stdout = os.Stdout
|
||||
|
||||
// run the pager and handle errors
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to write to pager: %v\n", err)
|
||||
// handle errors
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to write to pager: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Package display handles output formatting and presentation for the cheat application.
|
||||
//
|
||||
// The display package provides utilities for:
|
||||
// - Writing output to stdout or a pager
|
||||
// - Formatting text with indentation
|
||||
// - Creating faint (dimmed) text for de-emphasis
|
||||
// - Managing colored output
|
||||
//
|
||||
// # Pager Integration
|
||||
//
|
||||
// The package integrates with system pagers (less, more, etc.) to handle
|
||||
// long output. If a pager is configured and the output is to a terminal,
|
||||
// content is automatically piped through the pager.
|
||||
//
|
||||
// # Text Formatting
|
||||
//
|
||||
// Various formatting utilities are provided:
|
||||
// - Faint: Creates dimmed text using ANSI escape codes
|
||||
// - Indent: Adds consistent indentation to text blocks
|
||||
// - Write: Intelligent output that uses stdout or pager as appropriate
|
||||
//
|
||||
// Example Usage
|
||||
//
|
||||
// // Write output, using pager if configured
|
||||
// if err := display.Write(output, config); err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Create faint text for de-emphasis
|
||||
// fainted := display.Faint("(read-only)", config)
|
||||
//
|
||||
// // Indent a block of text
|
||||
// indented := display.Indent(text, " ")
|
||||
//
|
||||
// # Color Support
|
||||
//
|
||||
// The package respects the colorization settings from the config.
|
||||
// When colorization is disabled, formatting functions like Faint
|
||||
// return unmodified text.
|
||||
//
|
||||
// # Terminal Detection
|
||||
//
|
||||
// The package uses isatty to detect if output is to a terminal,
|
||||
// which affects decisions about using a pager and applying colors.
|
||||
package display
|
||||
@@ -1,21 +0,0 @@
|
||||
// Package display implement functions pertaining to writing formatted
|
||||
// cheatsheet content to stdout, or alternatively the system pager.
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// Faint returns a faintly-colored string that's used to de-prioritize text
|
||||
// written to stdout
|
||||
func Faint(str string, conf config.Config) string {
|
||||
// make `str` faint only if colorization has been requested
|
||||
if conf.Colorize {
|
||||
return fmt.Sprintf("\033[2m%s\033[0m", str)
|
||||
}
|
||||
|
||||
// otherwise, return the string unmodified
|
||||
return str
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// TestFaint asserts that Faint applies faint formatting
|
||||
func TestFaint(t *testing.T) {
|
||||
|
||||
// case: apply colorization
|
||||
conf := config.Config{Colorize: true}
|
||||
want := "\033[2mfoo\033[0m"
|
||||
got := Faint("foo", conf)
|
||||
if want != got {
|
||||
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
||||
}
|
||||
|
||||
// case: do not apply colorization
|
||||
conf.Colorize = false
|
||||
want = "foo"
|
||||
got = Faint("foo", conf)
|
||||
if want != got {
|
||||
t.Errorf("failed to faint: want: %s, got: %s", want, got)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Indent prepends each line of a string with a tab
|
||||
func Indent(str string) string {
|
||||
|
||||
// trim superfluous whitespace
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
// prepend each line with a tab character
|
||||
out := ""
|
||||
for _, line := range strings.Split(str, "\n") {
|
||||
out += fmt.Sprintf("\t%s\n", line)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package display
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestIndent asserts that Indent prepends a tab to each line
|
||||
func TestIndent(t *testing.T) {
|
||||
got := Indent("foo\nbar\nbaz")
|
||||
want := "\tfoo\n\tbar\n\tbaz\n"
|
||||
if got != want {
|
||||
t.Errorf("failed to indent: want: %s, got: %s", want, got)
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// TestWriteToPager tests the writeToPager function
|
||||
func TestWriteToPager(t *testing.T) {
|
||||
// Skip these tests in CI/CD environments where interactive commands might not work
|
||||
if os.Getenv("CI") != "" {
|
||||
t.Skip("Skipping pager tests in CI environment")
|
||||
}
|
||||
|
||||
// Note: We can't easily test os.Exit calls, so we focus on testing writeToPager
|
||||
// which contains the core logic
|
||||
|
||||
t.Run("successful pager execution", func(t *testing.T) {
|
||||
// Save original stdout
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
// Create pipe for capturing output
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Use 'cat' as a simple pager that just outputs input
|
||||
conf := config.Config{
|
||||
Pager: "cat",
|
||||
}
|
||||
|
||||
// This will call os.Exit on error, so we need to be careful
|
||||
// We're using 'cat' which should always succeed
|
||||
input := "Test output\n"
|
||||
|
||||
// Run in a goroutine to avoid blocking
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
writeToPager(input, conf)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Wait for completion or timeout
|
||||
select {
|
||||
case <-done:
|
||||
// Success
|
||||
}
|
||||
|
||||
// Close write end and read output
|
||||
w.Close()
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
|
||||
// Verify output
|
||||
if buf.String() != input {
|
||||
t.Errorf("expected output %q, got %q", input, buf.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pager with arguments", func(t *testing.T) {
|
||||
// Save original stdout
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
// Create pipe for capturing output
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Use 'cat' with '-A' flag (shows non-printing characters)
|
||||
conf := config.Config{
|
||||
Pager: "cat -A",
|
||||
}
|
||||
|
||||
input := "Test\toutput\n"
|
||||
|
||||
// Run in a goroutine
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
writeToPager(input, conf)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Wait for completion
|
||||
select {
|
||||
case <-done:
|
||||
// Success
|
||||
}
|
||||
|
||||
// Close write end and read output
|
||||
w.Close()
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
|
||||
// cat -A shows tabs as ^I and line endings as $
|
||||
expected := "Test^Ioutput$\n"
|
||||
if buf.String() != expected {
|
||||
t.Errorf("expected output %q, got %q", expected, buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestWriteToPagerError tests error handling in writeToPager
|
||||
func TestWriteToPagerError(t *testing.T) {
|
||||
if os.Getenv("TEST_PAGER_ERROR_SUBPROCESS") == "1" {
|
||||
// This is the subprocess - run the actual test
|
||||
conf := config.Config{Pager: "/nonexistent/command"}
|
||||
writeToPager("test", conf)
|
||||
return
|
||||
}
|
||||
|
||||
// Run test in subprocess to handle os.Exit
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestWriteToPagerError$")
|
||||
cmd.Env = append(os.Environ(), "TEST_PAGER_ERROR_SUBPROCESS=1")
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
// Should exit with error
|
||||
if err == nil {
|
||||
t.Error("expected process to exit with error")
|
||||
}
|
||||
|
||||
// Should contain error message
|
||||
if !strings.Contains(string(output), "failed to write to pager") {
|
||||
t.Errorf("expected error message about pager failure, got %q", string(output))
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,30 @@
|
||||
package sheet
|
||||
package frontmatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"gopkg.in/yaml.v1"
|
||||
)
|
||||
|
||||
// Parse parses cheatsheet frontmatter
|
||||
func parse(markdown string) (frontmatter, string, error) {
|
||||
// Frontmatter encapsulates cheatsheet frontmatter data
|
||||
type Frontmatter struct {
|
||||
Tags []string
|
||||
Syntax string
|
||||
}
|
||||
|
||||
// determine the appropriate line-break for the platform
|
||||
linebreak := "\n"
|
||||
if runtime.GOOS == "windows" {
|
||||
linebreak = "\r\n"
|
||||
}
|
||||
// Parse parses cheatsheet frontmatter
|
||||
func Parse(markdown string) (string, Frontmatter, error) {
|
||||
|
||||
// specify the frontmatter delimiter
|
||||
delim := fmt.Sprintf("---%s", linebreak)
|
||||
delim := "---"
|
||||
|
||||
// initialize a frontmatter struct
|
||||
var fm frontmatter
|
||||
var fm Frontmatter
|
||||
|
||||
// if the markdown does not contain frontmatter, pass it through unmodified
|
||||
if !strings.HasPrefix(markdown, delim) {
|
||||
return fm, markdown, nil
|
||||
return strings.TrimSpace(markdown), fm, nil
|
||||
}
|
||||
|
||||
// otherwise, split the frontmatter and cheatsheet text
|
||||
@@ -33,13 +32,13 @@ func parse(markdown string) (frontmatter, string, error) {
|
||||
|
||||
// return an error if the frontmatter parses into the wrong number of parts
|
||||
if len(parts) != 3 {
|
||||
return fm, markdown, fmt.Errorf("failed to delimit frontmatter")
|
||||
return markdown, fm, fmt.Errorf("failed to delimit frontmatter")
|
||||
}
|
||||
|
||||
// return an error if the YAML cannot be unmarshalled
|
||||
if err := yaml.Unmarshal([]byte(parts[1]), &fm); err != nil {
|
||||
return fm, markdown, fmt.Errorf("failed to unmarshal frontmatter: %v", err)
|
||||
return markdown, fm, fmt.Errorf("failed to unmarshal frontmatter: %v", err)
|
||||
}
|
||||
|
||||
return fm, parts[2], nil
|
||||
return strings.TrimSpace(parts[2]), fm, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sheet
|
||||
package frontmatter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -16,7 +16,7 @@ tags: [ test ]
|
||||
To foo the bar: baz`
|
||||
|
||||
// parse the frontmatter
|
||||
fm, text, err := parse(markdown)
|
||||
text, fm, err := Parse(markdown)
|
||||
|
||||
// assert expectations
|
||||
if err != nil {
|
||||
@@ -50,7 +50,7 @@ func TestHasNoFrontmatter(t *testing.T) {
|
||||
markdown := "To foo the bar: baz"
|
||||
|
||||
// parse the frontmatter
|
||||
fm, text, err := parse(markdown)
|
||||
text, fm, err := Parse(markdown)
|
||||
|
||||
// assert expectations
|
||||
if err != nil {
|
||||
@@ -81,7 +81,7 @@ tags: [ test ]
|
||||
To foo the bar: baz`
|
||||
|
||||
// parse the frontmatter
|
||||
_, text, err := parse(markdown)
|
||||
text, _, err := Parse(markdown)
|
||||
|
||||
// assert that an error was returned
|
||||
if err == nil {
|
||||
24
internal/installer/clone.go
Normal file
24
internal/installer/clone.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
const cloneURL = "https://github.com/cheat/cheatsheets.git"
|
||||
|
||||
// Clone clones the community cheatsheets
|
||||
func Clone(path string) error {
|
||||
|
||||
// perform the clone in a shell
|
||||
cmd := exec.Command("git", "clone", cloneURL, path)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// Package installer implements functions that provide a first-time
|
||||
// installation wizard.
|
||||
package installer
|
||||
|
||||
import (
|
||||
@@ -16,7 +14,7 @@ func Prompt(prompt string, def bool) (bool, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
// display the prompt
|
||||
fmt.Printf("%s: ", prompt)
|
||||
fmt.Print(fmt.Sprintf("%s: ", prompt))
|
||||
|
||||
// read the answer
|
||||
ans, err := reader.ReadString('\n')
|
||||
@@ -25,7 +23,7 @@ func Prompt(prompt string, def bool) (bool, error) {
|
||||
}
|
||||
|
||||
// normalize the answer
|
||||
ans = strings.ToLower(strings.TrimSpace(ans))
|
||||
ans = strings.ToLower(strings.TrimRight(ans, "\n"))
|
||||
|
||||
// return the appropriate response
|
||||
switch ans {
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrompt(t *testing.T) {
|
||||
// Save original stdin/stdout
|
||||
oldStdin := os.Stdin
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prompt string
|
||||
input string
|
||||
defaultVal bool
|
||||
want bool
|
||||
wantErr bool
|
||||
wantPrompt string
|
||||
}{
|
||||
{
|
||||
name: "answer yes",
|
||||
prompt: "Continue?",
|
||||
input: "y\n",
|
||||
defaultVal: false,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "answer yes with uppercase",
|
||||
prompt: "Continue?",
|
||||
input: "Y\n",
|
||||
defaultVal: false,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "answer yes with spaces",
|
||||
prompt: "Continue?",
|
||||
input: " y \n",
|
||||
defaultVal: false,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "answer no",
|
||||
prompt: "Continue?",
|
||||
input: "n\n",
|
||||
defaultVal: true,
|
||||
want: false,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "answer no with any text",
|
||||
prompt: "Continue?",
|
||||
input: "anything\n",
|
||||
defaultVal: true,
|
||||
want: false,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "empty answer uses default true",
|
||||
prompt: "Continue?",
|
||||
input: "\n",
|
||||
defaultVal: true,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "empty answer uses default false",
|
||||
prompt: "Continue?",
|
||||
input: "\n",
|
||||
defaultVal: false,
|
||||
want: false,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
{
|
||||
name: "whitespace answer uses default",
|
||||
prompt: "Continue?",
|
||||
input: " \n",
|
||||
defaultVal: true,
|
||||
want: true,
|
||||
wantPrompt: "Continue?: ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a pipe for stdin
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
|
||||
// Create a pipe for stdout to capture the prompt
|
||||
rOut, wOut, _ := os.Pipe()
|
||||
os.Stdout = wOut
|
||||
|
||||
// Write input to stdin
|
||||
go func() {
|
||||
defer w.Close()
|
||||
io.WriteString(w, tt.input)
|
||||
}()
|
||||
|
||||
// Call the function
|
||||
got, err := Prompt(tt.prompt, tt.defaultVal)
|
||||
|
||||
// Close stdout write end and read the prompt
|
||||
wOut.Close()
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, rOut)
|
||||
|
||||
// Check error
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Prompt() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Check result
|
||||
if got != tt.want {
|
||||
t.Errorf("Prompt() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
// Check that prompt was displayed correctly
|
||||
if buf.String() != tt.wantPrompt {
|
||||
t.Errorf("Prompt display = %q, want %q", buf.String(), tt.wantPrompt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptError(t *testing.T) {
|
||||
// Save original stdin
|
||||
oldStdin := os.Stdin
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
}()
|
||||
|
||||
// Create a pipe and close it immediately to simulate read error
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
r.Close()
|
||||
w.Close()
|
||||
|
||||
// This should cause a read error
|
||||
_, err := Prompt("Test?", false)
|
||||
if err == nil {
|
||||
t.Error("expected error when reading from closed stdin, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to parse input") {
|
||||
t.Errorf("expected 'failed to parse input' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPromptIntegration provides a simple integration test
|
||||
func TestPromptIntegration(t *testing.T) {
|
||||
// This demonstrates how the prompt would be used in practice
|
||||
// It's skipped by default since it requires actual user input
|
||||
if os.Getenv("TEST_INTERACTIVE") != "1" {
|
||||
t.Skip("Skipping interactive test - set TEST_INTERACTIVE=1 to run")
|
||||
}
|
||||
|
||||
fmt.Println("\n=== Interactive Prompt Test ===")
|
||||
fmt.Println("You will be prompted to answer a question.")
|
||||
fmt.Println("Try different inputs: y, n, Y, N, empty (just press Enter)")
|
||||
|
||||
result, err := Prompt("Would you like to continue? [Y/n]", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Prompt failed: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("You answered: %v\n", result)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
"github.com/cheat/cheat/internal/repo"
|
||||
)
|
||||
|
||||
// Run runs the installer
|
||||
func Run(configs string, confpath string) error {
|
||||
|
||||
// determine the appropriate paths for config data and (optional) community
|
||||
// cheatsheets based on the user's platform
|
||||
confdir := filepath.Dir(confpath)
|
||||
|
||||
// create paths for community and personal cheatsheets
|
||||
community := filepath.Join(confdir, "cheatsheets", "community")
|
||||
personal := filepath.Join(confdir, "cheatsheets", "personal")
|
||||
|
||||
// set default cheatpaths
|
||||
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
|
||||
configs = strings.Replace(configs, "PERSONAL_PATH", personal, -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
|
||||
yes, err := Prompt(
|
||||
"Would you like to download the community cheatsheets? [Y/n]",
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prompt: %v", err)
|
||||
}
|
||||
|
||||
// clone the community cheatsheets if so instructed
|
||||
if yes {
|
||||
// clone the community cheatsheets
|
||||
fmt.Printf("Cloning community cheatsheets to %s.\n", community)
|
||||
if err := repo.Clone(community); err != nil {
|
||||
return fmt.Errorf("failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
|
||||
// also create a directory for personal cheatsheets
|
||||
fmt.Printf("Cloning personal cheatsheets to %s.\n", personal)
|
||||
if err := os.MkdirAll(personal, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// the config file does not exist, so we'll try to create one
|
||||
if err = config.Init(confpath, configs); err != nil {
|
||||
return fmt.Errorf("failed to create config file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "cheat-installer-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Save original stdin/stdout
|
||||
oldStdin := os.Stdin
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configs string
|
||||
confpath string
|
||||
userInput string
|
||||
wantErr bool
|
||||
wantInErr string
|
||||
checkFiles []string
|
||||
dontWantFiles []string
|
||||
}{
|
||||
{
|
||||
name: "user declines community cheatsheets",
|
||||
configs: `---
|
||||
editor: EDITOR_PATH
|
||||
pager: PAGER_PATH
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
`,
|
||||
confpath: filepath.Join(tempDir, "conf1", "conf.yml"),
|
||||
userInput: "n\n",
|
||||
wantErr: false,
|
||||
checkFiles: []string{"conf1/conf.yml"},
|
||||
dontWantFiles: []string{"conf1/cheatsheets/community", "conf1/cheatsheets/personal"},
|
||||
},
|
||||
{
|
||||
name: "user accepts but clone fails",
|
||||
configs: `---
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
`,
|
||||
confpath: filepath.Join(tempDir, "conf2", "conf.yml"),
|
||||
userInput: "y\n",
|
||||
wantErr: true,
|
||||
wantInErr: "failed to clone cheatsheets",
|
||||
},
|
||||
{
|
||||
name: "invalid config path",
|
||||
configs: "test",
|
||||
confpath: "/nonexistent/path/conf.yml",
|
||||
userInput: "n\n",
|
||||
wantErr: true,
|
||||
wantInErr: "failed to create config file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create stdin pipe
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
|
||||
// Create stdout pipe to suppress output
|
||||
_, wOut, _ := os.Pipe()
|
||||
os.Stdout = wOut
|
||||
|
||||
// Write user input
|
||||
go func() {
|
||||
defer w.Close()
|
||||
io.WriteString(w, tt.userInput)
|
||||
}()
|
||||
|
||||
// Run the installer
|
||||
err := Run(tt.configs, tt.confpath)
|
||||
|
||||
// Close pipes
|
||||
wOut.Close()
|
||||
|
||||
// Check error
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err != nil && tt.wantInErr != "" && !strings.Contains(err.Error(), tt.wantInErr) {
|
||||
t.Errorf("Run() error = %v, want error containing %q", err, tt.wantInErr)
|
||||
}
|
||||
|
||||
// Check created files
|
||||
for _, file := range tt.checkFiles {
|
||||
path := filepath.Join(tempDir, file)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
t.Errorf("expected file %s to exist, but it doesn't", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Check files that shouldn't exist
|
||||
for _, file := range tt.dontWantFiles {
|
||||
path := filepath.Join(tempDir, file)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
t.Errorf("expected file %s to not exist, but it does", path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPromptError(t *testing.T) {
|
||||
// Save original stdin
|
||||
oldStdin := os.Stdin
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
}()
|
||||
|
||||
// Close stdin to cause prompt error
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
r.Close()
|
||||
w.Close()
|
||||
|
||||
tempDir, _ := os.MkdirTemp("", "cheat-installer-prompt-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
err := Run("test", filepath.Join(tempDir, "conf.yml"))
|
||||
if err == nil {
|
||||
t.Error("expected error when prompt fails, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to prompt") {
|
||||
t.Errorf("expected 'failed to prompt' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunStringReplacements(t *testing.T) {
|
||||
// Test that path replacements work correctly
|
||||
configs := `---
|
||||
editor: EDITOR_PATH
|
||||
pager: PAGER_PATH
|
||||
cheatpaths:
|
||||
- name: community
|
||||
path: COMMUNITY_PATH
|
||||
- name: personal
|
||||
path: PERSONAL_PATH
|
||||
`
|
||||
|
||||
// Create temp directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-installer-replace-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
confpath := filepath.Join(tempDir, "conf.yml")
|
||||
confdir := filepath.Dir(confpath)
|
||||
|
||||
// Expected paths
|
||||
expectedCommunity := filepath.Join(confdir, "cheatsheets", "community")
|
||||
expectedPersonal := filepath.Join(confdir, "cheatsheets", "personal")
|
||||
|
||||
// Save original stdin/stdout
|
||||
oldStdin := os.Stdin
|
||||
oldStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
// Create stdin pipe with "n" answer
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
go func() {
|
||||
defer w.Close()
|
||||
io.WriteString(w, "n\n")
|
||||
}()
|
||||
|
||||
// Suppress stdout
|
||||
_, wOut, _ := os.Pipe()
|
||||
os.Stdout = wOut
|
||||
defer wOut.Close()
|
||||
|
||||
// Run installer
|
||||
err = Run(configs, confpath)
|
||||
if err != nil {
|
||||
t.Fatalf("Run() failed: %v", err)
|
||||
}
|
||||
|
||||
// Read the created config file
|
||||
content, err := os.ReadFile(confpath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
// Check replacements
|
||||
contentStr := string(content)
|
||||
if strings.Contains(contentStr, "COMMUNITY_PATH") {
|
||||
t.Error("COMMUNITY_PATH was not replaced")
|
||||
}
|
||||
if strings.Contains(contentStr, "PERSONAL_PATH") {
|
||||
t.Error("PERSONAL_PATH was not replaced")
|
||||
}
|
||||
if strings.Contains(contentStr, "EDITOR_PATH") && !strings.Contains(contentStr, fmt.Sprintf("editor: %s", "")) {
|
||||
t.Error("EDITOR_PATH was not replaced")
|
||||
}
|
||||
if strings.Contains(contentStr, "PAGER_PATH") && !strings.Contains(contentStr, fmt.Sprintf("pager: %s", "")) {
|
||||
t.Error("PAGER_PATH was not replaced")
|
||||
}
|
||||
|
||||
// Verify correct paths were used
|
||||
if !strings.Contains(contentStr, expectedCommunity) {
|
||||
t.Errorf("expected community path %q in config", expectedCommunity)
|
||||
}
|
||||
if !strings.Contains(contentStr, expectedPersonal) {
|
||||
t.Errorf("expected personal path %q in config", expectedPersonal)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package mock implements mock functions used in unit-tests.
|
||||
package mock
|
||||
|
||||
import (
|
||||
@@ -14,7 +13,7 @@ func Path(filename string) string {
|
||||
// determine the path of this file during runtime
|
||||
_, thisfile, _, _ := runtime.Caller(0)
|
||||
|
||||
// compute the mock path
|
||||
// compute the config path
|
||||
file, err := filepath.Abs(
|
||||
path.Join(
|
||||
filepath.Dir(thisfile),
|
||||
@@ -23,7 +22,7 @@ func Path(filename string) string {
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to resolve mock path: %v", err))
|
||||
panic(fmt.Errorf("failed to resolve config path: %v", err))
|
||||
}
|
||||
|
||||
return file
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Package repo implements functions pertaining to the management of git repos.
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// Clone clones the community cheatsheets repository to the specified directory
|
||||
func Clone(dir string) error {
|
||||
|
||||
// clone the community cheatsheets
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||
URL: "https://github.com/cheat/cheatsheets.git",
|
||||
Depth: 1,
|
||||
Progress: os.Stdout,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clone cheatsheets: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCloneIntegration performs a real clone operation to verify functionality
|
||||
// Run with: go test -tags=integration ./internal/repo -v -run TestCloneIntegration
|
||||
func TestCloneIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Create a temporary directory
|
||||
tmpDir, err := os.MkdirTemp("", "cheat-clone-integration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
destDir := filepath.Join(tmpDir, "cheatsheets")
|
||||
|
||||
t.Logf("Cloning to: %s", destDir)
|
||||
|
||||
// Perform the actual clone
|
||||
err = Clone(destDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Clone() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the clone succeeded
|
||||
info, err := os.Stat(destDir)
|
||||
if err != nil {
|
||||
t.Fatalf("destination directory not created: %v", err)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
t.Fatal("destination is not a directory")
|
||||
}
|
||||
|
||||
// Check for .git directory
|
||||
gitDir := filepath.Join(destDir, ".git")
|
||||
if _, err := os.Stat(gitDir); err != nil {
|
||||
t.Error(".git directory not found")
|
||||
}
|
||||
|
||||
// Check for some expected cheatsheets
|
||||
expectedFiles := []string{
|
||||
"bash", // bash cheatsheet should exist
|
||||
"git", // git cheatsheet should exist
|
||||
"ls", // ls cheatsheet should exist
|
||||
}
|
||||
|
||||
foundCount := 0
|
||||
for _, file := range expectedFiles {
|
||||
path := filepath.Join(destDir, file)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
foundCount++
|
||||
}
|
||||
}
|
||||
|
||||
if foundCount < 2 {
|
||||
t.Errorf("expected at least 2 common cheatsheets, found %d", foundCount)
|
||||
}
|
||||
|
||||
t.Log("Clone integration test passed!")
|
||||
|
||||
// Test cloning to existing directory (should fail)
|
||||
err = Clone(destDir)
|
||||
if err == nil {
|
||||
t.Error("expected error when cloning to existing repository, got nil")
|
||||
} else {
|
||||
t.Logf("Expected error when cloning to existing dir: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestClone tests the Clone function
|
||||
func TestClone(t *testing.T) {
|
||||
// This test requires network access, so we'll only test error cases
|
||||
// that don't require actual cloning
|
||||
|
||||
t.Run("clone to read-only directory", func(t *testing.T) {
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("Cannot test read-only directory as root")
|
||||
}
|
||||
|
||||
// Create a temporary directory
|
||||
tempDir, err := os.MkdirTemp("", "cheat-clone-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create a read-only subdirectory
|
||||
readOnlyDir := filepath.Join(tempDir, "readonly")
|
||||
if err := os.Mkdir(readOnlyDir, 0555); err != nil {
|
||||
t.Fatalf("failed to create read-only dir: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to clone to read-only directory
|
||||
targetDir := filepath.Join(readOnlyDir, "cheatsheets")
|
||||
err = Clone(targetDir)
|
||||
|
||||
// Should fail because we can't write to read-only directory
|
||||
if err == nil {
|
||||
t.Error("expected error when cloning to read-only directory, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("clone to invalid path", func(t *testing.T) {
|
||||
// Try to clone to a path with null bytes (invalid on most filesystems)
|
||||
err := Clone("/tmp/invalid\x00path")
|
||||
if err == nil {
|
||||
t.Error("expected error with invalid path, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GitDir returns `true` if we are iterating over a directory contained within
|
||||
// a repositories `.git` directory.
|
||||
func GitDir(path string) (bool, error) {
|
||||
|
||||
/*
|
||||
A bit of context is called for here, because this functionality has
|
||||
previously caused a number of tricky, subtle bugs.
|
||||
|
||||
Fundamentally, here we are simply trying to avoid walking over the
|
||||
contents of the `.git` directory. Doing so potentially makes
|
||||
hundreds/thousands of needless syscalls, and can noticeably harm
|
||||
performance on machines with slow disks.
|
||||
|
||||
The earliest effort to solve this problem involved simply returning
|
||||
`fs.SkipDir` when the cheatsheet file path began with `.`, signifying a
|
||||
hidden directory. This, however, caused two problems:
|
||||
|
||||
1. The `.cheat` directory was ignored
|
||||
2. Cheatsheets installed by `brew` (which were by default installed to
|
||||
`~/.config/cheat`) were ignored
|
||||
|
||||
See: https://github.com/cheat/cheat/issues/690
|
||||
|
||||
To remedy this, the exclusion criteria were narrowed, and the search
|
||||
for a literal `.` was replaced with a search for a literal `.git`.
|
||||
This, however, broke user installations that stored cheatsheets in
|
||||
`git` submodules, because such an installation would contain a `.git`
|
||||
file that pointed to the upstream repository.
|
||||
|
||||
See: https://github.com/cheat/cheat/issues/694
|
||||
|
||||
The next attempt at solving this was to search for a `.git` literal
|
||||
string in the cheatsheet file path. If a match was not found, we would
|
||||
continue to walk the directory, as before.
|
||||
|
||||
If a match *was* found, we determined whether `.git` referred to a file
|
||||
or directory, and would only stop walking the path in the latter case.
|
||||
|
||||
This, however, caused crashes if a cheatpath contained a `.gitignore`
|
||||
file. (Presumably, a crash would likewise occur on the presence of
|
||||
`.gitattributes`, `.gitmodules`, etc.)
|
||||
|
||||
See: https://github.com/cheat/cheat/issues/699
|
||||
|
||||
Accounting for all of the above (hopefully?), the current solution is
|
||||
not to search for `.git`, but `.git/` (including the directory
|
||||
separator), and then only ceasing to walk the directory on a match.
|
||||
|
||||
To summarize, this code must account for the following possibilities:
|
||||
|
||||
1. A cheatpath is not a repository
|
||||
2. A cheatpath is a repository
|
||||
3. A cheatpath is a repository, and contains a `.git*` file
|
||||
4. A cheatpath is a submodule
|
||||
5. A cheatpath is a hidden directory
|
||||
|
||||
Care must be taken to support the above on both Unix and Windows
|
||||
systems, which have different directory separators and line-endings.
|
||||
|
||||
There is a lot of nuance to all of this, and it would be worthwhile to
|
||||
do two things to stop writing bugs here:
|
||||
|
||||
1. Build integration tests around all of this
|
||||
2. Discard string-matching solutions entirely, and use `go-git` instead
|
||||
|
||||
NB: A reasonable smoke-test for ensuring that skipping is being applied
|
||||
correctly is to run the following command:
|
||||
|
||||
make && strace ./dist/cheat -l | wc -l
|
||||
|
||||
That check should be run twice: once normally, and once after
|
||||
commenting out the "skip" check in `sheets.Load`.
|
||||
|
||||
The specific line counts don't matter; what matters is that the number
|
||||
of syscalls should be significantly lower with the skip check enabled.
|
||||
*/
|
||||
|
||||
// determine if the literal string `.git` appears within `path`
|
||||
pos := strings.Index(path, fmt.Sprintf(".git%s", string(os.PathSeparator)))
|
||||
|
||||
// if it does not, we know for certain that we are not within a `.git`
|
||||
// directory.
|
||||
if pos == -1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If `path` does contain the string `.git`, we need to determine if we're
|
||||
// inside of a `.git` directory, or if `path` points to a cheatsheet that's
|
||||
// stored within a `git` submodule.
|
||||
//
|
||||
// See: https://github.com/cheat/cheat/issues/694
|
||||
|
||||
// truncate `path` to the occurrence of `.git`
|
||||
f, err := os.Stat(path[:pos+5])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to stat path %s: %v", path, err)
|
||||
}
|
||||
|
||||
// return true or false depending on whether the truncated path is a
|
||||
// directory
|
||||
return f.Mode().IsDir(), nil
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGitDir(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create test directory structure
|
||||
testDirs := []string{
|
||||
filepath.Join(tempDir, ".git"),
|
||||
filepath.Join(tempDir, ".git", "objects"),
|
||||
filepath.Join(tempDir, ".git", "refs"),
|
||||
filepath.Join(tempDir, "regular"),
|
||||
filepath.Join(tempDir, "regular", ".git"),
|
||||
filepath.Join(tempDir, "submodule"),
|
||||
}
|
||||
|
||||
for _, dir := range testDirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatalf("failed to create dir %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create test files
|
||||
testFiles := map[string]string{
|
||||
filepath.Join(tempDir, ".gitignore"): "*.tmp\n",
|
||||
filepath.Join(tempDir, ".gitattributes"): "* text=auto\n",
|
||||
filepath.Join(tempDir, "submodule", ".git"): "gitdir: ../.git/modules/submodule\n",
|
||||
filepath.Join(tempDir, "regular", "sheet.txt"): "content\n",
|
||||
}
|
||||
|
||||
for file, content := range testFiles {
|
||||
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to create file %s: %v", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "not in git directory",
|
||||
path: filepath.Join(tempDir, "regular", "sheet.txt"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "in .git directory",
|
||||
path: filepath.Join(tempDir, ".git", "objects", "file"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "in .git/refs directory",
|
||||
path: filepath.Join(tempDir, ".git", "refs", "heads", "main"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: ".gitignore file",
|
||||
path: filepath.Join(tempDir, ".gitignore"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: ".gitattributes file",
|
||||
path: filepath.Join(tempDir, ".gitattributes"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "submodule with .git file",
|
||||
path: filepath.Join(tempDir, "submodule", "sheet.txt"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "path with .git in middle",
|
||||
path: filepath.Join(tempDir, "regular", ".git", "sheet.txt"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "nonexistent path without .git",
|
||||
path: filepath.Join(tempDir, "nonexistent", "file"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GitDir(tt.path)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GitDir() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GitDir() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitDirEdgeCases(t *testing.T) {
|
||||
// Test with paths that have .git but not as a directory separator
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "file ending with .git",
|
||||
path: "/tmp/myfile.git",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "directory ending with .git",
|
||||
path: "/tmp/myrepo.git",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: ".github directory",
|
||||
path: "/tmp/.github/workflows",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "legitimate.git-repo name",
|
||||
path: "/tmp/legitimate.git-repo/file",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GitDir(tt.path)
|
||||
if err != nil {
|
||||
// It's ok if the path doesn't exist for these edge case tests
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("GitDir(%q) = %v, want %v", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitDirPathSeparator(t *testing.T) {
|
||||
// Test that the function correctly uses os.PathSeparator
|
||||
// This is important for cross-platform compatibility
|
||||
|
||||
// Create a path with the wrong separator for the current OS
|
||||
var wrongSep string
|
||||
if os.PathSeparator == '/' {
|
||||
wrongSep = `\`
|
||||
} else {
|
||||
wrongSep = `/`
|
||||
}
|
||||
|
||||
// Path with wrong separator should not be detected as git dir
|
||||
path := fmt.Sprintf("some%spath%s.git%sfile", wrongSep, wrongSep, wrongSep)
|
||||
isGit, err := GitDir(path)
|
||||
|
||||
if err != nil {
|
||||
// Path doesn't exist, which is fine
|
||||
return
|
||||
}
|
||||
|
||||
if isGit {
|
||||
t.Errorf("GitDir() incorrectly detected git dir with wrong path separator")
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package repo
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/quick"
|
||||
"github.com/alecthomas/chroma/quick"
|
||||
)
|
||||
|
||||
// Colorize applies syntax-highlighting to a cheatsheet's Text.
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// TestColorize asserts that syntax-highlighting is correctly applied
|
||||
func TestColorize(t *testing.T) {
|
||||
|
||||
// mock configs
|
||||
conf := config.Config{
|
||||
Formatter: "terminal16m",
|
||||
Style: "solarized-dark",
|
||||
}
|
||||
|
||||
// mock a sheet
|
||||
s := Sheet{
|
||||
Text: "echo 'foo'",
|
||||
}
|
||||
|
||||
// colorize the sheet text
|
||||
s.Colorize(conf)
|
||||
|
||||
// initialize expectations
|
||||
want := "[38;2;181;137;0mecho[0m[38;2;147;161;161m"
|
||||
want += " [0m[38;2;42;161;152m'foo'[0m"
|
||||
|
||||
// assert
|
||||
if s.Text != want {
|
||||
t.Errorf("failed to colorize sheet: want: %s, got: %s", want, s.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestColorizeError tests the error handling in Colorize
|
||||
func TestColorizeError(_ *testing.T) {
|
||||
// Create a sheet with content
|
||||
sheet := Sheet{
|
||||
Text: "some text",
|
||||
Syntax: "invalidlexer12345", // Use an invalid lexer that might cause issues
|
||||
}
|
||||
|
||||
// Create a config with invalid formatter/style
|
||||
conf := config.Config{
|
||||
Formatter: "invalidformatter",
|
||||
Style: "invalidstyle",
|
||||
}
|
||||
|
||||
// Store original text
|
||||
originalText := sheet.Text
|
||||
|
||||
// Colorize should not panic even with invalid settings
|
||||
sheet.Colorize(conf)
|
||||
|
||||
// The text might be unchanged if there was an error, or it might be colorized
|
||||
// We're mainly testing that it doesn't panic
|
||||
_ = sheet.Text
|
||||
_ = originalText
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
)
|
||||
|
||||
// Copy copies a cheatsheet to a new location
|
||||
@@ -22,7 +22,7 @@ func (s *Sheet) Copy(dest string) error {
|
||||
defer infile.Close()
|
||||
|
||||
// create any necessary subdirectories
|
||||
dirs := filepath.Dir(dest)
|
||||
dirs := path.Dir(dest)
|
||||
if dirs != "." {
|
||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %s, %v", dirs, err)
|
||||
@@ -39,8 +39,6 @@ func (s *Sheet) Copy(dest string) error {
|
||||
// copy file contents
|
||||
_, err = io.Copy(outfile, infile)
|
||||
if err != nil {
|
||||
// Clean up the partially written file on error
|
||||
os.Remove(dest)
|
||||
return fmt.Errorf(
|
||||
"failed to copy file: infile: %s, outfile: %s, err: %v",
|
||||
s.Path,
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCopyErrors tests error cases for the Copy method
|
||||
func TestCopyErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() (*Sheet, string, func())
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "source file does not exist",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a sheet with non-existent path
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: "/non/existent/file.txt",
|
||||
CheatPath: "test",
|
||||
}
|
||||
dest := filepath.Join(os.TempDir(), "copy-test-dest.txt")
|
||||
cleanup := func() {
|
||||
os.Remove(dest)
|
||||
}
|
||||
return sheet, dest, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to open cheatsheet",
|
||||
},
|
||||
{
|
||||
name: "destination directory creation fails",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a source file
|
||||
src, err := os.CreateTemp("", "copy-test-src-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
src.WriteString("test content")
|
||||
src.Close()
|
||||
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: src.Name(),
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Create a file where we want a directory
|
||||
blockerFile := filepath.Join(os.TempDir(), "copy-blocker-file")
|
||||
if err := os.WriteFile(blockerFile, []byte("blocker"), 0644); err != nil {
|
||||
t.Fatalf("failed to create blocker file: %v", err)
|
||||
}
|
||||
|
||||
// Try to create dest under the blocker file (will fail)
|
||||
dest := filepath.Join(blockerFile, "subdir", "dest.txt")
|
||||
|
||||
cleanup := func() {
|
||||
os.Remove(src.Name())
|
||||
os.Remove(blockerFile)
|
||||
}
|
||||
return sheet, dest, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to create directory",
|
||||
},
|
||||
{
|
||||
name: "destination file creation fails",
|
||||
setup: func() (*Sheet, string, func()) {
|
||||
// Create a source file
|
||||
src, err := os.CreateTemp("", "copy-test-src-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
src.WriteString("test content")
|
||||
src.Close()
|
||||
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: src.Name(),
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Create a directory where we want the file
|
||||
destDir := filepath.Join(os.TempDir(), "copy-test-dir")
|
||||
if err := os.Mkdir(destDir, 0755); err != nil && !os.IsExist(err) {
|
||||
t.Fatalf("failed to create dest dir: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
os.Remove(src.Name())
|
||||
os.RemoveAll(destDir)
|
||||
}
|
||||
return sheet, destDir, cleanup
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "failed to create outfile",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sheet, dest, cleanup := tt.setup()
|
||||
defer cleanup()
|
||||
|
||||
err := sheet.Copy(dest)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Copy() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && tt.errMsg != "" {
|
||||
if !contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("Copy() error = %v, want error containing %q", err, tt.errMsg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCopyIOError tests the io.Copy error case
|
||||
func TestCopyIOError(t *testing.T) {
|
||||
// This is difficult to test without mocking io.Copy
|
||||
// The error case would occur if the source file is modified
|
||||
// or removed after opening but before copying
|
||||
t.Skip("Skipping io.Copy error test - requires file system race condition")
|
||||
}
|
||||
|
||||
// TestCopyCleanupOnError verifies that partially written files are cleaned up on error
|
||||
func TestCopyCleanupOnError(t *testing.T) {
|
||||
// Create a source file that we'll make unreadable after opening
|
||||
src, err := os.CreateTemp("", "copy-test-cleanup-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(src.Name())
|
||||
|
||||
// Write some content
|
||||
content := "test content for cleanup"
|
||||
if _, err := src.WriteString(content); err != nil {
|
||||
t.Fatalf("failed to write content: %v", err)
|
||||
}
|
||||
src.Close()
|
||||
|
||||
sheet := &Sheet{
|
||||
Title: "test",
|
||||
Path: src.Name(),
|
||||
CheatPath: "test",
|
||||
}
|
||||
|
||||
// Destination path
|
||||
dest := filepath.Join(os.TempDir(), "copy-cleanup-test.txt")
|
||||
defer os.Remove(dest) // Clean up if test fails
|
||||
|
||||
// Make the source file unreadable (simulating a read error during copy)
|
||||
// This is platform-specific, but should work on Unix-like systems
|
||||
if err := os.Chmod(src.Name(), 0000); err != nil {
|
||||
t.Skip("Cannot change file permissions on this platform")
|
||||
}
|
||||
defer os.Chmod(src.Name(), 0644) // Restore permissions for cleanup
|
||||
|
||||
// Attempt to copy - this should fail during io.Copy
|
||||
err = sheet.Copy(dest)
|
||||
if err == nil {
|
||||
t.Error("Expected Copy to fail with permission error")
|
||||
}
|
||||
|
||||
// Verify the destination file was cleaned up
|
||||
if _, err := os.Stat(dest); !os.IsNotExist(err) {
|
||||
t.Error("Destination file should have been removed after copy failure")
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
|
||||
}
|
||||
|
||||
func containsHelper(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
@@ -12,7 +13,7 @@ func TestCopyFlat(t *testing.T) {
|
||||
|
||||
// mock a cheatsheet file
|
||||
text := "this is the cheatsheet text"
|
||||
src, err := os.CreateTemp("", "foo-src")
|
||||
src, err := ioutil.TempFile("", "foo-src")
|
||||
if err != nil {
|
||||
t.Errorf("failed to mock cheatsheet: %v", err)
|
||||
}
|
||||
@@ -24,7 +25,7 @@ func TestCopyFlat(t *testing.T) {
|
||||
}
|
||||
|
||||
// mock a cheatsheet struct
|
||||
sheet, err := New("foo", "community", src.Name(), []string{}, false)
|
||||
sheet, err := New("foo", src.Name(), []string{}, false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to init cheatsheet: %v", err)
|
||||
}
|
||||
@@ -40,7 +41,7 @@ func TestCopyFlat(t *testing.T) {
|
||||
}
|
||||
|
||||
// assert that the destination file contains the correct text
|
||||
got, err := os.ReadFile(outpath)
|
||||
got, err := ioutil.ReadFile(outpath)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read destination file: %v", err)
|
||||
}
|
||||
@@ -59,7 +60,7 @@ func TestCopyDeep(t *testing.T) {
|
||||
|
||||
// mock a cheatsheet file
|
||||
text := "this is the cheatsheet text"
|
||||
src, err := os.CreateTemp("", "foo-src")
|
||||
src, err := ioutil.TempFile("", "foo-src")
|
||||
if err != nil {
|
||||
t.Errorf("failed to mock cheatsheet: %v", err)
|
||||
}
|
||||
@@ -71,13 +72,7 @@ func TestCopyDeep(t *testing.T) {
|
||||
}
|
||||
|
||||
// mock a cheatsheet struct
|
||||
sheet, err := New(
|
||||
"/cheat-tests/alpha/bravo/foo",
|
||||
"community",
|
||||
src.Name(),
|
||||
[]string{},
|
||||
false,
|
||||
)
|
||||
sheet, err := New("/cheat-tests/alpha/bravo/foo", src.Name(), []string{}, false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to init cheatsheet: %v", err)
|
||||
}
|
||||
@@ -93,7 +88,7 @@ func TestCopyDeep(t *testing.T) {
|
||||
}
|
||||
|
||||
// assert that the destination file contains the correct text
|
||||
got, err := os.ReadFile(outpath)
|
||||
got, err := ioutil.ReadFile(outpath)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read destination file: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
// Package sheet provides functionality for parsing and managing individual cheat sheets.
|
||||
//
|
||||
// A sheet represents a single cheatsheet file containing helpful commands, notes,
|
||||
// or documentation. Sheets can include optional YAML frontmatter for metadata
|
||||
// such as tags and syntax highlighting preferences.
|
||||
//
|
||||
// # Sheet Format
|
||||
//
|
||||
// Sheets are plain text files that may begin with YAML frontmatter:
|
||||
//
|
||||
// ---
|
||||
// syntax: bash
|
||||
// tags: [networking, linux, ssh]
|
||||
// ---
|
||||
// # Connect to remote server
|
||||
// ssh user@hostname
|
||||
//
|
||||
// # Copy files over SSH
|
||||
// scp local_file user@hostname:/remote/path
|
||||
//
|
||||
// The frontmatter is optional. If omitted, the sheet will use default values.
|
||||
//
|
||||
// # Core Types
|
||||
//
|
||||
// The Sheet type contains:
|
||||
// - Title: The sheet's name (derived from filename)
|
||||
// - Path: Full filesystem path to the sheet
|
||||
// - Text: The content of the sheet (without frontmatter)
|
||||
// - Tags: Categories assigned to the sheet
|
||||
// - Syntax: Language hint for syntax highlighting
|
||||
// - ReadOnly: Whether the sheet can be modified
|
||||
//
|
||||
// Key Functions
|
||||
//
|
||||
// - New: Creates a new Sheet from a file path
|
||||
// - Parse: Extracts frontmatter and content from sheet text
|
||||
// - Search: Searches sheet content using regular expressions
|
||||
// - Colorize: Applies syntax highlighting to sheet content
|
||||
//
|
||||
// # Syntax Highlighting
|
||||
//
|
||||
// The package integrates with the Chroma library to provide syntax highlighting.
|
||||
// Supported languages include bash, python, go, javascript, and many others.
|
||||
// The syntax can be specified in the frontmatter or auto-detected.
|
||||
//
|
||||
// Example Usage
|
||||
//
|
||||
// // Load a sheet from disk
|
||||
// s, err := sheet.New("/path/to/sheet", []string{"personal"}, false)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Search for content
|
||||
// matches, err := s.Search("ssh", false)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Apply syntax highlighting
|
||||
// colorized, err := s.Colorize(config)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
package sheet
|
||||
@@ -1,54 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestParseWindowsLineEndings tests parsing with Windows line endings
|
||||
func TestParseWindowsLineEndings(t *testing.T) {
|
||||
// Only test Windows line endings on Windows
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("Skipping Windows line ending test on non-Windows platform")
|
||||
}
|
||||
|
||||
// stub our cheatsheet content with Windows line endings
|
||||
markdown := "---\r\nsyntax: go\r\ntags: [ test ]\r\n---\r\nTo foo the bar: baz"
|
||||
|
||||
// parse the frontmatter
|
||||
fm, text, err := parse(markdown)
|
||||
|
||||
// assert expectations
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse markdown: %v", err)
|
||||
}
|
||||
|
||||
want := "To foo the bar: baz"
|
||||
if text != want {
|
||||
t.Errorf("failed to parse text: want: %s, got: %s", want, text)
|
||||
}
|
||||
|
||||
want = "go"
|
||||
if fm.Syntax != want {
|
||||
t.Errorf("failed to parse syntax: want: %s, got: %s", want, fm.Syntax)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseInvalidYAML tests parsing with invalid YAML in frontmatter
|
||||
func TestParseInvalidYAML(t *testing.T) {
|
||||
// stub our cheatsheet content with invalid YAML
|
||||
markdown := `---
|
||||
syntax: go
|
||||
tags: [ test
|
||||
unclosed bracket
|
||||
---
|
||||
To foo the bar: baz`
|
||||
|
||||
// parse the frontmatter
|
||||
_, _, err := parse(markdown)
|
||||
|
||||
// assert that an error was returned for invalid YAML
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid YAML, got nil")
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzParse tests the parse function with fuzzing to uncover edge cases
|
||||
// and potential panics in YAML frontmatter parsing
|
||||
func FuzzParse(f *testing.F) {
|
||||
// Add seed corpus with various valid and edge case inputs
|
||||
// Valid frontmatter
|
||||
f.Add("---\nsyntax: go\n---\nContent")
|
||||
f.Add("---\ntags: [a, b]\n---\n")
|
||||
f.Add("---\nsyntax: bash\ntags: [linux, shell]\n---\n#!/bin/bash\necho hello")
|
||||
|
||||
// No frontmatter
|
||||
f.Add("No frontmatter here")
|
||||
f.Add("")
|
||||
f.Add("Just plain text\nwith multiple lines")
|
||||
|
||||
// Edge cases with delimiters
|
||||
f.Add("---")
|
||||
f.Add("---\n")
|
||||
f.Add("---\n---")
|
||||
f.Add("---\n---\n")
|
||||
f.Add("---\n---\n---")
|
||||
f.Add("---\n---\n---\n---")
|
||||
f.Add("------\n------")
|
||||
|
||||
// Invalid YAML
|
||||
f.Add("---\n{invalid yaml\n---\n")
|
||||
f.Add("---\nsyntax: \"unclosed quote\n---\n")
|
||||
f.Add("---\ntags: [a, b,\n---\n")
|
||||
|
||||
// Windows line endings
|
||||
f.Add("---\r\nsyntax: go\r\n---\r\nContent")
|
||||
f.Add("---\r\n---\r\n")
|
||||
|
||||
// Mixed line endings
|
||||
f.Add("---\nsyntax: go\r\n---\nContent")
|
||||
f.Add("---\r\nsyntax: go\n---\r\nContent")
|
||||
|
||||
// Unicode and special characters
|
||||
f.Add("---\ntags: [emoji, 🎉]\n---\n")
|
||||
f.Add("---\nsyntax: 中文\n---\n")
|
||||
f.Add("---\ntags: [\x00, \x01]\n---\n")
|
||||
|
||||
// Very long inputs
|
||||
f.Add("---\ntags: [" + strings.Repeat("a,", 1000) + "a]\n---\n")
|
||||
f.Add("---\n" + strings.Repeat("field: value\n", 1000) + "---\n")
|
||||
|
||||
// Nested structures
|
||||
f.Add("---\ntags:\n - nested\n - list\n---\n")
|
||||
f.Add("---\nmeta:\n author: test\n version: 1.0\n---\n")
|
||||
|
||||
f.Fuzz(func(t *testing.T, input string) {
|
||||
// The parse function should never panic, regardless of input
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("parse panicked with input %q: %v", input, r)
|
||||
}
|
||||
}()
|
||||
|
||||
fm, text, err := parse(input)
|
||||
|
||||
// Verify invariants
|
||||
if err == nil {
|
||||
// If parsing succeeded, validate the result
|
||||
|
||||
// The returned text should be a suffix of the input
|
||||
// (either the whole input if no frontmatter, or the part after frontmatter)
|
||||
if !strings.HasSuffix(input, text) && text != input {
|
||||
t.Errorf("returned text %q is not a valid suffix of input %q", text, input)
|
||||
}
|
||||
|
||||
// If input starts with delimiter and has valid frontmatter,
|
||||
// text should be shorter than input
|
||||
if strings.HasPrefix(input, "---\n") || strings.HasPrefix(input, "---\r\n") {
|
||||
if len(fm.Tags) > 0 || fm.Syntax != "" {
|
||||
// We successfully parsed frontmatter, so text should be shorter
|
||||
if len(text) >= len(input) {
|
||||
t.Errorf("text length %d should be less than input length %d when frontmatter is parsed",
|
||||
len(text), len(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Tags can be nil when frontmatter is not present or empty
|
||||
// This is expected behavior in Go for uninitialized slices
|
||||
} else {
|
||||
// If parsing failed, the original input should be returned as text
|
||||
if text != input {
|
||||
t.Errorf("on error, text should equal input: got %q, want %q", text, input)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzParseDelimiterHandling specifically tests delimiter edge cases
|
||||
func FuzzParseDelimiterHandling(f *testing.F) {
|
||||
// Seed corpus focusing on delimiter variations
|
||||
f.Add("---", "content")
|
||||
f.Add("", "---")
|
||||
f.Add("---", "---")
|
||||
f.Add("", "")
|
||||
|
||||
f.Fuzz(func(t *testing.T, prefix string, suffix string) {
|
||||
// Build input with controllable parts around delimiters
|
||||
inputs := []string{
|
||||
prefix + "---\n" + suffix,
|
||||
prefix + "---\r\n" + suffix,
|
||||
prefix + "---\n---\n" + suffix,
|
||||
prefix + "---\r\n---\r\n" + suffix,
|
||||
prefix + "---\n" + "yaml: data\n" + "---\n" + suffix,
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("parse panicked with constructed input: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
_, _, _ = parse(input)
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -9,17 +9,16 @@ import (
|
||||
func (s *Sheet) Search(reg *regexp.Regexp) string {
|
||||
|
||||
// record matches
|
||||
var matches []string
|
||||
matches := ""
|
||||
|
||||
// search through the cheatsheet's text line by line
|
||||
for _, line := range strings.Split(s.Text, "\n\n") {
|
||||
|
||||
// save matching lines
|
||||
// exit early if the line doesn't match the regex
|
||||
if reg.MatchString(line) {
|
||||
matches = append(matches, line)
|
||||
matches += line + "\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Join matches with the same delimiter used for splitting
|
||||
return strings.Join(matches, "\n\n")
|
||||
return strings.TrimSpace(matches)
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FuzzSearchRegex tests the regex compilation and search functionality
|
||||
// to ensure it handles malformed patterns gracefully and doesn't suffer
|
||||
// from catastrophic backtracking
|
||||
func FuzzSearchRegex(f *testing.F) {
|
||||
// Add seed corpus with various regex patterns
|
||||
// Valid patterns
|
||||
f.Add("test", "This is a test string")
|
||||
f.Add("(?i)test", "This is a TEST string")
|
||||
f.Add("foo|bar", "foo and bar")
|
||||
f.Add("^start", "start of line\nnext line")
|
||||
f.Add("end$", "at the end\nnext line")
|
||||
f.Add("\\d+", "123 numbers 456")
|
||||
f.Add("[a-z]+", "lowercase UPPERCASE")
|
||||
|
||||
// Edge cases and potentially problematic patterns
|
||||
f.Add("", "empty pattern")
|
||||
f.Add(".", "any character")
|
||||
f.Add(".*", "match everything")
|
||||
f.Add(".+", "match something")
|
||||
f.Add("\\", "backslash")
|
||||
f.Add("(", "unclosed paren")
|
||||
f.Add(")", "unmatched paren")
|
||||
f.Add("[", "unclosed bracket")
|
||||
f.Add("]", "unmatched bracket")
|
||||
f.Add("[^]", "negated empty class")
|
||||
f.Add("(?", "incomplete group")
|
||||
|
||||
// Patterns that might cause performance issues
|
||||
f.Add("(a+)+", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
||||
f.Add("(a*)*", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
||||
f.Add("(a|a)*", "aaaaaaaaaaaaaaaaaaaaaaaab")
|
||||
f.Add("(.*)*", "any text here")
|
||||
f.Add("(\\d+)+", "123456789012345678901234567890x")
|
||||
|
||||
// Unicode patterns
|
||||
f.Add("☺", "Unicode ☺ smiley")
|
||||
f.Add("[一-龯]", "Chinese 中文 characters")
|
||||
f.Add("\\p{L}+", "Unicode letters")
|
||||
|
||||
// Very long patterns
|
||||
f.Add(strings.Repeat("a", 1000), "long pattern")
|
||||
f.Add(strings.Repeat("(a|b)", 100), "complex pattern")
|
||||
|
||||
f.Fuzz(func(t *testing.T, pattern string, text string) {
|
||||
// Test 1: Regex compilation should not panic
|
||||
var reg *regexp.Regexp
|
||||
var compileErr error
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("regexp.Compile panicked with pattern %q: %v", pattern, r)
|
||||
}
|
||||
}()
|
||||
|
||||
reg, compileErr = regexp.Compile(pattern)
|
||||
}()
|
||||
|
||||
// If compilation failed, that's OK - we're testing error handling
|
||||
if compileErr != nil {
|
||||
// This is expected for invalid patterns
|
||||
return
|
||||
}
|
||||
|
||||
// Test 2: Create a sheet and test Search method
|
||||
sheet := Sheet{
|
||||
Title: "test",
|
||||
Text: text,
|
||||
}
|
||||
|
||||
// Search should not panic
|
||||
var result string
|
||||
done := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Search panicked with pattern %q on text %q: %v", pattern, text, r)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
result = sheet.Search(reg)
|
||||
}()
|
||||
|
||||
// Timeout after 100ms to catch catastrophic backtracking
|
||||
select {
|
||||
case <-done:
|
||||
// Search completed successfully
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Errorf("Search timed out (possible catastrophic backtracking) with pattern %q on text %q", pattern, text)
|
||||
}
|
||||
|
||||
// Test 3: Verify search result invariants
|
||||
if result != "" {
|
||||
// The Search function splits by "\n\n", so we need to compare using the same logic
|
||||
resultLines := strings.Split(result, "\n\n")
|
||||
textLines := strings.Split(text, "\n\n")
|
||||
|
||||
// Every result line should exist in the original text lines
|
||||
for _, rLine := range resultLines {
|
||||
found := false
|
||||
for _, tLine := range textLines {
|
||||
if rLine == tLine {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found && rLine != "" {
|
||||
t.Errorf("Search result contains line not in original text: %q", rLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzSearchCatastrophicBacktracking specifically tests for regex patterns
|
||||
// that could cause performance issues
|
||||
func FuzzSearchCatastrophicBacktracking(f *testing.F) {
|
||||
// Seed with patterns known to potentially cause issues
|
||||
f.Add("a", 10, 5)
|
||||
f.Add("x", 20, 3)
|
||||
|
||||
f.Fuzz(func(t *testing.T, char string, repeats int, groups int) {
|
||||
// Limit the size to avoid memory issues in the test
|
||||
if repeats > 30 || repeats < 0 || groups > 10 || groups < 0 || len(char) > 5 {
|
||||
t.Skip("Skipping invalid or overly large test case")
|
||||
}
|
||||
|
||||
// Construct patterns that might cause backtracking
|
||||
patterns := []string{
|
||||
strings.Repeat(char, repeats),
|
||||
"(" + char + "+)+",
|
||||
"(" + char + "*)*",
|
||||
"(" + char + "|" + char + ")*",
|
||||
}
|
||||
|
||||
// Add nested groups
|
||||
if groups > 0 && groups < 10 {
|
||||
nested := char
|
||||
for i := 0; i < groups; i++ {
|
||||
nested = "(" + nested + ")+"
|
||||
}
|
||||
patterns = append(patterns, nested)
|
||||
}
|
||||
|
||||
// Test text that might trigger backtracking
|
||||
testText := strings.Repeat(char, repeats) + "x"
|
||||
|
||||
for _, pattern := range patterns {
|
||||
// Try to compile the pattern
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
// Invalid pattern, skip
|
||||
continue
|
||||
}
|
||||
|
||||
// Test with timeout
|
||||
done := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Search panicked with backtracking pattern %q: %v", pattern, r)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
sheet := Sheet{Text: testText}
|
||||
_ = sheet.Search(reg)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Completed successfully
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
t.Logf("Warning: potential backtracking issue with pattern %q (completed slowly)", pattern)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,47 +1,39 @@
|
||||
// Package sheet implements functions pertaining to parsing, searching, and
|
||||
// displaying cheatsheets.
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Frontmatter encapsulates cheatsheet frontmatter data
|
||||
type frontmatter struct {
|
||||
Tags []string
|
||||
Syntax string
|
||||
}
|
||||
"github.com/cheat/cheat/internal/frontmatter"
|
||||
)
|
||||
|
||||
// Sheet encapsulates sheet information
|
||||
type Sheet struct {
|
||||
Title string
|
||||
CheatPath string
|
||||
Path string
|
||||
Text string
|
||||
Tags []string
|
||||
Syntax string
|
||||
ReadOnly bool
|
||||
Title string
|
||||
Path string
|
||||
Text string
|
||||
Tags []string
|
||||
Syntax string
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
// New initializes a new Sheet
|
||||
func New(
|
||||
title string,
|
||||
cheatpath string,
|
||||
path string,
|
||||
tags []string,
|
||||
readOnly bool,
|
||||
) (Sheet, error) {
|
||||
|
||||
// read the cheatsheet file
|
||||
markdown, err := os.ReadFile(path)
|
||||
markdown, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return Sheet{}, fmt.Errorf("failed to read file: %s, %v", path, err)
|
||||
}
|
||||
|
||||
// parse the raw cheatsheet text
|
||||
fm, text, err := parse(string(markdown))
|
||||
// parse the cheatsheet frontmatter
|
||||
text, fm, err := frontmatter.Parse(string(markdown))
|
||||
if err != nil {
|
||||
return Sheet{}, fmt.Errorf("failed to parse front-matter: %v", err)
|
||||
}
|
||||
@@ -54,12 +46,11 @@ func New(
|
||||
|
||||
// initialize and return a sheet
|
||||
return Sheet{
|
||||
Title: title,
|
||||
CheatPath: cheatpath,
|
||||
Path: path,
|
||||
Text: text,
|
||||
Tags: tags,
|
||||
Syntax: fm.Syntax,
|
||||
ReadOnly: readOnly,
|
||||
Title: title,
|
||||
Path: path,
|
||||
Text: text + "\n",
|
||||
Tags: tags,
|
||||
Syntax: fm.Syntax,
|
||||
ReadOnly: readOnly,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ func TestSheetSuccess(t *testing.T) {
|
||||
// initialize a sheet
|
||||
sheet, err := New(
|
||||
"foo",
|
||||
"community",
|
||||
mock.Path("sheet/foo"),
|
||||
[]string{"alpha", "bravo"},
|
||||
false,
|
||||
@@ -62,7 +61,6 @@ func TestSheetFailure(t *testing.T) {
|
||||
// initialize a sheet
|
||||
_, err := New(
|
||||
"foo",
|
||||
"community",
|
||||
mock.Path("/does-not-exist"),
|
||||
[]string{"alpha", "bravo"},
|
||||
false,
|
||||
@@ -71,20 +69,3 @@ func TestSheetFailure(t *testing.T) {
|
||||
t.Errorf("failed to return an error on unreadable sheet")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSheetFrontMatterFailure asserts that an error is returned if the sheet's
|
||||
// frontmatter cannot be parsed.
|
||||
func TestSheetFrontMatterFailure(t *testing.T) {
|
||||
|
||||
// initialize a sheet
|
||||
_, err := New(
|
||||
"foo",
|
||||
"community",
|
||||
mock.Path("sheet/bad-fm"),
|
||||
[]string{"alpha", "bravo"},
|
||||
false,
|
||||
)
|
||||
if err == nil {
|
||||
t.Errorf("failed to return an error on malformed front-matter")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzTagged tests the Tagged function with potentially malicious tag inputs
|
||||
//
|
||||
// Threat model: An attacker crafts a malicious cheatsheet with specially
|
||||
// crafted tags that could cause issues when a user searches/filters by tags.
|
||||
// This is particularly relevant for shared community cheatsheets.
|
||||
func FuzzTagged(f *testing.F) {
|
||||
// Add seed corpus with potentially problematic inputs
|
||||
// These represent tags an attacker might use in a malicious cheatsheet
|
||||
f.Add("normal", "normal")
|
||||
f.Add("", "")
|
||||
f.Add(" ", " ")
|
||||
f.Add("\n", "\n")
|
||||
f.Add("\r\n", "\r\n")
|
||||
f.Add("\x00", "\x00") // Null byte
|
||||
f.Add("../../etc/passwd", "../../etc/passwd") // Path traversal attempt
|
||||
f.Add("'; DROP TABLE sheets;--", "sql") // SQL injection attempt
|
||||
f.Add("<script>alert('xss')</script>", "xss") // XSS attempt
|
||||
f.Add("${HOME}", "${HOME}") // Environment variable
|
||||
f.Add("$(whoami)", "$(whoami)") // Command substitution
|
||||
f.Add("`date`", "`date`") // Command substitution
|
||||
f.Add("\\x41\\x42", "\\x41\\x42") // Escape sequences
|
||||
f.Add("%00", "%00") // URL encoded null
|
||||
f.Add("tag\nwith\nnewlines", "tag")
|
||||
f.Add(strings.Repeat("a", 10000), "a") // Very long tag
|
||||
f.Add("🎉", "🎉") // Unicode
|
||||
f.Add("\U0001F4A9", "\U0001F4A9") // Unicode poop emoji
|
||||
f.Add("tag with spaces", "tag with spaces")
|
||||
f.Add("TAG", "tag") // Case sensitivity check
|
||||
f.Add("tag", "TAG") // Case sensitivity check
|
||||
|
||||
f.Fuzz(func(t *testing.T, sheetTag string, searchTag string) {
|
||||
// Create a sheet with the potentially malicious tag
|
||||
sheet := Sheet{
|
||||
Title: "test",
|
||||
Tags: []string{sheetTag},
|
||||
}
|
||||
|
||||
// The Tagged function should never panic regardless of input
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Tagged panicked with sheetTag=%q, searchTag=%q: %v",
|
||||
sheetTag, searchTag, r)
|
||||
}
|
||||
}()
|
||||
|
||||
result := sheet.Tagged(searchTag)
|
||||
|
||||
// Verify the result is consistent with a simple string comparison
|
||||
expected := false
|
||||
for _, tag := range sheet.Tags {
|
||||
if tag == searchTag {
|
||||
expected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Tagged returned %v but expected %v for sheetTag=%q, searchTag=%q",
|
||||
result, expected, sheetTag, searchTag)
|
||||
}
|
||||
|
||||
// Additional invariant: Tagged should be case-sensitive
|
||||
if sheetTag != searchTag && result {
|
||||
t.Errorf("Tagged matched different strings: sheetTag=%q, searchTag=%q",
|
||||
sheetTag, searchTag)
|
||||
}
|
||||
}()
|
||||
|
||||
// Test with multiple tags including the fuzzed one
|
||||
sheetMulti := Sheet{
|
||||
Title: "test",
|
||||
Tags: []string{"safe1", sheetTag, "safe2", sheetTag}, // Duplicate tags
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Tagged panicked with multiple tags including %q: %v",
|
||||
sheetTag, r)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = sheetMulti.Tagged(searchTag)
|
||||
}()
|
||||
})
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
go test fuzz v1
|
||||
string("0")
|
||||
int(-6)
|
||||
int(5)
|
||||
@@ -1,3 +0,0 @@
|
||||
go test fuzz v1
|
||||
string(".")
|
||||
string(" 0000\n\n\n\n00000")
|
||||
@@ -1,5 +1,3 @@
|
||||
// Package sheets implements functions pertaining to loading, sorting,
|
||||
// filtering, and tagging cheatsheets.
|
||||
package sheets
|
||||
|
||||
import (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user