mirror of
https://github.com/cheat/cheat.git
synced 2025-09-01 09:38:29 +02:00
Compare commits
173 Commits
Author | SHA1 | Date | |
---|---|---|---|
0d9c92c8c0 | |||
16c50bb659 | |||
1a85c9e9c8 | |||
c9ccefa607 | |||
3a6b6e58f0 | |||
2edc0ee299 | |||
bd9fa1ba70 | |||
bb85e611f4 | |||
a2f538f114 | |||
80c91cbdee | |||
ede2d2dbaa | |||
db3d7e53a4 | |||
06c4ff52fc | |||
cbc2638d96 | |||
fd93da799d | |||
5c5ed7344f | |||
d773383f70 | |||
2717044b62 | |||
2d635293c5 | |||
f0bfeda47a | |||
f1540290a7 | |||
0b80a608c3 | |||
3c1e24a0e8 | |||
2a6586b41b | |||
6421953183 | |||
0c47f44ff9 | |||
77f9c3fdd0 | |||
b53a14b1a7 | |||
f1e8602369 | |||
ddbe710881 | |||
d598d96fce | |||
4fdec50487 | |||
9de866dfb6 | |||
eb99a070ce | |||
73f80bde48 | |||
8130b2f3bd | |||
f4e6c76e58 | |||
85f5ae8ec7 | |||
484b447391 | |||
cfd1702bc6 | |||
7406ebfb5e | |||
0737af2fec | |||
a23d372d1f | |||
fe66ff3768 | |||
7fed1f63a6 | |||
a297d1619c | |||
ef1da90a77 | |||
d8f405c112 | |||
f8403ff241 | |||
65f6be3fd8 | |||
1cb53697d2 | |||
14f321b0e6 | |||
d3250fda79 | |||
c482488c41 | |||
fe8f39013e | |||
1016b20ef2 | |||
def8985dcd | |||
e6f12147df | |||
a8c2c396ed | |||
35262df4f2 | |||
12ffa4cb5c | |||
d9c602f9e1 | |||
b67ff8b6a8 | |||
a500a621a1 | |||
23b6928874 | |||
9de39fb12b | |||
ad501c4cbe | |||
f17de401e5 | |||
2c097adeda | |||
b825e0f535 | |||
8385277b28 | |||
768d55e5d4 | |||
6aedc5c116 | |||
e881bb1f97 | |||
501f9c66ad | |||
a2aa82d9f3 | |||
018bce7ad5 | |||
17acefdd9b | |||
37918e09a4 | |||
86967873a8 | |||
d237d98c15 | |||
eb9b3e7798 | |||
b0a351033d | |||
1eb44e8809 | |||
55b18b4897 | |||
883a17092f | |||
4f2a57fce8 | |||
ecc96c64f9 | |||
a81dd96ff4 | |||
fb538baba5 | |||
1a7b5c6127 | |||
cdddfbb516 | |||
4ef4c35d8c | |||
a58294859e | |||
606092e288 | |||
233a9de1aa | |||
aa16f68620 | |||
367673d5d9 | |||
08fb9e11a9 | |||
3f4d4bddb2 | |||
6c6753b35c | |||
0718b606e1 | |||
857119b443 | |||
f421483eea | |||
4adddbf504 | |||
b9c86b6975 | |||
0b21ccf6f8 | |||
a3ad8c5101 | |||
bacb74929a | |||
82e1c27494 | |||
45beeb2edb | |||
c2c479b36c | |||
cb0243e7fc | |||
e5d04d41ea | |||
2474ea4fb1 | |||
7467c9fbc0 | |||
dfba3da003 | |||
ad7ad64a75 | |||
c4dcfd5da0 | |||
278a5d9154 | |||
9fa0c466fd | |||
4e9b2928b3 | |||
fa5eb44be8 | |||
49afd7c16b | |||
59d5c96c24 | |||
8e602b0e93 | |||
fb04cb1fcd | |||
d42726101e | |||
93b3a711f5 | |||
9c3d41c8bd | |||
4eeec6c868 | |||
1b17ab1914 | |||
477650ee44 | |||
c4dd3b52fd | |||
e8a0ea0dc3 | |||
992ee66a56 | |||
c9840c2d6f | |||
bd53768f67 | |||
8092687956 | |||
16ade50672 | |||
62c80d76eb | |||
3e67eaa3b7 | |||
38b13655fe | |||
749d5c1182 | |||
521f83377c | |||
b15ff10537 | |||
5288bd0c1c | |||
bddbee4158 | |||
ce27cf2cc0 | |||
5733b1d6d4 | |||
2d221050d8 | |||
ce37b670c7 | |||
47a9eeb4fd | |||
be56c9cf0c | |||
7be57cb01c | |||
8453af8601 | |||
6e388c3693 | |||
b13246978a | |||
a39d36cd34 | |||
87cba04ff2 | |||
bc623da74b | |||
a6c25d4b9c | |||
e24ac2b385 | |||
e0c35a74d4 | |||
3e4c1818a9 | |||
7b4a268ebd | |||
f7183aa17a | |||
1ce6c29e6a | |||
219db679e1 | |||
53177cb09d | |||
ef7a41f9a9 | |||
008316d030 | |||
a59c019642 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: github.com/alecthomas/chroma
|
||||
versions:
|
||||
- 0.9.1
|
46
.github/workflows/build.yml
vendored
Normal file
46
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
# TODO: is it possible to DRY out these jobs? Aside from `runs-on`, they are
|
||||
# identical.
|
||||
# See: https://github.com/actions/runner/issues/1182
|
||||
build-linux:
|
||||
runs-on: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Set up Revive (linter)
|
||||
run: go get -u github.com/boyter/scc github.com/mgechev/revive
|
||||
env:
|
||||
GO111MODULE: "off"
|
||||
- name: Build
|
||||
run: make build
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
build-osx:
|
||||
runs-on: [macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Set up Revive (linter)
|
||||
run: go get -u github.com/boyter/scc github.com/mgechev/revive
|
||||
env:
|
||||
GO111MODULE: "off"
|
||||
- name: Build
|
||||
run: make build
|
||||
- name: Test
|
||||
run: make test
|
30
.github/workflows/codeql-analysis.yml
vendored
Normal file
30
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
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
|
19
.github/workflows/homebrew.yml
vendored
Normal file
19
.github/workflows/homebrew.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
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 }}
|
15
.travis.yml
15
.travis.yml
@ -1,15 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.13.x
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
install: true
|
||||
|
||||
script: make ci
|
@ -19,7 +19,8 @@ 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.
|
||||
new features. See [HACKING.md][hacking] for a quick-start guide to `cheat`
|
||||
development.
|
||||
|
||||
#### Add documentation ####
|
||||
Did you encounter features, bugs, edge-cases, use-cases, or environment
|
||||
@ -35,9 +36,13 @@ 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
|
||||
|
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
# 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
|
57
HACKING.md
Normal file
57
HACKING.md
Normal file
@ -0,0 +1,57 @@
|
||||
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/
|
79
INSTALLING.md
Normal file
79
INSTALLING.md
Normal file
@ -0,0 +1,79 @@
|
||||
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.0/cheat-linux-amd64.gz \
|
||||
&& gunzip cheat-linux-amd64.gz \
|
||||
&& chmod +x cheat-linux-amd64 \
|
||||
&& sudo mv cheat-linux-amd64 /usr/local/bin/cheat
|
||||
```
|
||||
|
||||
You may need to need to change the version number (`4.4.0`) 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
|
115
Makefile
115
Makefile
@ -7,48 +7,61 @@ dist_dir := ./dist
|
||||
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-plan9-amd64 \
|
||||
$(dist_dir)/cheat-solaris-amd64 \
|
||||
$(dist_dir)/cheat-windows-amd64.exe
|
||||
|
||||
## build: builds an executable for your architecture
|
||||
## build: build an executable for your architecture
|
||||
.PHONY: build
|
||||
build: $(dist_dir) clean generate
|
||||
build: | clean $(dist_dir) generate fmt lint vet vendor man
|
||||
$(GO) build $(BUILD_FLAGS) -o $(dist_dir)/cheat $(cmd_dir)
|
||||
|
||||
## build-release: builds release executables
|
||||
## build-release: build release executables
|
||||
.PHONY: build-release
|
||||
build-release: $(releases)
|
||||
|
||||
## ci: builds a "release" executable for the current architecture (used in ci)
|
||||
.PHONY: ci
|
||||
ci: | setup prepare build
|
||||
|
||||
# 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 \
|
||||
@ -68,11 +81,36 @@ $(dist_dir)/cheat-linux-arm6: prepare
|
||||
$(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 $@
|
||||
$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(ZIP) $@.zip $@ -j
|
||||
|
||||
# ./dist
|
||||
$(dist_dir):
|
||||
@ -82,69 +120,96 @@ $(dist_dir):
|
||||
generate:
|
||||
$(GO) generate $(cmd_dir)
|
||||
|
||||
## install: builds and installs cheat on your PATH
|
||||
## install: build and install cheat on your PATH
|
||||
.PHONY: install
|
||||
install:
|
||||
install: build
|
||||
$(GO) install $(BUILD_FLAGS) $(GOBIN) $(cmd_dir)
|
||||
|
||||
## clean: removes compiled executables
|
||||
## clean: remove compiled executables
|
||||
.PHONY: clean
|
||||
clean: $(dist_dir)
|
||||
$(RM) -f $(dist_dir)/*
|
||||
clean:
|
||||
$(RM) -f $(dist_dir)/* $(cmd_dir)/str_config.go $(cmd_dir)/str_usage.go
|
||||
|
||||
## distclean: removes the tags file
|
||||
## distclean: remove the tags file
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
$(RM) -f tags
|
||||
@$(DOCKER) image rm -f $(docker_image)
|
||||
|
||||
## setup: installs revive (linter) and scc (sloc tool)
|
||||
## 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: counts "semantic lines of code"
|
||||
## sloc: count "semantic lines of code"
|
||||
.PHONY: sloc
|
||||
sloc:
|
||||
$(SCC) --exclude-dir=vendor
|
||||
|
||||
## tags: builds a tags file
|
||||
## tags: build a tags file
|
||||
.PHONY: tags
|
||||
tags:
|
||||
$(CTAGS) -R --exclude=vendor --languages=go
|
||||
|
||||
## vendor: downloads, tidies, and verifies dependencies
|
||||
## 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
|
||||
|
||||
## fmt: runs go fmt
|
||||
## 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: lints go source files
|
||||
## lint: lint go source files
|
||||
.PHONY: lint
|
||||
lint: vendor
|
||||
$(LINT) -exclude vendor/... ./...
|
||||
|
||||
## vet: vets go source files
|
||||
## vet: vet go source files
|
||||
.PHONY: vet
|
||||
vet:
|
||||
$(GO) vet ./...
|
||||
|
||||
## test: runs unit-tests
|
||||
## test: run unit-tests
|
||||
.PHONY: test
|
||||
test:
|
||||
$(GO) test ./...
|
||||
|
||||
## check: formats, lints, vets, vendors, and run unit-tests
|
||||
## 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: | $(dist_dir) clean generate vendor fmt lint vet test
|
||||
prepare: | clean $(dist_dir) generate vendor fmt lint vet test
|
||||
|
||||
## help: displays this help text
|
||||
## 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) | \
|
||||
|
188
README.md
188
README.md
@ -1,8 +1,9 @@
|
||||

|
||||
|
||||
|
||||
cheat
|
||||
=====
|
||||
|
||||
[](https://travis-ci.com/cheat/cheat)
|
||||
|
||||
`cheat` allows you to create and view interactive cheatsheets on the
|
||||
command-line. It was designed to help remind \*nix system administrators of
|
||||
options for commands that they use frequently, but not frequently enough to
|
||||
@ -41,96 +42,6 @@ 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.
|
||||
|
||||
### Directory-scoped Cheatpaths ###
|
||||
At times, it can be useful to closely associate cheatsheets with a directory on
|
||||
your filesystem. `cheat` facilitates this by searching for a `.cheat` folder in
|
||||
the current working directory. If found, the `.cheat` directory will
|
||||
(temporarily) be added to the cheatpaths.
|
||||
|
||||
Usage
|
||||
-----
|
||||
To view a cheatsheet:
|
||||
@ -191,13 +102,92 @@ cheat -p personal -t networking --regex -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
|
||||
```
|
||||
|
||||
|
||||
Advanced Usage
|
||||
|
||||
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
|
||||
--------------
|
||||
`cheat` may be integrated with [fzf][]. See [fzf.bash][bash] for instructions.
|
||||
(Support for other shells will be added in future releases.)
|
||||
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.)
|
||||
|
||||
Additionally, `cheat` supports enhanced autocompletion via integration with
|
||||
[fzf][]. To enable `fzf` integration:
|
||||
|
||||
[Releases]: https://github.com/cheat/cheat/releases
|
||||
[bash]: https://github.com/cheat/cheat/blob/master/scripts/fzf.bash
|
||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||
[fzf]: https://github.com/junegunn/fzf
|
||||
1. Ensure that `fzf` is available on your `$PATH`
|
||||
2. Set an envvar: `export CHEAT_USE_FZF=true`
|
||||
|
||||
[INSTALLING.md]: INSTALLING.md
|
||||
[Releases]: https://github.com/cheat/cheat/releases
|
||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||
[completions]: https://github.com/cheat/cheat/tree/master/scripts
|
||||
[fzf]: https://github.com/junegunn/fzf
|
||||
[go]: https://golang.org
|
||||
|
@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TODO: this script has been made obsolete by the Makefile, yet downstream
|
||||
# package managers plausibly rely on it for compiling locally. Remove this file
|
||||
# after downstream maintainers have had time to modify their packages to simply
|
||||
# invoke `make` in the project root.
|
||||
|
||||
# locate the cheat project root
|
||||
BINDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
APPDIR=$(readlink -f "$BINDIR/..")
|
||||
|
||||
# compile the executable
|
||||
cd $APPDIR
|
||||
|
||||
make
|
@ -1,3 +1,4 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// This script embeds `docopt.txt and `conf.yml` into the binary during at
|
||||
@ -5,13 +6,11 @@
|
||||
|
||||
package main
|
||||
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
@ -52,10 +51,10 @@ func main() {
|
||||
for _, file := range files {
|
||||
|
||||
// delete the outfile
|
||||
os.Remove(path.Join(root, file.Out))
|
||||
os.Remove(filepath.Join(root, file.Out))
|
||||
|
||||
// read the static template
|
||||
bytes, err := ioutil.ReadFile(path.Join(root, file.In))
|
||||
bytes, err := ioutil.ReadFile(filepath.Join(root, file.In))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -64,7 +63,7 @@ func main() {
|
||||
data := template(file.Method, string(bytes))
|
||||
|
||||
// write the file to the specified outpath
|
||||
spath := path.Join(root, file.Out)
|
||||
spath := filepath.Join(root, file.Out)
|
||||
err = ioutil.WriteFile(spath, []byte(data), 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
11
cmd/cheat/cmd_conf.go
Normal file
11
cmd/cheat/cmd_conf.go
Normal file
@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
func cmdConf(opts map[string]interface{}, conf config.Config) {
|
||||
fmt.Println(conf.Path)
|
||||
}
|
@ -1,28 +1,27 @@
|
||||
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(opts map[string]interface{}, conf config.Config) {
|
||||
|
||||
// initialize a tabwriter to produce cleanly columnized output
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
var out bytes.Buffer
|
||||
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
|
||||
|
||||
// generate sorted, columnized output
|
||||
for _, path := range conf.Cheatpaths {
|
||||
fmt.Fprintln(w, fmt.Sprintf(
|
||||
"%s:\t%s",
|
||||
path.Name,
|
||||
path.Path,
|
||||
))
|
||||
fmt.Fprintf(w, "%s:\t%s\n", path.Name, path.Path)
|
||||
}
|
||||
|
||||
// write columnized output to stdout
|
||||
w.Flush()
|
||||
display.Write(out.String(), conf)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"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.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -58,10 +58,10 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
||||
}
|
||||
|
||||
// compute the new edit path
|
||||
editpath = path.Join(writepath.Path, sheet.Title)
|
||||
editpath = filepath.Join(writepath.Path, sheet.Title)
|
||||
|
||||
// create any necessary subdirectories
|
||||
dirs := path.Dir(editpath)
|
||||
dirs := filepath.Dir(editpath)
|
||||
if dirs != "." {
|
||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create directory: %s, %v\n", dirs, err)
|
||||
@ -87,10 +87,10 @@ func cmdEdit(opts map[string]interface{}, conf config.Config) {
|
||||
}
|
||||
|
||||
// compute the new edit path
|
||||
editpath = path.Join(writepath.Path, cheatsheet)
|
||||
editpath = filepath.Join(writepath.Path, cheatsheet)
|
||||
|
||||
// create any necessary subdirectories
|
||||
dirs := path.Dir(editpath)
|
||||
dirs := filepath.Dir(editpath)
|
||||
if dirs != "." {
|
||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create directory: %s, %v\n", dirs, err)
|
||||
@ -99,8 +99,15 @@ 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(conf.Editor, editpath)
|
||||
cmd := exec.Command(editor, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
|
@ -2,9 +2,56 @@ 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() {
|
||||
fmt.Println(configs())
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@ -19,11 +21,11 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// filter cheatcheats by tag if --tag was provided
|
||||
// filter cheatsheets by tag if --tag was provided
|
||||
if opts["--tag"] != nil {
|
||||
cheatsheets = sheets.Filter(
|
||||
cheatsheets,
|
||||
@ -35,8 +37,8 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||
// sheets with local sheets), here we simply want to create a slice
|
||||
// containing all sheets.
|
||||
flattened := []sheet.Sheet{}
|
||||
for _, pathSheets := range cheatsheets {
|
||||
for _, s := range pathSheets {
|
||||
for _, pathsheets := range cheatsheets {
|
||||
for _, s := range pathsheets {
|
||||
flattened = append(flattened, s)
|
||||
}
|
||||
}
|
||||
@ -61,10 +63,7 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||
// compile the regex
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintln(
|
||||
os.Stderr,
|
||||
fmt.Sprintf("failed to compile regexp: %s, %v", pattern, err),
|
||||
)
|
||||
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -79,25 +78,24 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
|
||||
flattened = filtered
|
||||
}
|
||||
|
||||
// exit early if no cheatsheets are available
|
||||
// return exit code 2 if no cheatsheets are available
|
||||
if len(flattened) == 0 {
|
||||
os.Exit(0)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// initialize a tabwriter to produce cleanly columnized output
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
||||
var out bytes.Buffer
|
||||
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
|
||||
|
||||
// write a header row
|
||||
fmt.Fprintln(w, "title:\tfile:\ttags:")
|
||||
|
||||
// generate sorted, columnized output
|
||||
fmt.Fprintln(w, "title:\tfile:\ttags:")
|
||||
for _, sheet := range flattened {
|
||||
fmt.Fprintln(w, fmt.Sprintf(
|
||||
"%s\t%s\t%s",
|
||||
sheet.Title,
|
||||
sheet.Path,
|
||||
strings.Join(sheet.Tags, ","),
|
||||
))
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", sheet.Title, sheet.Path, strings.Join(sheet.Tags, ","))
|
||||
}
|
||||
|
||||
// write columnized output to stdout
|
||||
w.Flush()
|
||||
display.Write(out.String(), conf)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -37,19 +37,19 @@ func cmdRemove(opts map[string]interface{}, conf config.Config) {
|
||||
// fail early if the requested cheatsheet does not exist
|
||||
sheet, ok := consolidated[cheatsheet]
|
||||
if !ok {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("no cheatsheet found for '%s'.\n", cheatsheet))
|
||||
os.Exit(1)
|
||||
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.Fprintln(os.Stderr, fmt.Sprintf("cheatsheet '%s' is read-only.", cheatsheet))
|
||||
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.Fprintln(os.Stderr, fmt.Sprintf("failed to delete sheet: %s, %v", sheet.Title, err))
|
||||
fmt.Fprintf(os.Stderr, "failed to delete sheet: %s, %v\n", sheet.Title, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
"github.com/cheat/cheat/internal/display"
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
@ -18,7 +19,7 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -30,40 +31,67 @@ func cmdSearch(opts map[string]interface{}, conf config.Config) {
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// iterate over each cheatpath
|
||||
out := ""
|
||||
for _, pathcheats := range cheatsheets {
|
||||
|
||||
// sort the cheatsheets alphabetically, and search for matches
|
||||
for _, sheet := range sheets.Sort(consolidated) {
|
||||
// sort the cheatsheets alphabetically, and search for matches
|
||||
for _, sheet := range sheets.Sort(pathcheats) {
|
||||
|
||||
// assume that we want to perform a case-insensitive search for <phrase>
|
||||
pattern := "(?i)" + phrase
|
||||
|
||||
// unless --regex is provided, in which case we pass the regex unaltered
|
||||
if opts["--regex"] == true {
|
||||
pattern = phrase
|
||||
}
|
||||
|
||||
// compile the regex
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to compile regexp: %s, %v", pattern, err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// search the sheet
|
||||
matches := sheet.Search(reg, conf.Color(opts))
|
||||
|
||||
// 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 <cheatsheet> was provided, constrain the search only to
|
||||
// matching cheatsheets
|
||||
if opts["<cheatsheet>"] != nil && sheet.Title != opts["<cheatsheet>"] {
|
||||
continue
|
||||
}
|
||||
fmt.Print("\n")
|
||||
|
||||
// assume that we want to perform a case-insensitive search for <phrase>
|
||||
pattern := "(?i)" + phrase
|
||||
|
||||
// unless --regex is provided, in which case we pass the regex unaltered
|
||||
if opts["--regex"] == true {
|
||||
pattern = phrase
|
||||
}
|
||||
|
||||
// compile the regex
|
||||
reg, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to compile regexp: %s, %v\n", pattern, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// `Search` will return text entries that match the search terms.
|
||||
// We're using it here to overwrite the prior cheatsheet Text,
|
||||
// filtering it to only what is relevant.
|
||||
sheet.Text = sheet.Search(reg)
|
||||
|
||||
// if the sheet did not match the search, ignore it and move on
|
||||
if sheet.Text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// if colorization was requested, apply it here
|
||||
if conf.Color(opts) {
|
||||
sheet.Colorize(conf)
|
||||
}
|
||||
|
||||
// display the cheatsheet body
|
||||
out += fmt.Sprintf(
|
||||
"%s %s\n%s\n",
|
||||
// append the cheatsheet title
|
||||
sheet.Title,
|
||||
// append the cheatsheet path
|
||||
display.Faint(fmt.Sprintf("(%s)", sheet.CheatPath), conf),
|
||||
// indent each line of content
|
||||
display.Indent(sheet.Text),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
"github.com/cheat/cheat/internal/display"
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
@ -14,12 +15,16 @@ func cmdTags(opts map[string]interface{}, conf config.Config) {
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// write sheet tags to stdout
|
||||
// assemble the output
|
||||
out := ""
|
||||
for _, tag := range sheets.Tags(cheatsheets) {
|
||||
fmt.Println(tag)
|
||||
out += fmt.Sprintln(tag)
|
||||
}
|
||||
|
||||
// display the output
|
||||
display.Write(out, conf)
|
||||
}
|
||||
|
@ -5,9 +5,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/quick"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
"github.com/cheat/cheat/internal/display"
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
@ -19,7 +18,7 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
||||
// load the cheatsheets
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf("failed to list cheatsheets: %v", err))
|
||||
fmt.Fprintf(os.Stderr, "failed to list cheatsheets: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@ -31,41 +30,53 @@ func cmdView(opts map[string]interface{}, conf config.Config) {
|
||||
)
|
||||
}
|
||||
|
||||
// consolidate the cheatsheets found on all paths into a single map of
|
||||
// `title` => `sheet` (ie, allow more local cheatsheets to override less
|
||||
// local cheatsheets)
|
||||
// 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)
|
||||
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(0)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if !conf.Color(opts) {
|
||||
fmt.Print(sheet.Text)
|
||||
os.Exit(0)
|
||||
// apply colorization if requested
|
||||
if conf.Color(opts) {
|
||||
sheet.Colorize(conf)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// display the cheatsheet
|
||||
display.Write(sheet.Text, conf)
|
||||
}
|
||||
|
@ -2,18 +2,20 @@ Usage:
|
||||
cheat [options] [<cheatsheet>]
|
||||
|
||||
Options:
|
||||
--init Write a default config file to stdout
|
||||
-c --colorize Colorize output
|
||||
-d --directories List cheatsheet directories
|
||||
-e --edit=<sheet> Edit <sheet>
|
||||
-l --list List cheatsheets
|
||||
-p --path=<name> Return only sheets found on path <name>
|
||||
-r --regex Treat search <phrase> as a regex
|
||||
-s --search=<phrase> Search cheatsheets for <phrase>
|
||||
-t --tag=<tag> Return only sheets matching <tag>
|
||||
-T --tags List all tags in use
|
||||
-v --version Print the version number
|
||||
--rm=<sheet> Remove (delete) <sheet>
|
||||
--init Write a default config file to stdout
|
||||
-a --all Search among all cheatpaths
|
||||
-c --colorize Colorize output
|
||||
-d --directories List cheatsheet directories
|
||||
-e --edit=<cheatsheet> Edit <cheatsheet>
|
||||
-l --list List cheatsheets
|
||||
-p --path=<name> Return only sheets found on cheatpath <name>
|
||||
-r --regex Treat search <phrase> as a regex
|
||||
-s --search=<phrase> Search cheatsheets for <phrase>
|
||||
-t --tag=<tag> Return only sheets matching <tag>
|
||||
-T --tags List all tags in use
|
||||
-v --version Print the version number
|
||||
--rm=<cheatsheet> Remove (delete) <cheatsheet>
|
||||
--conf Display the config file path
|
||||
|
||||
Examples:
|
||||
|
||||
@ -52,3 +54,6 @@ Examples:
|
||||
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
cheat --rm foo/bar
|
||||
|
||||
To view the configuration file path:
|
||||
cheat --conf
|
||||
|
@ -9,17 +9,19 @@ import (
|
||||
"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 = "3.4.1"
|
||||
const version = "4.4.0"
|
||||
|
||||
func main() {
|
||||
|
||||
// initialize options
|
||||
opts, err := docopt.Parse(usage(), nil, true, version, false)
|
||||
opts, err := docopt.ParseArgs(usage(), nil, version)
|
||||
if err != nil {
|
||||
// panic here, because this should never happen
|
||||
panic(fmt.Errorf("docopt failed to parse: %v", err))
|
||||
@ -32,15 +34,25 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if runtime.GOOS == "windows" {
|
||||
pair[0] = strings.ToUpper(pair[0])
|
||||
}
|
||||
envvars[pair[0]] = pair[1]
|
||||
}
|
||||
|
||||
// load the os-specifc paths at which the config file may be located
|
||||
confpaths, err := config.Paths(runtime.GOOS, envvars)
|
||||
// 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)
|
||||
@ -49,22 +61,33 @@ func main() {
|
||||
// search for the config file in the above paths
|
||||
confpath, err := config.Path(confpaths)
|
||||
if err != nil {
|
||||
|
||||
// the config file does not exist, so we'll try to create one
|
||||
if err = config.Init(confpaths[0], configs()); err != nil {
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"failed to create config file: %s: %v\n",
|
||||
confpaths[0],
|
||||
err,
|
||||
)
|
||||
// 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 edit this file now to configure cheat.")
|
||||
fmt.Println("Please read this file for advanced configuration information.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@ -97,6 +120,9 @@ func main() {
|
||||
var cmd func(map[string]interface{}, config.Config)
|
||||
|
||||
switch {
|
||||
case opts["--conf"].(bool):
|
||||
cmd = cmdConf
|
||||
|
||||
case opts["--directories"].(bool):
|
||||
cmd = cmdDirectories
|
||||
|
||||
@ -118,6 +144,9 @@ func main() {
|
||||
case opts["<cheatsheet>"] != nil:
|
||||
cmd = cmdView
|
||||
|
||||
case opts["--tag"] != nil && opts["--tag"].(string) != "":
|
||||
cmd = cmdList
|
||||
|
||||
default:
|
||||
fmt.Println(usage())
|
||||
os.Exit(0)
|
||||
|
@ -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: vim
|
||||
editor: EDITOR_PATH
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: true
|
||||
colorize: false
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
@ -21,56 +21,73 @@ style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal16m
|
||||
formatter: terminal256
|
||||
|
||||
# The paths at which cheatsheets are available. Tags associated with a cheatpath
|
||||
# are automatically attached to all cheatsheets residing on that path.
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# 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 are scoped, such that more "local" cheatpaths take priority over
|
||||
# more "global" cheatpaths. (The most global cheatpath is listed first in this
|
||||
# file; the most local is listed last.) For example, if there is a 'tar'
|
||||
# cheatsheet on both global and local paths, you'll be presented with the local
|
||||
# one by default. ('cheat -p' can be used to view cheatsheets from alternative
|
||||
# cheatpaths.)
|
||||
#
|
||||
# Cheatpaths can also be tagged as "read only". This instructs cheat not to
|
||||
# automatically create cheatsheets on a read-only cheatpath. Instead, when you
|
||||
# would like to edit a read-only cheatsheet using 'cheat -e', cheat will
|
||||
# perform a copy-on-write of that cheatsheet from a read-only cheatpath to a
|
||||
# writeable cheatpath.
|
||||
#
|
||||
# This is very useful when you would like to maintain, for example, a
|
||||
# "pristine" repository of community cheatsheets on one cheatpath, and an
|
||||
# editable personal reponsity of cheatsheets on another cheatpath.
|
||||
#
|
||||
# Cheatpaths can be also configured to automatically apply tags to cheatsheets
|
||||
# on certain paths, which can be useful for querying purposes.
|
||||
# Example: 'cheat -t work jenkins'.
|
||||
#
|
||||
# Community cheatsheets must be installed separately, though you may have
|
||||
# downloaded them automatically when installing 'cheat'. If not, you may
|
||||
# download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
cheatpaths:
|
||||
|
||||
# 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.
|
||||
#
|
||||
# TODO: regarding community cheatsheets: these must be installed separately.
|
||||
# You may download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
#
|
||||
# Once downloaded, ensure that 'path' below points to the location at which
|
||||
# you downloaded the community cheatsheets.
|
||||
# Cheatpath properties mean the following:
|
||||
# 'name': the name of the cheatpath (view with 'cheat -d', filter with 'cheat -p')
|
||||
# 'path': the filesystem path of the cheatsheet directory (view with 'cheat -d')
|
||||
# 'tags': tags that should be automatically applied to sheets on this path
|
||||
# 'readonly': shall user-created ('cheat -e') cheatsheets be saved here?
|
||||
- name: community
|
||||
path: ~/cheat/cheatsheets/community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# If you have personalized cheatsheets, list them last. They will take
|
||||
# precedence over the more global cheatsheets.
|
||||
- name: personal
|
||||
path: ~/cheat/cheatsheets/personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# While it requires no specific 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.
|
||||
# While it requires no configuration here, it's also worth noting that
|
||||
# cheat will automatically append directories named '.cheat' within the
|
||||
# current working directory to the 'cheatpath'. This can be very useful if
|
||||
# you'd like to closely associate cheatsheets with, for example, a directory
|
||||
# containing source code.
|
||||
#
|
||||
# Such "directory-scoped" cheatsheets will be treated as the most "local"
|
||||
# cheatsheets, and will override less "local" cheatsheets. Likewise,
|
||||
# cheatsheets, and will override less "local" cheatsheets. Similarly,
|
||||
# directory-scoped cheatsheets will always be editable ('readonly: false').
|
||||
`)
|
||||
}
|
||||
|
@ -11,18 +11,20 @@ func usage() string {
|
||||
cheat [options] [<cheatsheet>]
|
||||
|
||||
Options:
|
||||
--init Write a default config file to stdout
|
||||
-c --colorize Colorize output
|
||||
-d --directories List cheatsheet directories
|
||||
-e --edit=<sheet> Edit <sheet>
|
||||
-l --list List cheatsheets
|
||||
-p --path=<name> Return only sheets found on path <name>
|
||||
-r --regex Treat search <phrase> as a regex
|
||||
-s --search=<phrase> Search cheatsheets for <phrase>
|
||||
-t --tag=<tag> Return only sheets matching <tag>
|
||||
-T --tags List all tags in use
|
||||
-v --version Print the version number
|
||||
--rm=<sheet> Remove (delete) <sheet>
|
||||
--init Write a default config file to stdout
|
||||
-a --all Search among all cheatpaths
|
||||
-c --colorize Colorize output
|
||||
-d --directories List cheatsheet directories
|
||||
-e --edit=<cheatsheet> Edit <cheatsheet>
|
||||
-l --list List cheatsheets
|
||||
-p --path=<name> Return only sheets found on cheatpath <name>
|
||||
-r --regex Treat search <phrase> as a regex
|
||||
-s --search=<phrase> Search cheatsheets for <phrase>
|
||||
-t --tag=<tag> Return only sheets matching <tag>
|
||||
-T --tags List all tags in use
|
||||
-v --version Print the version number
|
||||
--rm=<cheatsheet> Remove (delete) <cheatsheet>
|
||||
--conf Display the config file path
|
||||
|
||||
Examples:
|
||||
|
||||
@ -61,5 +63,8 @@ Examples:
|
||||
|
||||
To remove (delete) the foo/bar cheatsheet:
|
||||
cheat --rm foo/bar
|
||||
|
||||
To view the configuration file path:
|
||||
cheat --conf
|
||||
`)
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
# The editor to use with 'cheat -e <sheet>'. Defaults to $EDITOR or $VISUAL.
|
||||
editor: vim
|
||||
editor: EDITOR_PATH
|
||||
|
||||
# Should 'cheat' always colorize output?
|
||||
colorize: true
|
||||
colorize: false
|
||||
|
||||
# Which 'chroma' colorscheme should be applied to the output?
|
||||
# Options are available here:
|
||||
@ -12,54 +12,71 @@ style: monokai
|
||||
|
||||
# Which 'chroma' "formatter" should be applied?
|
||||
# One of: "terminal", "terminal256", "terminal16m"
|
||||
formatter: terminal16m
|
||||
formatter: terminal256
|
||||
|
||||
# The paths at which cheatsheets are available. Tags associated with a cheatpath
|
||||
# are automatically attached to all cheatsheets residing on that path.
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# 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 are scoped, such that more "local" cheatpaths take priority over
|
||||
# more "global" cheatpaths. (The most global cheatpath is listed first in this
|
||||
# file; the most local is listed last.) For example, if there is a 'tar'
|
||||
# cheatsheet on both global and local paths, you'll be presented with the local
|
||||
# one by default. ('cheat -p' can be used to view cheatsheets from alternative
|
||||
# cheatpaths.)
|
||||
#
|
||||
# Cheatpaths can also be tagged as "read only". This instructs cheat not to
|
||||
# automatically create cheatsheets on a read-only cheatpath. Instead, when you
|
||||
# would like to edit a read-only cheatsheet using 'cheat -e', cheat will
|
||||
# perform a copy-on-write of that cheatsheet from a read-only cheatpath to a
|
||||
# writeable cheatpath.
|
||||
#
|
||||
# This is very useful when you would like to maintain, for example, a
|
||||
# "pristine" repository of community cheatsheets on one cheatpath, and an
|
||||
# editable personal reponsity of cheatsheets on another cheatpath.
|
||||
#
|
||||
# Cheatpaths can be also configured to automatically apply tags to cheatsheets
|
||||
# on certain paths, which can be useful for querying purposes.
|
||||
# Example: 'cheat -t work jenkins'.
|
||||
#
|
||||
# Community cheatsheets must be installed separately, though you may have
|
||||
# downloaded them automatically when installing 'cheat'. If not, you may
|
||||
# download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
cheatpaths:
|
||||
|
||||
# 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.
|
||||
#
|
||||
# TODO: regarding community cheatsheets: these must be installed separately.
|
||||
# You may download them here:
|
||||
#
|
||||
# https://github.com/cheat/cheatsheets
|
||||
#
|
||||
# Once downloaded, ensure that 'path' below points to the location at which
|
||||
# you downloaded the community cheatsheets.
|
||||
# Cheatpath properties mean the following:
|
||||
# 'name': the name of the cheatpath (view with 'cheat -d', filter with 'cheat -p')
|
||||
# 'path': the filesystem path of the cheatsheet directory (view with 'cheat -d')
|
||||
# 'tags': tags that should be automatically applied to sheets on this path
|
||||
# 'readonly': shall user-created ('cheat -e') cheatsheets be saved here?
|
||||
- name: community
|
||||
path: ~/cheat/cheatsheets/community
|
||||
path: COMMUNITY_PATH
|
||||
tags: [ community ]
|
||||
readonly: true
|
||||
|
||||
# If you have personalized cheatsheets, list them last. They will take
|
||||
# precedence over the more global cheatsheets.
|
||||
- name: personal
|
||||
path: ~/cheat/cheatsheets/personal
|
||||
path: PERSONAL_PATH
|
||||
tags: [ personal ]
|
||||
readonly: false
|
||||
|
||||
# While it requires no specific 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.
|
||||
# While it requires no configuration here, it's also worth noting that
|
||||
# cheat will automatically append directories named '.cheat' within the
|
||||
# current working directory to the 'cheatpath'. This can be very useful if
|
||||
# you'd like to closely associate cheatsheets with, for example, a directory
|
||||
# containing source code.
|
||||
#
|
||||
# Such "directory-scoped" cheatsheets will be treated as the most "local"
|
||||
# cheatsheets, and will override less "local" cheatsheets. Likewise,
|
||||
# cheatsheets, and will override less "local" cheatsheets. Similarly,
|
||||
# directory-scoped cheatsheets will always be editable ('readonly: false').
|
||||
|
221
doc/cheat.1
Normal file
221
doc/cheat.1
Normal file
@ -0,0 +1,221 @@
|
||||
.\" Automatically generated by Pandoc 2.2.1
|
||||
.\"
|
||||
.TH "CHEAT" "1" "" "" "General Commands Manual"
|
||||
.hy
|
||||
.SH NAME
|
||||
.PP
|
||||
\f[B]cheat\f[] \[em] create and view command\-line cheatsheets
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\f[B]cheat\f[] [options] [\f[I]CHEATSHEET\f[]]
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
\f[B]cheat\f[] allows you to create and view interactive cheatsheets on
|
||||
the command\-line.
|
||||
It was designed to help remind *nix system administrators of options for
|
||||
commands that they use frequently, but not frequently enough to
|
||||
remember.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \[en]init
|
||||
Print a config file to stdout.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-c, \[en]colorize
|
||||
Colorize output.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-d, \[en]directories
|
||||
List cheatsheet directories.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-e, \[en]edit=\f[I]CHEATSHEET\f[]
|
||||
Open \f[I]CHEATSHEET\f[] for editing.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-l, \[en]list
|
||||
List available cheatsheets.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-p, \[en]path=\f[I]PATH\f[]
|
||||
Filter only to sheets found on path \f[I]PATH\f[].
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-r, \[en]regex
|
||||
Treat search \f[I]PHRASE\f[] as a regular expression.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-s, \[en]search=\f[I]PHRASE\f[]
|
||||
Search cheatsheets for \f[I]PHRASE\f[].
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-t, \[en]tag=\f[I]TAG\f[]
|
||||
Filter only to sheets tagged with \f[I]TAG\f[].
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-T, \[en]tags
|
||||
List all tags in use.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \-v, \[en]version
|
||||
Print the version number.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \[en]rm=\f[I]CHEATSHEET\f[]
|
||||
Remove (deletes) \f[I]CHEATSHEET\f[].
|
||||
.RS
|
||||
.RE
|
||||
.SH EXAMPLES
|
||||
.TP
|
||||
.B To view the foo cheatsheet:
|
||||
cheat \f[I]foo\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To edit (or create) the foo cheatsheet:
|
||||
cheat \-e \f[I]foo\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To edit (or create) the foo/bar cheatsheet on the `work' cheatpath:
|
||||
cheat \-p \f[I]work\f[] \-e \f[I]foo/bar\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To view all cheatsheet directories:
|
||||
cheat \-d
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To list all available cheatsheets:
|
||||
cheat \-l
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To list all cheatsheets whose titles match `apt':
|
||||
cheat \-l \f[I]apt\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To list all tags in use:
|
||||
cheat \-T
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To list available cheatsheets that are tagged as `personal':
|
||||
cheat \-l \-t \f[I]personal\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To search for `ssh' among all cheatsheets, and colorize matches:
|
||||
cheat \-c \-s \f[I]ssh\f[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B 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[]
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B To remove (delete) the foo/bar cheatsheet:
|
||||
cheat \[en]rm \f[I]foo/bar\f[]
|
||||
.RS
|
||||
.RE
|
||||
.SH FILES
|
||||
.SS Configuration
|
||||
.PP
|
||||
\f[B]cheat\f[] is configured via a YAML file that is conventionally
|
||||
named \f[I]conf.yaml\f[].
|
||||
\f[B]cheat\f[] will search for \f[I]conf.yaml\f[] in varying locations,
|
||||
depending upon your platform:
|
||||
.SS Linux, OSX, and other Unixes
|
||||
.IP "1." 3
|
||||
\f[B]CHEAT_CONFIG_PATH\f[]
|
||||
.IP "2." 3
|
||||
\f[B]XDG_CONFIG_HOME\f[]/cheat/conf.yaml
|
||||
.IP "3." 3
|
||||
\f[B]$HOME\f[]/.config/cheat/conf.yml
|
||||
.IP "4." 3
|
||||
\f[B]$HOME\f[]/.cheat/conf.yml
|
||||
.SS Windows
|
||||
.IP "1." 3
|
||||
\f[B]CHEAT_CONFIG_PATH\f[]
|
||||
.IP "2." 3
|
||||
\f[B]APPDATA\f[]/cheat/conf.yml
|
||||
.IP "3." 3
|
||||
\f[B]PROGRAMDATA\f[]/cheat/conf.yml
|
||||
.PP
|
||||
\f[B]cheat\f[] will search in the order specified above.
|
||||
The first \f[I]conf.yaml\f[] encountered will be respected.
|
||||
.PP
|
||||
If \f[B]cheat\f[] 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
|
||||
\f[B]cheat \[en]init\f[] and saving its output to the appropriate
|
||||
location for your platform.
|
||||
.SS Cheatpaths
|
||||
.PP
|
||||
\f[B]cheat\f[] 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[], and viewed via
|
||||
\f[B]cheat \-d\f[].
|
||||
.PP
|
||||
For detailed instructions on how to configure cheatpaths, please refer
|
||||
to the comments in conf.yml.
|
||||
.SS Autocompletion
|
||||
.PP
|
||||
Autocompletion scripts for \f[B]bash\f[], \f[B]zsh\f[], and
|
||||
\f[B]fish\f[] are available for download:
|
||||
.IP \[bu] 2
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.bash>
|
||||
.IP \[bu] 2
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.fish>
|
||||
.IP \[bu] 2
|
||||
<https://github.com/cheat/cheat/blob/master/scripts/cheat.zsh>
|
||||
.PP
|
||||
The \f[B]bash\f[] and \f[B]zsh\f[] scripts provide optional integration
|
||||
with \f[B]fzf\f[], if the latter is available on your \f[B]PATH\f[].
|
||||
.PP
|
||||
The installation process will vary per system and shell configuration,
|
||||
and thus will not be discussed here.
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B \f[B]CHEAT_CONFIG_PATH\f[]
|
||||
The path at which the config file is available.
|
||||
If \f[B]CHEAT_CONFIG_PATH\f[] is set, all other config paths will be
|
||||
ignored.
|
||||
.RS
|
||||
.RE
|
||||
.TP
|
||||
.B \f[B]CHEAT_USE_FZF\f[]
|
||||
If set, autocompletion scripts will attempt to integrate with
|
||||
\f[B]fzf\f[].
|
||||
.RS
|
||||
.RE
|
||||
.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[]
|
192
doc/cheat.1.md
Normal file
192
doc/cheat.1.md
Normal file
@ -0,0 +1,192 @@
|
||||
% 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)**
|
||||
|
33
go.mod
33
go.mod
@ -1,14 +1,35 @@
|
||||
module github.com/cheat/cheat
|
||||
|
||||
go 1.13
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.7.1
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
||||
github.com/go-git/go-git/v5 v5.4.2
|
||||
github.com/mattn/go-isatty v0.0.16
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/cloudflare/circl v1.2.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.2 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
168
go.sum
168
go.sum
@ -1,48 +1,146 @@
|
||||
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.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ=
|
||||
github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
|
||||
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
|
||||
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/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
|
||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
|
||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0AE5csawV4YXMNGNQQXvLRps3z2Z59OPO+I=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
|
||||
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec=
|
||||
github.com/cloudflare/circl v1.2.0/go.mod h1:Ch2UgYr6ti2KTtlejELlROl0YIYj7SLjAC8M+INXlMk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/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.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
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/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
|
||||
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
|
||||
github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
|
||||
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
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/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xanzy/ssh-agent v0.3.2 h1:eKj4SX2Fe7mui28ZgnFW5fmTz1EIr7ugo5s6wDxdHBM=
|
||||
github.com/xanzy/ssh-agent v0.3.2/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/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-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
||||
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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -46,7 +46,7 @@ func TestFilterFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
// filter the paths
|
||||
paths, err := Filter(paths, "qux")
|
||||
_, err := Filter(paths, "qux")
|
||||
if err == nil {
|
||||
t.Errorf("failed to return an error on non-existent cheatpath")
|
||||
}
|
||||
|
@ -11,12 +11,10 @@ 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 == false {
|
||||
if !cheatpaths[i].ReadOnly {
|
||||
return cheatpaths[i], nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// otherwise, return an error
|
||||
|
22
internal/config/color_test.go
Normal file
22
internal/config/color_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
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)")
|
||||
}
|
||||
}
|
@ -2,9 +2,9 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
cp "github.com/cheat/cheat/internal/cheatpath"
|
||||
|
||||
@ -19,13 +19,15 @@ type Config struct {
|
||||
Cheatpaths []cp.Cheatpath `yaml:"cheatpaths"`
|
||||
Style string `yaml:"style"`
|
||||
Formatter string `yaml:"formatter"`
|
||||
Pager string `yaml:"pager"`
|
||||
Path string
|
||||
}
|
||||
|
||||
// New returns a new Config struct
|
||||
func New(opts map[string]interface{}, confPath string, resolve bool) (Config, error) {
|
||||
|
||||
// read the config file
|
||||
buf, err := ioutil.ReadFile(confPath)
|
||||
buf, err := os.ReadFile(confPath)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("could not read config file: %v", err)
|
||||
}
|
||||
@ -33,6 +35,9 @@ func New(opts map[string]interface{}, confPath string, resolve bool) (Config, er
|
||||
// initialize a config object
|
||||
conf := Config{}
|
||||
|
||||
// store the config path
|
||||
conf.Path = confPath
|
||||
|
||||
// unmarshal the yaml
|
||||
err = yaml.UnmarshalStrict(buf, &conf)
|
||||
if err != nil {
|
||||
@ -90,14 +95,11 @@ func New(opts map[string]interface{}, confPath string, resolve bool) (Config, er
|
||||
conf.Cheatpaths[i].Path = expanded
|
||||
}
|
||||
|
||||
// if an editor was not provided in the configs, look to envvars
|
||||
// if an editor was not provided in the configs, attempt to choose one
|
||||
// that's appropriate for the environment
|
||||
if conf.Editor == "" {
|
||||
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")
|
||||
if conf.Editor, err = Editor(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,8 +110,11 @@ func New(opts map[string]interface{}, confPath string, resolve bool) (Config, er
|
||||
|
||||
// if a chroma formatter was not provided, set a default
|
||||
if conf.Formatter == "" {
|
||||
conf.Formatter = "terminal16m"
|
||||
conf.Formatter = "terminal"
|
||||
}
|
||||
|
||||
// load the pager
|
||||
conf.Pager = strings.TrimSpace(conf.Pager)
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
@ -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 return an error on empty editor")
|
||||
if err != nil {
|
||||
t.Errorf("failed to initialize test: %v", err)
|
||||
}
|
||||
|
||||
// set editor, and assert that it is respected
|
||||
|
41
internal/config/editor.go
Normal file
41
internal/config/editor.go
Normal file
@ -0,0 +1,41 @@
|
||||
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")
|
||||
}
|
@ -2,7 +2,6 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
@ -16,7 +15,7 @@ func Init(confpath string, configs string) error {
|
||||
}
|
||||
|
||||
// write the config file
|
||||
if err := ioutil.WriteFile(confpath, []byte(configs), 0644); err != nil {
|
||||
if err := os.WriteFile(confpath, []byte(configs), 0644); err != nil {
|
||||
return fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
|
||||
|
37
internal/config/init_test.go
Normal file
37
internal/config/init_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
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)
|
||||
}
|
||||
}
|
32
internal/config/pager.go
Normal file
32
internal/config/pager.go
Normal file
@ -0,0 +1,32 @@
|
||||
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 ""
|
||||
}
|
52
internal/config/path_test.go
Normal file
52
internal/config/path_test.go
Normal file
@ -0,0 +1,52 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPathConfigNotExists asserts that `Path` identifies non-existent config
|
||||
// files
|
||||
func TestPathConfigNotExists(t *testing.T) {
|
||||
|
||||
// package (invalid) cheatpaths
|
||||
paths := []string{"/cheat-test-conf-does-not-exist"}
|
||||
|
||||
// assert
|
||||
if _, err := Path(paths); err == nil {
|
||||
t.Errorf("failed to identify non-existent config file")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TestPathConfigExists asserts that `Path` identifies existent config files
|
||||
func TestPathConfigExists(t *testing.T) {
|
||||
|
||||
// initialize a temporary config file
|
||||
confFile, err := os.CreateTemp("", "cheat-test")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
// clean up the temp file
|
||||
defer os.Remove(confFile.Name())
|
||||
|
||||
// package cheatpaths
|
||||
paths := []string{
|
||||
"/cheat-test-conf-does-not-exist",
|
||||
confFile.Name(),
|
||||
}
|
||||
|
||||
// assert
|
||||
got, err := Path(paths)
|
||||
if err != nil {
|
||||
t.Errorf("failed to identify config file: %v", err)
|
||||
}
|
||||
if got != confFile.Name() {
|
||||
t.Errorf(
|
||||
"failed to return config path: want: %s, got: %s",
|
||||
confFile.Name(),
|
||||
got,
|
||||
)
|
||||
}
|
||||
}
|
@ -2,14 +2,18 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// Paths returns config file paths that are appropriate for the operating
|
||||
// system
|
||||
func Paths(sys string, envvars map[string]string) ([]string, error) {
|
||||
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 {
|
||||
@ -24,25 +28,30 @@ func Paths(sys string, envvars map[string]string) ([]string, error) {
|
||||
}
|
||||
|
||||
switch sys {
|
||||
case "darwin", "linux", "freebsd":
|
||||
|
||||
// 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, path.Join(xdgpath, "/cheat/conf.yml"))
|
||||
paths = append(paths, filepath.Join(xdgpath, "cheat", "conf.yml"))
|
||||
}
|
||||
|
||||
// `HOME` will always be set on a POSIX-compliant system, though
|
||||
paths = append(paths, []string{
|
||||
path.Join(envvars["HOME"], ".config/cheat/conf.yml"),
|
||||
path.Join(envvars["HOME"], ".cheat/conf.yml"),
|
||||
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{
|
||||
path.Join(envvars["APPDATA"], "/cheat/conf.yml"),
|
||||
path.Join(envvars["PROGRAMDATA"], "/cheat/conf.yml"),
|
||||
filepath.Join(envvars["APPDATA"], "cheat", "conf.yml"),
|
||||
filepath.Join(envvars["PROGRAMDATA"], "cheat", "conf.yml"),
|
||||
}, nil
|
||||
default:
|
||||
return []string{}, fmt.Errorf("unsupported os: %s", sys)
|
||||
|
@ -11,14 +11,17 @@ import (
|
||||
// *nix platforms
|
||||
func TestValidatePathsNix(t *testing.T) {
|
||||
|
||||
// mock the user's home directory
|
||||
home := "/home/foo"
|
||||
|
||||
// mock some envvars
|
||||
envvars := map[string]string{
|
||||
"HOME": "/home/foo",
|
||||
"XDG_CONFIG_HOME": "/home/bar",
|
||||
}
|
||||
|
||||
// specify the platforms to test
|
||||
oses := []string{
|
||||
"android",
|
||||
"darwin",
|
||||
"freebsd",
|
||||
"linux",
|
||||
@ -27,7 +30,7 @@ func TestValidatePathsNix(t *testing.T) {
|
||||
// test each *nix os
|
||||
for _, os := range oses {
|
||||
// get the paths for the platform
|
||||
paths, err := Paths(os, envvars)
|
||||
paths, err := Paths(os, home, envvars)
|
||||
if err != nil {
|
||||
t.Errorf("paths returned an error: %v", err)
|
||||
}
|
||||
@ -37,6 +40,7 @@ func TestValidatePathsNix(t *testing.T) {
|
||||
"/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
|
||||
@ -54,10 +58,11 @@ func TestValidatePathsNix(t *testing.T) {
|
||||
// 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{
|
||||
"HOME": "/home/foo",
|
||||
}
|
||||
envvars := map[string]string{}
|
||||
|
||||
// specify the platforms to test
|
||||
oses := []string{
|
||||
@ -69,7 +74,7 @@ func TestValidatePathsNixNoXDG(t *testing.T) {
|
||||
// test each *nix os
|
||||
for _, os := range oses {
|
||||
// get the paths for the platform
|
||||
paths, err := Paths(os, envvars)
|
||||
paths, err := Paths(os, home, envvars)
|
||||
if err != nil {
|
||||
t.Errorf("paths returned an error: %v", err)
|
||||
}
|
||||
@ -78,6 +83,7 @@ func TestValidatePathsNixNoXDG(t *testing.T) {
|
||||
want := []string{
|
||||
"/home/foo/.config/cheat/conf.yml",
|
||||
"/home/foo/.cheat/conf.yml",
|
||||
"/etc/cheat/conf.yml",
|
||||
}
|
||||
|
||||
// assert that output matches expectations
|
||||
@ -95,6 +101,9 @@ func TestValidatePathsNixNoXDG(t *testing.T) {
|
||||
// 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",
|
||||
@ -102,7 +111,7 @@ func TestValidatePathsWindows(t *testing.T) {
|
||||
}
|
||||
|
||||
// get the paths for the platform
|
||||
paths, err := Paths("windows", envvars)
|
||||
paths, err := Paths("windows", home, envvars)
|
||||
if err != nil {
|
||||
t.Errorf("paths returned an error: %v", err)
|
||||
}
|
||||
@ -126,7 +135,7 @@ func TestValidatePathsWindows(t *testing.T) {
|
||||
// TestValidatePathsUnsupported asserts that an error is returned on
|
||||
// unsupported platforms
|
||||
func TestValidatePathsUnsupported(t *testing.T) {
|
||||
_, err := Paths("unsupported", map[string]string{})
|
||||
_, err := Paths("unsupported", "", map[string]string{})
|
||||
if err == nil {
|
||||
t.Errorf("failed to return error on unsupported platform")
|
||||
}
|
||||
@ -136,15 +145,17 @@ func TestValidatePathsUnsupported(t *testing.T) {
|
||||
// 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{
|
||||
"HOME": "/home/foo",
|
||||
"XDG_CONFIG_HOME": "/home/bar",
|
||||
"CHEAT_CONFIG_PATH": "/home/baz/conf.yml",
|
||||
}
|
||||
|
||||
// get the paths for the platform
|
||||
paths, err := Paths("linux", envvars)
|
||||
paths, err := Paths("linux", home, envvars)
|
||||
if err != nil {
|
||||
t.Errorf("paths returned an error: %v", err)
|
||||
}
|
||||
|
18
internal/display/faint.go
Normal file
18
internal/display/faint.go
Normal file
@ -0,0 +1,18 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// Faint returns an faint string
|
||||
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
|
||||
}
|
27
internal/display/faint_test.go
Normal file
27
internal/display/faint_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
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)
|
||||
}
|
||||
}
|
21
internal/display/indent.go
Normal file
21
internal/display/indent.go
Normal file
@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
12
internal/display/indent_test.go
Normal file
12
internal/display/indent_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
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)
|
||||
}
|
||||
}
|
36
internal/display/write.go
Normal file
36
internal/display/write.go
Normal file
@ -0,0 +1,36 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
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
|
||||
}
|
37
internal/installer/prompt.go
Normal file
37
internal/installer/prompt.go
Normal file
@ -0,0 +1,37 @@
|
||||
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
|
||||
}
|
||||
}
|
66
internal/installer/run.go
Normal file
66
internal/installer/run.go
Normal file
@ -0,0 +1,66 @@
|
||||
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
|
||||
}
|
@ -13,7 +13,7 @@ func Path(filename string) string {
|
||||
// determine the path of this file during runtime
|
||||
_, thisfile, _, _ := runtime.Caller(0)
|
||||
|
||||
// compute the config path
|
||||
// compute the mock path
|
||||
file, err := filepath.Abs(
|
||||
path.Join(
|
||||
filepath.Dir(thisfile),
|
||||
@ -22,7 +22,7 @@ func Path(filename string) string {
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to resolve config path: %v", err))
|
||||
panic(fmt.Errorf("failed to resolve mock path: %v", err))
|
||||
}
|
||||
|
||||
return file
|
||||
|
25
internal/repo/clone.go
Normal file
25
internal/repo/clone.go
Normal file
@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
110
internal/repo/gitdir.go
Normal file
110
internal/repo/gitdir.go
Normal file
@ -0,0 +1,110 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GitDir returns `true` if we are iterating over a directory contained within
|
||||
// a repositories `.git` directory.
|
||||
func GitDir(path string) (bool, error) {
|
||||
|
||||
/*
|
||||
A bit of context is called for here, because this functionality has
|
||||
previously caused a number of tricky, subtle bugs.
|
||||
|
||||
Fundamentally, here we are simply trying to avoid walking over the
|
||||
contents of the `.git` directory. Doing so potentially makes
|
||||
hundreds/thousands of needless syscalls, and can noticeably harm
|
||||
performance on machines with slow disks.
|
||||
|
||||
The earliest effort to solve this problem involved simply returning
|
||||
`fs.SkipDir` when the cheatsheet file path began with `.`, signifying a
|
||||
hidden directory. This, however, caused two problems:
|
||||
|
||||
1. The `.cheat` directory was ignored
|
||||
2. Cheatsheets installed by `brew` (which were by default installed to
|
||||
`~/.config/cheat`) were ignored
|
||||
|
||||
See: https://github.com/cheat/cheat/issues/690
|
||||
|
||||
To remedy this, the exclusion criteria were narrowed, and the search
|
||||
for a literal `.` was replaced with a search for a literal `.git`.
|
||||
This, however, broke user installations that stored cheatsheets in
|
||||
`git` submodules, because such an installation would contain a `.git`
|
||||
file that pointed to the upstream repository.
|
||||
|
||||
See: https://github.com/cheat/cheat/issues/694
|
||||
|
||||
The next attempt at solving this was to search for a `.git` literal
|
||||
string in the cheatsheet file path. If a match was not found, we would
|
||||
continue to walk the directory, as before.
|
||||
|
||||
If a match *was* found, we determined whether `.git` referred to a file
|
||||
or directory, and would only stop walking the path in the latter case.
|
||||
|
||||
This, however, caused crashes if a cheatpath contained a `.gitignore`
|
||||
file. (Presumably, a crash would likewise occur on the presence of
|
||||
`.gitattributes`, `.gitmodules`, etc.)
|
||||
|
||||
See: https://github.com/cheat/cheat/issues/699
|
||||
|
||||
Accounting for all of the above (hopefully?), the current solution is
|
||||
not to search for `.git`, but `.git/` (including the directory
|
||||
separator), and then only ceasing to walk the directory on a match.
|
||||
|
||||
To summarize, this code must account for the following possibilities:
|
||||
|
||||
1. A cheatpath is not a repository
|
||||
2. A cheatpath is a repository
|
||||
3. A cheatpath is a repository, and contains a `.git*` file
|
||||
4. A cheatpath is a submodule
|
||||
5. A cheatpath is a hidden directory
|
||||
|
||||
Care must be taken to support the above on both Unix and Windows
|
||||
systems, which have different directory separators and line-endings.
|
||||
|
||||
There is a lot of nuance to all of this, and it would be worthwhile to
|
||||
do two things to stop writing bugs here:
|
||||
|
||||
1. Build integration tests around all of this
|
||||
2. Discard string-matching solutions entirely, and use `go-git` instead
|
||||
|
||||
NB: A reasonable smoke-test for ensuring that skipping is being applied
|
||||
correctly is to run the following command:
|
||||
|
||||
make && strace ./dist/cheat -l | wc -l
|
||||
|
||||
That check should be run twice: once normally, and once after
|
||||
commenting out the "skip" check in `sheets.Load`.
|
||||
|
||||
The specific line counts don't matter; what matters is that the number
|
||||
of syscalls should be significantly lower with the skip check enabled.
|
||||
*/
|
||||
|
||||
// determine if the literal string `.git` appears within `path`
|
||||
pos := strings.Index(path, fmt.Sprintf(".git%s", string(os.PathSeparator)))
|
||||
|
||||
// if it does not, we know for certain that we are not within a `.git`
|
||||
// directory.
|
||||
if pos == -1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If `path` does contain the string `.git`, we need to determine if we're
|
||||
// inside of a `.git` directory, or if `path` points to a cheatsheet that's
|
||||
// stored within a `git` submodule.
|
||||
//
|
||||
// See: https://github.com/cheat/cheat/issues/694
|
||||
|
||||
// truncate `path` to the occurrence of `.git`
|
||||
f, err := os.Stat(path[:pos+5])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to stat path %s: %v", path, err)
|
||||
}
|
||||
|
||||
// return true or false depending on whether the truncated path is a
|
||||
// directory
|
||||
return f.Mode().IsDir(), nil
|
||||
}
|
1
internal/repo/update.go
Normal file
1
internal/repo/update.go
Normal file
@ -0,0 +1 @@
|
||||
package repo
|
37
internal/sheet/colorize.go
Normal file
37
internal/sheet/colorize.go
Normal file
@ -0,0 +1,37 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
|
||||
"github.com/alecthomas/chroma/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()
|
||||
}
|
34
internal/sheet/colorize_test.go
Normal file
34
internal/sheet/colorize_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// TestColorize asserts that syntax-highlighting is correctly applied
|
||||
func TestColorize(t *testing.T) {
|
||||
|
||||
// mock configs
|
||||
conf := config.Config{
|
||||
Formatter: "terminal16m",
|
||||
Style: "solarized-dark",
|
||||
}
|
||||
|
||||
// mock a sheet
|
||||
s := Sheet{
|
||||
Text: "echo 'foo'",
|
||||
}
|
||||
|
||||
// colorize the sheet text
|
||||
s.Colorize(conf)
|
||||
|
||||
// initialize expectations
|
||||
want := "[38;2;181;137;0mecho[0m[38;2;147;161;161m"
|
||||
want += " [0m[38;2;42;161;152m'foo'[0m"
|
||||
|
||||
// assert
|
||||
if s.Text != want {
|
||||
t.Errorf("failed to colorize sheet: want: %s, got: %s", want, s.Text)
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// 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 := path.Dir(dest)
|
||||
dirs := filepath.Dir(dest)
|
||||
if dirs != "." {
|
||||
if err := os.MkdirAll(dirs, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %s, %v", dirs, err)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
@ -13,7 +12,7 @@ func TestCopyFlat(t *testing.T) {
|
||||
|
||||
// mock a cheatsheet file
|
||||
text := "this is the cheatsheet text"
|
||||
src, err := ioutil.TempFile("", "foo-src")
|
||||
src, err := os.CreateTemp("", "foo-src")
|
||||
if err != nil {
|
||||
t.Errorf("failed to mock cheatsheet: %v", err)
|
||||
}
|
||||
@ -25,7 +24,7 @@ func TestCopyFlat(t *testing.T) {
|
||||
}
|
||||
|
||||
// mock a cheatsheet struct
|
||||
sheet, err := New("foo", src.Name(), []string{}, false)
|
||||
sheet, err := New("foo", "community", src.Name(), []string{}, false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to init cheatsheet: %v", err)
|
||||
}
|
||||
@ -41,7 +40,7 @@ func TestCopyFlat(t *testing.T) {
|
||||
}
|
||||
|
||||
// assert that the destination file contains the correct text
|
||||
got, err := ioutil.ReadFile(outpath)
|
||||
got, err := os.ReadFile(outpath)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read destination file: %v", err)
|
||||
}
|
||||
@ -60,7 +59,7 @@ func TestCopyDeep(t *testing.T) {
|
||||
|
||||
// mock a cheatsheet file
|
||||
text := "this is the cheatsheet text"
|
||||
src, err := ioutil.TempFile("", "foo-src")
|
||||
src, err := os.CreateTemp("", "foo-src")
|
||||
if err != nil {
|
||||
t.Errorf("failed to mock cheatsheet: %v", err)
|
||||
}
|
||||
@ -72,7 +71,13 @@ func TestCopyDeep(t *testing.T) {
|
||||
}
|
||||
|
||||
// mock a cheatsheet struct
|
||||
sheet, err := New("/cheat-tests/alpha/bravo/foo", src.Name(), []string{}, false)
|
||||
sheet, err := New(
|
||||
"/cheat-tests/alpha/bravo/foo",
|
||||
"community",
|
||||
src.Name(),
|
||||
[]string{},
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("failed to init cheatsheet: %v", err)
|
||||
}
|
||||
@ -88,7 +93,7 @@ func TestCopyDeep(t *testing.T) {
|
||||
}
|
||||
|
||||
// assert that the destination file contains the correct text
|
||||
got, err := ioutil.ReadFile(outpath)
|
||||
got, err := os.ReadFile(outpath)
|
||||
if err != nil {
|
||||
t.Errorf("failed to read destination file: %v", err)
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package sheet
|
||||
|
||||
// Match encapsulates search matches within cheatsheets
|
||||
type Match struct {
|
||||
Line int
|
||||
Text string
|
||||
}
|
45
internal/sheet/parse.go
Normal file
45
internal/sheet/parse.go
Normal file
@ -0,0 +1,45 @@
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package frontmatter
|
||||
package sheet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@ -16,7 +16,7 @@ tags: [ test ]
|
||||
To foo the bar: baz`
|
||||
|
||||
// parse the frontmatter
|
||||
text, fm, err := Parse(markdown)
|
||||
fm, text, 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
|
||||
text, fm, err := Parse(markdown)
|
||||
fm, text, err := parse(markdown)
|
||||
|
||||
// assert expectations
|
||||
if err != nil {
|
||||
@ -69,3 +69,27 @@ 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)
|
||||
}
|
||||
}
|
@ -3,43 +3,22 @@ package sheet
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mgutz/ansi"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// Search returns lines within a sheet's Text that match the search regex
|
||||
func (s *Sheet) Search(reg *regexp.Regexp) string {
|
||||
|
||||
// record matches
|
||||
matches := []Match{}
|
||||
matches := ""
|
||||
|
||||
// search through the cheatsheet's text line by line
|
||||
// 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") {
|
||||
for _, line := range strings.Split(s.Text, "\n\n") {
|
||||
|
||||
// exit early if the line doesn't match the regex
|
||||
if !reg.MatchString(line) {
|
||||
continue
|
||||
if reg.MatchString(line) {
|
||||
matches += line + "\n\n"
|
||||
}
|
||||
|
||||
// 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
|
||||
return strings.TrimSpace(matches)
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// TestSearchNoMatch ensures that the expected output is returned when no
|
||||
@ -24,21 +22,21 @@ func TestSearchNoMatch(t *testing.T) {
|
||||
}
|
||||
|
||||
// search the sheet
|
||||
matches := sheet.Search(reg, false)
|
||||
matches := sheet.Search(reg)
|
||||
|
||||
// assert that no matches were found
|
||||
if len(matches) != 0 {
|
||||
t.Errorf("failure: expected no matches: got: %s", spew.Sdump(matches))
|
||||
if matches != "" {
|
||||
t.Errorf("failure: expected no matches: got: %s", matches)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchSingleMatchNoColor asserts that the expected output is returned
|
||||
// when a single match is returned, and no colorization is applied.
|
||||
func TestSearchSingleMatchNoColor(t *testing.T) {
|
||||
// TestSearchSingleMatch asserts that the expected output is returned
|
||||
// when a single match is returned
|
||||
func TestSearchSingleMatch(t *testing.T) {
|
||||
|
||||
// mock a cheatsheet
|
||||
sheet := Sheet{
|
||||
Text: "The quick brown fox\njumped over\nthe lazy dog.",
|
||||
Text: "The quick brown fox\njumped over\n\nthe lazy dog.",
|
||||
}
|
||||
|
||||
// compile the search regex
|
||||
@ -48,69 +46,28 @@ func TestSearchSingleMatchNoColor(t *testing.T) {
|
||||
}
|
||||
|
||||
// search the sheet
|
||||
matches := sheet.Search(reg, false)
|
||||
matches := sheet.Search(reg)
|
||||
|
||||
// specify the expected results
|
||||
want := []Match{
|
||||
Match{
|
||||
Line: 1,
|
||||
Text: "The quick brown fox",
|
||||
},
|
||||
}
|
||||
want := "The quick brown fox\njumped over"
|
||||
|
||||
// assert that the correct matches were returned
|
||||
if !reflect.DeepEqual(matches, want) {
|
||||
if matches != want {
|
||||
t.Errorf(
|
||||
"failed to return expected matches: want:\n%s, got:\n%s",
|
||||
spew.Sdump(want),
|
||||
spew.Sdump(matches),
|
||||
want,
|
||||
matches,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchSingleMatchColorized asserts that the expected output is returned
|
||||
// when a single match is returned, and colorization is applied
|
||||
func TestSearchSingleMatchColorized(t *testing.T) {
|
||||
// TestSearchMultiMatch asserts that the expected output is returned
|
||||
// when a multiple matches are returned
|
||||
func TestSearchMultiMatch(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)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.",
|
||||
Text: "The quick brown fox\n\njumped over\n\nthe lazy dog.",
|
||||
}
|
||||
|
||||
// compile the search regex
|
||||
@ -120,66 +77,17 @@ func TestSearchMultiMatchNoColor(t *testing.T) {
|
||||
}
|
||||
|
||||
// search the sheet
|
||||
matches := sheet.Search(reg, false)
|
||||
matches := sheet.Search(reg)
|
||||
|
||||
// specify the expected results
|
||||
want := []Match{
|
||||
Match{
|
||||
Line: 1,
|
||||
Text: "The quick brown fox",
|
||||
},
|
||||
Match{
|
||||
Line: 3,
|
||||
Text: "the lazy dog.",
|
||||
},
|
||||
}
|
||||
want := "The quick brown fox\n\nthe 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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),
|
||||
want,
|
||||
matches,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2,38 +2,44 @@ package sheet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/cheat/cheat/internal/frontmatter"
|
||||
)
|
||||
|
||||
// Frontmatter encapsulates cheatsheet frontmatter data
|
||||
type frontmatter struct {
|
||||
Tags []string
|
||||
Syntax string
|
||||
}
|
||||
|
||||
// Sheet encapsulates sheet information
|
||||
type Sheet struct {
|
||||
Title string
|
||||
Path string
|
||||
Text string
|
||||
Tags []string
|
||||
Syntax string
|
||||
ReadOnly bool
|
||||
Title string
|
||||
CheatPath string
|
||||
Path string
|
||||
Text string
|
||||
Tags []string
|
||||
Syntax string
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
// New initializes a new Sheet
|
||||
func New(
|
||||
title string,
|
||||
cheatpath string,
|
||||
path string,
|
||||
tags []string,
|
||||
readOnly bool,
|
||||
) (Sheet, error) {
|
||||
|
||||
// read the cheatsheet file
|
||||
markdown, err := ioutil.ReadFile(path)
|
||||
markdown, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Sheet{}, fmt.Errorf("failed to read file: %s, %v", path, err)
|
||||
}
|
||||
|
||||
// parse the cheatsheet frontmatter
|
||||
text, fm, err := frontmatter.Parse(string(markdown))
|
||||
// parse the raw cheatsheet text
|
||||
fm, text, err := parse(string(markdown))
|
||||
if err != nil {
|
||||
return Sheet{}, fmt.Errorf("failed to parse front-matter: %v", err)
|
||||
}
|
||||
@ -46,11 +52,12 @@ func New(
|
||||
|
||||
// initialize and return a sheet
|
||||
return Sheet{
|
||||
Title: title,
|
||||
Path: path,
|
||||
Text: text + "\n",
|
||||
Tags: tags,
|
||||
Syntax: fm.Syntax,
|
||||
ReadOnly: readOnly,
|
||||
Title: title,
|
||||
CheatPath: cheatpath,
|
||||
Path: path,
|
||||
Text: text,
|
||||
Tags: tags,
|
||||
Syntax: fm.Syntax,
|
||||
ReadOnly: readOnly,
|
||||
}, nil
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ func TestSheetSuccess(t *testing.T) {
|
||||
// initialize a sheet
|
||||
sheet, err := New(
|
||||
"foo",
|
||||
"community",
|
||||
mock.Path("sheet/foo"),
|
||||
[]string{"alpha", "bravo"},
|
||||
false,
|
||||
@ -61,6 +62,7 @@ func TestSheetFailure(t *testing.T) {
|
||||
// initialize a sheet
|
||||
_, err := New(
|
||||
"foo",
|
||||
"community",
|
||||
mock.Path("/does-not-exist"),
|
||||
[]string{"alpha", "bravo"},
|
||||
false,
|
||||
@ -69,3 +71,20 @@ 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")
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,13 @@ 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"
|
||||
)
|
||||
|
||||
@ -33,7 +35,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("error walking path: %v", err)
|
||||
return fmt.Errorf("failed to walk path: %v", err)
|
||||
}
|
||||
|
||||
// don't register directories as cheatsheets
|
||||
@ -48,20 +50,32 @@ func Load(cheatpaths []cp.Cheatpath) ([]map[string]sheet.Sheet, error) {
|
||||
string(os.PathSeparator),
|
||||
)
|
||||
|
||||
// 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
|
||||
// Don't walk the `.git` directory. Doing so creates
|
||||
// hundreds/thousands of needless syscalls and could
|
||||
// potentially harm performance on machines with slow disks.
|
||||
skip, err := repo.GitDir(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to identify .git directory: %v", err)
|
||||
}
|
||||
if skip {
|
||||
return fs.SkipDir
|
||||
}
|
||||
|
||||
// parse the cheatsheet file into a `sheet` struct
|
||||
s, err := sheet.New(title, path, cheatpath.Tags, cheatpath.ReadOnly)
|
||||
s, err := sheet.New(
|
||||
title,
|
||||
cheatpath.Name,
|
||||
path,
|
||||
cheatpath.Tags,
|
||||
cheatpath.ReadOnly,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create sheet: %v", err)
|
||||
return fmt.Errorf(
|
||||
"failed to load sheet: %s, path: %s, err: %v",
|
||||
title,
|
||||
path,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
// register the cheatsheet on its cheatpath, keyed by its title
|
||||
|
@ -1,3 +1,62 @@
|
||||
package sheets
|
||||
|
||||
// TODO
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"github.com/cheat/cheat/internal/sheet"
|
||||
)
|
||||
|
||||
// TestTags asserts that cheetsheet tags are properly returned
|
||||
// TestTags asserts that cheatsheet tags are properly returned
|
||||
func TestTags(t *testing.T) {
|
||||
|
||||
// mock cheatsheets available on multiple cheatpaths
|
||||
|
0
mocks/cheatsheets/community/.hiddenfile
Normal file
0
mocks/cheatsheets/community/.hiddenfile
Normal file
4
mocks/cheatsheets/community/bar
Normal file
4
mocks/cheatsheets/community/bar
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
tags: [ community ]
|
||||
---
|
||||
This is the bar cheatsheet.
|
4
mocks/cheatsheets/community/foo
Normal file
4
mocks/cheatsheets/community/foo
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
tags: [ community ]
|
||||
---
|
||||
This is the foo cheatsheet.
|
4
mocks/cheatsheets/personal/bat
Normal file
4
mocks/cheatsheets/personal/bat
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
tags: [ personal ]
|
||||
---
|
||||
This is the bat cheatsheet.
|
4
mocks/cheatsheets/personal/baz
Normal file
4
mocks/cheatsheets/personal/baz
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
tags: [ personal ]
|
||||
---
|
||||
This is the baz cheatsheet.
|
4
mocks/sheet/bad-fm
Normal file
4
mocks/sheet/bad-fm
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
syntax: sh
|
||||
|
||||
This is malformed frontmatter.
|
@ -1,9 +0,0 @@
|
||||
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
|
74
scripts/cheat.bash
Executable file
74
scripts/cheat.bash
Executable file
@ -0,0 +1,74 @@
|
||||
# cheat(1) completion -*- shell-script -*-
|
||||
|
||||
# generate cheatsheet completions, optionally using `fzf`
|
||||
_cheat_complete_cheatsheets()
|
||||
{
|
||||
if [[ "$CHEAT_USE_FZF" = true ]]; then
|
||||
FZF_COMPLETION_TRIGGER='' _fzf_complete "--no-multi" "$@" < <(
|
||||
cheat -l | tail -n +2 | cut -d' ' -f1
|
||||
)
|
||||
else
|
||||
COMPREPLY=( $(compgen -W "$(cheat -l | tail -n +2 | cut -d' ' -f1)" -- "$cur") )
|
||||
fi
|
||||
}
|
||||
|
||||
# generate tag completions, optionally using `fzf`
|
||||
_cheat_complete_tags()
|
||||
{
|
||||
if [ "$CHEAT_USE_FZF" = true ]; then
|
||||
FZF_COMPLETION_TRIGGER='' _fzf_complete "--no-multi" "$@" < <(cheat -T)
|
||||
else
|
||||
COMPREPLY=( $(compgen -W "$(cheat -T)" -- "$cur") )
|
||||
fi
|
||||
}
|
||||
|
||||
# implement the `cheat` autocompletions
|
||||
_cheat()
|
||||
{
|
||||
local cur prev words cword split
|
||||
_init_completion -s || return
|
||||
|
||||
# complete options that are currently being typed: `--col` => `--colorize`
|
||||
if [[ $cur == -* ]]; then
|
||||
COMPREPLY=( $(compgen -W '$(_parse_help "$1" | sed "s/=//g")' -- "$cur") )
|
||||
[[ $COMPREPLY == *= ]] && compopt -o nospace
|
||||
return
|
||||
fi
|
||||
|
||||
# implement completions
|
||||
case $prev in
|
||||
--colorize|-c|\
|
||||
--directories|-d|\
|
||||
--init|\
|
||||
--regex|-r|\
|
||||
--search|-s|\
|
||||
--tags|-T|\
|
||||
--version|-v)
|
||||
# noop the above, which should implement no completions
|
||||
;;
|
||||
--edit|-e)
|
||||
_cheat_complete_cheatsheets
|
||||
;;
|
||||
--list|-l)
|
||||
_cheat_complete_cheatsheets
|
||||
;;
|
||||
--path|-p)
|
||||
COMPREPLY=( $(compgen -W "$(cheat -d | cut -d':' -f1)" -- "$cur") )
|
||||
;;
|
||||
--rm)
|
||||
_cheat_complete_cheatsheets
|
||||
;;
|
||||
--tag|-t)
|
||||
_cheat_complete_tags
|
||||
;;
|
||||
*)
|
||||
_cheat_complete_cheatsheets
|
||||
;;
|
||||
esac
|
||||
|
||||
$split && return
|
||||
|
||||
} &&
|
||||
complete -F _cheat cheat
|
||||
|
||||
# ex: filetype=sh
|
65
scripts/cheat.zsh
Executable file
65
scripts/cheat.zsh
Executable file
@ -0,0 +1,65 @@
|
||||
#compdef cheat
|
||||
|
||||
local cheats taglist pathlist
|
||||
|
||||
_cheat_complete_personal_cheatsheets()
|
||||
{
|
||||
cheats=("${(f)$(cheat -l -t personal | tail -n +2 | cut -d' ' -f1)}")
|
||||
_describe -t cheats 'cheats' cheats
|
||||
}
|
||||
|
||||
_cheat_complete_full_cheatsheets()
|
||||
{
|
||||
cheats=("${(f)$(cheat -l | tail -n +2 | cut -d' ' -f1)}")
|
||||
_describe -t cheats 'cheats' cheats
|
||||
}
|
||||
|
||||
_cheat_complete_tags()
|
||||
{
|
||||
taglist=("${(f)$(cheat -T)}")
|
||||
_describe -t taglist 'taglist' taglist
|
||||
}
|
||||
|
||||
_cheat_complete_paths()
|
||||
{
|
||||
pathlist=("${(f)$(cheat -d | cut -d':' -f1)}")
|
||||
_describe -t pathlist 'pathlist' pathlist
|
||||
}
|
||||
|
||||
_cheat() {
|
||||
|
||||
_arguments -C \
|
||||
'(--init)--init[Write a default config file to stdout]: :->none' \
|
||||
'(-c --colorize)'{-c,--colorize}'[Colorize output]: :->none' \
|
||||
'(-d --directories)'{-d,--directories}'[List cheatsheet directories]: :->none' \
|
||||
'(-e --edit)'{-e,--edit}'[Edit <sheet>]: :->personal' \
|
||||
'(-l --list)'{-l,--list}'[List cheatsheets]: :->full' \
|
||||
'(-p --path)'{-p,--path}'[Return only sheets found on path <name>]: :->pathlist' \
|
||||
'(-r --regex)'{-r,--regex}'[Treat search <phrase> as a regex]: :->none' \
|
||||
'(-s --search)'{-s,--search}'[Search cheatsheets for <phrase>]: :->none' \
|
||||
'(-t --tag)'{-t,--tag}'[Return only sheets matching <tag>]: :->taglist' \
|
||||
'(-T --tags)'{-T,--tags}'[List all tags in use]: :->none' \
|
||||
'(-v --version)'{-v,--version}'[Print the version number]: :->none' \
|
||||
'(--rm)--rm[Remove (delete) <sheet>]: :->personal'
|
||||
|
||||
case $state in
|
||||
(none)
|
||||
;;
|
||||
(full)
|
||||
_cheat_complete_full_cheatsheets
|
||||
;;
|
||||
(personal)
|
||||
_cheat_complete_personal_cheatsheets
|
||||
;;
|
||||
(taglist)
|
||||
_cheat_complete_tags
|
||||
;;
|
||||
(pathlist)
|
||||
_cheat_complete_paths
|
||||
;;
|
||||
(*)
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
compdef _cheat cheat
|
@ -1,11 +0,0 @@
|
||||
#!/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 }'`
|
||||
}
|
46
scripts/git/cheatsheets
Executable file
46
scripts/git/cheatsheets
Executable file
@ -0,0 +1,46 @@
|
||||
#!/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
|
1
vendor/github.com/Microsoft/go-winio/.gitattributes
generated
vendored
Normal file
1
vendor/github.com/Microsoft/go-winio/.gitattributes
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
10
vendor/github.com/Microsoft/go-winio/.gitignore
generated
vendored
Normal file
10
vendor/github.com/Microsoft/go-winio/.gitignore
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.vscode/
|
||||
|
||||
*.exe
|
||||
|
||||
# testing
|
||||
testdata
|
||||
|
||||
# go workspaces
|
||||
go.work
|
||||
go.work.sum
|
144
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
Normal file
144
vendor/github.com/Microsoft/go-winio/.golangci.yml
generated
vendored
Normal file
@ -0,0 +1,144 @@
|
||||
run:
|
||||
skip-dirs:
|
||||
- pkg/etw/sample
|
||||
|
||||
linters:
|
||||
enable:
|
||||
# style
|
||||
- containedctx # struct contains a context
|
||||
- dupl # duplicate code
|
||||
- errname # erorrs are named correctly
|
||||
- goconst # strings that should be constants
|
||||
- godot # comments end in a period
|
||||
- misspell
|
||||
- nolintlint # "//nolint" directives are properly explained
|
||||
- revive # golint replacement
|
||||
- stylecheck # golint replacement, less configurable than revive
|
||||
- unconvert # unnecessary conversions
|
||||
- wastedassign
|
||||
|
||||
# bugs, performance, unused, etc ...
|
||||
- contextcheck # function uses a non-inherited context
|
||||
- errorlint # errors not wrapped for 1.13
|
||||
- exhaustive # check exhaustiveness of enum switch statements
|
||||
- gofmt # files are gofmt'ed
|
||||
- gosec # security
|
||||
- nestif # deeply nested ifs
|
||||
- nilerr # returns nil even with non-nil error
|
||||
- prealloc # slices that can be pre-allocated
|
||||
- structcheck # unused struct fields
|
||||
- unparam # unused function params
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# err is very often shadowed in nested scopes
|
||||
- linters:
|
||||
- govet
|
||||
text: '^shadow: declaration of "err" shadows declaration'
|
||||
|
||||
# ignore long lines for skip autogen directives
|
||||
- linters:
|
||||
- revive
|
||||
text: "^line-length-limit: "
|
||||
source: "^//(go:generate|sys) "
|
||||
|
||||
# allow unjustified ignores of error checks in defer statements
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errcheck` should provide explanation"
|
||||
source: '^\s*defer '
|
||||
|
||||
# allow unjustified ignores of error lints for io.EOF
|
||||
- linters:
|
||||
- nolintlint
|
||||
text: "^directive `//nolint:errorlint` should provide explanation"
|
||||
source: '[=|!]= io.EOF'
|
||||
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# struct order is often for Win32 compat
|
||||
# also, ignore pointer bytes/GC issues for now until performance becomes an issue
|
||||
- fieldalignment
|
||||
check-shadowing: true
|
||||
nolintlint:
|
||||
allow-leading-space: false
|
||||
require-explanation: true
|
||||
require-specific: true
|
||||
revive:
|
||||
# revive is more configurable than static check, so likely the preferred alternative to static-check
|
||||
# (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997)
|
||||
enable-all-rules:
|
||||
true
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
|
||||
rules:
|
||||
# rules with required arguments
|
||||
- name: argument-limit
|
||||
disabled: true
|
||||
- name: banned-characters
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
- name: file-header
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: function-result-limit
|
||||
disabled: true
|
||||
- name: max-public-structs
|
||||
disabled: true
|
||||
# geneally annoying rules
|
||||
- name: add-constant # complains about any and all strings and integers
|
||||
disabled: true
|
||||
- name: confusing-naming # we frequently use "Foo()" and "foo()" together
|
||||
disabled: true
|
||||
- name: flag-parameter # excessive, and a common idiom we use
|
||||
disabled: true
|
||||
# general config
|
||||
- name: line-length-limit
|
||||
arguments:
|
||||
- 140
|
||||
- name: var-naming
|
||||
arguments:
|
||||
- []
|
||||
- - CID
|
||||
- CRI
|
||||
- CTRD
|
||||
- DACL
|
||||
- DLL
|
||||
- DOS
|
||||
- ETW
|
||||
- FSCTL
|
||||
- GCS
|
||||
- GMSA
|
||||
- HCS
|
||||
- HV
|
||||
- IO
|
||||
- LCOW
|
||||
- LDAP
|
||||
- LPAC
|
||||
- LTSC
|
||||
- MMIO
|
||||
- NT
|
||||
- OCI
|
||||
- PMEM
|
||||
- PWSH
|
||||
- RX
|
||||
- SACl
|
||||
- SID
|
||||
- SMB
|
||||
- TX
|
||||
- VHD
|
||||
- VHDX
|
||||
- VMID
|
||||
- VPCI
|
||||
- WCOW
|
||||
- WIM
|
||||
stylecheck:
|
||||
checks:
|
||||
- "all"
|
||||
- "-ST1003" # use revive's var naming
|
1
vendor/github.com/Microsoft/go-winio/CODEOWNERS
generated
vendored
Normal file
1
vendor/github.com/Microsoft/go-winio/CODEOWNERS
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
* @microsoft/containerplat
|
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Yasuhiro Matsumoto
|
||||
Copyright (c) 2015 Microsoft
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
89
vendor/github.com/Microsoft/go-winio/README.md
generated
vendored
Normal file
89
vendor/github.com/Microsoft/go-winio/README.md
generated
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
# go-winio [](https://github.com/microsoft/go-winio/actions/workflows/ci.yml)
|
||||
|
||||
This repository contains utilities for efficiently performing Win32 IO operations in
|
||||
Go. Currently, this is focused on accessing named pipes and other file handles, and
|
||||
for using named pipes as a net transport.
|
||||
|
||||
This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go
|
||||
to reuse the thread to schedule another goroutine. This limits support to Windows Vista and
|
||||
newer operating systems. This is similar to the implementation of network sockets in Go's net
|
||||
package.
|
||||
|
||||
Please see the LICENSE file for licensing information.
|
||||
|
||||
## Contributing
|
||||
|
||||
This project welcomes contributions and suggestions.
|
||||
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that
|
||||
you have the right to, and actually do, grant us the rights to use your contribution.
|
||||
For details, visit [Microsoft CLA](https://cla.microsoft.com).
|
||||
|
||||
When you submit a pull request, a CLA-bot will automatically determine whether you need to
|
||||
provide a CLA and decorate the PR appropriately (e.g., label, comment).
|
||||
Simply follow the instructions provided by the bot.
|
||||
You will only need to do this once across all repos using our CLA.
|
||||
|
||||
Additionally, the pull request pipeline requires the following steps to be performed before
|
||||
mergining.
|
||||
|
||||
### Code Sign-Off
|
||||
|
||||
We require that contributors sign their commits using [`git commit --signoff`][git-commit-s]
|
||||
to certify they either authored the work themselves or otherwise have permission to use it in this project.
|
||||
|
||||
A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s].
|
||||
|
||||
Please see [the developer certificate](https://developercertificate.org) for more info,
|
||||
as well as to make sure that you can attest to the rules listed.
|
||||
Our CI uses the DCO Github app to ensure that all commits in a given PR are signed-off.
|
||||
|
||||
### Linting
|
||||
|
||||
Code must pass a linting stage, which uses [`golangci-lint`][lint].
|
||||
The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run
|
||||
automatically with VSCode by adding the following to your workspace or folder settings:
|
||||
|
||||
```json
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package",
|
||||
```
|
||||
|
||||
Additional editor [integrations options are also available][lint-ide].
|
||||
|
||||
Alternatively, `golangci-lint` can be [installed locally][lint-install] and run from the repo root:
|
||||
|
||||
```shell
|
||||
# use . or specify a path to only lint a package
|
||||
# to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0"
|
||||
> golangci-lint run ./...
|
||||
```
|
||||
|
||||
### Go Generate
|
||||
|
||||
The pipeline checks that auto-generated code, via `go generate`, are up to date.
|
||||
|
||||
This can be done for the entire repo:
|
||||
|
||||
```shell
|
||||
> go generate ./...
|
||||
```
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Special Thanks
|
||||
|
||||
Thanks to [natefinch][natefinch] for the inspiration for this library.
|
||||
See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation.
|
||||
|
||||
[lint]: https://golangci-lint.run/
|
||||
[lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration
|
||||
[lint-install]: https://golangci-lint.run/usage/install/#local-installation
|
||||
|
||||
[git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s
|
||||
[git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff
|
||||
|
||||
[natefinch]: https://github.com/natefinch
|
41
vendor/github.com/Microsoft/go-winio/SECURITY.md
generated
vendored
Normal file
41
vendor/github.com/Microsoft/go-winio/SECURITY.md
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
290
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
Normal file
290
vendor/github.com/Microsoft/go-winio/backup.go
generated
vendored
Normal file
@ -0,0 +1,290 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys backupRead(h syscall.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
|
||||
//sys backupWrite(h syscall.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
|
||||
|
||||
const (
|
||||
BackupData = uint32(iota + 1)
|
||||
BackupEaData
|
||||
BackupSecurity
|
||||
BackupAlternateData
|
||||
BackupLink
|
||||
BackupPropertyData
|
||||
BackupObjectId //revive:disable-line:var-naming ID, not Id
|
||||
BackupReparseData
|
||||
BackupSparseBlock
|
||||
BackupTxfsData
|
||||
)
|
||||
|
||||
const (
|
||||
StreamSparseAttributes = uint32(8)
|
||||
)
|
||||
|
||||
//nolint:revive // var-naming: ALL_CAPS
|
||||
const (
|
||||
WRITE_DAC = windows.WRITE_DAC
|
||||
WRITE_OWNER = windows.WRITE_OWNER
|
||||
ACCESS_SYSTEM_SECURITY = windows.ACCESS_SYSTEM_SECURITY
|
||||
)
|
||||
|
||||
// BackupHeader represents a backup stream of a file.
|
||||
type BackupHeader struct {
|
||||
//revive:disable-next-line:var-naming ID, not Id
|
||||
Id uint32 // The backup stream ID
|
||||
Attributes uint32 // Stream attributes
|
||||
Size int64 // The size of the stream in bytes
|
||||
Name string // The name of the stream (for BackupAlternateData only).
|
||||
Offset int64 // The offset of the stream in the file (for BackupSparseBlock only).
|
||||
}
|
||||
|
||||
type win32StreamID struct {
|
||||
StreamID uint32
|
||||
Attributes uint32
|
||||
Size uint64
|
||||
NameSize uint32
|
||||
}
|
||||
|
||||
// BackupStreamReader reads from a stream produced by the BackupRead Win32 API and produces a series
|
||||
// of BackupHeader values.
|
||||
type BackupStreamReader struct {
|
||||
r io.Reader
|
||||
bytesLeft int64
|
||||
}
|
||||
|
||||
// NewBackupStreamReader produces a BackupStreamReader from any io.Reader.
|
||||
func NewBackupStreamReader(r io.Reader) *BackupStreamReader {
|
||||
return &BackupStreamReader{r, 0}
|
||||
}
|
||||
|
||||
// Next returns the next backup stream and prepares for calls to Read(). It skips the remainder of the current stream if
|
||||
// it was not completely read.
|
||||
func (r *BackupStreamReader) Next() (*BackupHeader, error) {
|
||||
if r.bytesLeft > 0 { //nolint:nestif // todo: flatten this
|
||||
if s, ok := r.r.(io.Seeker); ok {
|
||||
// Make sure Seek on io.SeekCurrent sometimes succeeds
|
||||
// before trying the actual seek.
|
||||
if _, err := s.Seek(0, io.SeekCurrent); err == nil {
|
||||
if _, err = s.Seek(r.bytesLeft, io.SeekCurrent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.bytesLeft = 0
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var wsi win32StreamID
|
||||
if err := binary.Read(r.r, binary.LittleEndian, &wsi); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr := &BackupHeader{
|
||||
Id: wsi.StreamID,
|
||||
Attributes: wsi.Attributes,
|
||||
Size: int64(wsi.Size),
|
||||
}
|
||||
if wsi.NameSize != 0 {
|
||||
name := make([]uint16, int(wsi.NameSize/2))
|
||||
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr.Name = syscall.UTF16ToString(name)
|
||||
}
|
||||
if wsi.StreamID == BackupSparseBlock {
|
||||
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr.Size -= 8
|
||||
}
|
||||
r.bytesLeft = hdr.Size
|
||||
return hdr, nil
|
||||
}
|
||||
|
||||
// Read reads from the current backup stream.
|
||||
func (r *BackupStreamReader) Read(b []byte) (int, error) {
|
||||
if r.bytesLeft == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(b)) > r.bytesLeft {
|
||||
b = b[:r.bytesLeft]
|
||||
}
|
||||
n, err := r.r.Read(b)
|
||||
r.bytesLeft -= int64(n)
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
} else if r.bytesLeft == 0 && err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// BackupStreamWriter writes a stream compatible with the BackupWrite Win32 API.
|
||||
type BackupStreamWriter struct {
|
||||
w io.Writer
|
||||
bytesLeft int64
|
||||
}
|
||||
|
||||
// NewBackupStreamWriter produces a BackupStreamWriter on top of an io.Writer.
|
||||
func NewBackupStreamWriter(w io.Writer) *BackupStreamWriter {
|
||||
return &BackupStreamWriter{w, 0}
|
||||
}
|
||||
|
||||
// WriteHeader writes the next backup stream header and prepares for calls to Write().
|
||||
func (w *BackupStreamWriter) WriteHeader(hdr *BackupHeader) error {
|
||||
if w.bytesLeft != 0 {
|
||||
return fmt.Errorf("missing %d bytes", w.bytesLeft)
|
||||
}
|
||||
name := utf16.Encode([]rune(hdr.Name))
|
||||
wsi := win32StreamID{
|
||||
StreamID: hdr.Id,
|
||||
Attributes: hdr.Attributes,
|
||||
Size: uint64(hdr.Size),
|
||||
NameSize: uint32(len(name) * 2),
|
||||
}
|
||||
if hdr.Id == BackupSparseBlock {
|
||||
// Include space for the int64 block offset
|
||||
wsi.Size += 8
|
||||
}
|
||||
if err := binary.Write(w.w, binary.LittleEndian, &wsi); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(name) != 0 {
|
||||
if err := binary.Write(w.w, binary.LittleEndian, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if hdr.Id == BackupSparseBlock {
|
||||
if err := binary.Write(w.w, binary.LittleEndian, hdr.Offset); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.bytesLeft = hdr.Size
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes to the current backup stream.
|
||||
func (w *BackupStreamWriter) Write(b []byte) (int, error) {
|
||||
if w.bytesLeft < int64(len(b)) {
|
||||
return 0, fmt.Errorf("too many bytes by %d", int64(len(b))-w.bytesLeft)
|
||||
}
|
||||
n, err := w.w.Write(b)
|
||||
w.bytesLeft -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// BackupFileReader provides an io.ReadCloser interface on top of the BackupRead Win32 API.
|
||||
type BackupFileReader struct {
|
||||
f *os.File
|
||||
includeSecurity bool
|
||||
ctx uintptr
|
||||
}
|
||||
|
||||
// NewBackupFileReader returns a new BackupFileReader from a file handle. If includeSecurity is true,
|
||||
// Read will attempt to read the security descriptor of the file.
|
||||
func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader {
|
||||
r := &BackupFileReader{f, includeSecurity, 0}
|
||||
return r
|
||||
}
|
||||
|
||||
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
|
||||
func (r *BackupFileReader) Read(b []byte) (int, error) {
|
||||
var bytesRead uint32
|
||||
err := backupRead(syscall.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(r.f)
|
||||
if bytesRead == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return int(bytesRead), nil
|
||||
}
|
||||
|
||||
// Close frees Win32 resources associated with the BackupFileReader. It does not close
|
||||
// the underlying file.
|
||||
func (r *BackupFileReader) Close() error {
|
||||
if r.ctx != 0 {
|
||||
_ = backupRead(syscall.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
|
||||
runtime.KeepAlive(r.f)
|
||||
r.ctx = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupFileWriter provides an io.WriteCloser interface on top of the BackupWrite Win32 API.
|
||||
type BackupFileWriter struct {
|
||||
f *os.File
|
||||
includeSecurity bool
|
||||
ctx uintptr
|
||||
}
|
||||
|
||||
// NewBackupFileWriter returns a new BackupFileWriter from a file handle. If includeSecurity is true,
|
||||
// Write() will attempt to restore the security descriptor from the stream.
|
||||
func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
|
||||
w := &BackupFileWriter{f, includeSecurity, 0}
|
||||
return w
|
||||
}
|
||||
|
||||
// Write restores a portion of the file using the provided backup stream.
|
||||
func (w *BackupFileWriter) Write(b []byte) (int, error) {
|
||||
var bytesWritten uint32
|
||||
err := backupWrite(syscall.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(w.f)
|
||||
if int(bytesWritten) != len(b) {
|
||||
return int(bytesWritten), errors.New("not all bytes could be written")
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Close frees Win32 resources associated with the BackupFileWriter. It does not
|
||||
// close the underlying file.
|
||||
func (w *BackupFileWriter) Close() error {
|
||||
if w.ctx != 0 {
|
||||
_ = backupWrite(syscall.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
|
||||
runtime.KeepAlive(w.f)
|
||||
w.ctx = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OpenForBackup opens a file or directory, potentially skipping access checks if the backup
|
||||
// or restore privileges have been acquired.
|
||||
//
|
||||
// If the file opened was a directory, it cannot be used with Readdir().
|
||||
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
|
||||
winPath, err := syscall.UTF16FromString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err := syscall.CreateFile(&winPath[0],
|
||||
access,
|
||||
share,
|
||||
nil,
|
||||
createmode,
|
||||
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
0)
|
||||
if err != nil {
|
||||
err = &os.PathError{Op: "open", Path: path, Err: err}
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(uintptr(h), path), nil
|
||||
}
|
22
vendor/github.com/Microsoft/go-winio/doc.go
generated
vendored
Normal file
22
vendor/github.com/Microsoft/go-winio/doc.go
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
// This package provides utilities for efficiently performing Win32 IO operations in Go.
|
||||
// Currently, this package is provides support for genreal IO and management of
|
||||
// - named pipes
|
||||
// - files
|
||||
// - [Hyper-V sockets]
|
||||
//
|
||||
// This code is similar to Go's [net] package, and uses IO completion ports to avoid
|
||||
// blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines.
|
||||
//
|
||||
// This limits support to Windows Vista and newer operating systems.
|
||||
//
|
||||
// Additionally, this package provides support for:
|
||||
// - creating and managing GUIDs
|
||||
// - writing to [ETW]
|
||||
// - opening and manageing VHDs
|
||||
// - parsing [Windows Image files]
|
||||
// - auto-generating Win32 API code
|
||||
//
|
||||
// [Hyper-V sockets]: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service
|
||||
// [ETW]: https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw-
|
||||
// [Windows Image files]: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/work-with-windows-images
|
||||
package winio
|
137
vendor/github.com/Microsoft/go-winio/ea.go
generated
vendored
Normal file
137
vendor/github.com/Microsoft/go-winio/ea.go
generated
vendored
Normal file
@ -0,0 +1,137 @@
|
||||
package winio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type fileFullEaInformation struct {
|
||||
NextEntryOffset uint32
|
||||
Flags uint8
|
||||
NameLength uint8
|
||||
ValueLength uint16
|
||||
}
|
||||
|
||||
var (
|
||||
fileFullEaInformationSize = binary.Size(&fileFullEaInformation{})
|
||||
|
||||
errInvalidEaBuffer = errors.New("invalid extended attribute buffer")
|
||||
errEaNameTooLarge = errors.New("extended attribute name too large")
|
||||
errEaValueTooLarge = errors.New("extended attribute value too large")
|
||||
)
|
||||
|
||||
// ExtendedAttribute represents a single Windows EA.
|
||||
type ExtendedAttribute struct {
|
||||
Name string
|
||||
Value []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) {
|
||||
var info fileFullEaInformation
|
||||
err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
err = errInvalidEaBuffer
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
nameOffset := fileFullEaInformationSize
|
||||
nameLen := int(info.NameLength)
|
||||
valueOffset := nameOffset + int(info.NameLength) + 1
|
||||
valueLen := int(info.ValueLength)
|
||||
nextOffset := int(info.NextEntryOffset)
|
||||
if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) {
|
||||
err = errInvalidEaBuffer
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
ea.Name = string(b[nameOffset : nameOffset+nameLen])
|
||||
ea.Value = b[valueOffset : valueOffset+valueLen]
|
||||
ea.Flags = info.Flags
|
||||
if info.NextEntryOffset != 0 {
|
||||
nb = b[info.NextEntryOffset:]
|
||||
}
|
||||
return ea, nb, err
|
||||
}
|
||||
|
||||
// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION
|
||||
// buffer retrieved from BackupRead, ZwQueryEaFile, etc.
|
||||
func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) {
|
||||
for len(b) != 0 {
|
||||
ea, nb, err := parseEa(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eas = append(eas, ea)
|
||||
b = nb
|
||||
}
|
||||
return eas, err
|
||||
}
|
||||
|
||||
func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
|
||||
if int(uint8(len(ea.Name))) != len(ea.Name) {
|
||||
return errEaNameTooLarge
|
||||
}
|
||||
if int(uint16(len(ea.Value))) != len(ea.Value) {
|
||||
return errEaValueTooLarge
|
||||
}
|
||||
entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value))
|
||||
withPadding := (entrySize + 3) &^ 3
|
||||
nextOffset := uint32(0)
|
||||
if !last {
|
||||
nextOffset = withPadding
|
||||
}
|
||||
info := fileFullEaInformation{
|
||||
NextEntryOffset: nextOffset,
|
||||
Flags: ea.Flags,
|
||||
NameLength: uint8(len(ea.Name)),
|
||||
ValueLength: uint16(len(ea.Value)),
|
||||
}
|
||||
|
||||
err := binary.Write(buf, binary.LittleEndian, &info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte(ea.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = buf.WriteByte(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write(ea.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION
|
||||
// buffer for use with BackupWrite, ZwSetEaFile, etc.
|
||||
func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for i := range eas {
|
||||
last := false
|
||||
if i == len(eas)-1 {
|
||||
last = true
|
||||
}
|
||||
|
||||
err := writeEa(&buf, &eas[i], last)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
331
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
Normal file
331
vendor/github.com/Microsoft/go-winio/file.go
generated
vendored
Normal file
@ -0,0 +1,331 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys cancelIoEx(file syscall.Handle, o *syscall.Overlapped) (err error) = CancelIoEx
|
||||
//sys createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) = CreateIoCompletionPort
|
||||
//sys getQueuedCompletionStatus(port syscall.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
|
||||
//sys setFileCompletionNotificationModes(h syscall.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
|
||||
//sys wsaGetOverlappedResult(h syscall.Handle, o *syscall.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
|
||||
|
||||
type atomicBool int32
|
||||
|
||||
func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
|
||||
func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) }
|
||||
func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) }
|
||||
|
||||
//revive:disable-next-line:predeclared Keep "new" to maintain consistency with "atomic" pkg
|
||||
func (b *atomicBool) swap(new bool) bool {
|
||||
var newInt int32
|
||||
if new {
|
||||
newInt = 1
|
||||
}
|
||||
return atomic.SwapInt32((*int32)(b), newInt) == 1
|
||||
}
|
||||
|
||||
var (
|
||||
ErrFileClosed = errors.New("file has already been closed")
|
||||
ErrTimeout = &timeoutError{}
|
||||
)
|
||||
|
||||
type timeoutError struct{}
|
||||
|
||||
func (*timeoutError) Error() string { return "i/o timeout" }
|
||||
func (*timeoutError) Timeout() bool { return true }
|
||||
func (*timeoutError) Temporary() bool { return true }
|
||||
|
||||
type timeoutChan chan struct{}
|
||||
|
||||
var ioInitOnce sync.Once
|
||||
var ioCompletionPort syscall.Handle
|
||||
|
||||
// ioResult contains the result of an asynchronous IO operation.
|
||||
type ioResult struct {
|
||||
bytes uint32
|
||||
err error
|
||||
}
|
||||
|
||||
// ioOperation represents an outstanding asynchronous Win32 IO.
|
||||
type ioOperation struct {
|
||||
o syscall.Overlapped
|
||||
ch chan ioResult
|
||||
}
|
||||
|
||||
func initIO() {
|
||||
h, err := createIoCompletionPort(syscall.InvalidHandle, 0, 0, 0xffffffff)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ioCompletionPort = h
|
||||
go ioCompletionProcessor(h)
|
||||
}
|
||||
|
||||
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
|
||||
// It takes ownership of this handle and will close it if it is garbage collected.
|
||||
type win32File struct {
|
||||
handle syscall.Handle
|
||||
wg sync.WaitGroup
|
||||
wgLock sync.RWMutex
|
||||
closing atomicBool
|
||||
socket bool
|
||||
readDeadline deadlineHandler
|
||||
writeDeadline deadlineHandler
|
||||
}
|
||||
|
||||
type deadlineHandler struct {
|
||||
setLock sync.Mutex
|
||||
channel timeoutChan
|
||||
channelLock sync.RWMutex
|
||||
timer *time.Timer
|
||||
timedout atomicBool
|
||||
}
|
||||
|
||||
// makeWin32File makes a new win32File from an existing file handle.
|
||||
func makeWin32File(h syscall.Handle) (*win32File, error) {
|
||||
f := &win32File{handle: h}
|
||||
ioInitOnce.Do(initIO)
|
||||
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.readDeadline.channel = make(timeoutChan)
|
||||
f.writeDeadline.channel = make(timeoutChan)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
|
||||
// If we return the result of makeWin32File directly, it can result in an
|
||||
// interface-wrapped nil, rather than a nil interface value.
|
||||
f, err := makeWin32File(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// closeHandle closes the resources associated with a Win32 handle.
|
||||
func (f *win32File) closeHandle() {
|
||||
f.wgLock.Lock()
|
||||
// Atomically set that we are closing, releasing the resources only once.
|
||||
if !f.closing.swap(true) {
|
||||
f.wgLock.Unlock()
|
||||
// cancel all IO and wait for it to complete
|
||||
_ = cancelIoEx(f.handle, nil)
|
||||
f.wg.Wait()
|
||||
// at this point, no new IO can start
|
||||
syscall.Close(f.handle)
|
||||
f.handle = 0
|
||||
} else {
|
||||
f.wgLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes a win32File.
|
||||
func (f *win32File) Close() error {
|
||||
f.closeHandle()
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsClosed checks if the file has been closed.
|
||||
func (f *win32File) IsClosed() bool {
|
||||
return f.closing.isSet()
|
||||
}
|
||||
|
||||
// prepareIO prepares for a new IO operation.
|
||||
// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
|
||||
func (f *win32File) prepareIO() (*ioOperation, error) {
|
||||
f.wgLock.RLock()
|
||||
if f.closing.isSet() {
|
||||
f.wgLock.RUnlock()
|
||||
return nil, ErrFileClosed
|
||||
}
|
||||
f.wg.Add(1)
|
||||
f.wgLock.RUnlock()
|
||||
c := &ioOperation{}
|
||||
c.ch = make(chan ioResult)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ioCompletionProcessor processes completed async IOs forever.
|
||||
func ioCompletionProcessor(h syscall.Handle) {
|
||||
for {
|
||||
var bytes uint32
|
||||
var key uintptr
|
||||
var op *ioOperation
|
||||
err := getQueuedCompletionStatus(h, &bytes, &key, &op, syscall.INFINITE)
|
||||
if op == nil {
|
||||
panic(err)
|
||||
}
|
||||
op.ch <- ioResult{bytes, err}
|
||||
}
|
||||
}
|
||||
|
||||
// todo: helsaawy - create an asyncIO version that takes a context
|
||||
|
||||
// asyncIO processes the return value from ReadFile or WriteFile, blocking until
|
||||
// the operation has actually completed.
|
||||
func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
|
||||
if err != syscall.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
|
||||
return int(bytes), err
|
||||
}
|
||||
|
||||
if f.closing.isSet() {
|
||||
_ = cancelIoEx(f.handle, &c.o)
|
||||
}
|
||||
|
||||
var timeout timeoutChan
|
||||
if d != nil {
|
||||
d.channelLock.Lock()
|
||||
timeout = d.channel
|
||||
d.channelLock.Unlock()
|
||||
}
|
||||
|
||||
var r ioResult
|
||||
select {
|
||||
case r = <-c.ch:
|
||||
err = r.err
|
||||
if err == syscall.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
if f.closing.isSet() {
|
||||
err = ErrFileClosed
|
||||
}
|
||||
} else if err != nil && f.socket {
|
||||
// err is from Win32. Query the overlapped structure to get the winsock error.
|
||||
var bytes, flags uint32
|
||||
err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags)
|
||||
}
|
||||
case <-timeout:
|
||||
_ = cancelIoEx(f.handle, &c.o)
|
||||
r = <-c.ch
|
||||
err = r.err
|
||||
if err == syscall.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
|
||||
err = ErrTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// runtime.KeepAlive is needed, as c is passed via native
|
||||
// code to ioCompletionProcessor, c must remain alive
|
||||
// until the channel read is complete.
|
||||
// todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive?
|
||||
runtime.KeepAlive(c)
|
||||
return int(r.bytes), err
|
||||
}
|
||||
|
||||
// Read reads from a file handle.
|
||||
func (f *win32File) Read(b []byte) (int, error) {
|
||||
c, err := f.prepareIO()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.wg.Done()
|
||||
|
||||
if f.readDeadline.timedout.isSet() {
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
var bytes uint32
|
||||
err = syscall.ReadFile(f.handle, b, &bytes, &c.o)
|
||||
n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
|
||||
runtime.KeepAlive(b)
|
||||
|
||||
// Handle EOF conditions.
|
||||
if err == nil && n == 0 && len(b) != 0 {
|
||||
return 0, io.EOF
|
||||
} else if err == syscall.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
|
||||
return 0, io.EOF
|
||||
} else {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes to a file handle.
|
||||
func (f *win32File) Write(b []byte) (int, error) {
|
||||
c, err := f.prepareIO()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.wg.Done()
|
||||
|
||||
if f.writeDeadline.timedout.isSet() {
|
||||
return 0, ErrTimeout
|
||||
}
|
||||
|
||||
var bytes uint32
|
||||
err = syscall.WriteFile(f.handle, b, &bytes, &c.o)
|
||||
n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
|
||||
runtime.KeepAlive(b)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (f *win32File) SetReadDeadline(deadline time.Time) error {
|
||||
return f.readDeadline.set(deadline)
|
||||
}
|
||||
|
||||
func (f *win32File) SetWriteDeadline(deadline time.Time) error {
|
||||
return f.writeDeadline.set(deadline)
|
||||
}
|
||||
|
||||
func (f *win32File) Flush() error {
|
||||
return syscall.FlushFileBuffers(f.handle)
|
||||
}
|
||||
|
||||
func (f *win32File) Fd() uintptr {
|
||||
return uintptr(f.handle)
|
||||
}
|
||||
|
||||
func (d *deadlineHandler) set(deadline time.Time) error {
|
||||
d.setLock.Lock()
|
||||
defer d.setLock.Unlock()
|
||||
|
||||
if d.timer != nil {
|
||||
if !d.timer.Stop() {
|
||||
<-d.channel
|
||||
}
|
||||
d.timer = nil
|
||||
}
|
||||
d.timedout.setFalse()
|
||||
|
||||
select {
|
||||
case <-d.channel:
|
||||
d.channelLock.Lock()
|
||||
d.channel = make(chan struct{})
|
||||
d.channelLock.Unlock()
|
||||
default:
|
||||
}
|
||||
|
||||
if deadline.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
timeoutIO := func() {
|
||||
d.timedout.setTrue()
|
||||
close(d.channel)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
duration := deadline.Sub(now)
|
||||
if deadline.After(now) {
|
||||
// Deadline is in the future, set a timer to wait
|
||||
d.timer = time.AfterFunc(duration, timeoutIO)
|
||||
} else {
|
||||
// Deadline is in the past. Cancel all pending IO now.
|
||||
timeoutIO()
|
||||
}
|
||||
return nil
|
||||
}
|
92
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
Normal file
92
vendor/github.com/Microsoft/go-winio/fileinfo.go
generated
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// FileBasicInfo contains file access time and file attributes information.
|
||||
type FileBasicInfo struct {
|
||||
CreationTime, LastAccessTime, LastWriteTime, ChangeTime windows.Filetime
|
||||
FileAttributes uint32
|
||||
_ uint32 // padding
|
||||
}
|
||||
|
||||
// GetFileBasicInfo retrieves times and attributes for a file.
|
||||
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
|
||||
bi := &FileBasicInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileBasicInfo,
|
||||
(*byte)(unsafe.Pointer(bi)),
|
||||
uint32(unsafe.Sizeof(*bi)),
|
||||
); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
// SetFileBasicInfo sets times and attributes for a file.
|
||||
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
|
||||
if err := windows.SetFileInformationByHandle(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileBasicInfo,
|
||||
(*byte)(unsafe.Pointer(bi)),
|
||||
uint32(unsafe.Sizeof(*bi)),
|
||||
); err != nil {
|
||||
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileStandardInfo contains extended information for the file.
|
||||
// FILE_STANDARD_INFO in WinBase.h
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info
|
||||
type FileStandardInfo struct {
|
||||
AllocationSize, EndOfFile int64
|
||||
NumberOfLinks uint32
|
||||
DeletePending, Directory bool
|
||||
}
|
||||
|
||||
// GetFileStandardInfo retrieves ended information for the file.
|
||||
func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) {
|
||||
si := &FileStandardInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()),
|
||||
windows.FileStandardInfo,
|
||||
(*byte)(unsafe.Pointer(si)),
|
||||
uint32(unsafe.Sizeof(*si))); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return si, nil
|
||||
}
|
||||
|
||||
// FileIDInfo contains the volume serial number and file ID for a file. This pair should be
|
||||
// unique on a system.
|
||||
type FileIDInfo struct {
|
||||
VolumeSerialNumber uint64
|
||||
FileID [16]byte
|
||||
}
|
||||
|
||||
// GetFileID retrieves the unique (volume, file ID) pair for a file.
|
||||
func GetFileID(f *os.File) (*FileIDInfo, error) {
|
||||
fileID := &FileIDInfo{}
|
||||
if err := windows.GetFileInformationByHandleEx(
|
||||
windows.Handle(f.Fd()),
|
||||
windows.FileIdInfo,
|
||||
(*byte)(unsafe.Pointer(fileID)),
|
||||
uint32(unsafe.Sizeof(*fileID)),
|
||||
); err != nil {
|
||||
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
|
||||
}
|
||||
runtime.KeepAlive(f)
|
||||
return fileID, nil
|
||||
}
|
575
vendor/github.com/Microsoft/go-winio/hvsock.go
generated
vendored
Normal file
575
vendor/github.com/Microsoft/go-winio/hvsock.go
generated
vendored
Normal file
@ -0,0 +1,575 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/Microsoft/go-winio/internal/socket"
|
||||
"github.com/Microsoft/go-winio/pkg/guid"
|
||||
)
|
||||
|
||||
const afHVSock = 34 // AF_HYPERV
|
||||
|
||||
// Well known Service and VM IDs
|
||||
//https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service#vmid-wildcards
|
||||
|
||||
// HvsockGUIDWildcard is the wildcard VmId for accepting connections from all partitions.
|
||||
func HvsockGUIDWildcard() guid.GUID { // 00000000-0000-0000-0000-000000000000
|
||||
return guid.GUID{}
|
||||
}
|
||||
|
||||
// HvsockGUIDBroadcast is the wildcard VmId for broadcasting sends to all partitions.
|
||||
func HvsockGUIDBroadcast() guid.GUID { //ffffffff-ffff-ffff-ffff-ffffffffffff
|
||||
return guid.GUID{
|
||||
Data1: 0xffffffff,
|
||||
Data2: 0xffff,
|
||||
Data3: 0xffff,
|
||||
Data4: [8]uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
|
||||
}
|
||||
}
|
||||
|
||||
// HvsockGUIDLoopback is the Loopback VmId for accepting connections to the same partition as the connector.
|
||||
func HvsockGUIDLoopback() guid.GUID { // e0e16197-dd56-4a10-9195-5ee7a155a838
|
||||
return guid.GUID{
|
||||
Data1: 0xe0e16197,
|
||||
Data2: 0xdd56,
|
||||
Data3: 0x4a10,
|
||||
Data4: [8]uint8{0x91, 0x95, 0x5e, 0xe7, 0xa1, 0x55, 0xa8, 0x38},
|
||||
}
|
||||
}
|
||||
|
||||
// HvsockGUIDSiloHost is the address of a silo's host partition:
|
||||
// - The silo host of a hosted silo is the utility VM.
|
||||
// - The silo host of a silo on a physical host is the physical host.
|
||||
func HvsockGUIDSiloHost() guid.GUID { // 36bd0c5c-7276-4223-88ba-7d03b654c568
|
||||
return guid.GUID{
|
||||
Data1: 0x36bd0c5c,
|
||||
Data2: 0x7276,
|
||||
Data3: 0x4223,
|
||||
Data4: [8]byte{0x88, 0xba, 0x7d, 0x03, 0xb6, 0x54, 0xc5, 0x68},
|
||||
}
|
||||
}
|
||||
|
||||
// HvsockGUIDChildren is the wildcard VmId for accepting connections from the connector's child partitions.
|
||||
func HvsockGUIDChildren() guid.GUID { // 90db8b89-0d35-4f79-8ce9-49ea0ac8b7cd
|
||||
return guid.GUID{
|
||||
Data1: 0x90db8b89,
|
||||
Data2: 0xd35,
|
||||
Data3: 0x4f79,
|
||||
Data4: [8]uint8{0x8c, 0xe9, 0x49, 0xea, 0xa, 0xc8, 0xb7, 0xcd},
|
||||
}
|
||||
}
|
||||
|
||||
// HvsockGUIDParent is the wildcard VmId for accepting connections from the connector's parent partition.
|
||||
// Listening on this VmId accepts connection from:
|
||||
// - Inside silos: silo host partition.
|
||||
// - Inside hosted silo: host of the VM.
|
||||
// - Inside VM: VM host.
|
||||
// - Physical host: Not supported.
|
||||
func HvsockGUIDParent() guid.GUID { // a42e7cda-d03f-480c-9cc2-a4de20abb878
|
||||
return guid.GUID{
|
||||
Data1: 0xa42e7cda,
|
||||
Data2: 0xd03f,
|
||||
Data3: 0x480c,
|
||||
Data4: [8]uint8{0x9c, 0xc2, 0xa4, 0xde, 0x20, 0xab, 0xb8, 0x78},
|
||||
}
|
||||
}
|
||||
|
||||
// hvsockVsockServiceTemplate is the Service GUID used for the VSOCK protocol.
|
||||
func hvsockVsockServiceTemplate() guid.GUID { // 00000000-facb-11e6-bd58-64006a7986d3
|
||||
return guid.GUID{
|
||||
Data2: 0xfacb,
|
||||
Data3: 0x11e6,
|
||||
Data4: [8]uint8{0xbd, 0x58, 0x64, 0x00, 0x6a, 0x79, 0x86, 0xd3},
|
||||
}
|
||||
}
|
||||
|
||||
// An HvsockAddr is an address for a AF_HYPERV socket.
|
||||
type HvsockAddr struct {
|
||||
VMID guid.GUID
|
||||
ServiceID guid.GUID
|
||||
}
|
||||
|
||||
type rawHvsockAddr struct {
|
||||
Family uint16
|
||||
_ uint16
|
||||
VMID guid.GUID
|
||||
ServiceID guid.GUID
|
||||
}
|
||||
|
||||
var _ socket.RawSockaddr = &rawHvsockAddr{}
|
||||
|
||||
// Network returns the address's network name, "hvsock".
|
||||
func (*HvsockAddr) Network() string {
|
||||
return "hvsock"
|
||||
}
|
||||
|
||||
func (addr *HvsockAddr) String() string {
|
||||
return fmt.Sprintf("%s:%s", &addr.VMID, &addr.ServiceID)
|
||||
}
|
||||
|
||||
// VsockServiceID returns an hvsock service ID corresponding to the specified AF_VSOCK port.
|
||||
func VsockServiceID(port uint32) guid.GUID {
|
||||
g := hvsockVsockServiceTemplate() // make a copy
|
||||
g.Data1 = port
|
||||
return g
|
||||
}
|
||||
|
||||
func (addr *HvsockAddr) raw() rawHvsockAddr {
|
||||
return rawHvsockAddr{
|
||||
Family: afHVSock,
|
||||
VMID: addr.VMID,
|
||||
ServiceID: addr.ServiceID,
|
||||
}
|
||||
}
|
||||
|
||||
func (addr *HvsockAddr) fromRaw(raw *rawHvsockAddr) {
|
||||
addr.VMID = raw.VMID
|
||||
addr.ServiceID = raw.ServiceID
|
||||
}
|
||||
|
||||
// Sockaddr returns a pointer to and the size of this struct.
|
||||
//
|
||||
// Implements the [socket.RawSockaddr] interface, and allows use in
|
||||
// [socket.Bind] and [socket.ConnectEx].
|
||||
func (r *rawHvsockAddr) Sockaddr() (unsafe.Pointer, int32, error) {
|
||||
return unsafe.Pointer(r), int32(unsafe.Sizeof(rawHvsockAddr{})), nil
|
||||
}
|
||||
|
||||
// Sockaddr interface allows use with `sockets.Bind()` and `.ConnectEx()`.
|
||||
func (r *rawHvsockAddr) FromBytes(b []byte) error {
|
||||
n := int(unsafe.Sizeof(rawHvsockAddr{}))
|
||||
|
||||
if len(b) < n {
|
||||
return fmt.Errorf("got %d, want %d: %w", len(b), n, socket.ErrBufferSize)
|
||||
}
|
||||
|
||||
copy(unsafe.Slice((*byte)(unsafe.Pointer(r)), n), b[:n])
|
||||
if r.Family != afHVSock {
|
||||
return fmt.Errorf("got %d, want %d: %w", r.Family, afHVSock, socket.ErrAddrFamily)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HvsockListener is a socket listener for the AF_HYPERV address family.
|
||||
type HvsockListener struct {
|
||||
sock *win32File
|
||||
addr HvsockAddr
|
||||
}
|
||||
|
||||
var _ net.Listener = &HvsockListener{}
|
||||
|
||||
// HvsockConn is a connected socket of the AF_HYPERV address family.
|
||||
type HvsockConn struct {
|
||||
sock *win32File
|
||||
local, remote HvsockAddr
|
||||
}
|
||||
|
||||
var _ net.Conn = &HvsockConn{}
|
||||
|
||||
func newHVSocket() (*win32File, error) {
|
||||
fd, err := syscall.Socket(afHVSock, syscall.SOCK_STREAM, 1)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("socket", err)
|
||||
}
|
||||
f, err := makeWin32File(fd)
|
||||
if err != nil {
|
||||
syscall.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
f.socket = true
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ListenHvsock listens for connections on the specified hvsock address.
|
||||
func ListenHvsock(addr *HvsockAddr) (_ *HvsockListener, err error) {
|
||||
l := &HvsockListener{addr: *addr}
|
||||
sock, err := newHVSocket()
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", err)
|
||||
}
|
||||
sa := addr.raw()
|
||||
err = socket.Bind(windows.Handle(sock.handle), &sa)
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", os.NewSyscallError("socket", err))
|
||||
}
|
||||
err = syscall.Listen(sock.handle, 16)
|
||||
if err != nil {
|
||||
return nil, l.opErr("listen", os.NewSyscallError("listen", err))
|
||||
}
|
||||
return &HvsockListener{sock: sock, addr: *addr}, nil
|
||||
}
|
||||
|
||||
func (l *HvsockListener) opErr(op string, err error) error {
|
||||
return &net.OpError{Op: op, Net: "hvsock", Addr: &l.addr, Err: err}
|
||||
}
|
||||
|
||||
// Addr returns the listener's network address.
|
||||
func (l *HvsockListener) Addr() net.Addr {
|
||||
return &l.addr
|
||||
}
|
||||
|
||||
// Accept waits for the next connection and returns it.
|
||||
func (l *HvsockListener) Accept() (_ net.Conn, err error) {
|
||||
sock, err := newHVSocket()
|
||||
if err != nil {
|
||||
return nil, l.opErr("accept", err)
|
||||
}
|
||||
defer func() {
|
||||
if sock != nil {
|
||||
sock.Close()
|
||||
}
|
||||
}()
|
||||
c, err := l.sock.prepareIO()
|
||||
if err != nil {
|
||||
return nil, l.opErr("accept", err)
|
||||
}
|
||||
defer l.sock.wg.Done()
|
||||
|
||||
// AcceptEx, per documentation, requires an extra 16 bytes per address.
|
||||
//
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-acceptex
|
||||
const addrlen = uint32(16 + unsafe.Sizeof(rawHvsockAddr{}))
|
||||
var addrbuf [addrlen * 2]byte
|
||||
|
||||
var bytes uint32
|
||||
err = syscall.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /*rxdatalen*/, addrlen, addrlen, &bytes, &c.o)
|
||||
if _, err = l.sock.asyncIO(c, nil, bytes, err); err != nil {
|
||||
return nil, l.opErr("accept", os.NewSyscallError("acceptex", err))
|
||||
}
|
||||
|
||||
conn := &HvsockConn{
|
||||
sock: sock,
|
||||
}
|
||||
// The local address returned in the AcceptEx buffer is the same as the Listener socket's
|
||||
// address. However, the service GUID reported by GetSockName is different from the Listeners
|
||||
// socket, and is sometimes the same as the local address of the socket that dialed the
|
||||
// address, with the service GUID.Data1 incremented, but othertimes is different.
|
||||
// todo: does the local address matter? is the listener's address or the actual address appropriate?
|
||||
conn.local.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[0])))
|
||||
conn.remote.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[addrlen])))
|
||||
|
||||
// initialize the accepted socket and update its properties with those of the listening socket
|
||||
if err = windows.Setsockopt(windows.Handle(sock.handle),
|
||||
windows.SOL_SOCKET, windows.SO_UPDATE_ACCEPT_CONTEXT,
|
||||
(*byte)(unsafe.Pointer(&l.sock.handle)), int32(unsafe.Sizeof(l.sock.handle))); err != nil {
|
||||
return nil, conn.opErr("accept", os.NewSyscallError("setsockopt", err))
|
||||
}
|
||||
|
||||
sock = nil
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Close closes the listener, causing any pending Accept calls to fail.
|
||||
func (l *HvsockListener) Close() error {
|
||||
return l.sock.Close()
|
||||
}
|
||||
|
||||
// HvsockDialer configures and dials a Hyper-V Socket (ie, [HvsockConn]).
|
||||
type HvsockDialer struct {
|
||||
// Deadline is the time the Dial operation must connect before erroring.
|
||||
Deadline time.Time
|
||||
|
||||
// Retries is the number of additional connects to try if the connection times out, is refused,
|
||||
// or the host is unreachable
|
||||
Retries uint
|
||||
|
||||
// RetryWait is the time to wait after a connection error to retry
|
||||
RetryWait time.Duration
|
||||
|
||||
rt *time.Timer // redial wait timer
|
||||
}
|
||||
|
||||
// Dial the Hyper-V socket at addr.
|
||||
//
|
||||
// See [HvsockDialer.Dial] for more information.
|
||||
func Dial(ctx context.Context, addr *HvsockAddr) (conn *HvsockConn, err error) {
|
||||
return (&HvsockDialer{}).Dial(ctx, addr)
|
||||
}
|
||||
|
||||
// Dial attempts to connect to the Hyper-V socket at addr, and returns a connection if successful.
|
||||
// Will attempt (HvsockDialer).Retries if dialing fails, waiting (HvsockDialer).RetryWait between
|
||||
// retries.
|
||||
//
|
||||
// Dialing can be cancelled either by providing (HvsockDialer).Deadline, or cancelling ctx.
|
||||
func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *HvsockConn, err error) {
|
||||
op := "dial"
|
||||
// create the conn early to use opErr()
|
||||
conn = &HvsockConn{
|
||||
remote: *addr,
|
||||
}
|
||||
|
||||
if !d.Deadline.IsZero() {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, d.Deadline)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// preemptive timeout/cancellation check
|
||||
if err = ctx.Err(); err != nil {
|
||||
return nil, conn.opErr(op, err)
|
||||
}
|
||||
|
||||
sock, err := newHVSocket()
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, err)
|
||||
}
|
||||
defer func() {
|
||||
if sock != nil {
|
||||
sock.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
sa := addr.raw()
|
||||
err = socket.Bind(windows.Handle(sock.handle), &sa)
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("bind", err))
|
||||
}
|
||||
|
||||
c, err := sock.prepareIO()
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, err)
|
||||
}
|
||||
defer sock.wg.Done()
|
||||
var bytes uint32
|
||||
for i := uint(0); i <= d.Retries; i++ {
|
||||
err = socket.ConnectEx(
|
||||
windows.Handle(sock.handle),
|
||||
&sa,
|
||||
nil, // sendBuf
|
||||
0, // sendDataLen
|
||||
&bytes,
|
||||
(*windows.Overlapped)(unsafe.Pointer(&c.o)))
|
||||
_, err = sock.asyncIO(c, nil, bytes, err)
|
||||
if i < d.Retries && canRedial(err) {
|
||||
if err = d.redialWait(ctx); err == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("connectex", err))
|
||||
}
|
||||
|
||||
// update the connection properties, so shutdown can be used
|
||||
if err = windows.Setsockopt(
|
||||
windows.Handle(sock.handle),
|
||||
windows.SOL_SOCKET,
|
||||
windows.SO_UPDATE_CONNECT_CONTEXT,
|
||||
nil, // optvalue
|
||||
0, // optlen
|
||||
); err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("setsockopt", err))
|
||||
}
|
||||
|
||||
// get the local name
|
||||
var sal rawHvsockAddr
|
||||
err = socket.GetSockName(windows.Handle(sock.handle), &sal)
|
||||
if err != nil {
|
||||
return nil, conn.opErr(op, os.NewSyscallError("getsockname", err))
|
||||
}
|
||||
conn.local.fromRaw(&sal)
|
||||
|
||||
// one last check for timeout, since asyncIO doesn't check the context
|
||||
if err = ctx.Err(); err != nil {
|
||||
return nil, conn.opErr(op, err)
|
||||
}
|
||||
|
||||
conn.sock = sock
|
||||
sock = nil
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// redialWait waits before attempting to redial, resetting the timer as appropriate.
|
||||
func (d *HvsockDialer) redialWait(ctx context.Context) (err error) {
|
||||
if d.RetryWait == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.rt == nil {
|
||||
d.rt = time.NewTimer(d.RetryWait)
|
||||
} else {
|
||||
// should already be stopped and drained
|
||||
d.rt.Reset(d.RetryWait)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-d.rt.C:
|
||||
return nil
|
||||
}
|
||||
|
||||
// stop and drain the timer
|
||||
if !d.rt.Stop() {
|
||||
<-d.rt.C
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// assumes error is a plain, unwrapped syscall.Errno provided by direct syscall.
|
||||
func canRedial(err error) bool {
|
||||
//nolint:errorlint // guaranteed to be an Errno
|
||||
switch err {
|
||||
case windows.WSAECONNREFUSED, windows.WSAENETUNREACH, windows.WSAETIMEDOUT,
|
||||
windows.ERROR_CONNECTION_REFUSED, windows.ERROR_CONNECTION_UNAVAIL:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) opErr(op string, err error) error {
|
||||
// translate from "file closed" to "socket closed"
|
||||
if errors.Is(err, ErrFileClosed) {
|
||||
err = socket.ErrSocketClosed
|
||||
}
|
||||
return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err}
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) Read(b []byte) (int, error) {
|
||||
c, err := conn.sock.prepareIO()
|
||||
if err != nil {
|
||||
return 0, conn.opErr("read", err)
|
||||
}
|
||||
defer conn.sock.wg.Done()
|
||||
buf := syscall.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||
var flags, bytes uint32
|
||||
err = syscall.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil)
|
||||
n, err := conn.sock.asyncIO(c, &conn.sock.readDeadline, bytes, err)
|
||||
if err != nil {
|
||||
var eno windows.Errno
|
||||
if errors.As(err, &eno) {
|
||||
err = os.NewSyscallError("wsarecv", eno)
|
||||
}
|
||||
return 0, conn.opErr("read", err)
|
||||
} else if n == 0 {
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) Write(b []byte) (int, error) {
|
||||
t := 0
|
||||
for len(b) != 0 {
|
||||
n, err := conn.write(b)
|
||||
if err != nil {
|
||||
return t + n, err
|
||||
}
|
||||
t += n
|
||||
b = b[n:]
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) write(b []byte) (int, error) {
|
||||
c, err := conn.sock.prepareIO()
|
||||
if err != nil {
|
||||
return 0, conn.opErr("write", err)
|
||||
}
|
||||
defer conn.sock.wg.Done()
|
||||
buf := syscall.WSABuf{Buf: &b[0], Len: uint32(len(b))}
|
||||
var bytes uint32
|
||||
err = syscall.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil)
|
||||
n, err := conn.sock.asyncIO(c, &conn.sock.writeDeadline, bytes, err)
|
||||
if err != nil {
|
||||
var eno windows.Errno
|
||||
if errors.As(err, &eno) {
|
||||
err = os.NewSyscallError("wsasend", eno)
|
||||
}
|
||||
return 0, conn.opErr("write", err)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close closes the socket connection, failing any pending read or write calls.
|
||||
func (conn *HvsockConn) Close() error {
|
||||
return conn.sock.Close()
|
||||
}
|
||||
|
||||
func (conn *HvsockConn) IsClosed() bool {
|
||||
return conn.sock.IsClosed()
|
||||
}
|
||||
|
||||
// shutdown disables sending or receiving on a socket.
|
||||
func (conn *HvsockConn) shutdown(how int) error {
|
||||
if conn.IsClosed() {
|
||||
return socket.ErrSocketClosed
|
||||
}
|
||||
|
||||
err := syscall.Shutdown(conn.sock.handle, how)
|
||||
if err != nil {
|
||||
// If the connection was closed, shutdowns fail with "not connected"
|
||||
if errors.Is(err, windows.WSAENOTCONN) ||
|
||||
errors.Is(err, windows.WSAESHUTDOWN) {
|
||||
err = socket.ErrSocketClosed
|
||||
}
|
||||
return os.NewSyscallError("shutdown", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseRead shuts down the read end of the socket, preventing future read operations.
|
||||
func (conn *HvsockConn) CloseRead() error {
|
||||
err := conn.shutdown(syscall.SHUT_RD)
|
||||
if err != nil {
|
||||
return conn.opErr("closeread", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseWrite shuts down the write end of the socket, preventing future write operations and
|
||||
// notifying the other endpoint that no more data will be written.
|
||||
func (conn *HvsockConn) CloseWrite() error {
|
||||
err := conn.shutdown(syscall.SHUT_WR)
|
||||
if err != nil {
|
||||
return conn.opErr("closewrite", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalAddr returns the local address of the connection.
|
||||
func (conn *HvsockConn) LocalAddr() net.Addr {
|
||||
return &conn.local
|
||||
}
|
||||
|
||||
// RemoteAddr returns the remote address of the connection.
|
||||
func (conn *HvsockConn) RemoteAddr() net.Addr {
|
||||
return &conn.remote
|
||||
}
|
||||
|
||||
// SetDeadline implements the net.Conn SetDeadline method.
|
||||
func (conn *HvsockConn) SetDeadline(t time.Time) error {
|
||||
// todo: implement `SetDeadline` for `win32File`
|
||||
if err := conn.SetReadDeadline(t); err != nil {
|
||||
return fmt.Errorf("set read deadline: %w", err)
|
||||
}
|
||||
if err := conn.SetWriteDeadline(t); err != nil {
|
||||
return fmt.Errorf("set write deadline: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetReadDeadline implements the net.Conn SetReadDeadline method.
|
||||
func (conn *HvsockConn) SetReadDeadline(t time.Time) error {
|
||||
return conn.sock.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline implements the net.Conn SetWriteDeadline method.
|
||||
func (conn *HvsockConn) SetWriteDeadline(t time.Time) error {
|
||||
return conn.sock.SetWriteDeadline(t)
|
||||
}
|
20
vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.go
generated
vendored
Normal file
20
vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.go
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// RawSockaddr allows structs to be used with [Bind] and [ConnectEx]. The
|
||||
// struct must meet the Win32 sockaddr requirements specified here:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2
|
||||
//
|
||||
// Specifically, the struct size must be least larger than an int16 (unsigned short)
|
||||
// for the address family.
|
||||
type RawSockaddr interface {
|
||||
// Sockaddr returns a pointer to the RawSockaddr and its struct size, allowing
|
||||
// for the RawSockaddr's data to be overwritten by syscalls (if necessary).
|
||||
//
|
||||
// It is the callers responsibility to validate that the values are valid; invalid
|
||||
// pointers or size can cause a panic.
|
||||
Sockaddr() (unsafe.Pointer, int32, error)
|
||||
}
|
179
vendor/github.com/Microsoft/go-winio/internal/socket/socket.go
generated
vendored
Normal file
179
vendor/github.com/Microsoft/go-winio/internal/socket/socket.go
generated
vendored
Normal file
@ -0,0 +1,179 @@
|
||||
//go:build windows
|
||||
|
||||
package socket
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/Microsoft/go-winio/pkg/guid"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go socket.go
|
||||
|
||||
//sys getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getsockname
|
||||
//sys getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getpeername
|
||||
//sys bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) [failretval==socketError] = ws2_32.bind
|
||||
|
||||
const socketError = uintptr(^uint32(0))
|
||||
|
||||
var (
|
||||
// todo(helsaawy): create custom error types to store the desired vs actual size and addr family?
|
||||
|
||||
ErrBufferSize = errors.New("buffer size")
|
||||
ErrAddrFamily = errors.New("address family")
|
||||
ErrInvalidPointer = errors.New("invalid pointer")
|
||||
ErrSocketClosed = fmt.Errorf("socket closed: %w", net.ErrClosed)
|
||||
)
|
||||
|
||||
// todo(helsaawy): replace these with generics, ie: GetSockName[S RawSockaddr](s windows.Handle) (S, error)
|
||||
|
||||
// GetSockName writes the local address of socket s to the [RawSockaddr] rsa.
|
||||
// If rsa is not large enough, the [windows.WSAEFAULT] is returned.
|
||||
func GetSockName(s windows.Handle, rsa RawSockaddr) error {
|
||||
ptr, l, err := rsa.Sockaddr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve socket pointer and size: %w", err)
|
||||
}
|
||||
|
||||
// although getsockname returns WSAEFAULT if the buffer is too small, it does not set
|
||||
// &l to the correct size, so--apart from doubling the buffer repeatedly--there is no remedy
|
||||
return getsockname(s, ptr, &l)
|
||||
}
|
||||
|
||||
// GetPeerName returns the remote address the socket is connected to.
|
||||
//
|
||||
// See [GetSockName] for more information.
|
||||
func GetPeerName(s windows.Handle, rsa RawSockaddr) error {
|
||||
ptr, l, err := rsa.Sockaddr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve socket pointer and size: %w", err)
|
||||
}
|
||||
|
||||
return getpeername(s, ptr, &l)
|
||||
}
|
||||
|
||||
func Bind(s windows.Handle, rsa RawSockaddr) (err error) {
|
||||
ptr, l, err := rsa.Sockaddr()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve socket pointer and size: %w", err)
|
||||
}
|
||||
|
||||
return bind(s, ptr, l)
|
||||
}
|
||||
|
||||
// "golang.org/x/sys/windows".ConnectEx and .Bind only accept internal implementations of the
|
||||
// their sockaddr interface, so they cannot be used with HvsockAddr
|
||||
// Replicate functionality here from
|
||||
// https://cs.opensource.google/go/x/sys/+/master:windows/syscall_windows.go
|
||||
|
||||
// The function pointers to `AcceptEx`, `ConnectEx` and `GetAcceptExSockaddrs` must be loaded at
|
||||
// runtime via a WSAIoctl call:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/Mswsock/nc-mswsock-lpfn_connectex#remarks
|
||||
|
||||
type runtimeFunc struct {
|
||||
id guid.GUID
|
||||
once sync.Once
|
||||
addr uintptr
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *runtimeFunc) Load() error {
|
||||
f.once.Do(func() {
|
||||
var s windows.Handle
|
||||
s, f.err = windows.Socket(windows.AF_INET, windows.SOCK_STREAM, windows.IPPROTO_TCP)
|
||||
if f.err != nil {
|
||||
return
|
||||
}
|
||||
defer windows.CloseHandle(s) //nolint:errcheck
|
||||
|
||||
var n uint32
|
||||
f.err = windows.WSAIoctl(s,
|
||||
windows.SIO_GET_EXTENSION_FUNCTION_POINTER,
|
||||
(*byte)(unsafe.Pointer(&f.id)),
|
||||
uint32(unsafe.Sizeof(f.id)),
|
||||
(*byte)(unsafe.Pointer(&f.addr)),
|
||||
uint32(unsafe.Sizeof(f.addr)),
|
||||
&n,
|
||||
nil, //overlapped
|
||||
0, //completionRoutine
|
||||
)
|
||||
})
|
||||
return f.err
|
||||
}
|
||||
|
||||
var (
|
||||
// todo: add `AcceptEx` and `GetAcceptExSockaddrs`
|
||||
WSAID_CONNECTEX = guid.GUID{ //revive:disable-line:var-naming ALL_CAPS
|
||||
Data1: 0x25a207b9,
|
||||
Data2: 0xddf3,
|
||||
Data3: 0x4660,
|
||||
Data4: [8]byte{0x8e, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e},
|
||||
}
|
||||
|
||||
connectExFunc = runtimeFunc{id: WSAID_CONNECTEX}
|
||||
)
|
||||
|
||||
func ConnectEx(
|
||||
fd windows.Handle,
|
||||
rsa RawSockaddr,
|
||||
sendBuf *byte,
|
||||
sendDataLen uint32,
|
||||
bytesSent *uint32,
|
||||
overlapped *windows.Overlapped,
|
||||
) error {
|
||||
if err := connectExFunc.Load(); err != nil {
|
||||
return fmt.Errorf("failed to load ConnectEx function pointer: %w", err)
|
||||
}
|
||||
ptr, n, err := rsa.Sockaddr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return connectEx(fd, ptr, n, sendBuf, sendDataLen, bytesSent, overlapped)
|
||||
}
|
||||
|
||||
// BOOL LpfnConnectex(
|
||||
// [in] SOCKET s,
|
||||
// [in] const sockaddr *name,
|
||||
// [in] int namelen,
|
||||
// [in, optional] PVOID lpSendBuffer,
|
||||
// [in] DWORD dwSendDataLength,
|
||||
// [out] LPDWORD lpdwBytesSent,
|
||||
// [in] LPOVERLAPPED lpOverlapped
|
||||
// )
|
||||
|
||||
func connectEx(
|
||||
s windows.Handle,
|
||||
name unsafe.Pointer,
|
||||
namelen int32,
|
||||
sendBuf *byte,
|
||||
sendDataLen uint32,
|
||||
bytesSent *uint32,
|
||||
overlapped *windows.Overlapped,
|
||||
) (err error) {
|
||||
// todo: after upgrading to 1.18, switch from syscall.Syscall9 to syscall.SyscallN
|
||||
r1, _, e1 := syscall.Syscall9(connectExFunc.addr,
|
||||
7,
|
||||
uintptr(s),
|
||||
uintptr(name),
|
||||
uintptr(namelen),
|
||||
uintptr(unsafe.Pointer(sendBuf)),
|
||||
uintptr(sendDataLen),
|
||||
uintptr(unsafe.Pointer(bytesSent)),
|
||||
uintptr(unsafe.Pointer(overlapped)),
|
||||
0,
|
||||
0)
|
||||
if r1 == 0 {
|
||||
if e1 != 0 {
|
||||
err = error(e1)
|
||||
} else {
|
||||
err = syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
72
vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.go
generated
vendored
Normal file
72
vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.go
generated
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
//go:build windows
|
||||
|
||||
// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT.
|
||||
|
||||
package socket
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modws2_32 = windows.NewLazySystemDLL("ws2_32.dll")
|
||||
|
||||
procbind = modws2_32.NewProc("bind")
|
||||
procgetpeername = modws2_32.NewProc("getpeername")
|
||||
procgetsockname = modws2_32.NewProc("getsockname")
|
||||
)
|
||||
|
||||
func bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procbind.Addr(), 3, uintptr(s), uintptr(name), uintptr(namelen))
|
||||
if r1 == socketError {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procgetpeername.Addr(), 3, uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen)))
|
||||
if r1 == socketError {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procgetsockname.Addr(), 3, uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen)))
|
||||
if r1 == socketError {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
521
vendor/github.com/Microsoft/go-winio/pipe.go
generated
vendored
Normal file
521
vendor/github.com/Microsoft/go-winio/pipe.go
generated
vendored
Normal file
@ -0,0 +1,521 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package winio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
//sys connectNamedPipe(pipe syscall.Handle, o *syscall.Overlapped) (err error) = ConnectNamedPipe
|
||||
//sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateNamedPipeW
|
||||
//sys createFile(name string, access uint32, mode uint32, sa *syscall.SecurityAttributes, createmode uint32, attrs uint32, templatefile syscall.Handle) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateFileW
|
||||
//sys getNamedPipeInfo(pipe syscall.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo
|
||||
//sys getNamedPipeHandleState(pipe syscall.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
|
||||
//sys localAlloc(uFlags uint32, length uint32) (ptr uintptr) = LocalAlloc
|
||||
//sys ntCreateNamedPipeFile(pipe *syscall.Handle, access uint32, oa *objectAttributes, iosb *ioStatusBlock, share uint32, disposition uint32, options uint32, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) = ntdll.NtCreateNamedPipeFile
|
||||
//sys rtlNtStatusToDosError(status ntStatus) (winerr error) = ntdll.RtlNtStatusToDosErrorNoTeb
|
||||
//sys rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) = ntdll.RtlDosPathNameToNtPathName_U
|
||||
//sys rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) = ntdll.RtlDefaultNpAcl
|
||||
|
||||
type ioStatusBlock struct {
|
||||
Status, Information uintptr
|
||||
}
|
||||
|
||||
type objectAttributes struct {
|
||||
Length uintptr
|
||||
RootDirectory uintptr
|
||||
ObjectName *unicodeString
|
||||
Attributes uintptr
|
||||
SecurityDescriptor *securityDescriptor
|
||||
SecurityQoS uintptr
|
||||
}
|
||||
|
||||
type unicodeString struct {
|
||||
Length uint16
|
||||
MaximumLength uint16
|
||||
Buffer uintptr
|
||||
}
|
||||
|
||||
type securityDescriptor struct {
|
||||
Revision byte
|
||||
Sbz1 byte
|
||||
Control uint16
|
||||
Owner uintptr
|
||||
Group uintptr
|
||||
Sacl uintptr //revive:disable-line:var-naming SACL, not Sacl
|
||||
Dacl uintptr //revive:disable-line:var-naming DACL, not Dacl
|
||||
}
|
||||
|
||||
type ntStatus int32
|
||||
|
||||
func (status ntStatus) Err() error {
|
||||
if status >= 0 {
|
||||
return nil
|
||||
}
|
||||
return rtlNtStatusToDosError(status)
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
|
||||
ErrPipeListenerClosed = net.ErrClosed
|
||||
|
||||
errPipeWriteClosed = errors.New("pipe has been closed for write")
|
||||
)
|
||||
|
||||
type win32Pipe struct {
|
||||
*win32File
|
||||
path string
|
||||
}
|
||||
|
||||
type win32MessageBytePipe struct {
|
||||
win32Pipe
|
||||
writeClosed bool
|
||||
readEOF bool
|
||||
}
|
||||
|
||||
type pipeAddress string
|
||||
|
||||
func (f *win32Pipe) LocalAddr() net.Addr {
|
||||
return pipeAddress(f.path)
|
||||
}
|
||||
|
||||
func (f *win32Pipe) RemoteAddr() net.Addr {
|
||||
return pipeAddress(f.path)
|
||||
}
|
||||
|
||||
func (f *win32Pipe) SetDeadline(t time.Time) error {
|
||||
if err := f.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
// CloseWrite closes the write side of a message pipe in byte mode.
|
||||
func (f *win32MessageBytePipe) CloseWrite() error {
|
||||
if f.writeClosed {
|
||||
return errPipeWriteClosed
|
||||
}
|
||||
err := f.win32File.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.win32File.Write(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.writeClosed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
|
||||
// they are used to implement CloseWrite().
|
||||
func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
|
||||
if f.writeClosed {
|
||||
return 0, errPipeWriteClosed
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return f.win32File.Write(b)
|
||||
}
|
||||
|
||||
// Read reads bytes from a message pipe in byte mode. A read of a zero-byte message on a message
|
||||
// mode pipe will return io.EOF, as will all subsequent reads.
|
||||
func (f *win32MessageBytePipe) Read(b []byte) (int, error) {
|
||||
if f.readEOF {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err := f.win32File.Read(b)
|
||||
if err == io.EOF { //nolint:errorlint
|
||||
// If this was the result of a zero-byte read, then
|
||||
// it is possible that the read was due to a zero-size
|
||||
// message. Since we are simulating CloseWrite with a
|
||||
// zero-byte message, ensure that all future Read() calls
|
||||
// also return EOF.
|
||||
f.readEOF = true
|
||||
} else if err == syscall.ERROR_MORE_DATA { //nolint:errorlint // err is Errno
|
||||
// ERROR_MORE_DATA indicates that the pipe's read mode is message mode
|
||||
// and the message still has more bytes. Treat this as a success, since
|
||||
// this package presents all named pipes as byte streams.
|
||||
err = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (pipeAddress) Network() string {
|
||||
return "pipe"
|
||||
}
|
||||
|
||||
func (s pipeAddress) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout.
|
||||
func tryDialPipe(ctx context.Context, path *string, access uint32) (syscall.Handle, error) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return syscall.Handle(0), ctx.Err()
|
||||
default:
|
||||
h, err := createFile(*path,
|
||||
access,
|
||||
0,
|
||||
nil,
|
||||
syscall.OPEN_EXISTING,
|
||||
windows.FILE_FLAG_OVERLAPPED|windows.SECURITY_SQOS_PRESENT|windows.SECURITY_ANONYMOUS,
|
||||
0)
|
||||
if err == nil {
|
||||
return h, nil
|
||||
}
|
||||
if err != windows.ERROR_PIPE_BUSY { //nolint:errorlint // err is Errno
|
||||
return h, &os.PathError{Err: err, Op: "open", Path: *path}
|
||||
}
|
||||
// Wait 10 msec and try again. This is a rather simplistic
|
||||
// view, as we always try each 10 milliseconds.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DialPipe connects to a named pipe by path, timing out if the connection
|
||||
// takes longer than the specified duration. If timeout is nil, then we use
|
||||
// a default timeout of 2 seconds. (We do not use WaitNamedPipe.)
|
||||
func DialPipe(path string, timeout *time.Duration) (net.Conn, error) {
|
||||
var absTimeout time.Time
|
||||
if timeout != nil {
|
||||
absTimeout = time.Now().Add(*timeout)
|
||||
} else {
|
||||
absTimeout = time.Now().Add(2 * time.Second)
|
||||
}
|
||||
ctx, cancel := context.WithDeadline(context.Background(), absTimeout)
|
||||
defer cancel()
|
||||
conn, err := DialPipeContext(ctx, path)
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// DialPipeContext attempts to connect to a named pipe by `path` until `ctx`
|
||||
// cancellation or timeout.
|
||||
func DialPipeContext(ctx context.Context, path string) (net.Conn, error) {
|
||||
return DialPipeAccess(ctx, path, syscall.GENERIC_READ|syscall.GENERIC_WRITE)
|
||||
}
|
||||
|
||||
// DialPipeAccess attempts to connect to a named pipe by `path` with `access` until `ctx`
|
||||
// cancellation or timeout.
|
||||
func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) {
|
||||
var err error
|
||||
var h syscall.Handle
|
||||
h, err = tryDialPipe(ctx, &path, access)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var flags uint32
|
||||
err = getNamedPipeInfo(h, &flags, nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := makeWin32File(h)
|
||||
if err != nil {
|
||||
syscall.Close(h)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the pipe is in message mode, return a message byte pipe, which
|
||||
// supports CloseWrite().
|
||||
if flags&windows.PIPE_TYPE_MESSAGE != 0 {
|
||||
return &win32MessageBytePipe{
|
||||
win32Pipe: win32Pipe{win32File: f, path: path},
|
||||
}, nil
|
||||
}
|
||||
return &win32Pipe{win32File: f, path: path}, nil
|
||||
}
|
||||
|
||||
type acceptResponse struct {
|
||||
f *win32File
|
||||
err error
|
||||
}
|
||||
|
||||
type win32PipeListener struct {
|
||||
firstHandle syscall.Handle
|
||||
path string
|
||||
config PipeConfig
|
||||
acceptCh chan (chan acceptResponse)
|
||||
closeCh chan int
|
||||
doneCh chan int
|
||||
}
|
||||
|
||||
func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (syscall.Handle, error) {
|
||||
path16, err := syscall.UTF16FromString(path)
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
|
||||
var oa objectAttributes
|
||||
oa.Length = unsafe.Sizeof(oa)
|
||||
|
||||
var ntPath unicodeString
|
||||
if err := rtlDosPathNameToNtPathName(&path16[0],
|
||||
&ntPath,
|
||||
0,
|
||||
0,
|
||||
).Err(); err != nil {
|
||||
return 0, &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
defer localFree(ntPath.Buffer)
|
||||
oa.ObjectName = &ntPath
|
||||
|
||||
// The security descriptor is only needed for the first pipe.
|
||||
if first {
|
||||
if sd != nil {
|
||||
l := uint32(len(sd))
|
||||
sdb := localAlloc(0, l)
|
||||
defer localFree(sdb)
|
||||
copy((*[0xffff]byte)(unsafe.Pointer(sdb))[:], sd)
|
||||
oa.SecurityDescriptor = (*securityDescriptor)(unsafe.Pointer(sdb))
|
||||
} else {
|
||||
// Construct the default named pipe security descriptor.
|
||||
var dacl uintptr
|
||||
if err := rtlDefaultNpAcl(&dacl).Err(); err != nil {
|
||||
return 0, fmt.Errorf("getting default named pipe ACL: %w", err)
|
||||
}
|
||||
defer localFree(dacl)
|
||||
|
||||
sdb := &securityDescriptor{
|
||||
Revision: 1,
|
||||
Control: windows.SE_DACL_PRESENT,
|
||||
Dacl: dacl,
|
||||
}
|
||||
oa.SecurityDescriptor = sdb
|
||||
}
|
||||
}
|
||||
|
||||
typ := uint32(windows.FILE_PIPE_REJECT_REMOTE_CLIENTS)
|
||||
if c.MessageMode {
|
||||
typ |= windows.FILE_PIPE_MESSAGE_TYPE
|
||||
}
|
||||
|
||||
disposition := uint32(windows.FILE_OPEN)
|
||||
access := uint32(syscall.GENERIC_READ | syscall.GENERIC_WRITE | syscall.SYNCHRONIZE)
|
||||
if first {
|
||||
disposition = windows.FILE_CREATE
|
||||
// By not asking for read or write access, the named pipe file system
|
||||
// will put this pipe into an initially disconnected state, blocking
|
||||
// client connections until the next call with first == false.
|
||||
access = syscall.SYNCHRONIZE
|
||||
}
|
||||
|
||||
timeout := int64(-50 * 10000) // 50ms
|
||||
|
||||
var (
|
||||
h syscall.Handle
|
||||
iosb ioStatusBlock
|
||||
)
|
||||
err = ntCreateNamedPipeFile(&h,
|
||||
access,
|
||||
&oa,
|
||||
&iosb,
|
||||
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE,
|
||||
disposition,
|
||||
0,
|
||||
typ,
|
||||
0,
|
||||
0,
|
||||
0xffffffff,
|
||||
uint32(c.InputBufferSize),
|
||||
uint32(c.OutputBufferSize),
|
||||
&timeout).Err()
|
||||
if err != nil {
|
||||
return 0, &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
|
||||
runtime.KeepAlive(ntPath)
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) makeServerPipe() (*win32File, error) {
|
||||
h, err := makeServerPipeHandle(l.path, nil, &l.config, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := makeWin32File(h)
|
||||
if err != nil {
|
||||
syscall.Close(h)
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) {
|
||||
p, err := l.makeServerPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wait for the client to connect.
|
||||
ch := make(chan error)
|
||||
go func(p *win32File) {
|
||||
ch <- connectPipe(p)
|
||||
}(p)
|
||||
|
||||
select {
|
||||
case err = <-ch:
|
||||
if err != nil {
|
||||
p.Close()
|
||||
p = nil
|
||||
}
|
||||
case <-l.closeCh:
|
||||
// Abort the connect request by closing the handle.
|
||||
p.Close()
|
||||
p = nil
|
||||
err = <-ch
|
||||
if err == nil || err == ErrFileClosed { //nolint:errorlint // err is Errno
|
||||
err = ErrPipeListenerClosed
|
||||
}
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) listenerRoutine() {
|
||||
closed := false
|
||||
for !closed {
|
||||
select {
|
||||
case <-l.closeCh:
|
||||
closed = true
|
||||
case responseCh := <-l.acceptCh:
|
||||
var (
|
||||
p *win32File
|
||||
err error
|
||||
)
|
||||
for {
|
||||
p, err = l.makeConnectedServerPipe()
|
||||
// If the connection was immediately closed by the client, try
|
||||
// again.
|
||||
if err != windows.ERROR_NO_DATA { //nolint:errorlint // err is Errno
|
||||
break
|
||||
}
|
||||
}
|
||||
responseCh <- acceptResponse{p, err}
|
||||
closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno
|
||||
}
|
||||
}
|
||||
syscall.Close(l.firstHandle)
|
||||
l.firstHandle = 0
|
||||
// Notify Close() and Accept() callers that the handle has been closed.
|
||||
close(l.doneCh)
|
||||
}
|
||||
|
||||
// PipeConfig contain configuration for the pipe listener.
|
||||
type PipeConfig struct {
|
||||
// SecurityDescriptor contains a Windows security descriptor in SDDL format.
|
||||
SecurityDescriptor string
|
||||
|
||||
// MessageMode determines whether the pipe is in byte or message mode. In either
|
||||
// case the pipe is read in byte mode by default. The only practical difference in
|
||||
// this implementation is that CloseWrite() is only supported for message mode pipes;
|
||||
// CloseWrite() is implemented as a zero-byte write, but zero-byte writes are only
|
||||
// transferred to the reader (and returned as io.EOF in this implementation)
|
||||
// when the pipe is in message mode.
|
||||
MessageMode bool
|
||||
|
||||
// InputBufferSize specifies the size of the input buffer, in bytes.
|
||||
InputBufferSize int32
|
||||
|
||||
// OutputBufferSize specifies the size of the output buffer, in bytes.
|
||||
OutputBufferSize int32
|
||||
}
|
||||
|
||||
// ListenPipe creates a listener on a Windows named pipe path, e.g. \\.\pipe\mypipe.
|
||||
// The pipe must not already exist.
|
||||
func ListenPipe(path string, c *PipeConfig) (net.Listener, error) {
|
||||
var (
|
||||
sd []byte
|
||||
err error
|
||||
)
|
||||
if c == nil {
|
||||
c = &PipeConfig{}
|
||||
}
|
||||
if c.SecurityDescriptor != "" {
|
||||
sd, err = SddlToSecurityDescriptor(c.SecurityDescriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
h, err := makeServerPipeHandle(path, sd, c, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l := &win32PipeListener{
|
||||
firstHandle: h,
|
||||
path: path,
|
||||
config: *c,
|
||||
acceptCh: make(chan (chan acceptResponse)),
|
||||
closeCh: make(chan int),
|
||||
doneCh: make(chan int),
|
||||
}
|
||||
go l.listenerRoutine()
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func connectPipe(p *win32File) error {
|
||||
c, err := p.prepareIO()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer p.wg.Done()
|
||||
|
||||
err = connectNamedPipe(p.handle, &c.o)
|
||||
_, err = p.asyncIO(c, nil, 0, err)
|
||||
if err != nil && err != windows.ERROR_PIPE_CONNECTED { //nolint:errorlint // err is Errno
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) Accept() (net.Conn, error) {
|
||||
ch := make(chan acceptResponse)
|
||||
select {
|
||||
case l.acceptCh <- ch:
|
||||
response := <-ch
|
||||
err := response.err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l.config.MessageMode {
|
||||
return &win32MessageBytePipe{
|
||||
win32Pipe: win32Pipe{win32File: response.f, path: l.path},
|
||||
}, nil
|
||||
}
|
||||
return &win32Pipe{win32File: response.f, path: l.path}, nil
|
||||
case <-l.doneCh:
|
||||
return nil, ErrPipeListenerClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) Close() error {
|
||||
select {
|
||||
case l.closeCh <- 1:
|
||||
<-l.doneCh
|
||||
case <-l.doneCh:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *win32PipeListener) Addr() net.Addr {
|
||||
return pipeAddress(l.path)
|
||||
}
|
232
vendor/github.com/Microsoft/go-winio/pkg/guid/guid.go
generated
vendored
Normal file
232
vendor/github.com/Microsoft/go-winio/pkg/guid/guid.go
generated
vendored
Normal file
@ -0,0 +1,232 @@
|
||||
// Package guid provides a GUID type. The backing structure for a GUID is
|
||||
// identical to that used by the golang.org/x/sys/windows GUID type.
|
||||
// There are two main binary encodings used for a GUID, the big-endian encoding,
|
||||
// and the Windows (mixed-endian) encoding. See here for details:
|
||||
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding
|
||||
package guid
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint:gosec // not used for secure application
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//go:generate go run golang.org/x/tools/cmd/stringer -type=Variant -trimprefix=Variant -linecomment
|
||||
|
||||
// Variant specifies which GUID variant (or "type") of the GUID. It determines
|
||||
// how the entirety of the rest of the GUID is interpreted.
|
||||
type Variant uint8
|
||||
|
||||
// The variants specified by RFC 4122 section 4.1.1.
|
||||
const (
|
||||
// VariantUnknown specifies a GUID variant which does not conform to one of
|
||||
// the variant encodings specified in RFC 4122.
|
||||
VariantUnknown Variant = iota
|
||||
VariantNCS
|
||||
VariantRFC4122 // RFC 4122
|
||||
VariantMicrosoft
|
||||
VariantFuture
|
||||
)
|
||||
|
||||
// Version specifies how the bits in the GUID were generated. For instance, a
|
||||
// version 4 GUID is randomly generated, and a version 5 is generated from the
|
||||
// hash of an input string.
|
||||
type Version uint8
|
||||
|
||||
func (v Version) String() string {
|
||||
return strconv.FormatUint(uint64(v), 10)
|
||||
}
|
||||
|
||||
var _ = (encoding.TextMarshaler)(GUID{})
|
||||
var _ = (encoding.TextUnmarshaler)(&GUID{})
|
||||
|
||||
// NewV4 returns a new version 4 (pseudorandom) GUID, as defined by RFC 4122.
|
||||
func NewV4() (GUID, error) {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return GUID{}, err
|
||||
}
|
||||
|
||||
g := FromArray(b)
|
||||
g.setVersion(4) // Version 4 means randomly generated.
|
||||
g.setVariant(VariantRFC4122)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// NewV5 returns a new version 5 (generated from a string via SHA-1 hashing)
|
||||
// GUID, as defined by RFC 4122. The RFC is unclear on the encoding of the name,
|
||||
// and the sample code treats it as a series of bytes, so we do the same here.
|
||||
//
|
||||
// Some implementations, such as those found on Windows, treat the name as a
|
||||
// big-endian UTF16 stream of bytes. If that is desired, the string can be
|
||||
// encoded as such before being passed to this function.
|
||||
func NewV5(namespace GUID, name []byte) (GUID, error) {
|
||||
b := sha1.New() //nolint:gosec // not used for secure application
|
||||
namespaceBytes := namespace.ToArray()
|
||||
b.Write(namespaceBytes[:])
|
||||
b.Write(name)
|
||||
|
||||
a := [16]byte{}
|
||||
copy(a[:], b.Sum(nil))
|
||||
|
||||
g := FromArray(a)
|
||||
g.setVersion(5) // Version 5 means generated from a string.
|
||||
g.setVariant(VariantRFC4122)
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func fromArray(b [16]byte, order binary.ByteOrder) GUID {
|
||||
var g GUID
|
||||
g.Data1 = order.Uint32(b[0:4])
|
||||
g.Data2 = order.Uint16(b[4:6])
|
||||
g.Data3 = order.Uint16(b[6:8])
|
||||
copy(g.Data4[:], b[8:16])
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GUID) toArray(order binary.ByteOrder) [16]byte {
|
||||
b := [16]byte{}
|
||||
order.PutUint32(b[0:4], g.Data1)
|
||||
order.PutUint16(b[4:6], g.Data2)
|
||||
order.PutUint16(b[6:8], g.Data3)
|
||||
copy(b[8:16], g.Data4[:])
|
||||
return b
|
||||
}
|
||||
|
||||
// FromArray constructs a GUID from a big-endian encoding array of 16 bytes.
|
||||
func FromArray(b [16]byte) GUID {
|
||||
return fromArray(b, binary.BigEndian)
|
||||
}
|
||||
|
||||
// ToArray returns an array of 16 bytes representing the GUID in big-endian
|
||||
// encoding.
|
||||
func (g GUID) ToArray() [16]byte {
|
||||
return g.toArray(binary.BigEndian)
|
||||
}
|
||||
|
||||
// FromWindowsArray constructs a GUID from a Windows encoding array of bytes.
|
||||
func FromWindowsArray(b [16]byte) GUID {
|
||||
return fromArray(b, binary.LittleEndian)
|
||||
}
|
||||
|
||||
// ToWindowsArray returns an array of 16 bytes representing the GUID in Windows
|
||||
// encoding.
|
||||
func (g GUID) ToWindowsArray() [16]byte {
|
||||
return g.toArray(binary.LittleEndian)
|
||||
}
|
||||
|
||||
func (g GUID) String() string {
|
||||
return fmt.Sprintf(
|
||||
"%08x-%04x-%04x-%04x-%012x",
|
||||
g.Data1,
|
||||
g.Data2,
|
||||
g.Data3,
|
||||
g.Data4[:2],
|
||||
g.Data4[2:])
|
||||
}
|
||||
|
||||
// FromString parses a string containing a GUID and returns the GUID. The only
|
||||
// format currently supported is the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
|
||||
// format.
|
||||
func FromString(s string) (GUID, error) {
|
||||
if len(s) != 36 {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
|
||||
var g GUID
|
||||
|
||||
data1, err := strconv.ParseUint(s[0:8], 16, 32)
|
||||
if err != nil {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
g.Data1 = uint32(data1)
|
||||
|
||||
data2, err := strconv.ParseUint(s[9:13], 16, 16)
|
||||
if err != nil {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
g.Data2 = uint16(data2)
|
||||
|
||||
data3, err := strconv.ParseUint(s[14:18], 16, 16)
|
||||
if err != nil {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
g.Data3 = uint16(data3)
|
||||
|
||||
for i, x := range []int{19, 21, 24, 26, 28, 30, 32, 34} {
|
||||
v, err := strconv.ParseUint(s[x:x+2], 16, 8)
|
||||
if err != nil {
|
||||
return GUID{}, fmt.Errorf("invalid GUID %q", s)
|
||||
}
|
||||
g.Data4[i] = uint8(v)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (g *GUID) setVariant(v Variant) {
|
||||
d := g.Data4[0]
|
||||
switch v {
|
||||
case VariantNCS:
|
||||
d = (d & 0x7f)
|
||||
case VariantRFC4122:
|
||||
d = (d & 0x3f) | 0x80
|
||||
case VariantMicrosoft:
|
||||
d = (d & 0x1f) | 0xc0
|
||||
case VariantFuture:
|
||||
d = (d & 0x0f) | 0xe0
|
||||
case VariantUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid variant: %d", v))
|
||||
}
|
||||
g.Data4[0] = d
|
||||
}
|
||||
|
||||
// Variant returns the GUID variant, as defined in RFC 4122.
|
||||
func (g GUID) Variant() Variant {
|
||||
b := g.Data4[0]
|
||||
if b&0x80 == 0 {
|
||||
return VariantNCS
|
||||
} else if b&0xc0 == 0x80 {
|
||||
return VariantRFC4122
|
||||
} else if b&0xe0 == 0xc0 {
|
||||
return VariantMicrosoft
|
||||
} else if b&0xe0 == 0xe0 {
|
||||
return VariantFuture
|
||||
}
|
||||
return VariantUnknown
|
||||
}
|
||||
|
||||
func (g *GUID) setVersion(v Version) {
|
||||
g.Data3 = (g.Data3 & 0x0fff) | (uint16(v) << 12)
|
||||
}
|
||||
|
||||
// Version returns the GUID version, as defined in RFC 4122.
|
||||
func (g GUID) Version() Version {
|
||||
return Version((g.Data3 & 0xF000) >> 12)
|
||||
}
|
||||
|
||||
// MarshalText returns the textual representation of the GUID.
|
||||
func (g GUID) MarshalText() ([]byte, error) {
|
||||
return []byte(g.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText takes the textual representation of a GUID, and unmarhals it
|
||||
// into this GUID.
|
||||
func (g *GUID) UnmarshalText(text []byte) error {
|
||||
g2, err := FromString(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*g = g2
|
||||
return nil
|
||||
}
|
16
vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go
generated
vendored
Normal file
16
vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go
generated
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package guid
|
||||
|
||||
// GUID represents a GUID/UUID. It has the same structure as
|
||||
// golang.org/x/sys/windows.GUID so that it can be used with functions expecting
|
||||
// that type. It is defined as its own type as that is only available to builds
|
||||
// targeted at `windows`. The representation matches that used by native Windows
|
||||
// code.
|
||||
type GUID struct {
|
||||
Data1 uint32
|
||||
Data2 uint16
|
||||
Data3 uint16
|
||||
Data4 [8]byte
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user