Compare commits

..

No commits in common. "master" and "3.0.6" have entirely different histories.

1821 changed files with 144583 additions and 278433 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,19 +0,0 @@
---
name: homebrew
on:
push:
tags: '*'
jobs:
homebrew:
name: Bump Homebrew formula
runs-on: ubuntu-latest
steps:
- uses: mislav/bump-homebrew-formula-action@v1
with:
# A PR will be sent to github.com/Homebrew/homebrew-core to update
# this formula:
formula-name: cheat
env:
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}

1
.gitignore vendored
View File

@ -1,2 +1 @@
dist
tags

4
.travis.yml Normal file
View File

@ -0,0 +1,4 @@
language: go
go:
- 1.13.x

View File

@ -19,8 +19,7 @@ tracker][issues] to discuss with the maintainer whether it would be considered
for merging.
`cheat` is mostly mature and feature-complete, but may still have some room for
new features. See [HACKING.md][hacking] for a quick-start guide to `cheat`
development.
new features.
#### Add documentation ####
Did you encounter features, bugs, edge-cases, use-cases, or environment
@ -36,13 +35,9 @@ 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.
#### Pull Requests ####
Please open all pull-requests against the `develop` branch.
[cheat]: https://github.com/cheat/cheat
[cheatsheets]: https://github.com/cheat/cheatsheets
[hacking]: HACKING.md
[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

View File

@ -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

View File

@ -1,57 +0,0 @@
Hacking
=======
The following is a quickstart guide for developing `cheat`.
## 1. Install system dependencies
Before you begin, you must install a handful of system dependencies. The
following are required, and must be available on your `PATH`:
- `git`
- `go` (>= 1.17 is recommended)
- `make`
The following dependencies are optional:
- `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
After your environment has been configured, your development workflow will
resemble the following:
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`. 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.
### 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.
If you would like to build the docker container, run:
```sh
make docker-setup
```
To shell into the container, run:
```sh
make docker-sh
```
The `cheat` source code will be mounted at `/app` within the container.
If you would like to destroy this container, you may run:
```sh
make distclean
```
[go]: https://go.dev/

View File

@ -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.4.2/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.4.2`) and the archive
(`cheat-linux-amd64.gz`) depending on your platform.
See the [releases page][releases] for a list of supported platforms.
#### Windows
TODO: community support is requested here. Please open a PR if you'd like to
contribute installation instructions for Windows.
### 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

218
Makefile
View File

@ -1,218 +0,0 @@
# paths
makefile := $(realpath $(lastword $(MAKEFILE_LIST)))
cmd_dir := ./cmd/cheat
dist_dir := ./dist
# executables
CAT := cat
COLUMN := column
CTAGS := ctags
DOCKER := docker
GO := go
GREP := grep
GZIP := gzip --best
LINT := revive
MAN := man
MKDIR := mkdir -p
PANDOC := pandoc
RM := rm
SCC := scc
SED := sed
SORT := sort
ZIP := zip -m
docker_image := cheat-devel:latest
# build flags
BUILD_FLAGS := -ldflags="-s -w" -mod vendor -trimpath
GOBIN :=
TMPDIR := /tmp
# release binaries
releases := \
$(dist_dir)/cheat-darwin-amd64 \
$(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) generate fmt lint vet vendor man
$(GO) build $(BUILD_FLAGS) -o $(dist_dir)/cheat $(cmd_dir)
## build-release: build release executables
.PHONY: build-release
build-release: $(releases)
# 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-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: prepare
GOARCH=amd64 GOOS=linux \
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
# 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: 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: 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: prepare
GOARCH=arm64 GOOS=linux \
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
# cheat-netbsd-amd64
$(dist_dir)/cheat-netbsd-amd64: prepare
GOARCH=amd64 GOOS=netbsd \
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
# cheat-openbsd-amd64
$(dist_dir)/cheat-openbsd-amd64: prepare
GOARCH=amd64 GOOS=openbsd \
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
# cheat-plan9-amd64
$(dist_dir)/cheat-plan9-amd64: prepare
GOARCH=amd64 GOOS=plan9 \
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
# cheat-solaris-amd64
$(dist_dir)/cheat-solaris-amd64: prepare
GOARCH=amd64 GOOS=solaris \
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
# cheat-windows-amd64
$(dist_dir)/cheat-windows-amd64.exe: prepare
GOARCH=amd64 GOOS=windows \
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(ZIP) $@.zip $@ -j
# ./dist
$(dist_dir):
$(MKDIR) $(dist_dir)
.PHONY: generate
generate:
$(GO) generate $(cmd_dir)
## install: build and install cheat on your PATH
.PHONY: install
install: build
$(GO) install $(BUILD_FLAGS) $(GOBIN) $(cmd_dir)
## clean: remove compiled executables
.PHONY: clean
clean:
$(RM) -f $(dist_dir)/* $(cmd_dir)/str_config.go $(cmd_dir)/str_usage.go
## 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:
GO111MODULE=off $(GO) get -u github.com/boyter/scc github.com/mgechev/revive
## sloc: count "semantic lines of code"
.PHONY: sloc
sloc:
$(SCC) --exclude-dir=vendor
## tags: build a tags file
.PHONY: tags
tags:
$(CTAGS) -R --exclude=vendor --languages=go
## man: build a man page
# NB: pandoc may not be installed, so we're ignoring this error on failure
.PHONY: man
man:
-$(PANDOC) -s -t man doc/cheat.1.md -o doc/cheat.1
## vendor: download, tidy, and verify dependencies
.PHONY: vendor
vendor:
$(GO) mod vendor && $(GO) mod tidy && $(GO) mod verify
## vendor-update: update vendored dependencies
vendor-update:
$(GO) get -t -u ./... && $(GO) mod vendor && $(GO) mod tidy && $(GO) mod verify
## fmt: run go fmt
.PHONY: fmt
fmt:
$(GO) fmt ./...
## lint: lint go source files
.PHONY: lint
lint: vendor
$(LINT) -exclude vendor/... ./...
## vet: vet go source files
.PHONY: vet
vet:
$(GO) vet ./...
## test: run unit-tests
.PHONY: test
test:
$(GO) test ./...
## coverage: generate a test coverage report
.PHONY: coverage
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) generate 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
## help: display this help text
.PHONY: help
help:
@$(CAT) $(makefile) | \
$(SORT) | \
$(GREP) "^##" | \
$(SED) 's/## //g' | \
$(COLUMN) -t -s ':'

177
README.md
View File

@ -1,9 +1,8 @@
![Workflow status](https://github.com/cheat/cheat/actions/workflows/build.yml/badge.svg)
cheat
=====
[![Build Status](https://travis-ci.com/cheat/cheat.svg?branch=master)](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,91 @@ 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 can be generated with `cheat --init`:
```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].
Cheatpaths
----------
Cheatsheets are stored on "cheatpaths", which are directories that contain
cheetsheets. 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.
Usage
-----
To view a cheatsheet:
@ -102,92 +186,13 @@ 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 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.
Autocompletion
Advanced Usage
--------------
Shell autocompletion is currently available for `bash`, `fish`, and `zsh`. Copy
the relevant [completion script][completions] into the appropriate directory on
your filesystem to enable autocompletion. (This directory will vary depending
on operating system and shell specifics.)
`cheat` may be integrated with [fzf][]. See [fzf.bash][bash] for instructions.
(Support for other shells will be added in future releases.)
Additionally, `cheat` supports enhanced autocompletion via integration with
[fzf][]. To enable `fzf` integration:
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
[bash]: https://github.com/cheat/cheat/blob/master/scripts/fzf.bash
[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

13
bin/build_devel.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
# locate the cheat project root
BINDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APPDIR=$(readlink -f "$BINDIR/..")
# compile the executable
cd "$APPDIR/cmd/cheat"
go clean && go generate && go build -mod vendor
mv "$APPDIR/cmd/cheat/cheat" "$APPDIR/dist/cheat"
# display a build checksum
md5sum "$APPDIR/dist/cheat"

19
bin/build_release.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
# locate the cheat project root
BINDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APPDIR=$(readlink -f "$BINDIR/..")
# build embeds
cd "$APPDIR/cmd/cheat"
go clean && go generate
# compile AMD64 for Linux, OSX, and Windows
env GOOS=darwin GOARCH=amd64 go build -mod vendor -o \
"$APPDIR/dist/cheat-darwin-amd64" "$APPDIR/cmd/cheat"
env GOOS=linux GOARCH=amd64 go build -mod vendor -o \
"$APPDIR/dist/cheat-linux-amd64" "$APPDIR/cmd/cheat"
env GOOS=windows GOARCH=amd64 go build -mod vendor -o \
"$APPDIR/dist/cheat-win-amd64.exe" "$APPDIR/cmd/cheat"

View File

@ -1,4 +1,3 @@
//go:build ignore
// +build ignore
// This script embeds `docopt.txt and `conf.yml` into the binary during at
@ -6,11 +5,13 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
)
@ -51,10 +52,10 @@ func main() {
for _, file := range files {
// delete the outfile
os.Remove(filepath.Join(root, file.Out))
os.Remove(path.Join(root, file.Out))
// read the static template
bytes, err := ioutil.ReadFile(filepath.Join(root, file.In))
bytes, err := ioutil.ReadFile(path.Join(root, file.In))
if err != nil {
log.Fatal(err)
}
@ -63,7 +64,7 @@ func main() {
data := template(file.Method, string(bytes))
// write the file to the specified outpath
spath := filepath.Join(root, file.Out)
spath := path.Join(root, file.Out)
err = ioutil.WriteFile(spath, []byte(data), 0644)
if err != nil {
log.Fatal(err)

View File

@ -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)
}

View File

@ -1,27 +1,28 @@
package main
import (
"bytes"
"fmt"
"os"
"text/tabwriter"
"github.com/cheat/cheat/internal/config"
"github.com/cheat/cheat/internal/display"
)
// 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
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
// 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)
}

View File

@ -4,7 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"path"
"strings"
"github.com/cheat/cheat/internal/cheatpath"
@ -20,7 +20,7 @@ func cmdEdit(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)
}
@ -58,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)
@ -87,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)
@ -99,15 +99,8 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
}
}
// split `conf.Editor` into parts to separate the editor's executable from
// any arguments it may have been passed. If this is not done, the nearby
// call to `exec.Command` will fail.
parts := strings.Fields(conf.Editor)
editor := parts[0]
args := append(parts[1:], editpath)
// edit the cheatsheet
cmd := exec.Command(editor, args...)
cmd := exec.Command(conf.Editor, editpath)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr

View File

@ -2,56 +2,9 @@ package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/mitchellh/go-homedir"
"github.com/cheat/cheat/internal/config"
)
// cmdInit displays an example config file.
func cmdInit() {
// get the user's home directory
home, err := homedir.Dir()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to get user home directory: %v\n", err)
os.Exit(1)
}
// read the envvars into a map of strings
envvars := map[string]string{}
for _, e := range os.Environ() {
pair := strings.SplitN(e, "=", 2)
envvars[pair[0]] = pair[1]
}
// load the config template
configs := configs()
// identify the os-specifc paths at which configs may be located
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read config paths: %v\n", err)
os.Exit(1)
}
// determine the appropriate paths for config data and (optional) community
// cheatsheets based on the user's platform
confpath := confpaths[0]
confdir := filepath.Dir(confpath)
// create paths for community and personal cheatsheets
community := filepath.Join(confdir, "cheatsheets", "community")
personal := filepath.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)
// output the templated configs
fmt.Println(configs)
fmt.Println(configs())
}

View File

@ -1,16 +1,13 @@
package main
import (
"bytes"
"fmt"
"os"
"regexp"
"sort"
"strings"
"text/tabwriter"
"github.com/cheat/cheat/internal/config"
"github.com/cheat/cheat/internal/display"
"github.com/cheat/cheat/internal/sheet"
"github.com/cheat/cheat/internal/sheets"
)
@ -21,11 +18,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 +34,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)
}
}
@ -48,54 +45,25 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
return flattened[i].Title < flattened[j].Title
})
// filter if <cheatsheet> was specified
// NB: our docopt specification is misleading here. When used in conjunction
// with `-l`, `<cheatsheet>` is really a pattern against which to filter
// sheet titles.
if opts["<cheatsheet>"] != nil {
// initialize a slice of filtered sheets
filtered := []sheet.Sheet{}
// initialize our filter pattern
pattern := "(?i)" + opts["<cheatsheet>"].(string)
// compile the regex
reg, err := regexp.Compile(pattern)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
os.Exit(1)
}
// iterate over each cheatsheet, and pass-through those which match the
// filter pattern
for _, s := range flattened {
if reg.MatchString(s.Title) {
filtered = append(filtered, s)
}
}
flattened = filtered
}
// return exit code 2 if no cheatsheets are available
// exit early if no cheatsheets are available
if len(flattened) == 0 {
os.Exit(2)
os.Exit(0)
}
// initialize a tabwriter to produce cleanly columnized output
var out bytes.Buffer
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
// write a header row
fmt.Fprintln(w, "title:\tfile:\ttags:")
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
// generate sorted, columnized output
fmt.Fprintln(w, "title:\tfile:\ttags:")
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)
}

View File

@ -1,55 +0,0 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/cheat/cheat/internal/config"
"github.com/cheat/cheat/internal/sheets"
)
// cmdRemove opens a cheatsheet for editing (or creates it if it doesn't exist).
func cmdRemove(opts map[string]interface{}, conf config.Config) {
cheatsheet := opts["--rm"].(string)
// load the cheatsheets
cheatsheets, err := sheets.Load(conf.Cheatpaths)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
os.Exit(1)
}
// filter cheatcheats by tag if --tag was provided
if opts["--tag"] != nil {
cheatsheets = sheets.Filter(
cheatsheets,
strings.Split(opts["--tag"].(string), ","),
)
}
// 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
sheet, ok := consolidated[cheatsheet]
if !ok {
fmt.Fprintf(os.Stderr, "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)
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)
os.Exit(1)
}
}

View File

@ -7,7 +7,6 @@ import (
"strings"
"github.com/cheat/cheat/internal/config"
"github.com/cheat/cheat/internal/display"
"github.com/cheat/cheat/internal/sheets"
)
@ -19,7 +18,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,17 +30,18 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
)
}
// iterate over each cheatpath
out := ""
for _, pathcheats := range 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)
// sort the cheatsheets alphabetically, and search for matches
for _, sheet := range sheets.Sort(pathcheats) {
for _, sheet := range sheets.Sort(consolidated) {
// if <cheatsheet> was provided, constrain the search only to
// matching cheatsheets
if opts["<cheatsheet>"] != nil && sheet.Title != opts["<cheatsheet>"] {
continue
// colorize output?
colorize := false
if conf.Colorize == true || opts["--colorize"] == true {
colorize = true
}
// assume that we want to perform a case-insensitive search for <phrase>
@ -55,43 +55,21 @@ func cmdSearch(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.Errorf("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)
// search the sheet
matches := sheet.Search(reg, colorize)
// if the sheet did not match the search, ignore it and move on
if sheet.Text == "" {
continue
// display the results
if len(matches) > 0 {
fmt.Printf("%s:\n", sheet.Title)
for _, m := range matches {
fmt.Printf(" %d: %s\n", m.Line, m.Text)
}
// 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),
)
fmt.Print("\n")
}
}
// trim superfluous newlines
out = strings.TrimSpace(out)
// 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)
}

View File

@ -1,30 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/cheat/cheat/internal/config"
"github.com/cheat/cheat/internal/display"
"github.com/cheat/cheat/internal/sheets"
)
// cmdTags lists all tags in use.
func cmdTags(_ 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)
os.Exit(1)
}
// assemble the output
out := ""
for _, tag := range sheets.Tags(cheatsheets) {
out += fmt.Sprintln(tag)
}
// display the output
display.Write(out, conf)
}

View File

@ -5,8 +5,10 @@ import (
"os"
"strings"
"github.com/alecthomas/chroma/quick"
"github.com/mattn/go-isatty"
"github.com/cheat/cheat/internal/config"
"github.com/cheat/cheat/internal/display"
"github.com/cheat/cheat/internal/sheets"
)
@ -18,7 +20,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,53 +32,54 @@ 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
sheet, ok := consolidated[cheatsheet]
if !ok {
fmt.Printf("No cheatsheet found for '%s'.\n", cheatsheet)
os.Exit(2)
os.Exit(0)
}
// apply colorization if requested
if conf.Color(opts) {
sheet.Colorize(conf)
// apply colorization if so configured ...
colorize := conf.Colorize
// ... or if --colorized were passed ...
if opts["--colorize"] == true {
colorize = true
}
// display the cheatsheet
display.Write(sheet.Text, conf)
// ... unless we're outputting to a non-TTY
if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
colorize = false
}
if !colorize {
fmt.Print(sheet.Text)
os.Exit(0)
}
// otherwise, colorize the output
// if the syntax was not specified, default to bash
lex := sheet.Syntax
if lex == "" {
lex = "bash"
}
// apply syntax highlighting
err = quick.Highlight(
os.Stdout,
sheet.Text,
lex,
conf.Formatter,
conf.Style,
)
// if colorization somehow failed, output non-colorized text
if err != nil {
fmt.Print(sheet.Text)
}
}

View File

@ -3,19 +3,15 @@ Usage:
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>
-e --edit=<sheet> 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:
@ -37,12 +33,6 @@ Examples:
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
@ -51,9 +41,3 @@ Examples:
To search (by regex) for cheatsheets that contain an IP address:
cheat -c -r -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
To remove (delete) the foo/bar cheatsheet:
cheat --rm foo/bar
To view the configuration file path:
cheat --conf

View File

@ -1,4 +1,3 @@
// Package main serves as the executable entrypoint.
package main
//go:generate go run ../../build/embed.go
@ -7,22 +6,19 @@ import (
"fmt"
"os"
"runtime"
"strings"
"github.com/docopt/docopt-go"
"github.com/mitchellh/go-homedir"
"github.com/cheat/cheat/internal/cheatpath"
"github.com/cheat/cheat/internal/config"
"github.com/cheat/cheat/internal/installer"
)
const version = "4.4.2"
const version = "3.0.6"
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))
@ -35,63 +31,13 @@ func main() {
os.Exit(0)
}
// get the user's home directory
home, err := homedir.Dir()
// load the config file
confpath, err := config.Path(runtime.GOOS)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to get user home directory: %v\n", err)
fmt.Fprintln(os.Stderr, "could not locate config file")
os.Exit(1)
}
// read the envvars into a map of strings
envvars := map[string]string{}
for _, e := range os.Environ() {
pair := strings.SplitN(e, "=", 2)
if runtime.GOOS == "windows" {
pair[0] = strings.ToUpper(pair[0])
}
envvars[pair[0]] = pair[1]
}
// identify the os-specifc paths at which configs may be located
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err)
os.Exit(1)
}
// search for the config file in the above paths
confpath, err := config.Path(confpaths)
if err != nil {
// prompt the user to create a config file
yes, err := installer.Prompt(
"A config file was not found. Would you like to create one now? [Y/n]",
true,
)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create config: %v\n", err)
os.Exit(1)
}
// exit early on a negative answer
if !yes {
os.Exit(0)
}
// choose a confpath
confpath = confpaths[0]
// run the installer
if err := installer.Run(configs(), confpath); err != nil {
fmt.Fprintf(os.Stderr, "failed to run installer: %v\n", 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)
}
// initialize the configs
conf, err := config.New(opts, confpath, true)
if err != nil {
@ -121,9 +67,6 @@ func main() {
var cmd func(map[string]interface{}, config.Config)
switch {
case opts["--conf"].(bool):
cmd = cmdConf
case opts["--directories"].(bool):
cmd = cmdDirectories
@ -133,21 +76,12 @@ func main() {
case opts["--list"].(bool):
cmd = cmdList
case opts["--tags"].(bool):
cmd = cmdTags
case opts["--search"] != nil:
cmd = cmdSearch
case opts["--rm"] != nil:
cmd = cmdRemove
case opts["<cheatsheet>"] != nil:
cmd = cmdView
case opts["--tag"] != nil && opts["--tag"].(string) != "":
cmd = cmdList
default:
fmt.Println(usage())
os.Exit(0)

View File

@ -9,10 +9,10 @@ import (
func configs() string {
return strings.TrimSpace(`---
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
editor: EDITOR_PATH
editor: vim
# Should 'cheat' always colorize output?
colorize: false
colorize: true
# Which 'chroma' colorscheme should be applied to the output?
# Options are available here:
@ -21,73 +21,45 @@ style: monokai
# Which 'chroma' "formatter" should be applied?
# One of: "terminal", "terminal256", "terminal16m"
formatter: terminal256
formatter: terminal16m
# Through which pager should output be piped?
# 'less -FRX' is recommended on Unix systems
# 'more' is recommended on Windows
pager: PAGER_PATH
# Cheatpaths are paths at which cheatsheets are available on your local
# filesystem.
# The paths at which cheatsheets are available. Tags associated with a cheatpath
# are automatically attached to all cheatsheets residing on that path.
#
# It is useful to sort cheatsheets into different cheatpaths for organizational
# purposes. For example, you might want one cheatpath for community
# cheatsheets, one for personal cheatsheets, one for cheatsheets pertaining to
# your day job, one for code snippets, etc.
# 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.
#
# Cheatpaths are scoped, such that more "local" cheatpaths take priority over
# more "global" cheatpaths. (The most global cheatpath is listed first in this
# file; the most local is listed last.) For example, if there is a 'tar'
# cheatsheet on both global and local paths, you'll be presented with the local
# one by default. ('cheat -p' can be used to view cheatsheets from alternative
# cheatpaths.)
#
# Cheatpaths can also be tagged as "read only". This instructs cheat not to
# automatically create cheatsheets on a read-only cheatpath. Instead, when you
# would like to edit a read-only cheatsheet using 'cheat -e', cheat will
# perform a copy-on-write of that cheatsheet from a read-only cheatpath to a
# writeable cheatpath.
#
# This is very useful when you would like to maintain, for example, a
# "pristine" repository of community cheatsheets on one cheatpath, and an
# editable personal reponsity of cheatsheets on another cheatpath.
#
# Cheatpaths can be also configured to automatically apply tags to cheatsheets
# on certain paths, which can be useful for querying purposes.
# Example: 'cheat -t work jenkins'.
#
# Community cheatsheets must be installed separately, though you may have
# downloaded them automatically when installing 'cheat'. If not, you may
# download them here:
#
# https://github.com/cheat/cheatsheets
# 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:
# Cheatpath properties mean the following:
# 'name': the name of the cheatpath (view with 'cheat -d', filter with 'cheat -p')
# 'path': the filesystem path of the cheatsheet directory (view with 'cheat -d')
# 'tags': tags that should be automatically applied to sheets on this path
# 'readonly': shall user-created ('cheat -e') cheatsheets be saved here?
# 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 just examples. You may freely
# change them to suit your needs.
- name: community
path: COMMUNITY_PATH
path: ~/.dotfiles/cheat/community
tags: [ community ]
readonly: true
# Maybe your company or department maintains a repository of cheatsheets as
# well. It's probably sensible to list those second.
- name: work
path: ~/.dotfiles/cheat/work
tags: [ work ]
readonly: false
# If you have personalized cheatsheets, list them last. They will take
# precedence over the more global cheatsheets.
- name: personal
path: PERSONAL_PATH
path: ~/.dotfiles/cheat/personal
tags: [ personal ]
readonly: false
# While it requires no configuration here, it's also worth noting that
# cheat will automatically append directories named '.cheat' within the
# current working directory to the 'cheatpath'. This can be very useful if
# you'd like to closely associate cheatsheets with, for example, a directory
# containing source code.
#
# Such "directory-scoped" cheatsheets will be treated as the most "local"
# cheatsheets, and will override less "local" cheatsheets. Similarly,
# directory-scoped cheatsheets will always be editable ('readonly: false').
`)
}

View File

@ -12,19 +12,15 @@ func usage() string {
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>
-e --edit=<sheet> 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:
@ -46,12 +42,6 @@ Examples:
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
@ -60,11 +50,5 @@ Examples:
To search (by regex) for cheatsheets that contain an IP address:
cheat -c -r -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
To remove (delete) the foo/bar cheatsheet:
cheat --rm foo/bar
To view the configuration file path:
cheat --conf
`)
}

View File

@ -1,9 +1,9 @@
---
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
editor: EDITOR_PATH
editor: vim
# Should 'cheat' always colorize output?
colorize: false
colorize: true
# Which 'chroma' colorscheme should be applied to the output?
# Options are available here:
@ -12,71 +12,43 @@ style: monokai
# Which 'chroma' "formatter" should be applied?
# One of: "terminal", "terminal256", "terminal16m"
formatter: terminal256
formatter: terminal16m
# Through which pager should output be piped?
# 'less -FRX' is recommended on Unix systems
# 'more' is recommended on Windows
pager: PAGER_PATH
# Cheatpaths are paths at which cheatsheets are available on your local
# filesystem.
# The paths at which cheatsheets are available. Tags associated with a cheatpath
# are automatically attached to all cheatsheets residing on that path.
#
# It is useful to sort cheatsheets into different cheatpaths for organizational
# purposes. For example, you might want one cheatpath for community
# cheatsheets, one for personal cheatsheets, one for cheatsheets pertaining to
# your day job, one for code snippets, etc.
# 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.
#
# Cheatpaths are scoped, such that more "local" cheatpaths take priority over
# more "global" cheatpaths. (The most global cheatpath is listed first in this
# file; the most local is listed last.) For example, if there is a 'tar'
# cheatsheet on both global and local paths, you'll be presented with the local
# one by default. ('cheat -p' can be used to view cheatsheets from alternative
# cheatpaths.)
#
# Cheatpaths can also be tagged as "read only". This instructs cheat not to
# automatically create cheatsheets on a read-only cheatpath. Instead, when you
# would like to edit a read-only cheatsheet using 'cheat -e', cheat will
# perform a copy-on-write of that cheatsheet from a read-only cheatpath to a
# writeable cheatpath.
#
# This is very useful when you would like to maintain, for example, a
# "pristine" repository of community cheatsheets on one cheatpath, and an
# editable personal reponsity of cheatsheets on another cheatpath.
#
# Cheatpaths can be also configured to automatically apply tags to cheatsheets
# on certain paths, which can be useful for querying purposes.
# Example: 'cheat -t work jenkins'.
#
# Community cheatsheets must be installed separately, though you may have
# downloaded them automatically when installing 'cheat'. If not, you may
# download them here:
#
# https://github.com/cheat/cheatsheets
# 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:
# Cheatpath properties mean the following:
# 'name': the name of the cheatpath (view with 'cheat -d', filter with 'cheat -p')
# 'path': the filesystem path of the cheatsheet directory (view with 'cheat -d')
# 'tags': tags that should be automatically applied to sheets on this path
# 'readonly': shall user-created ('cheat -e') cheatsheets be saved here?
# 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 just examples. You may freely
# change them to suit your needs.
- name: community
path: COMMUNITY_PATH
path: ~/.dotfiles/cheat/community
tags: [ community ]
readonly: true
# Maybe your company or department maintains a repository of cheatsheets as
# well. It's probably sensible to list those second.
- name: work
path: ~/.dotfiles/cheat/work
tags: [ work ]
readonly: false
# If you have personalized cheatsheets, list them last. They will take
# precedence over the more global cheatsheets.
- name: personal
path: PERSONAL_PATH
path: ~/.dotfiles/cheat/personal
tags: [ personal ]
readonly: false
# While it requires no configuration here, it's also worth noting that
# cheat will automatically append directories named '.cheat' within the
# current working directory to the 'cheatpath'. This can be very useful if
# you'd like to closely associate cheatsheets with, for example, a directory
# containing source code.
#
# Such "directory-scoped" cheatsheets will be treated as the most "local"
# cheatsheets, and will override less "local" cheatsheets. Similarly,
# directory-scoped cheatsheets will always be editable ('readonly: false').

View File

@ -1,186 +0,0 @@
.\" Automatically generated by Pandoc 2.17.1.1
.\"
.\" Define V font for inline verbatim, using C font in formats
.\" that render this, and otherwise B font.
.ie "\f[CB]x\f[]"x" \{\
. ftr V B
. ftr VI BI
. ftr VB B
. ftr VBI BI
.\}
.el \{\
. ftr V CR
. ftr VI CI
. ftr VB CB
. ftr VBI CBI
.\}
.TH "CHEAT" "1" "" "" "General Commands Manual"
.hy
.SH NAME
.PP
\f[B]cheat\f[R] \[em] create and view command-line cheatsheets
.SH SYNOPSIS
.PP
\f[B]cheat\f[R] [options] [\f[I]CHEATSHEET\f[R]]
.SH DESCRIPTION
.PP
\f[B]cheat\f[R] 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
Print a config file to stdout.
.TP
-c, \[en]colorize
Colorize output.
.TP
-d, \[en]directories
List cheatsheet directories.
.TP
-e, \[en]edit=\f[I]CHEATSHEET\f[R]
Open \f[I]CHEATSHEET\f[R] for editing.
.TP
-l, \[en]list
List available cheatsheets.
.TP
-p, \[en]path=\f[I]PATH\f[R]
Filter only to sheets found on path \f[I]PATH\f[R].
.TP
-r, \[en]regex
Treat search \f[I]PHRASE\f[R] as a regular expression.
.TP
-s, \[en]search=\f[I]PHRASE\f[R]
Search cheatsheets for \f[I]PHRASE\f[R].
.TP
-t, \[en]tag=\f[I]TAG\f[R]
Filter only to sheets tagged with \f[I]TAG\f[R].
.TP
-T, \[en]tags
List all tags in use.
.TP
-v, \[en]version
Print the version number.
.TP
\[en]rm=\f[I]CHEATSHEET\f[R]
Remove (deletes) \f[I]CHEATSHEET\f[R].
.SH EXAMPLES
.TP
To view the foo cheatsheet:
cheat \f[I]foo\f[R]
.TP
To edit (or create) the foo cheatsheet:
cheat -e \f[I]foo\f[R]
.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]
.TP
To view all cheatsheet directories:
cheat -d
.TP
To list all available cheatsheets:
cheat -l
.TP
To list all cheatsheets whose titles match `apt':
cheat -l \f[I]apt\f[R]
.TP
To list all tags in use:
cheat -T
.TP
To list available cheatsheets that are tagged as `personal':
cheat -l -t \f[I]personal\f[R]
.TP
To search for `ssh' among all cheatsheets, and colorize matches:
cheat -c -s \f[I]ssh\f[R]
.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]
.TP
To remove (delete) the foo/bar cheatsheet:
cheat \[en]rm \f[I]foo/bar\f[R]
.SH FILES
.SS Configuration
.PP
\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:
.SS Linux, OSX, and other Unixes
.IP "1." 3
\f[B]CHEAT_CONFIG_PATH\f[R]
.IP "2." 3
\f[B]XDG_CONFIG_HOME\f[R]/cheat/conf.yaml
.IP "3." 3
\f[B]$HOME\f[R]/.config/cheat/conf.yml
.IP "4." 3
\f[B]$HOME\f[R]/.cheat/conf.yml
.SS Windows
.IP "1." 3
\f[B]CHEAT_CONFIG_PATH\f[R]
.IP "2." 3
\f[B]APPDATA\f[R]/cheat/conf.yml
.IP "3." 3
\f[B]PROGRAMDATA\f[R]/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.
.PP
If \f[B]cheat\f[R] cannot locate a config file, it will ask if you\[cq]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
location for your platform.
.SS Cheatpaths
.PP
\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
For detailed instructions on how to configure cheatpaths, please refer
to the comments in conf.yml.
.SS Autocompletion
.PP
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
<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[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].
.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]
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
ignored.
.TP
\f[B]CHEAT_USE_FZF\f[R]
If set, autocompletion scripts will attempt to integrate with
\f[B]fzf\f[R].
.SH RETURN VALUES
.IP "0." 3
Successful termination
.IP "1." 3
Application error
.IP "2." 3
Cheatsheet(s) not found
.SH BUGS
.PP
See GitHub issues: <https://github.com/cheat/cheat/issues>
.SH AUTHOR
.PP
Christopher Allen Lane <chris@chris-allen-lane.com>
.SH SEE ALSO
.PP
\f[B]fzf(1)\f[R]

View File

@ -1,192 +0,0 @@
% CHEAT(1) | General Commands Manual
NAME
====
**cheat** — create and view command-line cheatsheets
SYNOPSIS
========
| **cheat** \[options] \[_CHEATSHEET_]
DESCRIPTION
===========
**cheat** allows you to create and view interactive cheatsheets on the
command-line. It was designed to help remind \*nix system administrators of
options for commands that they use frequently, but not frequently enough to
remember.
OPTIONS
=======
--init
: Print a config file to stdout.
-c, --colorize
: Colorize output.
-d, --directories
: List cheatsheet directories.
-e, --edit=_CHEATSHEET_
: Open _CHEATSHEET_ for editing.
-l, --list
: List available cheatsheets.
-p, --path=_PATH_
: Filter only to sheets found on path _PATH_.
-r, --regex
: Treat search _PHRASE_ as a regular expression.
-s, --search=_PHRASE_
: Search cheatsheets for _PHRASE_.
-t, --tag=_TAG_
: Filter only to sheets tagged with _TAG_.
-T, --tags
: List all tags in use.
-v, --version
: Print the version number.
--rm=_CHEATSHEET_
: Remove (deletes) _CHEATSHEET_.
EXAMPLES
========
To view the foo cheatsheet:
: cheat _foo_
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_
FILES
=====
Configuration
-------------
**cheat** is configured via a YAML file that is conventionally named
_conf.yaml_. **cheat** will search for _conf.yaml_ in varying locations,
depending upon your platform:
### Linux, OSX, and other Unixes ###
1. **CHEAT_CONFIG_PATH**
2. **XDG_CONFIG_HOME**/cheat/conf.yaml
3. **$HOME**/.config/cheat/conf.yml
4. **$HOME**/.cheat/conf.yml
### Windows ###
1. **CHEAT_CONFIG_PATH**
2. **APPDATA**/cheat/conf.yml
3. **PROGRAMDATA**/cheat/conf.yml
**cheat** will search in the order specified above. The first _conf.yaml_
encountered will be respected.
If **cheat** cannot locate a config file, it will ask if you'd like to generate
one automatically. Alternatively, you may also generate a config file manually
by running **cheat --init** and saving its output to the appropriate location
for your platform.
Cheatpaths
----------
**cheat** reads its cheatsheets from "cheatpaths", which are the directories in
which cheatsheets are stored. Cheatpaths may be configured in _conf.yaml_, and
viewed via **cheat -d**.
For detailed instructions on how to configure cheatpaths, please refer to the
comments in conf.yml.
Autocompletion
--------------
Autocompletion scripts for **bash**, **zsh**, and **fish** are available for
download:
- <https://github.com/cheat/cheat/blob/master/scripts/cheat.bash>
- <https://github.com/cheat/cheat/blob/master/scripts/cheat.fish>
- <https://github.com/cheat/cheat/blob/master/scripts/cheat.zsh>
The **bash** and **zsh** scripts provide optional integration with **fzf**, if
the latter is available on your **PATH**.
The installation process will vary per system and shell configuration, and thus
will not be discussed here.
ENVIRONMENT
===========
**CHEAT_CONFIG_PATH**
: The path at which the config file is available. If **CHEAT_CONFIG_PATH** is
set, all other config paths will be ignored.
**CHEAT_USE_FZF**
: If set, autocompletion scripts will attempt to integrate with **fzf**.
RETURN VALUES
=============
0. Successful termination
1. Application error
2. Cheatsheet(s) not found
BUGS
====
See GitHub issues: <https://github.com/cheat/cheat/issues>
AUTHOR
======
Christopher Allen Lane <chris@chris-allen-lane.com>
SEE ALSO
========
**fzf(1)**

36
go.mod
View File

@ -1,38 +1,14 @@
module github.com/cheat/cheat
go 1.19
go 1.13
require (
github.com/alecthomas/chroma/v2 v2.12.0
github.com/alecthomas/chroma v0.6.8
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/mattn/go-isatty v0.0.10
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
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
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0
gopkg.in/yaml.v2 v2.2.4
)

185
go.sum
View File

@ -1,143 +1,66 @@
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/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
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.6.7 h1:1hKci+AyKOxJrugR9veaocu9DQGR2/GecI72BpaO0Rg=
github.com/alecthomas/chroma v0.6.7/go.mod h1:zVlgtbRS7BJDrDY9SB238RmpoCBCYFlLmcfZ3durxTk=
github.com/alecthomas/chroma v0.6.8 h1:TW4JJaIdbAbMyUtGEd6BukFlOKYvVQz3vVhLBEUNwMU=
github.com/alecthomas/chroma v0.6.8/go.mod h1:o9ohftueRi7H5be3+Q2cQCNa/YnLBFUNx40ZJfGVFKA=
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.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
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/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
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.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
github.com/dlclark/regexp2 v1.1.6/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/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/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/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.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/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.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=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/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/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.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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,11 +1,9 @@
// Package cheatpath implements functions pertaining to cheatsheet file path
// management.
package cheatpath
// Cheatpath encapsulates cheatsheet path information
type Cheatpath struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
ReadOnly bool `yaml:"readonly"`
Tags []string `yaml:"tags"`
Name string `yaml:name`
Path string `yaml:path`
ReadOnly bool `yaml:readonly`
Tags []string `yaml:tags`
}

View File

@ -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")
}

View File

@ -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

View File

@ -1,26 +0,0 @@
package config
import (
"os"
"github.com/mattn/go-isatty"
)
// Color indicates whether colorization should be applied to the output
func (c *Config) Color(opts map[string]interface{}) bool {
// default to the colorization specified in the configs...
colorize := c.Colorize
// ... however, only apply colorization if we're writing to a tty...
if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
colorize = false
}
// ... *unless* the --colorize flag was passed
if opts["--colorize"] == true {
colorize = true
}
return colorize
}

View File

@ -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)")
}
}

View File

@ -1,34 +1,31 @@
// Package config implements functions pertaining to configuration management.
package config
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
cp "github.com/cheat/cheat/internal/cheatpath"
"github.com/mitchellh/go-homedir"
"gopkg.in/yaml.v3"
"gopkg.in/yaml.v2"
)
// Config encapsulates configuration parameters
type Config struct {
Colorize bool `yaml:"colorize"`
Editor string `yaml:"editor"`
Cheatpaths []cp.Cheatpath `yaml:"cheatpaths"`
Style string `yaml:"style"`
Formatter string `yaml:"formatter"`
Pager string `yaml:"pager"`
Path string
Colorize bool `yaml:colorize`
Editor string `yaml:editor`
Cheatpaths []cp.Cheatpath `yaml:cheatpaths`
Style string `yaml:style`
Formatter string `yaml:formatter`
}
// New returns a new Config struct
func New(_ 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,33 +33,12 @@ 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)
}
// if a .cheat directory exists locally, append it to the cheatpaths
cwd, err := os.Getwd()
if err != nil {
return Config{}, fmt.Errorf("failed to get cwd: %v", err)
}
local := filepath.Join(cwd, ".cheat")
if _, err := os.Stat(local); err == nil {
path := cp.Cheatpath{
Name: "cwd",
Path: local,
ReadOnly: false,
Tags: []string{},
}
conf.Cheatpaths = append(conf.Cheatpaths, path)
}
// process cheatpaths
for i, cheatpath := range conf.Cheatpaths {
@ -81,26 +57,27 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
// `resolve` is a switch that allows us to turn off symlink resolution when
// running the config tests.
if resolve {
evaled, err := filepath.EvalSymlinks(expanded)
expanded, err = filepath.EvalSymlinks(expanded)
if err != nil {
return Config{}, fmt.Errorf(
"failed to resolve symlink: %s: %v",
"failed to resolve symlink: %s, %v",
expanded,
err,
)
}
expanded = evaled
}
conf.Cheatpaths[i].Path = expanded
}
// 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")
}
}
@ -111,11 +88,8 @@ 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)
return conf, nil
}

View File

@ -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

View File

@ -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")
}

View File

@ -1,23 +0,0 @@
package config
import (
"fmt"
"os"
"path/filepath"
)
// Init initializes a config file
func Init(confpath string, configs string) error {
// assert that the config directory exists
if err := os.MkdirAll(filepath.Dir(confpath), 0755); err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}
// write the config file
if err := os.WriteFile(confpath, []byte(configs), 0644); err != nil {
return fmt.Errorf("failed to create file: %v", err)
}
return nil
}

View File

@ -1,37 +0,0 @@
package config
import (
"os"
"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)
}
}

View File

@ -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 ""
}

View File

@ -3,10 +3,58 @@ package config
import (
"fmt"
"os"
"path"
"github.com/mitchellh/go-homedir"
)
// Path returns the config file path
func Path(paths []string) (string, error) {
func Path(sys string) (string, error) {
var paths []string
// if CHEAT_CONFIG_PATH is set, return it
if os.Getenv("CHEAT_CONFIG_PATH") != "" {
// expand ~
expanded, err := homedir.Expand(os.Getenv("CHEAT_CONFIG_PATH"))
if err != nil {
return "", fmt.Errorf("failed to expand ~: %v", err)
}
return expanded, nil
// OSX config paths
} else if sys == "darwin" {
paths = []string{
path.Join(os.Getenv("XDG_CONFIG_HOME"), "/cheat/conf.yml"),
path.Join(os.Getenv("HOME"), ".config/cheat/conf.yml"),
path.Join(os.Getenv("HOME"), ".cheat/conf.yml"),
}
// Linux config paths
} else if sys == "linux" {
paths = []string{
path.Join(os.Getenv("XDG_CONFIG_HOME"), "/cheat/conf.yml"),
path.Join(os.Getenv("HOME"), ".config/cheat/conf.yml"),
path.Join(os.Getenv("HOME"), ".cheat/conf.yml"),
"/etc/cheat/conf.yml",
}
// Windows config paths
} else if sys == "windows" {
paths = []string{
fmt.Sprintf("%s/cheat/conf.yml", os.Getenv("APPDATA")),
fmt.Sprintf("%s/cheat/conf.yml", os.Getenv("PROGRAMDATA")),
}
// Unsupported platforms
} else {
return "", fmt.Errorf("unsupported os: %s", sys)
}
// check if the config file exists on any paths
for _, p := range paths {

View File

@ -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,
)
}
}

View File

@ -1,59 +0,0 @@
package config
import (
"fmt"
"path/filepath"
"github.com/mitchellh/go-homedir"
)
// Paths returns config file paths that are appropriate for the operating
// system
func Paths(
sys string,
home string,
envvars map[string]string,
) ([]string, error) {
// if `CHEAT_CONFIG_PATH` is set, expand ~ and return it
if confpath, ok := envvars["CHEAT_CONFIG_PATH"]; ok {
// expand ~
expanded, err := homedir.Expand(confpath)
if err != nil {
return []string{}, fmt.Errorf("failed to expand ~: %v", err)
}
return []string{expanded}, nil
}
switch sys {
// darwin/linux/unix
case "aix", "android", "darwin", "dragonfly", "freebsd", "illumos", "ios",
"linux", "netbsd", "openbsd", "plan9", "solaris":
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, []string{
filepath.Join(home, ".config", "cheat", "conf.yml"),
filepath.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"),
}, nil
default:
return []string{}, fmt.Errorf("unsupported os: %s", sys)
}
}

View File

@ -1,176 +0,0 @@
package config
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
)
// TestValidatePathsNix asserts that the proper config paths are returned on
// *nix platforms
func TestValidatePathsNix(t *testing.T) {
// mock the user's home directory
home := "/home/foo"
// mock some envvars
envvars := map[string]string{
"XDG_CONFIG_HOME": "/home/bar",
}
// specify the platforms to test
oses := []string{
"android",
"darwin",
"freebsd",
"linux",
}
// test each *nix os
for _, os := range oses {
// get the paths for the platform
paths, err := Paths(os, home, envvars)
if err != nil {
t.Errorf("paths returned an error: %v", err)
}
// specify the expected output
want := []string{
"/home/bar/cheat/conf.yml",
"/home/foo/.config/cheat/conf.yml",
"/home/foo/.cheat/conf.yml",
"/etc/cheat/conf.yml",
}
// assert that output matches expectations
if !reflect.DeepEqual(paths, want) {
t.Errorf(
"failed to return expected paths: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(paths),
)
}
}
}
// TestValidatePathsNixNoXDG asserts that the proper config paths are returned
// on *nix platforms when `XDG_CONFIG_HOME is not set
func TestValidatePathsNixNoXDG(t *testing.T) {
// mock the user's home directory
home := "/home/foo"
// mock some envvars
envvars := map[string]string{}
// specify the platforms to test
oses := []string{
"darwin",
"freebsd",
"linux",
}
// test each *nix os
for _, os := range oses {
// get the paths for the platform
paths, err := Paths(os, home, envvars)
if err != nil {
t.Errorf("paths returned an error: %v", err)
}
// specify the expected output
want := []string{
"/home/foo/.config/cheat/conf.yml",
"/home/foo/.cheat/conf.yml",
"/etc/cheat/conf.yml",
}
// assert that output matches expectations
if !reflect.DeepEqual(paths, want) {
t.Errorf(
"failed to return expected paths: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(paths),
)
}
}
}
// TestValidatePathsWindows asserts that the proper config paths are returned
// on Windows platforms
func TestValidatePathsWindows(t *testing.T) {
// mock the user's home directory
home := "not-used-on-windows"
// mock some envvars
envvars := map[string]string{
"APPDATA": "/apps",
"PROGRAMDATA": "/programs",
}
// get the paths for the platform
paths, err := Paths("windows", home, envvars)
if err != nil {
t.Errorf("paths returned an error: %v", err)
}
// specify the expected output
want := []string{
"/apps/cheat/conf.yml",
"/programs/cheat/conf.yml",
}
// assert that output matches expectations
if !reflect.DeepEqual(paths, want) {
t.Errorf(
"failed to return expected paths: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(paths),
)
}
}
// TestValidatePathsUnsupported asserts that an error is returned on
// unsupported platforms
func TestValidatePathsUnsupported(t *testing.T) {
_, err := Paths("unsupported", "", map[string]string{})
if err == nil {
t.Errorf("failed to return error on unsupported platform")
}
}
// TestValidatePathsCheatConfigPath asserts that the proper config path is
// returned when `CHEAT_CONFIG_PATH` is explicitly specified.
func TestValidatePathsCheatConfigPath(t *testing.T) {
// mock the user's home directory
home := "/home/foo"
// mock some envvars
envvars := map[string]string{
"XDG_CONFIG_HOME": "/home/bar",
"CHEAT_CONFIG_PATH": "/home/baz/conf.yml",
}
// get the paths for the platform
paths, err := Paths("linux", home, envvars)
if err != nil {
t.Errorf("paths returned an error: %v", err)
}
// specify the expected output
want := []string{
"/home/baz/conf.yml",
}
// assert that output matches expectations
if !reflect.DeepEqual(paths, want) {
t.Errorf(
"failed to return expected paths: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(paths),
)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -1,36 +0,0 @@
package display
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/cheat/cheat/internal/config"
)
// Write writes output either directly to stdout, or through a pager,
// depending upon configuration.
func Write(out string, conf config.Config) {
// if no pager was configured, print the output to stdout and exit
if conf.Pager == "" {
fmt.Print(out)
os.Exit(0)
}
// otherwise, pipe output through the pager
parts := strings.Split(conf.Pager, " ")
pager := parts[0]
args := parts[1:]
// configure 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)
os.Exit(1)
}
}

View File

@ -0,0 +1,34 @@
package frontmatter
import (
"strings"
"gopkg.in/yaml.v1"
)
// Frontmatter encapsulates cheatsheet frontmatter data
type Frontmatter struct {
Tags []string
Syntax string
}
// Parse parses cheatsheet frontmatter
func Parse(markdown string) (string, Frontmatter, error) {
// specify the frontmatter delimiter
delim := "---"
// initialize a frontmatter struct
var fm Frontmatter
// if the markdown does not contain frontmatter, pass it through unmodified
if !strings.HasPrefix(markdown, delim) {
return strings.TrimSpace(markdown), fm, nil
}
// otherwise, split the frontmatter and cheatsheet text
parts := strings.SplitN(markdown, delim, 3)
err := yaml.Unmarshal([]byte(parts[1]), &fm)
return strings.TrimSpace(parts[2]), fm, err
}

View File

@ -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 {
@ -69,27 +69,3 @@ func TestHasNoFrontmatter(t *testing.T) {
t.Errorf("failed to parse tags: want: len 0, got: len %d", len(fm.Tags))
}
}
// TestHasInvalidFrontmatter asserts that markdown is properly parsed when it
// contains invalid frontmatter
func TestHasInvalidFrontmatter(t *testing.T) {
// stub our cheatsheet content (with invalid frontmatter)
markdown := `---
syntax: go
tags: [ test ]
To foo the bar: baz`
// parse the frontmatter
_, text, err := parse(markdown)
// assert that an error was returned
if err == nil {
t.Error("failed to error on invalid frontmatter")
}
// assert that the "raw" markdown was returned
if text != markdown {
t.Errorf("failed to parse text: want: %s, got: %s", markdown, text)
}
}

View File

@ -1,39 +0,0 @@
// Package installer implements functions that provide a first-time
// installation wizard.
package installer
import (
"bufio"
"fmt"
"os"
"strings"
)
// Prompt prompts the user for a answer
func Prompt(prompt string, def bool) (bool, error) {
// initialize a line reader
reader := bufio.NewReader(os.Stdin)
// display the prompt
fmt.Printf("%s: ", prompt)
// read the answer
ans, err := reader.ReadString('\n')
if err != nil {
return false, fmt.Errorf("failed to parse input: %v", err)
}
// normalize the answer
ans = strings.ToLower(strings.TrimSpace(ans))
// return the appropriate response
switch ans {
case "y":
return true, nil
case "":
return def, nil
default:
return false, nil
}
}

View File

@ -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
}

View File

@ -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

View 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 repo available at `url`
func Clone(url string) error {
// clone the community cheatsheets
_, err := git.PlainClone(url, 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
}

View File

@ -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
}

View File

@ -1 +0,0 @@
package repo

View File

@ -1,37 +0,0 @@
package sheet
import (
"bytes"
"github.com/cheat/cheat/internal/config"
"github.com/alecthomas/chroma/v2/quick"
)
// Colorize applies syntax-highlighting to a cheatsheet's Text.
func (s *Sheet) Colorize(conf config.Config) {
// if the syntax was not specified, default to bash
lex := s.Syntax
if lex == "" {
lex = "bash"
}
// write colorized text into a buffer
var buf bytes.Buffer
err := quick.Highlight(
&buf,
s.Text,
lex,
conf.Formatter,
conf.Style,
)
// if colorization somehow failed, do nothing
if err != nil {
return
}
// otherwise, swap the cheatsheet's Text with its colorized equivalent
s.Text = buf.String()
}

View File

@ -1,34 +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 := "echo"
want += " 'foo'"
// assert
if s.Text != want {
t.Errorf("failed to colorize sheet: want: %s, got: %s", want, s.Text)
}
}

View File

@ -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)

View File

@ -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)
}

7
internal/sheet/match.go Normal file
View File

@ -0,0 +1,7 @@
package sheet
// Match encapsulates search matches within cheatsheets
type Match struct {
Line int
Text string
}

View File

@ -1,45 +0,0 @@
package sheet
import (
"fmt"
"runtime"
"strings"
"gopkg.in/yaml.v3"
)
// Parse parses cheatsheet frontmatter
func parse(markdown string) (frontmatter, string, error) {
// determine the appropriate line-break for the platform
linebreak := "\n"
if runtime.GOOS == "windows" {
linebreak = "\r\n"
}
// specify the frontmatter delimiter
delim := fmt.Sprintf("---%s", linebreak)
// initialize a frontmatter struct
var fm frontmatter
// if the markdown does not contain frontmatter, pass it through unmodified
if !strings.HasPrefix(markdown, delim) {
return fm, markdown, nil
}
// otherwise, split the frontmatter and cheatsheet text
parts := strings.SplitN(markdown, delim, 3)
// 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 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 fm, parts[2], nil
}

View File

@ -3,22 +3,43 @@ package sheet
import (
"regexp"
"strings"
"github.com/mgutz/ansi"
)
// Search returns lines within a sheet's Text that match the search regex
func (s *Sheet) Search(reg *regexp.Regexp) string {
// Search searches for regexp matches in a cheatsheet's text, and optionally
// colorizes matching strings.
func (s *Sheet) Search(reg *regexp.Regexp, colorize bool) []Match {
// record matches
matches := ""
matches := []Match{}
// search through the cheatsheet's text line by line
for _, line := range strings.Split(s.Text, "\n\n") {
// TODO: searching line-by-line is surely the "naive" approach. Revisit this
// later with an eye for performance improvements.
for linenum, line := range strings.Split(s.Text, "\n") {
// exit early if the line doesn't match the regex
if reg.MatchString(line) {
matches += line + "\n\n"
}
if !reg.MatchString(line) {
continue
}
return strings.TrimSpace(matches)
// init the match
m := Match{
Line: linenum + 1,
Text: strings.TrimSpace(line),
}
// colorize the matching text if so configured
if colorize {
m.Text = reg.ReplaceAllStringFunc(m.Text, func(matched string) string {
return ansi.Color(matched, "red+b")
})
}
// record the match
matches = append(matches, m)
}
return matches
}

View File

@ -4,6 +4,8 @@ import (
"reflect"
"regexp"
"testing"
"github.com/davecgh/go-spew/spew"
)
// TestSearchNoMatch ensures that the expected output is returned when no
@ -22,21 +24,21 @@ func TestSearchNoMatch(t *testing.T) {
}
// search the sheet
matches := sheet.Search(reg)
matches := sheet.Search(reg, false)
// assert that no matches were found
if matches != "" {
t.Errorf("failure: expected no matches: got: %s", matches)
if len(matches) != 0 {
t.Errorf("failure: expected no matches: got: %s", spew.Sdump(matches))
}
}
// TestSearchSingleMatch asserts that the expected output is returned
// when a single match is returned
func TestSearchSingleMatch(t *testing.T) {
// TestSearchSingleMatchNoColor asserts that the expected output is returned
// when a single match is returned, and no colorization is applied.
func TestSearchSingleMatchNoColor(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\njumped over\n\nthe lazy dog.",
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
@ -46,28 +48,69 @@ func TestSearchSingleMatch(t *testing.T) {
}
// search the sheet
matches := sheet.Search(reg)
matches := sheet.Search(reg, false)
// specify the expected results
want := "The quick brown fox\njumped over"
want := []Match{
Match{
Line: 1,
Text: "The quick brown fox",
},
}
// assert that the correct matches were returned
if matches != want {
if !reflect.DeepEqual(matches, want) {
t.Errorf(
"failed to return expected matches: want:\n%s, got:\n%s",
want,
matches,
spew.Sdump(want),
spew.Sdump(matches),
)
}
}
// TestSearchMultiMatch asserts that the expected output is returned
// when a multiple matches are returned
func TestSearchMultiMatch(t *testing.T) {
// TestSearchSingleMatchColorized asserts that the expected output is returned
// when a single match is returned, and colorization is applied
func TestSearchSingleMatchColorized(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\n\njumped over\n\nthe lazy dog.",
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
reg, err := regexp.Compile("(?i)fox")
if err != nil {
t.Errorf("failed to compile regex: %v", err)
}
// search the sheet
matches := sheet.Search(reg, true)
// specify the expected results
want := []Match{
Match{
Line: 1,
Text: "The quick brown \x1b[1;31mfox\x1b[0m",
},
}
// assert that the correct matches were returned
if !reflect.DeepEqual(matches, want) {
t.Errorf(
"failed to return expected matches: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(matches),
)
}
}
// TestSearchMultiMatchNoColor asserts that the expected output is returned
// when a multiple matches are returned, and no colorization is applied
func TestSearchMultiMatchNoColor(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
@ -77,17 +120,66 @@ func TestSearchMultiMatch(t *testing.T) {
}
// search the sheet
matches := sheet.Search(reg)
matches := sheet.Search(reg, false)
// specify the expected results
want := "The quick brown fox\n\nthe lazy dog."
want := []Match{
Match{
Line: 1,
Text: "The quick brown fox",
},
Match{
Line: 3,
Text: "the lazy dog.",
},
}
// assert that the correct matches were returned
if !reflect.DeepEqual(matches, want) {
t.Errorf(
"failed to return expected matches: want:\n%s, got:\n%s",
want,
matches,
spew.Sdump(want),
spew.Sdump(matches),
)
}
}
// TestSearchMultiMatchColorized asserts that the expected output is returned
// when a multiple matches are returned, and colorization is applied
func TestSearchMultiMatchColorized(t *testing.T) {
// mock a cheatsheet
sheet := Sheet{
Text: "The quick brown fox\njumped over\nthe lazy dog.",
}
// compile the search regex
reg, err := regexp.Compile("(?i)the")
if err != nil {
t.Errorf("failed to compile regex: %v", err)
}
// search the sheet
matches := sheet.Search(reg, true)
// specify the expected results
want := []Match{
Match{
Line: 1,
Text: "\x1b[1;31mThe\x1b[0m quick brown fox",
},
Match{
Line: 3,
Text: "\x1b[1;31mthe\x1b[0m lazy dog.",
},
}
// assert that the correct matches were returned
if !reflect.DeepEqual(matches, want) {
t.Errorf(
"failed to return expected matches: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(matches),
)
}
}

View File

@ -1,23 +1,16 @@
// 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
@ -28,20 +21,19 @@ type Sheet struct {
// 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)
}
@ -55,9 +47,8 @@ func New(
// initialize and return a sheet
return Sheet{
Title: title,
CheatPath: cheatpath,
Path: path,
Text: text,
Text: text + "\n",
Tags: tags,
Syntax: fm.Syntax,
ReadOnly: readOnly,

View File

@ -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")
}
}

View File

@ -1,5 +1,3 @@
// Package sheets implements functions pertaining to loading, sorting,
// filtering, and tagging cheatsheets.
package sheets
import (

View File

@ -2,13 +2,11 @@ package sheets
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
cp "github.com/cheat/cheat/internal/cheatpath"
"github.com/cheat/cheat/internal/repo"
"github.com/cheat/cheat/internal/sheet"
)
@ -35,7 +33,7 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
// fail if an error occurred while walking the directory
if err != nil {
return fmt.Errorf("failed to walk path: %v", err)
return fmt.Errorf("error walking path: %v", err)
}
// don't register directories as cheatsheets
@ -47,35 +45,23 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
// accessed. Eg: `cheat tar` - `tar` is the title)
title := strings.TrimPrefix(
strings.TrimPrefix(path, cheatpath.Path),
string(os.PathSeparator),
"/",
)
// Don't walk the `.git` directory. Doing so creates
// hundreds/thousands of needless syscalls and could
// potentially harm performance on machines with slow disks.
skip, err := repo.GitDir(path)
if err != nil {
return fmt.Errorf("failed to identify .git directory: %v", err)
}
if skip {
return fs.SkipDir
// ignore hidden files and directories. Otherwise, we'll likely load
// .git/* and .DS_Store.
//
// NB: this is still somewhat brittle in that it will miss files
// contained within hidden directories in the middle of a path, though
// that should not realistically occur.
if strings.HasPrefix(title, ".") || strings.HasPrefix(info.Name(), ".") {
return nil
}
// parse the cheatsheet file into a `sheet` struct
s, err := sheet.New(
title,
cheatpath.Name,
path,
cheatpath.Tags,
cheatpath.ReadOnly,
)
s, err := sheet.New(title, path, cheatpath.Tags, cheatpath.ReadOnly)
if err != nil {
return fmt.Errorf(
"failed to load sheet: %s, path: %s, err: %v",
title,
path,
err,
)
return fmt.Errorf("could not create sheet: %v", err)
}
// register the cheatsheet on its cheatpath, keyed by its title

View File

@ -1,62 +1,3 @@
package sheets
import (
"path"
"testing"
"github.com/cheat/cheat/internal/cheatpath"
"github.com/cheat/cheat/internal/mock"
)
// TestLoad asserts that sheets on valid cheatpaths can be loaded successfully
func TestLoad(t *testing.T) {
// mock cheatpaths
cheatpaths := []cheatpath.Cheatpath{
{
Name: "community",
Path: path.Join(mock.Path("cheatsheets"), "community"),
ReadOnly: true,
},
{
Name: "personal",
Path: path.Join(mock.Path("cheatsheets"), "personal"),
ReadOnly: false,
},
}
// load cheatsheets
sheets, err := Load(cheatpaths)
if err != nil {
t.Errorf("failed to load cheatsheets: %v", err)
}
// assert that the correct number of sheets loaded
// (sheet load details are tested in `sheet_test.go`)
want := 4
if len(sheets) != want {
t.Errorf(
"failed to load correct number of cheatsheets: want: %d, got: %d",
want,
len(sheets),
)
}
}
// TestLoadBadPath asserts that an error is returned if a cheatpath is invalid
func TestLoadBadPath(t *testing.T) {
// mock a bad cheatpath
cheatpaths := []cheatpath.Cheatpath{
{
Name: "badpath",
Path: "/cheat/test/path/does/not/exist",
ReadOnly: true,
},
}
// attempt to load the cheatpath
if _, err := Load(cheatpaths); err == nil {
t.Errorf("failed to reject invalid cheatpath")
}
}
// TODO

View File

@ -1,36 +0,0 @@
package sheets
import (
"sort"
"github.com/cheat/cheat/internal/sheet"
)
// Tags returns a slice of all tags in use in any sheet
func Tags(cheatpaths []map[string]sheet.Sheet) []string {
// create a map of all tags in use in any sheet
tags := make(map[string]bool)
// iterate over all tags on all sheets on all cheatpaths
for _, path := range cheatpaths {
for _, sheet := range path {
for _, tag := range sheet.Tags {
tags[tag] = true
}
}
}
// restructure the map into a slice
sorted := []string{}
for tag := range tags {
sorted = append(sorted, tag)
}
// sort the slice
sort.Slice(sorted, func(i, j int) bool {
return sorted[i] < sorted[j]
})
return sorted
}

View File

@ -1,51 +0,0 @@
package sheets
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/cheat/cheat/internal/sheet"
)
// TestTags asserts that cheatsheet tags are properly returned
func TestTags(t *testing.T) {
// mock cheatsheets available on multiple cheatpaths
cheatpaths := []map[string]sheet.Sheet{
// mock community cheatsheets
map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha"}},
"bar": sheet.Sheet{Title: "bar", Tags: []string{"alpha", "bravo"}},
},
// mock local cheatsheets
map[string]sheet.Sheet{
"bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}},
"baz": sheet.Sheet{Title: "baz", Tags: []string{"delta"}},
},
}
// consolidate the cheatsheets
tags := Tags(cheatpaths)
// specify the expected output
want := []string{
"alpha",
"bravo",
"charlie",
"delta",
}
// assert that the cheatsheets properly consolidated
if !reflect.DeepEqual(tags, want) {
t.Errorf(
"failed to return tags: want:\n%s, got:\n%s",
spew.Sdump(want),
spew.Sdump(tags),
)
}
}

View File

@ -1,4 +0,0 @@
---
tags: [ community ]
---
This is the bar cheatsheet.

View File

@ -1,4 +0,0 @@
---
tags: [ community ]
---
This is the foo cheatsheet.

View File

@ -1,4 +0,0 @@
---
tags: [ personal ]
---
This is the bat cheatsheet.

View File

@ -1,4 +0,0 @@
---
tags: [ personal ]
---
This is the baz cheatsheet.

View File

@ -1,4 +0,0 @@
---
syntax: sh
This is malformed frontmatter.

View File

@ -0,0 +1,9 @@
function _cheat_autocomplete {
sheets=$(cheat -l | sed -n '2,$p'|cut -d' ' -f1)
COMPREPLY=()
if [ $COMP_CWORD = 1 ]; then
COMPREPLY=(`compgen -W "$sheets" -- $2`)
fi
}
complete -F _cheat_autocomplete cheat

View File

@ -7,7 +7,5 @@ complete -c cheat -s l -l list -d "List cheatsheets"
complete -c cheat -s p -l path -x -a "(cheat -d | cut -d ':' -f 1)" -d "Return only sheets found on given path"
complete -c cheat -s r -l regex -d "Treat search phrase as a regex"
complete -c cheat -s s -l search -x -d "Search cheatsheets for given phrase"
complete -c cheat -s t -l tag -x -a "(cheat -T)" -d "Return only sheets matching the given tag"
complete -c cheat -s T -l tags -d "List all tags in use"
complete -c cheat -s t -l tag -x -a "(cheat -l | tail -n +2 | rev | cut -d ' ' -f 1 | rev | sed 's/,/\n/g;/^\$/d' | sort -u)" -d "Return only sheets matching the given tag"
complete -c cheat -s v -l version -d "Print the version number"
complete -c cheat -l rm -x -a "(cheat -l | tail -n +2 | cut -d ' ' -f 1)" -d "Remove (delete) cheatsheet"

View File

@ -1,74 +0,0 @@
# cheat(1) completion -*- shell-script -*-
# generate cheatsheet completions, optionally using `fzf`
_cheat_complete_cheatsheets()
{
if [[ "$CHEAT_USE_FZF" = true ]]; then
FZF_COMPLETION_TRIGGER='' _fzf_complete "--no-multi" "$@" < <(
cheat -l | tail -n +2 | cut -d' ' -f1
)
else
COMPREPLY=( $(compgen -W "$(cheat -l | tail -n +2 | cut -d' ' -f1)" -- "$cur") )
fi
}
# generate tag completions, optionally using `fzf`
_cheat_complete_tags()
{
if [ "$CHEAT_USE_FZF" = true ]; then
FZF_COMPLETION_TRIGGER='' _fzf_complete "--no-multi" "$@" < <(cheat -T)
else
COMPREPLY=( $(compgen -W "$(cheat -T)" -- "$cur") )
fi
}
# implement the `cheat` autocompletions
_cheat()
{
local cur prev words cword split
_init_completion -s || return
# complete options that are currently being typed: `--col` => `--colorize`
if [[ $cur == -* ]]; then
COMPREPLY=( $(compgen -W '$(_parse_help "$1" | sed "s/=//g")' -- "$cur") )
[[ $COMPREPLY == *= ]] && compopt -o nospace
return
fi
# implement completions
case $prev in
--colorize|-c|\
--directories|-d|\
--init|\
--regex|-r|\
--search|-s|\
--tags|-T|\
--version|-v)
# noop the above, which should implement no completions
;;
--edit|-e)
_cheat_complete_cheatsheets
;;
--list|-l)
_cheat_complete_cheatsheets
;;
--path|-p)
COMPREPLY=( $(compgen -W "$(cheat -d | cut -d':' -f1)" -- "$cur") )
;;
--rm)
_cheat_complete_cheatsheets
;;
--tag|-t)
_cheat_complete_tags
;;
*)
_cheat_complete_cheatsheets
;;
esac
$split && return
} &&
complete -F _cheat cheat
# ex: filetype=sh

View File

@ -1,65 +0,0 @@
#compdef cheat
local cheats taglist pathlist
_cheat_complete_personal_cheatsheets()
{
cheats=("${(f)$(cheat -l -t personal | tail -n +2 | cut -d' ' -f1)}")
_describe -t cheats 'cheats' cheats
}
_cheat_complete_full_cheatsheets()
{
cheats=("${(f)$(cheat -l | tail -n +2 | cut -d' ' -f1)}")
_describe -t cheats 'cheats' cheats
}
_cheat_complete_tags()
{
taglist=("${(f)$(cheat -T)}")
_describe -t taglist 'taglist' taglist
}
_cheat_complete_paths()
{
pathlist=("${(f)$(cheat -d | cut -d':' -f1)}")
_describe -t pathlist 'pathlist' pathlist
}
_cheat() {
_arguments -C \
'(--init)--init[Write a default config file to stdout]: :->none' \
'(-c --colorize)'{-c,--colorize}'[Colorize output]: :->none' \
'(-d --directories)'{-d,--directories}'[List cheatsheet directories]: :->none' \
'(-e --edit)'{-e,--edit}'[Edit <sheet>]: :->personal' \
'(-l --list)'{-l,--list}'[List cheatsheets]: :->full' \
'(-p --path)'{-p,--path}'[Return only sheets found on path <name>]: :->pathlist' \
'(-r --regex)'{-r,--regex}'[Treat search <phrase> as a regex]: :->none' \
'(-s --search)'{-s,--search}'[Search cheatsheets for <phrase>]: :->none' \
'(-t --tag)'{-t,--tag}'[Return only sheets matching <tag>]: :->taglist' \
'(-T --tags)'{-T,--tags}'[List all tags in use]: :->none' \
'(-v --version)'{-v,--version}'[Print the version number]: :->none' \
'(--rm)--rm[Remove (delete) <sheet>]: :->personal'
case $state in
(none)
;;
(full)
_cheat_complete_full_cheatsheets
;;
(personal)
_cheat_complete_personal_cheatsheets
;;
(taglist)
_cheat_complete_tags
;;
(pathlist)
_cheat_complete_paths
;;
(*)
;;
esac
}
compdef _cheat cheat

11
scripts/fzf.bash Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
# This function enables you to choose a cheatsheet to view by selecting output
# from `cheat -l`. `source` it in your shell to enable it. (Consider renaming
# or aliasing it to something convenient.)
# Arguments passed to this function (like --color) will be passed to the second
# invokation of `cheat`.
function cheat-fzf {
eval `cheat -l | tail -n +2 | fzf | awk -v vars="$*" '{ print "cheat " $1 " -t " $3, vars }'`
}

View File

@ -1,46 +0,0 @@
#!/bin/sh -e
pull() {
for d in `cheat -d | awk '{print $2}'`;
do
echo "Update $d"
cd "$d"
[ -d ".git" ] && git pull || :
done
echo
echo "Finished update"
}
push() {
for d in `cheat -d | grep -v "community" | awk '{print $2}'`;
do
cd "$d"
if [ -d ".git" ]
then
echo "Push modifications $d"
files=$(git ls-files -mo | tr '\n' ' ')
git add -A && git commit -m "Edited files: $files" && git push || :
else
echo "$(pwd) is not a git managed folder"
echo "First connect this to your personal git repository"
fi
done
echo
echo "Finished push operation"
}
if [ "$1" = "pull" ]; then
pull
elif [ "$1" = "push" ]; then
push
else
echo "Usage:
# pull changes
cheatsheets pull
# push changes
cheatsheets push"
fi

View File

@ -1,12 +0,0 @@
version = 1
test_patterns = [
"*_test.go"
]
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_path = "dario.cat/mergo"

View File

@ -1,33 +0,0 @@
#### joe made this: http://goel.io/joe
#### go ####
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
#### vim ####
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags

View File

@ -1,12 +0,0 @@
language: go
arch:
- amd64
- ppc64le
install:
- go get -t
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- go test -race -v ./...
after_script:
- $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN

View File

@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at i@dario.im. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -1,112 +0,0 @@
<!-- omit in toc -->
# Contributing to mergo
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
<!-- omit in toc -->
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
## Code of Conduct
This project and everyone participating in it is governed by the
[mergo Code of Conduct](https://github.com/imdario/mergoblob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to <>.
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://pkg.go.dev/github.com/imdario/mergo).
Before you ask a question, it is best to search for existing [Issues](https://github.com/imdario/mergo/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/imdario/mergo/issues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
We will then take care of the issue as soon as possible.
## I Want To Contribute
> ### Legal Notice <!-- omit in toc -->
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs
<!-- omit in toc -->
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/imdario/mergoissues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
<!-- omit in toc -->
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to .
<!-- You may add a PGP key to allow the messages to be sent encrypted as well. -->
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/imdario/mergo/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be implemented by someone.
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for mergo, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
<!-- omit in toc -->
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/imdario/mergo/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
<!-- omit in toc -->
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/imdario/mergo/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most mergo users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
<!-- omit in toc -->
## Attribution
This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!

View File

@ -1,28 +0,0 @@
Copyright (c) 2013 Dario Castañé. All rights reserved.
Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,248 +0,0 @@
# Mergo
[![GitHub release][5]][6]
[![GoCard][7]][8]
[![Test status][1]][2]
[![OpenSSF Scorecard][21]][22]
[![OpenSSF Best Practices][19]][20]
[![Coverage status][9]][10]
[![Sourcegraph][11]][12]
[![FOSSA status][13]][14]
[![GoDoc][3]][4]
[![Become my sponsor][15]][16]
[![Tidelift][17]][18]
[1]: https://github.com/imdario/mergo/workflows/tests/badge.svg?branch=master
[2]: https://github.com/imdario/mergo/actions/workflows/tests.yml
[3]: https://godoc.org/github.com/imdario/mergo?status.svg
[4]: https://godoc.org/github.com/imdario/mergo
[5]: https://img.shields.io/github/release/imdario/mergo.svg
[6]: https://github.com/imdario/mergo/releases
[7]: https://goreportcard.com/badge/imdario/mergo
[8]: https://goreportcard.com/report/github.com/imdario/mergo
[9]: https://coveralls.io/repos/github/imdario/mergo/badge.svg?branch=master
[10]: https://coveralls.io/github/imdario/mergo?branch=master
[11]: https://sourcegraph.com/github.com/imdario/mergo/-/badge.svg
[12]: https://sourcegraph.com/github.com/imdario/mergo?badge
[13]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fimdario%2Fmergo.svg?type=shield
[14]: https://app.fossa.io/projects/git%2Bgithub.com%2Fimdario%2Fmergo?ref=badge_shield
[15]: https://img.shields.io/github/sponsors/imdario
[16]: https://github.com/sponsors/imdario
[17]: https://tidelift.com/badges/package/go/github.com%2Fimdario%2Fmergo
[18]: https://tidelift.com/subscription/pkg/go-github.com-imdario-mergo
[19]: https://bestpractices.coreinfrastructure.org/projects/7177/badge
[20]: https://bestpractices.coreinfrastructure.org/projects/7177
[21]: https://api.securityscorecards.dev/projects/github.com/imdario/mergo/badge
[22]: https://api.securityscorecards.dev/projects/github.com/imdario/mergo
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements.
Mergo merges same-type structs and maps by setting default values in zero-value fields. Mergo won't merge unexported (private) fields. It will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection).
Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region of Marche.
## Status
It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, Microsoft, etc](https://github.com/imdario/mergo#mergo-in-the-wild).
### Important notes
#### 1.0.0
In [1.0.0](//github.com/imdario/mergo/releases/tag/1.0.0) Mergo moves to a vanity URL `dario.cat/mergo`.
#### 0.3.9
Please keep in mind that a problematic PR broke [0.3.9](//github.com/imdario/mergo/releases/tag/0.3.9). I reverted it in [0.3.10](//github.com/imdario/mergo/releases/tag/0.3.10), and I consider it stable but not bug-free. Also, this version adds support for go modules.
Keep in mind that in [0.3.2](//github.com/imdario/mergo/releases/tag/0.3.2), Mergo changed `Merge()`and `Map()` signatures to support [transformers](#transformers). I added an optional/variadic argument so that it won't break the existing code.
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with ```go get -u dario.cat/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
### Donations
If Mergo is useful to you, consider buying me a coffee, a beer, or making a monthly donation to allow me to keep building great free software. :heart_eyes:
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
<a href='https://github.com/sponsors/imdario' target='_blank'><img alt="Become my sponsor" src="https://img.shields.io/github/sponsors/imdario?style=for-the-badge" /></a>
### Mergo in the wild
- [moby/moby](https://github.com/moby/moby)
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)
- [vmware/dispatch](https://github.com/vmware/dispatch)
- [Shopify/themekit](https://github.com/Shopify/themekit)
- [imdario/zas](https://github.com/imdario/zas)
- [matcornic/hermes](https://github.com/matcornic/hermes)
- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go)
- [kataras/iris](https://github.com/kataras/iris)
- [michaelsauter/crane](https://github.com/michaelsauter/crane)
- [go-task/task](https://github.com/go-task/task)
- [sensu/uchiwa](https://github.com/sensu/uchiwa)
- [ory/hydra](https://github.com/ory/hydra)
- [sisatech/vcli](https://github.com/sisatech/vcli)
- [dairycart/dairycart](https://github.com/dairycart/dairycart)
- [projectcalico/felix](https://github.com/projectcalico/felix)
- [resin-os/balena](https://github.com/resin-os/balena)
- [go-kivik/kivik](https://github.com/go-kivik/kivik)
- [Telefonica/govice](https://github.com/Telefonica/govice)
- [supergiant/supergiant](supergiant/supergiant)
- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce)
- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy)
- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel)
- [EagerIO/Stout](https://github.com/EagerIO/Stout)
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api)
- [russross/canvasassignments](https://github.com/russross/canvasassignments)
- [rdegges/cryptly-api](https://github.com/rdegges/cryptly-api)
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor)
- [divshot/gitling](https://github.com/divshot/gitling)
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl)
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42)
- [elwinar/rambler](https://github.com/elwinar/rambler)
- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman)
- [jfbus/impressionist](https://github.com/jfbus/impressionist)
- [Jmeyering/zealot](https://github.com/Jmeyering/zealot)
- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host)
- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go)
- [thoas/picfit](https://github.com/thoas/picfit)
- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server)
- [jnuthong/item_search](https://github.com/jnuthong/item_search)
- [bukalapak/snowboard](https://github.com/bukalapak/snowboard)
- [containerssh/containerssh](https://github.com/containerssh/containerssh)
- [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser)
- [tjpnz/structbot](https://github.com/tjpnz/structbot)
## Install
go get dario.cat/mergo
// use in your .go code
import (
"dario.cat/mergo"
)
## Usage
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as [they are zero values](https://golang.org/ref/spec#The_zero_value) too. Also, maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
```go
if err := mergo.Merge(&dst, src); err != nil {
// ...
}
```
Also, you can merge overwriting values using the transformer `WithOverride`.
```go
if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
// ...
}
```
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field.
```go
if err := mergo.Map(&dst, srcMap); err != nil {
// ...
}
```
Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as `map[string]interface{}`. They will be just assigned as values.
Here is a nice example:
```go
package main
import (
"fmt"
"dario.cat/mergo"
)
type Foo struct {
A string
B int64
}
func main() {
src := Foo{
A: "one",
B: 2,
}
dest := Foo{
A: "two",
}
mergo.Merge(&dest, src)
fmt.Println(dest)
// Will print
// {two 2}
}
```
Note: if test are failing due missing package, please execute:
go get gopkg.in/yaml.v3
### Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`?
```go
package main
import (
"fmt"
"dario.cat/mergo"
"reflect"
"time"
)
type timeTransformer struct {
}
func (t timeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ == reflect.TypeOf(time.Time{}) {
return func(dst, src reflect.Value) error {
if dst.CanSet() {
isZero := dst.MethodByName("IsZero")
result := isZero.Call([]reflect.Value{})
if result[0].Bool() {
dst.Set(src)
}
}
return nil
}
}
return nil
}
type Snapshot struct {
Time time.Time
// ...
}
func main() {
src := Snapshot{time.Now()}
dest := Snapshot{}
mergo.Merge(&dest, src, mergo.WithTransformers(timeTransformer{}))
fmt.Println(dest)
// Will print
// { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 }
}
```
## Contact me
If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): [@im_dario](https://twitter.com/im_dario)
## About
Written by [Dario Castañé](http://dario.im).
## License
[BSD 3-Clause](http://opensource.org/licenses/BSD-3-Clause) license, as [Go language](http://golang.org/LICENSE).
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fimdario%2Fmergo.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fimdario%2Fmergo?ref=badge_large)

View File

@ -1,14 +0,0 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 0.3.x | :white_check_mark: |
| < 0.3 | :x: |
## Security contact information
To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.

View File

@ -1,148 +0,0 @@
// Copyright 2013 Dario Castañé. All rights reserved.
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements.
Mergo merges same-type structs and maps by setting default values in zero-value fields. Mergo won't merge unexported (private) fields. It will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection).
# Status
It is ready for production use. It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc.
# Important notes
1.0.0
In 1.0.0 Mergo moves to a vanity URL `dario.cat/mergo`.
0.3.9
Please keep in mind that a problematic PR broke 0.3.9. We reverted it in 0.3.10. We consider 0.3.10 as stable but not bug-free. . Also, this version adds suppot for go modules.
Keep in mind that in 0.3.2, Mergo changed Merge() and Map() signatures to support transformers. We added an optional/variadic argument so that it won't break the existing code.
If you were using Mergo before April 6th, 2015, please check your project works as intended after updating your local copy with go get -u dario.cat/mergo. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause in existing projects after the change (release 0.2.0).
# Install
Do your usual installation procedure:
go get dario.cat/mergo
// use in your .go code
import (
"dario.cat/mergo"
)
# Usage
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as they are zero values too. Also, maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection).
if err := mergo.Merge(&dst, src); err != nil {
// ...
}
Also, you can merge overwriting values using the transformer WithOverride.
if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil {
// ...
}
Additionally, you can map a map[string]interface{} to a struct (and otherwise, from struct to map), following the same restrictions as in Merge(). Keys are capitalized to find each corresponding exported field.
if err := mergo.Map(&dst, srcMap); err != nil {
// ...
}
Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as map[string]interface{}. They will be just assigned as values.
Here is a nice example:
package main
import (
"fmt"
"dario.cat/mergo"
)
type Foo struct {
A string
B int64
}
func main() {
src := Foo{
A: "one",
B: 2,
}
dest := Foo{
A: "two",
}
mergo.Merge(&dest, src)
fmt.Println(dest)
// Will print
// {two 2}
}
# Transformers
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, time.Time is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero time.Time?
package main
import (
"fmt"
"dario.cat/mergo"
"reflect"
"time"
)
type timeTransformer struct {
}
func (t timeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ == reflect.TypeOf(time.Time{}) {
return func(dst, src reflect.Value) error {
if dst.CanSet() {
isZero := dst.MethodByName("IsZero")
result := isZero.Call([]reflect.Value{})
if result[0].Bool() {
dst.Set(src)
}
}
return nil
}
}
return nil
}
type Snapshot struct {
Time time.Time
// ...
}
func main() {
src := Snapshot{time.Now()}
dest := Snapshot{}
mergo.Merge(&dest, src, mergo.WithTransformers(timeTransformer{}))
fmt.Println(dest)
// Will print
// { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 }
}
# Contact me
If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): https://twitter.com/im_dario
# About
Written by Dario Castañé: https://da.rio.hn
# License
BSD 3-Clause license, as Go language.
*/
package mergo

View File

@ -1,178 +0,0 @@
// Copyright 2014 Dario Castañé. All rights reserved.
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Based on src/pkg/reflect/deepequal.go from official
// golang's stdlib.
package mergo
import (
"fmt"
"reflect"
"unicode"
"unicode/utf8"
)
func changeInitialCase(s string, mapper func(rune) rune) string {
if s == "" {
return s
}
r, n := utf8.DecodeRuneInString(s)
return string(mapper(r)) + s[n:]
}
func isExported(field reflect.StructField) bool {
r, _ := utf8.DecodeRuneInString(field.Name)
return r >= 'A' && r <= 'Z'
}
// Traverses recursively both values, assigning src's fields values to dst.
// The map argument tracks comparisons that have already been seen, which allows
// short circuiting on recursive types.
func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (err error) {
overwrite := config.Overwrite
if dst.CanAddr() {
addr := dst.UnsafeAddr()
h := 17 * addr
seen := visited[h]
typ := dst.Type()
for p := seen; p != nil; p = p.next {
if p.ptr == addr && p.typ == typ {
return nil
}
}
// Remember, remember...
visited[h] = &visit{typ, seen, addr}
}
zeroValue := reflect.Value{}
switch dst.Kind() {
case reflect.Map:
dstMap := dst.Interface().(map[string]interface{})
for i, n := 0, src.NumField(); i < n; i++ {
srcType := src.Type()
field := srcType.Field(i)
if !isExported(field) {
continue
}
fieldName := field.Name
fieldName = changeInitialCase(fieldName, unicode.ToLower)
if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v), !config.ShouldNotDereference) || overwrite) {
dstMap[fieldName] = src.Field(i).Interface()
}
}
case reflect.Ptr:
if dst.IsNil() {
v := reflect.New(dst.Type().Elem())
dst.Set(v)
}
dst = dst.Elem()
fallthrough
case reflect.Struct:
srcMap := src.Interface().(map[string]interface{})
for key := range srcMap {
config.overwriteWithEmptyValue = true
srcValue := srcMap[key]
fieldName := changeInitialCase(key, unicode.ToUpper)
dstElement := dst.FieldByName(fieldName)
if dstElement == zeroValue {
// We discard it because the field doesn't exist.
continue
}
srcElement := reflect.ValueOf(srcValue)
dstKind := dstElement.Kind()
srcKind := srcElement.Kind()
if srcKind == reflect.Ptr && dstKind != reflect.Ptr {
srcElement = srcElement.Elem()
srcKind = reflect.TypeOf(srcElement.Interface()).Kind()
} else if dstKind == reflect.Ptr {
// Can this work? I guess it can't.
if srcKind != reflect.Ptr && srcElement.CanAddr() {
srcPtr := srcElement.Addr()
srcElement = reflect.ValueOf(srcPtr)
srcKind = reflect.Ptr
}
}
if !srcElement.IsValid() {
continue
}
if srcKind == dstKind {
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
} else if dstKind == reflect.Interface && dstElement.Kind() == reflect.Interface {
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
} else if srcKind == reflect.Map {
if err = deepMap(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
} else {
return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind)
}
}
}
return
}
// Map sets fields' values in dst from src.
// src can be a map with string keys or a struct. dst must be the opposite:
// if src is a map, dst must be a valid pointer to struct. If src is a struct,
// dst must be map[string]interface{}.
// It won't merge unexported (private) fields and will do recursively
// any exported field.
// If dst is a map, keys will be src fields' names in lower camel case.
// Missing key in src that doesn't match a field in dst will be skipped. This
// doesn't apply if dst is a map.
// This is separated method from Merge because it is cleaner and it keeps sane
// semantics: merging equal types, mapping different (restricted) types.
func Map(dst, src interface{}, opts ...func(*Config)) error {
return _map(dst, src, opts...)
}
// MapWithOverwrite will do the same as Map except that non-empty dst attributes will be overridden by
// non-empty src attribute values.
// Deprecated: Use Map(…) with WithOverride
func MapWithOverwrite(dst, src interface{}, opts ...func(*Config)) error {
return _map(dst, src, append(opts, WithOverride)...)
}
func _map(dst, src interface{}, opts ...func(*Config)) error {
if dst != nil && reflect.ValueOf(dst).Kind() != reflect.Ptr {
return ErrNonPointerArgument
}
var (
vDst, vSrc reflect.Value
err error
)
config := &Config{}
for _, opt := range opts {
opt(config)
}
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
return err
}
// To be friction-less, we redirect equal-type arguments
// to deepMerge. Only because arguments can be anything.
if vSrc.Kind() == vDst.Kind() {
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config)
}
switch vSrc.Kind() {
case reflect.Struct:
if vDst.Kind() != reflect.Map {
return ErrExpectedMapAsDestination
}
case reflect.Map:
if vDst.Kind() != reflect.Struct {
return ErrExpectedStructAsDestination
}
default:
return ErrNotSupported
}
return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0, config)
}

View File

@ -1,409 +0,0 @@
// Copyright 2013 Dario Castañé. All rights reserved.
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Based on src/pkg/reflect/deepequal.go from official
// golang's stdlib.
package mergo
import (
"fmt"
"reflect"
)
func hasMergeableFields(dst reflect.Value) (exported bool) {
for i, n := 0, dst.NumField(); i < n; i++ {
field := dst.Type().Field(i)
if field.Anonymous && dst.Field(i).Kind() == reflect.Struct {
exported = exported || hasMergeableFields(dst.Field(i))
} else if isExportedComponent(&field) {
exported = exported || len(field.PkgPath) == 0
}
}
return
}
func isExportedComponent(field *reflect.StructField) bool {
pkgPath := field.PkgPath
if len(pkgPath) > 0 {
return false
}
c := field.Name[0]
if 'a' <= c && c <= 'z' || c == '_' {
return false
}
return true
}
type Config struct {
Transformers Transformers
Overwrite bool
ShouldNotDereference bool
AppendSlice bool
TypeCheck bool
overwriteWithEmptyValue bool
overwriteSliceWithEmptyValue bool
sliceDeepCopy bool
debug bool
}
type Transformers interface {
Transformer(reflect.Type) func(dst, src reflect.Value) error
}
// Traverses recursively both values, assigning src's fields values to dst.
// The map argument tracks comparisons that have already been seen, which allows
// short circuiting on recursive types.
func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (err error) {
overwrite := config.Overwrite
typeCheck := config.TypeCheck
overwriteWithEmptySrc := config.overwriteWithEmptyValue
overwriteSliceWithEmptySrc := config.overwriteSliceWithEmptyValue
sliceDeepCopy := config.sliceDeepCopy
if !src.IsValid() {
return
}
if dst.CanAddr() {
addr := dst.UnsafeAddr()
h := 17 * addr
seen := visited[h]
typ := dst.Type()
for p := seen; p != nil; p = p.next {
if p.ptr == addr && p.typ == typ {
return nil
}
}
// Remember, remember...
visited[h] = &visit{typ, seen, addr}
}
if config.Transformers != nil && !isReflectNil(dst) && dst.IsValid() {
if fn := config.Transformers.Transformer(dst.Type()); fn != nil {
err = fn(dst, src)
return
}
}
switch dst.Kind() {
case reflect.Struct:
if hasMergeableFields(dst) {
for i, n := 0, dst.NumField(); i < n; i++ {
if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1, config); err != nil {
return
}
}
} else {
if dst.CanSet() && (isReflectNil(dst) || overwrite) && (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc) {
dst.Set(src)
}
}
case reflect.Map:
if dst.IsNil() && !src.IsNil() {
if dst.CanSet() {
dst.Set(reflect.MakeMap(dst.Type()))
} else {
dst = src
return
}
}
if src.Kind() != reflect.Map {
if overwrite && dst.CanSet() {
dst.Set(src)
}
return
}
for _, key := range src.MapKeys() {
srcElement := src.MapIndex(key)
if !srcElement.IsValid() {
continue
}
dstElement := dst.MapIndex(key)
switch srcElement.Kind() {
case reflect.Chan, reflect.Func, reflect.Map, reflect.Interface, reflect.Slice:
if srcElement.IsNil() {
if overwrite {
dst.SetMapIndex(key, srcElement)
}
continue
}
fallthrough
default:
if !srcElement.CanInterface() {
continue
}
switch reflect.TypeOf(srcElement.Interface()).Kind() {
case reflect.Struct:
fallthrough
case reflect.Ptr:
fallthrough
case reflect.Map:
srcMapElm := srcElement
dstMapElm := dstElement
if srcMapElm.CanInterface() {
srcMapElm = reflect.ValueOf(srcMapElm.Interface())
if dstMapElm.IsValid() {
dstMapElm = reflect.ValueOf(dstMapElm.Interface())
}
}
if err = deepMerge(dstMapElm, srcMapElm, visited, depth+1, config); err != nil {
return
}
case reflect.Slice:
srcSlice := reflect.ValueOf(srcElement.Interface())
var dstSlice reflect.Value
if !dstElement.IsValid() || dstElement.IsNil() {
dstSlice = reflect.MakeSlice(srcSlice.Type(), 0, srcSlice.Len())
} else {
dstSlice = reflect.ValueOf(dstElement.Interface())
}
if (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) && !config.AppendSlice && !sliceDeepCopy {
if typeCheck && srcSlice.Type() != dstSlice.Type() {
return fmt.Errorf("cannot override two slices with different type (%s, %s)", srcSlice.Type(), dstSlice.Type())
}
dstSlice = srcSlice
} else if config.AppendSlice {
if srcSlice.Type() != dstSlice.Type() {
return fmt.Errorf("cannot append two slices with different type (%s, %s)", srcSlice.Type(), dstSlice.Type())
}
dstSlice = reflect.AppendSlice(dstSlice, srcSlice)
} else if sliceDeepCopy {
i := 0
for ; i < srcSlice.Len() && i < dstSlice.Len(); i++ {
srcElement := srcSlice.Index(i)
dstElement := dstSlice.Index(i)
if srcElement.CanInterface() {
srcElement = reflect.ValueOf(srcElement.Interface())
}
if dstElement.CanInterface() {
dstElement = reflect.ValueOf(dstElement.Interface())
}
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
}
}
dst.SetMapIndex(key, dstSlice)
}
}
if dstElement.IsValid() && !isEmptyValue(dstElement, !config.ShouldNotDereference) {
if reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Slice {
continue
}
if reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Map && reflect.TypeOf(dstElement.Interface()).Kind() == reflect.Map {
continue
}
}
if srcElement.IsValid() && ((srcElement.Kind() != reflect.Ptr && overwrite) || !dstElement.IsValid() || isEmptyValue(dstElement, !config.ShouldNotDereference)) {
if dst.IsNil() {
dst.Set(reflect.MakeMap(dst.Type()))
}
dst.SetMapIndex(key, srcElement)
}
}
// Ensure that all keys in dst are deleted if they are not in src.
if overwriteWithEmptySrc {
for _, key := range dst.MapKeys() {
srcElement := src.MapIndex(key)
if !srcElement.IsValid() {
dst.SetMapIndex(key, reflect.Value{})
}
}
}
case reflect.Slice:
if !dst.CanSet() {
break
}
if (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) && !config.AppendSlice && !sliceDeepCopy {
dst.Set(src)
} else if config.AppendSlice {
if src.Type() != dst.Type() {
return fmt.Errorf("cannot append two slice with different type (%s, %s)", src.Type(), dst.Type())
}
dst.Set(reflect.AppendSlice(dst, src))
} else if sliceDeepCopy {
for i := 0; i < src.Len() && i < dst.Len(); i++ {
srcElement := src.Index(i)
dstElement := dst.Index(i)
if srcElement.CanInterface() {
srcElement = reflect.ValueOf(srcElement.Interface())
}
if dstElement.CanInterface() {
dstElement = reflect.ValueOf(dstElement.Interface())
}
if err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil {
return
}
}
}
case reflect.Ptr:
fallthrough
case reflect.Interface:
if isReflectNil(src) {
if overwriteWithEmptySrc && dst.CanSet() && src.Type().AssignableTo(dst.Type()) {
dst.Set(src)
}
break
}
if src.Kind() != reflect.Interface {
if dst.IsNil() || (src.Kind() != reflect.Ptr && overwrite) {
if dst.CanSet() && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) {
dst.Set(src)
}
} else if src.Kind() == reflect.Ptr {
if !config.ShouldNotDereference {
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
return
}
} else {
if overwriteWithEmptySrc || (overwrite && !src.IsNil()) || dst.IsNil() {
dst.Set(src)
}
}
} else if dst.Elem().Type() == src.Type() {
if err = deepMerge(dst.Elem(), src, visited, depth+1, config); err != nil {
return
}
} else {
return ErrDifferentArgumentsTypes
}
break
}
if dst.IsNil() || overwrite {
if dst.CanSet() && (overwrite || isEmptyValue(dst, !config.ShouldNotDereference)) {
dst.Set(src)
}
break
}
if dst.Elem().Kind() == src.Elem().Kind() {
if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil {
return
}
break
}
default:
mustSet := (isEmptyValue(dst, !config.ShouldNotDereference) || overwrite) && (!isEmptyValue(src, !config.ShouldNotDereference) || overwriteWithEmptySrc)
if mustSet {
if dst.CanSet() {
dst.Set(src)
} else {
dst = src
}
}
}
return
}
// Merge will fill any empty for value type attributes on the dst struct using corresponding
// src attributes if they themselves are not empty. dst and src must be valid same-type structs
// and dst must be a pointer to struct.
// It won't merge unexported (private) fields and will do recursively any exported field.
func Merge(dst, src interface{}, opts ...func(*Config)) error {
return merge(dst, src, opts...)
}
// MergeWithOverwrite will do the same as Merge except that non-empty dst attributes will be overridden by
// non-empty src attribute values.
// Deprecated: use Merge(…) with WithOverride
func MergeWithOverwrite(dst, src interface{}, opts ...func(*Config)) error {
return merge(dst, src, append(opts, WithOverride)...)
}
// WithTransformers adds transformers to merge, allowing to customize the merging of some types.
func WithTransformers(transformers Transformers) func(*Config) {
return func(config *Config) {
config.Transformers = transformers
}
}
// WithOverride will make merge override non-empty dst attributes with non-empty src attributes values.
func WithOverride(config *Config) {
config.Overwrite = true
}
// WithOverwriteWithEmptyValue will make merge override non empty dst attributes with empty src attributes values.
func WithOverwriteWithEmptyValue(config *Config) {
config.Overwrite = true
config.overwriteWithEmptyValue = true
}
// WithOverrideEmptySlice will make merge override empty dst slice with empty src slice.
func WithOverrideEmptySlice(config *Config) {
config.overwriteSliceWithEmptyValue = true
}
// WithoutDereference prevents dereferencing pointers when evaluating whether they are empty
// (i.e. a non-nil pointer is never considered empty).
func WithoutDereference(config *Config) {
config.ShouldNotDereference = true
}
// WithAppendSlice will make merge append slices instead of overwriting it.
func WithAppendSlice(config *Config) {
config.AppendSlice = true
}
// WithTypeCheck will make merge check types while overwriting it (must be used with WithOverride).
func WithTypeCheck(config *Config) {
config.TypeCheck = true
}
// WithSliceDeepCopy will merge slice element one by one with Overwrite flag.
func WithSliceDeepCopy(config *Config) {
config.sliceDeepCopy = true
config.Overwrite = true
}
func merge(dst, src interface{}, opts ...func(*Config)) error {
if dst != nil && reflect.ValueOf(dst).Kind() != reflect.Ptr {
return ErrNonPointerArgument
}
var (
vDst, vSrc reflect.Value
err error
)
config := &Config{}
for _, opt := range opts {
opt(config)
}
if vDst, vSrc, err = resolveValues(dst, src); err != nil {
return err
}
if vDst.Type() != vSrc.Type() {
return ErrDifferentArgumentsTypes
}
return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config)
}
// IsReflectNil is the reflect value provided nil
func isReflectNil(v reflect.Value) bool {
k := v.Kind()
switch k {
case reflect.Interface, reflect.Slice, reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr:
// Both interface and slice are nil if first word is 0.
// Both are always bigger than a word; assume flagIndir.
return v.IsNil()
default:
return false
}
}

View File

@ -1,81 +0,0 @@
// Copyright 2013 Dario Castañé. All rights reserved.
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Based on src/pkg/reflect/deepequal.go from official
// golang's stdlib.
package mergo
import (
"errors"
"reflect"
)
// Errors reported by Mergo when it finds invalid arguments.
var (
ErrNilArguments = errors.New("src and dst must not be nil")
ErrDifferentArgumentsTypes = errors.New("src and dst must be of same type")
ErrNotSupported = errors.New("only structs, maps, and slices are supported")
ErrExpectedMapAsDestination = errors.New("dst was expected to be a map")
ErrExpectedStructAsDestination = errors.New("dst was expected to be a struct")
ErrNonPointerArgument = errors.New("dst must be a pointer")
)
// During deepMerge, must keep track of checks that are
// in progress. The comparison algorithm assumes that all
// checks in progress are true when it reencounters them.
// Visited are stored in a map indexed by 17 * a1 + a2;
type visit struct {
typ reflect.Type
next *visit
ptr uintptr
}
// From src/pkg/encoding/json/encode.go.
func isEmptyValue(v reflect.Value, shouldDereference bool) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
if v.IsNil() {
return true
}
if shouldDereference {
return isEmptyValue(v.Elem(), shouldDereference)
}
return false
case reflect.Func:
return v.IsNil()
case reflect.Invalid:
return true
}
return false
}
func resolveValues(dst, src interface{}) (vDst, vSrc reflect.Value, err error) {
if dst == nil || src == nil {
err = ErrNilArguments
return
}
vDst = reflect.ValueOf(dst).Elem()
if vDst.Kind() != reflect.Struct && vDst.Kind() != reflect.Map && vDst.Kind() != reflect.Slice {
err = ErrNotSupported
return
}
vSrc = reflect.ValueOf(src)
// We check if vSrc is a pointer to dereference it.
if vSrc.Kind() == reflect.Ptr {
vSrc = vSrc.Elem()
}
return
}

Some files were not shown because too many files have changed in this diff Show More