Repository: arsham/blush
Branch: master
Commit: 9bdf3b205c87
Files: 50
Total size: 133.7 KB
Directory structure:
gitextract_ym2oh4bj/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── go.yml
├── .gitignore
├── .golangci.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── blush/
│ ├── benchmarks_test.go
│ ├── blush.go
│ ├── blush_example_test.go
│ ├── blush_test.go
│ ├── colour.go
│ ├── colour_test.go
│ ├── doc.go
│ ├── errors.go
│ ├── find.go
│ ├── find_test.go
│ ├── helper_test.go
│ └── testdata/
│ ├── d1/
│ │ ├── f1.txt
│ │ └── f2.txt
│ └── f1.txt
├── cmd/
│ ├── args.go
│ ├── args_test.go
│ ├── doc.go
│ ├── errors.go
│ ├── helper_test.go
│ ├── main.go
│ ├── main_example_test.go
│ ├── main_internal_test.go
│ ├── main_test.go
│ ├── signal.go
│ ├── signal_test.go
│ └── usage.go
├── configs/
│ └── coverage.sh
├── doc.go
├── go.mod
├── go.sum
├── internal/
│ ├── reader/
│ │ ├── helper_test.go
│ │ ├── reader.go
│ │ └── reader_test.go
│ └── tools/
│ ├── dir.go
│ ├── dir_example_test.go
│ ├── dir_test.go
│ ├── strings.go
│ └── strings_test.go
└── main.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Run with '...'
2. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Linux]
- Distribution: [e.g. Arch Linux]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/go.yml
================================================
name: Continues Integration
on:
push:
branches:
- master
- feature/*
- bugfix/*
pull_request:
branches:
- master
- feature/*
- bugfix/*
- refactor/*
- chore/*
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
- uses: actions/cache@v2
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Running Tests
run: make ci_tests
- name: Upload coverage report
uses: codecov/codecov-action@v2
with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
file: ./coverage.out
flags: unittests
name: codecov-umbrella
audit:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
- name: WriteGoList
run: go list -json -deps > go.list
- name: Nancy
uses: sonatype-nexus-community/nancy-github-action@main
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.45.2
args: --timeout 5m0s
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
vendor/
deploy/
tmp/
================================================
FILE: .golangci.yml
================================================
service:
golangci-lint-version: 1.45.x
linters-settings:
funlen:
lines: 100
statements: 50
cyclop:
skip-tests: true
max-complexity: 30
package-average: 5
gocyclo:
min-complexity: 15
gofumpt:
lang-version: "1.18"
goimports:
local-prefixes: github.com/golangci/golangci-lint
goconst:
min-len: 2
min-occurrences: 3
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
godot:
capital: true
gosimple:
go: "1.18"
checks: ["all"]
govet:
settings:
printf:
funcs:
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
disable-all: true
enable:
- assign
- atomic
- atomicalign
- bools
- buildtag
- cgocall
- composites
- copylocks
- deepequalerrors
- errorsas
- fieldalignment
- findcall
- framepointer
- httpresponse
- ifaceassert
- loopclosure
- lostcancel
- nilfunc
- printf
- reflectvaluecompare
- shift
- sigchanyzer
- sortslice
- stdmethods
- stringintconv
- structtag
- testinggoroutine
- tests
- unmarshal
- unreachable
- unsafeptr
- unusedresult
maligned:
suggest-new: true
misspell:
locale: UK
staticcheck:
go: "1.18"
checks: ["all"]
stylecheck:
go: "1.18"
checks: ["all"]
unparam:
check-exported: true
issues:
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
- path: _test\.go
linters:
- gosec # security check is not important in tests
- dupl # we usualy duplicate code in tests
- govet
- errcheck
- forcetypeassert
- godot
fix: true
exclude-use-default: false
run:
go: "1.18"
timeout: 5m
skip-dirs:
- tmp
- bin
- scripts
allow-parallel-runners: true
tests: true
linters:
enable:
- bidichk
- bodyclose
- cyclop
- deadcode
- depguard
- dogsled
- dupl
- durationcheck
- errcheck
- errname
- errorlint
- exportloopref
- forcetypeassert
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- gofmt
- gofumpt
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- nestif
- nilerr
- noctx
- prealloc
- predeclared
- promlinter
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- structcheck
- stylecheck
- tenv
- tparallel
- typecheck
- unconvert
- unparam
- unused
- varcheck
- wastedassign
- whitespace
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at arshamshirvani+blush@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
1. [Dependencies](#dependencies)
2. [Testing](#testing)
3. [Benchmarking](#benchmarking)
4. [Pull Requests](#pull-requests)
## Dependencies
Dependency management is done with [glide](https://github.com/Masterminds/glide).
You can install it by running:
```bash
$ go get -u github.com/Masterminds/glide
```
Then make sure you run `glide install` to install the current version of
dependencies.
If you need to add a new dependency to the library, before you commit your
changes make sure you run:
```bash
$ glide get <DEPENDENCY>
```
as described in glide's documentations.
## Testing
Before you make a pull request, make sure all tests are passing. Here is a handy
snippet using [reflex](https://github.com/cespare/reflex) to run all tests every
time you change your codes:
```bash
$ reflex -d none -r "\.go$" -- zsh -c "go test ./..."
```
If you need a separator between each run you can run:
```bash
$ reflex -d none -r "\.go$" -- zsh -c "go test ./... ; repeat 100 printf '#'"
```
It's also a good idea to run tests with `-race` flag after the final iteration
to make sure you are not introducing any race conditions:
```bash
$ go test -race ./...
```
## Benchmarking
Benchmarking is done by running:
```bash
$ go test ./... -bench=.
```
## Pull Requests
Make sure each commit introduces one change at a time. This means if your
changes are changing a signature of a function and also adds a new feature, they
should be in two distinct commit. Make a new branch for your changes and make
the pull request based on that branch.
You can sign your commits with this command:
```bash
$ git commit -S
```
Please avoid the `Signed-off by ...` clause (-s switch).
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Arsham Shirvani
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
help: ## Show help messages.
@grep -E '^[0-9a-zA-Z_-]+:(.*?## .*)?$$' $(MAKEFILE_LIST) | sed 's/^Makefile://' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
run="."
dir="./..."
short="-short"
flags=""
timeout=40s
build_tag=$(shell git describe --abbrev=0 --tags)
current_sha=$(shell git rev-parse --short HEAD)
TARGET=$(shell git describe --abbrev=0 --tags)
RELEADE_NAME=blush
DEPLOY_FOLDER=deploy
CHECKSUM_FILE=CHECKSUM
MAKEFLAGS += -j1
.PHONY: unit_test_watch
unit_test_watch: ## Run unit tests in watch mode. You can set: [run, timeout, short, dir, flags]. Example: make unit_test flags="-race".
@echo "running tests on $(run). waiting for changes..."
@-zsh -c "go mod tidy; go test -trimpath --timeout=$(timeout) $(short) $(dir) -run $(run) $(flags); repeat 100 printf '#'; echo"
@reflex -d none -r "(\.go$$)|(go.mod)|(\.sql$$)" -- zsh -c "go mod tidy; go test -trimpath --timeout=$(timeout) $(short) $(dir) -run $(run) $(flags); repeat 100 printf '#'"
.PHONY: lint
lint: ## Run linters.
go fmt ./...
go vet ./...
golangci-lint run ./...
.PHONY: ci_tests
ci_tests: ## Run tests for CI.
go test -trimpath --timeout=10m -failfast -v -race -covermode=atomic -coverprofile=coverage.out ./...
.PHONY: dependencies
dependencies: ## Install dependencies requried for development operations.
@go install github.com/cespare/reflex@latest
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.45.2
@go install github.com/psampaz/go-mod-outdated@latest
@go install github.com/jondot/goweight@latest
@go get -t -u golang.org/x/tools/cmd/cover
@go get -t -u github.com/sonatype-nexus-community/nancy@latest
@go get -u ./...
@go mod tidy
.PHONY: clean
clean: ## Clean test caches and tidy up modules.
@go clean -testcache
@go mod tidy
@rm -rf $(DEPLOY_FOLDER)
.PHONY: tmpfolder
tmpfolder: ## Create the temporary folder.
@mkdir -p $(DEPLOY_FOLDER)
@rm -rf $(DEPLOY_FOLDER)/$(CHECKSUM_FILE) 2> /dev/null
.PHONY: linux
linux: tmpfolder
linux: ## Build for GNU/Linux.
@GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$(build_tag) -X main.currentSha=$(current_sha)" -o $(DEPLOY_FOLDER)/$(RELEADE_NAME) .
@tar -czf $(DEPLOY_FOLDER)/blush_linux_$(TARGET).tar.gz $(DEPLOY_FOLDER)/$(RELEADE_NAME)
@cd $(DEPLOY_FOLDER) ; sha256sum blush_linux_$(TARGET).tar.gz >> $(CHECKSUM_FILE)
@echo "Linux target:" $(DEPLOY_FOLDER)/blush_linux_$(TARGET).tar.gz
@rm $(DEPLOY_FOLDER)/$(RELEADE_NAME)
.PHONY: darwin
darwin: tmpfolder
darwin: ## Build for Mac.
@GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$(build_tag) -X main.currentSha=$(current_sha)" -o $(DEPLOY_FOLDER)/$(RELEADE_NAME) .
@tar -czf $(DEPLOY_FOLDER)/blush_darwin_$(TARGET).tar.gz $(DEPLOY_FOLDER)/$(RELEADE_NAME)
@cd $(DEPLOY_FOLDER) ; sha256sum blush_darwin_$(TARGET).tar.gz >> $(CHECKSUM_FILE)
@echo "Darwin target:" $(DEPLOY_FOLDER)/blush_darwin_$(TARGET).tar.gz
@rm $(DEPLOY_FOLDER)/$(RELEADE_NAME)
.PHONY: windows
windows: tmpfolder
windows: ## Build for windoze.
@GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=$(build_tag) -X main.currentSha=$(current_sha)" -o $(DEPLOY_FOLDER)/$(RELEADE_NAME).exe .
@zip -r $(DEPLOY_FOLDER)/blush_windows_$(TARGET).zip $(DEPLOY_FOLDER)/$(RELEADE_NAME).exe
@cd $(DEPLOY_FOLDER) ; sha256sum blush_windows_$(TARGET).zip >> $(CHECKSUM_FILE)
@echo "Windows target:" $(DEPLOY_FOLDER)/blush_windows_$(TARGET).zip
@rm $(DEPLOY_FOLDER)/$(RELEADE_NAME).exe
.PHONY: release
release: ## Create releases for Linux, Mac, and windoze.
release: linux darwin windows
.PHONY: coverage
coverage: ## Show the test coverage on browser.
go test -covermode=count -coverprofile=coverage.out ./...
go tool cover -func=coverage.out | tail -n 1
go tool cover -html=coverage.out
.PHONY: audit
audit: ## Audit the code for updates, vulnerabilities and binary weight.
go list -u -m -json all | go-mod-outdated -update -direct
go list -json -m all | nancy sleuth
goweight | head -n 20
================================================
FILE: PULL_REQUEST_TEMPLATE.md
================================================
### Description
Please explain the changes you made here.
### Checklist
- [ ] Code compiles correctly
- [ ] Created tests which fail without the change (if possible)
- [ ] All tests passing
- [ ] Ran tests with -race flag
- [ ] Extended the README / documentation, if necessary
================================================
FILE: README.md
================================================
# Blush
[](https://pkg.go.dev/github.com/arsham/dbtools)

[](https://github.com/arsham/dbtools/actions/workflows/go.yml)
[](https://opensource.org/licenses/MIT)
[](https://codecov.io/gh/arsham/blush)
[](https://goreportcard.com/report/github.com/arsham/blush)
With Blush, you can highlight matches with any colours of your choice.

1. [Install](#install)
2. [Usage](#usage)
- [Note](#note)
- [Normal Mode](#normal-mode)
- [Dropping Unmatched](#dropping-unmatched)
- [Piping](#piping)
3. [Arguments](#arguments)
- [Notes](#notes)
4. [Colour Groups](#colour-groups)
5. [Colours](#colours)
6. [Complex Grep](#complex-grep)
7. [Suggestions](#suggestions)
8. [License](#license)
## Install
You can grab a binary from [releases](https://github.com/arsham/blush/releases)
page. If you prefer to install it manually you can get the code and install it
with the following command:
```bash
$ go install github.com/arsham/blush@latest
```
Make sure you have `go>=1.18` installed.
## Usage
Blush can read from a file or a pipe:
```bash
$ cat FILENAME | blush -b "print in blue" -g "in green" -g "another green"
$ cat FILENAME | blush "some text"
$ blush -b "print in blue" -g "in green" -g "another green" FILENAME
$ blush "some text" FILENAME
```
### Note
Although this program has a good performance, but performance is not the main
concern. There are other tools you should use if you are searching in large
files. Two examples:
- [Ripgrep](https://github.com/BurntSushi/ripgrep)
- [The Silver Searcher](https://github.com/ggreer/the_silver_searcher)
### Normal Mode
This method shows matches with the given input:
```bash
$ blush -b "first search" -g "second one" -g "and another one" files/paths
```
Any occurrence of `first search` will be in blue, `second one` and `and another one`
are in green.

### Dropping Unmatched
By default, unmatched lines are not dropped. But you can use the `-d` flag to
drop them:

## Arguments
| Argument | Shortcut | Notes |
| :------------ | :------- | :---------------------------------------------- |
| N/A | -i | Case insensitive matching. |
| N/A | -R | Recursive matching. |
| --no-filename | -h | Suppress the prefixing of file names on output. |
| --drop | -d | Drop unmatched lines |
File names or paths are matched from the end. Any argument that doesn't match
any files or paths are considered as regular expression. If regular expressions
are not followed by colouring arguments are coloured based on previously
provided colour:
```bash
$ blush -b match1 match2 FILENAME
```

### Notes
- If no colour is provided, blush will choose blue.
- If you only provide file/path, it will print them out without colouring.
- If the matcher contains only alphabets and numbers, a non-regular expression is applied to search.
## Colour Groups
You can provide a number for a colour argument to create a colour group:
```bash
$ blush -r1 match1 -r2 match2 -r1 match3 FILENAME
```

All matches will be shown as blue. But `match1` and `match3` will have a
different background colour than `match2`. This means the numbers will create
colour groups.
You also can provide a colour with a series of match requests:
```bash
$ blush -r match1 match3 -g match2 FILENAME
```
## Colours
You can choose a pre-defined colour, or pass it your own colour with a hash:
| Argument | Shortcut |
| :-------- | :------- |
| --red | -r |
| --green | -g |
| --blue | -b |
| --white | -w |
| --black | -bl |
| --yellow | -yl |
| --magenta | -mg |
| --cyan | -cy |
You can also pass an RGB colour. It can be in short form (--#1b2, -#1b2), or
long format (--#11bb22, -#11bb22).

## Complex Grep
You must put your complex grep into quotations:
```bash
$ blush -b "^age: [0-9]+" FILENAME
```

## Suggestions
This tool is made to make your experience in terminal a more pleasant. Please
feel free to make any suggestions or request features by creating an issue.
## License
Use of this source code is governed by the MIT License. License file can be
found in the [LICENSE](./LICENSE) file.
================================================
FILE: blush/benchmarks_test.go
================================================
package blush_test
import (
"bytes"
"errors"
"io"
"regexp"
"strings"
"testing"
"github.com/arsham/blush/blush"
)
func BenchmarkColourise(b *testing.B) {
var (
shortStr = "jNa8SZ1RPM"
longStr = strings.Repeat("ZL5B2kNexCcTPvf9 ", 50)
)
// nf = not found
// cl = coloured
// ln = long
bcs := []struct {
name string
input string
colour blush.Colour
}{
{"nc", shortStr, blush.NoColour},
{"fg", shortStr, blush.Red},
{"bg", shortStr, blush.Colour{Foreground: blush.NoRGB, Background: blush.FgRed}},
{"both cl", shortStr, blush.Colour{Foreground: blush.FgRed, Background: blush.FgRed}},
{"ln nc", longStr, blush.NoColour},
{"ln fg", longStr, blush.Colour{Foreground: blush.FgRed, Background: blush.NoRGB}},
{"ln bg", longStr, blush.Colour{Foreground: blush.NoRGB, Background: blush.FgRed}},
{"ln both cl", longStr, blush.Colour{Foreground: blush.FgRed, Background: blush.FgRed}},
}
for _, bc := range bcs {
b.Run(bc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
blush.Colourise(bc.input, bc.colour)
}
})
}
}
func BenchmarkNewLocator(b *testing.B) {
var l blush.Finder
bcs := []struct {
name string
input string
insensitive bool
}{
{"plain", "aaa", false},
{"asterisk", "*aaa", false},
{"i plain", "aaa", true},
{"i asterisk", "*aaa", true},
{"rx empty", "^$", false},
{"irx empty", "^$", true},
{"rx starts with", "^aaa", false},
{"irx starts with", "^aaa", true},
{"rx ends with", "aaa$", false},
{"irx ends with", "aaa$", true},
{"rx with star", "blah blah.*", false},
{"irx with star", "blah blah.*", true},
{"rx with curly brackets", "a{3}", false},
{"irx with curly brackets", "a{3}", true},
{"rx with brackets", "[ab]", false},
{"irx with brackets", "[ab]", true},
}
for _, bc := range bcs {
b.Run(bc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
l = blush.NewLocator("", "aaa", bc.insensitive)
_ = l
}
})
}
}
func BenchmarkFind(b *testing.B) {
var (
find = "FIND"
blob = "you should FIND this \n"
blobLong = strings.Repeat(blob, 50)
notFound = "YWzvnLKGyU "
notFoundLong = strings.Repeat(notFound, 50)
got string
ok bool
)
// nf = not found
// nc = no colour
// cl = coloured
// ln = long
bcs := []struct {
name string
l blush.Finder
input string
finds bool
}{
{"E nc", blush.NewExact(find, blush.NoColour), blob, true},
{"E cl", blush.NewExact(find, blush.Blue), blob, true},
{"E nf", blush.NewExact(find, blush.NoColour), notFound, false},
{"E nf cl", blush.NewExact(find, blush.Blue), notFound, false},
{"E ln nc", blush.NewExact(find, blush.NoColour), blobLong, true},
{"E ln cl", blush.NewExact(find, blush.Blue), blobLong, true},
{"E ln nf", blush.NewExact(find, blush.NoColour), notFoundLong, false},
{"E ln nf cl", blush.NewExact(find, blush.Blue), notFoundLong, false},
{"IE nc", blush.NewIexact(find, blush.NoColour), blob, true},
{"IE cl", blush.NewIexact(find, blush.Blue), blob, true},
{"IE nf", blush.NewIexact(find, blush.NoColour), notFound, false},
{"IE nf cl", blush.NewIexact(find, blush.Blue), notFound, false},
{"IE ln nc", blush.NewIexact(find, blush.NoColour), blobLong, true},
{"IE ln cl", blush.NewIexact(find, blush.Blue), blobLong, true},
{"IE ln nf", blush.NewIexact(find, blush.NoColour), notFoundLong, false},
{"IE ln nf cl", blush.NewIexact(find, blush.Blue), notFoundLong, false},
{"rx nc", blush.NewRx(regexp.MustCompile(find), blush.NoColour), blob, true},
{"rx cl", blush.NewRx(regexp.MustCompile(find), blush.Blue), blob, true},
{"rx nf", blush.NewRx(regexp.MustCompile(find), blush.NoColour), notFound, false},
{"rx nf cl", blush.NewRx(regexp.MustCompile(find), blush.Blue), notFound, false},
{"rx ln nc", blush.NewRx(regexp.MustCompile(find), blush.NoColour), blobLong, true},
{"rx ln cl", blush.NewRx(regexp.MustCompile(find), blush.Blue), blobLong, true},
{"rx ln nf", blush.NewRx(regexp.MustCompile(find), blush.NoColour), notFoundLong, false},
{"rx ln nf cl", blush.NewRx(regexp.MustCompile(find), blush.Blue), notFoundLong, false},
{"irx nc", blush.NewRx(regexp.MustCompile("(?i)"+find), blush.NoColour), blob, true},
{"irx cl", blush.NewRx(regexp.MustCompile("(?i)"+find), blush.Blue), blob, true},
{"irx nf", blush.NewRx(regexp.MustCompile("(?i)"+find), blush.NoColour), notFound, false},
{"irx nf cl", blush.NewRx(regexp.MustCompile("(?i)"+find), blush.Blue), notFound, false},
{"irx ln nc", blush.NewRx(regexp.MustCompile("(?i)"+find), blush.NoColour), blobLong, true},
{"irx ln cl", blush.NewRx(regexp.MustCompile("(?i)"+find), blush.Blue), blobLong, true},
{"irx ln nf", blush.NewRx(regexp.MustCompile("(?i)"+find), blush.NoColour), notFoundLong, false},
{"irx ln nf cl", blush.NewRx(regexp.MustCompile("(?i)"+find), blush.Blue), notFoundLong, false},
}
for _, bc := range bcs {
b.Run(bc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
got, ok = bc.l.Find(bc.input)
if ok != bc.finds {
b.Fail()
}
_ = got
}
})
}
}
// lnr = long reader
// ml = multi
// rds = readers
// mdl = middle
type benchCase struct {
name string
reader func() io.Reader
match []blush.Finder
length int
}
func BenchmarkBlush(b *testing.B) {
readLarge := func() io.Reader {
input := bytes.Repeat([]byte("one two three four\n"), 200)
v := make([]io.Reader, 100)
for i := 0; i < 100; i++ {
v[i] = bytes.NewBuffer(input)
}
return io.MultiReader(v...)
}
readMedium := func() io.Reader {
input := bytes.Repeat([]byte("one two three four\n"), 200)
return io.MultiReader(bytes.NewBuffer(input), bytes.NewBuffer(input))
}
multiReader100 := func() io.Reader {
input := []byte("one two three four\n")
v := make([]io.Reader, 100)
for i := 0; i < 100; i++ {
v[i] = bytes.NewBuffer(input)
}
return io.MultiReader(v...)
}
readMiddle := func() io.Reader {
p := bytes.Repeat([]byte("one two three four"), 100)
return bytes.NewBuffer(bytes.Join([][]byte{p, p}, []byte(" MIDDLE ")))
}
bcs := []benchCase{
{
"short-10",
func() io.Reader {
return bytes.NewBuffer([]byte("one two three four"))
},
[]blush.Finder{blush.NewExact("three", blush.Blue)},
10,
},
{
"short-1000",
func() io.Reader {
return bytes.NewBuffer([]byte("one two three four"))
},
[]blush.Finder{blush.NewExact("three", blush.Blue)},
1000,
},
{
"lnr-10",
func() io.Reader {
return bytes.NewBuffer(bytes.Repeat([]byte("one two three four"), 200))
},
[]blush.Finder{blush.NewExact("three", blush.Blue)},
10,
},
{
"lnr-1000",
func() io.Reader {
return bytes.NewBuffer(bytes.Repeat([]byte("one two three four"), 200))
},
[]blush.Finder{blush.NewExact("three", blush.Blue)},
1000,
},
{
"lnr ml lines-10",
func() io.Reader {
return bytes.NewBuffer(bytes.Repeat([]byte("one two three four\n"), 200))
},
[]blush.Finder{blush.NewExact("three", blush.Blue)},
10,
},
{
"lnr ml lines-1000",
func() io.Reader {
return bytes.NewBuffer(bytes.Repeat([]byte("one two three four\n"), 200))
},
[]blush.Finder{blush.NewExact("three", blush.Blue)},
1000,
},
{
"lnr in mdl-10",
readMiddle,
[]blush.Finder{blush.NewExact("MIDDLE", blush.Blue)},
10,
},
{
"lnr in mdl-1000",
readMiddle,
[]blush.Finder{blush.NewExact("MIDDLE", blush.Blue)},
1000,
},
{
"two rds-10",
func() io.Reader {
input := []byte("one two three four\n")
return io.MultiReader(bytes.NewBuffer(input), bytes.NewBuffer(input))
},
[]blush.Finder{blush.NewExact("three", blush.Blue)},
10,
},
{
"two rds-1000",
func() io.Reader {
input := []byte("one two three four\n")
return io.MultiReader(bytes.NewBuffer(input), bytes.NewBuffer(input))
},
[]blush.Finder{blush.NewExact("three", blush.Blue)},
1000,
},
{
"ln two rds-10",
readMedium,
[]blush.Finder{blush.NewExact("three", blush.Blue)},
10,
},
{
"ln two rds-1000",
readMedium,
[]blush.Finder{blush.NewExact("three", blush.Blue)},
1000,
},
{
"100 rds-10",
multiReader100,
[]blush.Finder{blush.NewExact("three", blush.Blue)},
10,
},
{
"100 rds-1000",
multiReader100,
[]blush.Finder{blush.NewExact("three", blush.Blue)},
1000,
},
{
"ln 100 rds-10",
readLarge,
[]blush.Finder{blush.NewExact("three", blush.Blue)},
10,
},
{
"ln 100 rds-1000",
readLarge,
[]blush.Finder{blush.NewExact("three", blush.Blue)},
1000,
},
}
for _, bc := range bcs {
b.Run("read_"+bc.name, func(b *testing.B) {
benchmarkRead(b, bc)
})
b.Run("writeTo_"+bc.name, func(b *testing.B) {
benchmarkWriteTo(b, bc)
})
}
}
func benchmarkRead(b *testing.B, bc benchCase) {
p := make([]byte, bc.length)
for i := 0; i < b.N; i++ {
input := io.NopCloser(bc.reader())
bl := &blush.Blush{
Finders: bc.match,
Reader: input,
}
for {
_, err := bl.Read(p)
if err != nil {
if !errors.Is(err, io.EOF) {
b.Errorf("err = %v", err)
}
break
}
}
}
}
func benchmarkWriteTo(b *testing.B, bc benchCase) {
for i := 0; i < b.N; i++ {
input := io.NopCloser(bc.reader())
bl := &blush.Blush{
Finders: bc.match,
Reader: input,
}
buf := new(bytes.Buffer)
n, err := io.Copy(buf, bl)
if n == 0 {
b.Errorf("b.Read(): n = 0, want some read")
}
if err != nil {
b.Error(err)
}
}
}
================================================
FILE: blush/blush.go
================================================
package blush
import (
"bufio"
"io"
"github.com/arsham/blush/internal/reader"
)
type mode int
const (
// Separator string between name of the reader and the contents.
Separator = ": "
// DefaultLineCache is minimum lines to cache.
DefaultLineCache = 50
// DefaultCharCache is minimum characters to cache for each line. This is in
// effect only if Read() function is used.
DefaultCharCache = 1000
readMode mode = iota
writeToMode
)
// Blush reads from reader and matches against all finders. If NoCut is true,
// any unmatched lines are printed as well. If WithFileName is true, blush will
// write the filename before it writes the output. Read and WriteTo will return
// ErrReadWriteMix if both Read and WriteTo are called on the same object. See
// package docs for more details.
// nolint:govet // we are expecting lots of these objects.
type Blush struct {
Finders []Finder
Reader io.ReadCloser
LineCache uint
CharCache uint
Drop bool // do not cut out non-matched lines.
WithFileName bool
closed bool
readLineCh chan []byte
readCh chan byte
mode mode
}
// Read creates a goroutine on first invocation to read from the underlying
// reader. It is considerably slower than WriteTo as it reads the bytes one by
// one in order to produce the results, therefore you should use WriteTo
// directly or use io.Copy() on blush.
func (b *Blush) Read(p []byte) (n int, err error) {
if b.closed {
return 0, ErrClosed
}
if b.mode == writeToMode {
return 0, ErrReadWriteMix
}
if b.mode != readMode {
err := b.setup(readMode)
if err != nil {
return 0, err
}
}
for n = 0; n < cap(p); n++ {
c, ok := <-b.readCh
if !ok {
return n, io.EOF
}
p[n] = c
}
return n, err
}
// WriteTo writes matches to w. It returns an error if the writer is nil or
// there are not paths defined or there is no files found in the Reader.
func (b *Blush) WriteTo(w io.Writer) (int64, error) {
if b.closed {
return 0, ErrClosed
}
if b.mode == readMode {
return 0, ErrReadWriteMix
}
if b.mode != writeToMode {
if err := b.setup(writeToMode); err != nil {
return 0, err
}
}
var total int
if w == nil {
return 0, ErrNoWriter
}
for line := range b.readLineCh {
if n, err := w.Write(line); err != nil {
return int64(n), err
}
total += len(line)
}
return int64(total), nil
}
func (b *Blush) setup(m mode) error {
if b.Reader == nil {
return reader.ErrNoReader
}
if len(b.Finders) < 1 {
return ErrNoFinder
}
b.mode = m
if b.LineCache == 0 {
b.LineCache = DefaultLineCache
}
if b.CharCache == 0 {
b.CharCache = DefaultCharCache
}
b.readLineCh = make(chan []byte, b.LineCache)
b.readCh = make(chan byte, b.CharCache)
go b.readLines()
if m == readMode {
go b.transfer()
}
return nil
}
func (b *Blush) decorate(input string) (string, bool) {
str, ok := lookInto(b.Finders, input)
if ok || !b.Drop {
var prefix string
if b.WithFileName {
prefix = fileName(b.Reader)
}
return prefix + str, true
}
return "", false
}
func (b *Blush) readLines() {
var (
ok bool
sc = bufio.NewReader(b.Reader)
)
for {
line, err := sc.ReadString('\n')
if line, ok = b.decorate(line); ok {
b.readLineCh <- []byte(line)
}
if err != nil {
break
}
}
close(b.readLineCh)
}
func (b *Blush) transfer() {
for line := range b.readLineCh {
for _, c := range line {
b.readCh <- c
}
}
close(b.readCh)
}
// Close closes the reader and returns whatever error it returns.
func (b *Blush) Close() error {
b.closed = true
return b.Reader.Close()
}
// lookInto returns a new decorated line if any of the finders decorate it, or
// the given line as it is.
func lookInto(f []Finder, line string) (string, bool) {
var found bool
for _, a := range f {
if s, ok := a.Find(line); ok {
line = s
found = true
}
}
return line, found
}
// fileName returns an empty string if it could not query the fileName from r.
func fileName(r io.Reader) string {
type namer interface {
FileName() string
}
if o, ok := r.(namer); ok {
return o.FileName() + Separator
}
return ""
}
================================================
FILE: blush/blush_example_test.go
================================================
package blush_test
import (
"bytes"
"fmt"
"io"
"github.com/arsham/blush/blush"
)
func ExampleBlush() {
f := blush.NewExact("sword", blush.Red)
r := bytes.NewBufferString("He who lives by the sword, will surely also die")
b := &blush.Blush{
Finders: []blush.Finder{f},
Reader: io.NopCloser(r),
}
b.Close()
}
func ExampleBlush_Read() {
var p []byte
f := blush.NewExact("sin", blush.Red)
r := bytes.NewBufferString("He who lives in sin, will surely live the lie")
b := &blush.Blush{
Finders: []blush.Finder{f},
Reader: io.NopCloser(r),
}
b.Read(p)
}
func ExampleBlush_Read_inDetails() {
f := blush.NewExact("sin", blush.Red)
r := bytes.NewBufferString("He who lives in sin, will surely live the lie")
b := &blush.Blush{
Finders: []blush.Finder{f},
Reader: io.NopCloser(r),
}
expect := fmt.Sprintf("He who lives in %s, will surely live the lie", f)
// you should account for the additional characters for colour formatting.
length := r.Len() - len("sin") + len(f.String())
p := make([]byte, length)
n, err := b.Read(p)
fmt.Println("n == len(p):", n == len(p))
fmt.Println("err:", err)
fmt.Println("p == expect:", string(p) == expect)
// by the way
fmt.Println(`f == "sin":`, f.String() == "sin")
// Output:
// n == len(p): true
// err: <nil>
// p == expect: true
// f == "sin": false
}
func ExampleBlush_WriteTo() {
f := blush.NewExact("victor", blush.Red)
r := bytes.NewBufferString("It is a shield of passion and strong will from this I am the victor instead of the kill\n")
b := &blush.Blush{
Finders: []blush.Finder{f},
Reader: io.NopCloser(r),
}
buf := new(bytes.Buffer)
n, err := b.WriteTo(buf)
expected := fmt.Sprintf("It is a shield of passion and strong will from this I am the %s instead of the kill\n", f)
fmt.Println("err:", err)
fmt.Println("n == len(expected):", int(n) == len(expected))
fmt.Println("buf.String() == expected:", buf.String() == expected)
// Output:
// err: <nil>
// n == len(expected): true
// buf.String() == expected: true
}
func ExampleBlush_WriteTo_copy() {
f := blush.NewExact("you feel", blush.Cyan)
r := bytes.NewBufferString("Savour what you feel and what you see\n")
b := &blush.Blush{
Finders: []blush.Finder{f},
Reader: io.NopCloser(r),
}
buf := new(bytes.Buffer)
n, err := io.Copy(buf, b)
expected := fmt.Sprintf("Savour what %s and what you see\n", f)
fmt.Println("err:", err)
fmt.Println("n == len(expected):", int(n) == len(expected))
fmt.Println("buf.String() == expected:", buf.String() == expected)
// Output:
// err: <nil>
// n == len(expected): true
// buf.String() == expected: true
}
func ExampleBlush_WriteTo_multiReader() {
mg := blush.NewExact("truth", blush.Magenta)
g := blush.NewExact("Life", blush.Green)
r1 := bytes.NewBufferString("Life is like a mystery with many clues, but with few answers\n")
r2 := bytes.NewBufferString("To tell us what it is that we can do to look for messages that keep us from the truth\n")
mr := io.MultiReader(r1, r2)
b := &blush.Blush{
Finders: []blush.Finder{mg, g},
Reader: io.NopCloser(mr),
}
buf := new(bytes.Buffer)
b.WriteTo(buf)
}
func ExampleBlush_WriteTo_multiReaderInDetails() {
mg := blush.NewExact("truth", blush.Magenta)
g := blush.NewExact("Life", blush.Green)
r1 := bytes.NewBufferString("Life is like a mystery with many clues, but with few answers\n")
r2 := bytes.NewBufferString("To tell us what it is that we can do to look for messages that keep us from the truth\n")
mr := io.MultiReader(r1, r2)
b := &blush.Blush{
Finders: []blush.Finder{mg, g},
Reader: io.NopCloser(mr),
}
buf := new(bytes.Buffer)
n, err := b.WriteTo(buf)
line1 := fmt.Sprintf("%s is like a mystery with many clues, but with few answers\n", g)
line2 := fmt.Sprintf("To tell us what it is that we can do to look for messages that keep us from the %s\n", mg)
expected := line1 + line2
fmt.Println("err:", err)
fmt.Println("n == len(expected):", int(n) == len(expected))
fmt.Println("buf.String() == expected:", buf.String() == expected)
// Output:
// err: <nil>
// n == len(expected): true
// buf.String() == expected: true
}
================================================
FILE: blush/blush_test.go
================================================
package blush_test
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
"testing"
"github.com/alecthomas/assert"
"github.com/arsham/blush/blush"
"github.com/arsham/blush/internal/reader"
)
func TestBlush(t *testing.T) {
t.Parallel()
t.Run("WriteTo", testBlushWriteTo)
t.Run("ClosesReader", testBlushClosesReader)
t.Run("Read", testBlushRead)
t.Run("PrintName", testBlushPrintName)
t.Run("StdinPrintName", testBlushStdinPrintName)
t.Run("PrintFilename", testBlushPrintFilename)
t.Run("ReadContiniously", testBlushReadContiniously)
t.Run("ReadMiddleOfMatch", testBlushReadMiddleOfMatch)
t.Run("ReadComplete", testBlushReadComplete)
t.Run("ReadPartComplete", testBlushReadPartComplete)
t.Run("PartPartOver", testBlushReadPartPartOver)
t.Run("ReadMultiLine", testBlushReadMultiLine)
t.Run("ReadWriteToMode", testBlushReadWriteToMode)
}
func testBlushWriteTo(t *testing.T) {
t.Parallel()
t.Run("Errors", testBlushWriteToErrors)
t.Run("NoMatch", testBlushWriteToNoMatch)
t.Run("Match", testBlushWriteToMatch)
t.Run("Colour", testBlushWriteToColour)
t.Run("ColourNoCutMode", testBlushWriteToColourNoCutMode)
t.Run("MultipleMatchInOneLine", testBlushWriteToMultipleMatchInOneLine)
}
func testBlushWriteToErrors(t *testing.T) {
t.Parallel()
w := &bytes.Buffer{}
e := errors.New("something")
nn := 10
bw := &badWriter{
writeFunc: func([]byte) (int, error) {
return nn, e
},
}
getReader := func() io.ReadCloser {
return io.NopCloser(bytes.NewBufferString("something"))
}
tcs := []struct {
name string
b *blush.Blush
writer io.Writer
wantN int
wantErr error
}{
{"no input", &blush.Blush{}, w, 0, reader.ErrNoReader},
{"no writer", &blush.Blush{Reader: getReader()}, nil, 0, blush.ErrNoWriter},
{"bad writer", &blush.Blush{Reader: getReader(), Drop: true}, bw, nn, e},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tc.b.Finders = []blush.Finder{blush.NewExact("", blush.NoColour)}
n, err := tc.b.WriteTo(tc.writer)
assert.Error(t, err)
assert.EqualValues(t, tc.wantN, n)
assert.EqualError(t, err, tc.wantErr.Error())
})
}
}
func testBlushWriteToNoMatch(t *testing.T) {
t.Parallel()
pwd, err := os.Getwd()
assert.NoError(t, err)
location := path.Join(pwd, "testdata")
r, err := reader.NewMultiReader(reader.WithPaths([]string{location}, true))
assert.NoError(t, err)
b := &blush.Blush{
Reader: r,
Finders: []blush.Finder{blush.NewExact("SHOULDNOTFINDTHISONE", blush.NoColour)},
Drop: true,
}
buf := &bytes.Buffer{}
n, err := b.WriteTo(buf)
assert.NoError(t, err)
assert.Zero(t, n)
assert.Zero(t, buf.Len())
}
func testBlushWriteToMatch(t *testing.T) {
t.Parallel()
t.Run("Drop", testBlushWriteToMatchDrop)
t.Run("Colour", testBlushWriteToMatchColour)
t.Run("CountColour", testBlushWriteToMatchCountColour)
}
func testBlushWriteToMatchDrop(t *testing.T) {
t.Parallel()
match := "TOKEN"
pwd, err := os.Getwd()
assert.NoError(t, err)
location := path.Join(pwd, "testdata")
r, err := reader.NewMultiReader(reader.WithPaths([]string{location}, true))
assert.NoError(t, err)
b := &blush.Blush{
Reader: r,
Finders: []blush.Finder{blush.NewExact(match, blush.NoColour)},
Drop: true,
}
buf := &bytes.Buffer{}
n, err := b.WriteTo(buf)
assert.NoError(t, err)
assert.NotZero(t, buf.Len())
assert.EqualValues(t, buf.Len(), n)
assert.Contains(t, buf.String(), match)
assert.NotContains(t, buf.String(), leaveMeHere)
}
func testBlushWriteToMatchColour(t *testing.T) {
t.Parallel()
match := blush.Colourise("TOKEN", blush.Blue)
pwd, err := os.Getwd()
assert.NoError(t, err)
location := path.Join(pwd, "testdata")
r, err := reader.NewMultiReader(reader.WithPaths([]string{location}, true))
assert.NoError(t, err)
b := &blush.Blush{
Reader: r,
Finders: []blush.Finder{blush.NewExact("TOKEN", blush.Blue)},
Drop: true,
}
buf := &bytes.Buffer{}
n, err := b.WriteTo(buf)
assert.NoError(t, err)
assert.NotZero(t, buf.Len())
assert.EqualValues(t, buf.Len(), n)
assert.Contains(t, buf.String(), match)
assert.NotContains(t, buf.String(), leaveMeHere)
}
func testBlushWriteToMatchCountColour(t *testing.T) {
t.Parallel()
pwd, err := os.Getwd()
assert.NoError(t, err)
tcs := []struct {
name string
recursive bool
count int
}{
{"ONE", false, 1},
{"ONE", true, 3 * 1},
{"TWO", false, 2},
{"TWO", true, 3 * 2},
{"THREE", false, 3},
{"THREE", true, 3 * 3},
{"FOUR", false, 4},
{"FOUR", true, 3 * 4},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
location := path.Join(pwd, "testdata")
r, err := reader.NewMultiReader(reader.WithPaths([]string{location}, tc.recursive))
assert.NoError(t, err)
match := blush.Colourise(tc.name, blush.Red)
b := &blush.Blush{
Reader: r,
Finders: []blush.Finder{blush.NewExact(tc.name, blush.Red)},
Drop: true,
}
buf := &bytes.Buffer{}
n, err := b.WriteTo(buf)
assert.NoError(t, err)
assert.EqualValues(t, buf.Len(), n)
count := strings.Count(buf.String(), match)
assert.EqualValues(t, tc.count, count)
assert.NotContains(t, buf.String(), leaveMeHere)
})
}
}
func testBlushWriteToColour(t *testing.T) {
t.Parallel()
two := blush.Colourise("TWO", blush.Magenta)
three := blush.Colourise("THREE", blush.Red)
pwd, err := os.Getwd()
assert.NoError(t, err)
location := path.Join(pwd, "testdata")
r, err := reader.NewMultiReader(reader.WithPaths([]string{location}, true))
assert.NoError(t, err)
b := &blush.Blush{
Reader: r,
Finders: []blush.Finder{
blush.NewExact("TWO", blush.Magenta),
blush.NewExact("THREE", blush.Red),
},
Drop: true,
}
buf := &bytes.Buffer{}
n, err := b.WriteTo(buf)
assert.NoError(t, err)
assert.NotZero(t, buf.Len())
assert.EqualValues(t, buf.Len(), n)
count := strings.Count(buf.String(), two)
assert.EqualValues(t, 2*3, count)
count = strings.Count(buf.String(), three)
assert.EqualValues(t, 3*3, count)
if strings.Contains(buf.String(), leaveMeHere) {
t.Errorf("didn't expect to see %s", leaveMeHere)
}
}
func testBlushWriteToColourNoCutMode(t *testing.T) {
t.Parallel()
two := blush.Colourise("TWO", blush.Magenta)
three := blush.Colourise("THREE", blush.Red)
pwd, err := os.Getwd()
assert.NoError(t, err)
location := path.Join(pwd, "testdata")
r, err := reader.NewMultiReader(reader.WithPaths([]string{location}, true))
assert.NoError(t, err)
b := &blush.Blush{
Reader: r,
Drop: false,
Finders: []blush.Finder{
blush.NewExact("TWO", blush.Magenta),
blush.NewExact("THREE", blush.Red),
},
}
buf := &bytes.Buffer{}
n, err := b.WriteTo(buf)
assert.NoError(t, err)
assert.NotZero(t, buf.Len())
assert.EqualValues(t, buf.Len(), n)
count := strings.Count(buf.String(), two)
assert.EqualValues(t, 2*3, count)
count = strings.Count(buf.String(), three)
assert.EqualValues(t, 3*3, count)
count = strings.Count(buf.String(), leaveMeHere)
assert.EqualValues(t, 1, count)
}
func testBlushWriteToMultipleMatchInOneLine(t *testing.T) {
t.Parallel()
line1 := "this is an example\n"
line2 := "someone should find this line\n"
input1 := bytes.NewBuffer([]byte(line1))
input2 := bytes.NewBuffer([]byte(line2))
r := io.NopCloser(io.MultiReader(input1, input2))
match := fmt.Sprintf(
"someone %s find %s line",
blush.Colourise("should", blush.Red),
blush.Colourise("this", blush.Magenta),
)
out := &bytes.Buffer{}
b := &blush.Blush{
Reader: r,
Finders: []blush.Finder{
blush.NewExact("this", blush.Magenta),
blush.NewExact("should", blush.Red),
},
}
b.WriteTo(out)
lines := strings.Split(out.String(), "\n")
example := lines[1]
if strings.Contains(example, "is an example") {
example = lines[0]
}
assert.EqualValues(t, match, example)
}
func testBlushClosesReader(t *testing.T) {
t.Parallel()
var called bool
input := bytes.NewBuffer([]byte("DwgQnpvro5bVvrRwBB"))
w := nopCloser{
Reader: input,
closeFunc: func() error {
called = true
return nil
},
}
b := &blush.Blush{
Reader: w,
}
err := b.Close()
assert.NoError(t, err)
assert.True(t, called, "didn't close the reader")
}
func testBlushRead(t *testing.T) {
t.Parallel()
t.Run("OneStream", testBlushReadOneStream)
t.Run("TwoStreams", testBlushReadTwoStreams)
t.Run("HalfWay", testBlushReadHalfWay)
t.Run("OnClosed", testBlushReadOnClosed)
t.Run("LongOneLineText", testBlushReadLongOneLineText)
}
func testBlushReadOneStream(t *testing.T) {
t.Parallel()
input := bytes.NewBuffer([]byte("one two three four"))
match := blush.NewExact("three", blush.Blue)
r := io.NopCloser(input)
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
defer b.Close()
emptyP := make([]byte, 10)
tcs := []struct {
name string
p []byte
wantErr error
wantLen int
wantP string
}{
{"one", make([]byte, len("one ")), nil, len("one "), "one "},
{"two", make([]byte, len("two ")), nil, len("two "), "two "},
{"three", make([]byte, len(match.String())), nil, len(match.String()), match.String()},
{"four", make([]byte, len(" four")), nil, len(" four"), " four"},
{"empty", emptyP, io.EOF, 0, string(emptyP)},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
n, err := b.Read(tc.p)
assert.True(t, errors.Is(err, tc.wantErr))
assert.EqualValues(t, tc.wantLen, n)
assert.EqualValues(t, tc.wantP, tc.p)
})
}
}
func testBlushReadTwoStreams(t *testing.T) {
t.Parallel()
b1 := []byte("one for all\n")
b2 := []byte("all for one\n")
input1 := bytes.NewBuffer(b1)
input2 := bytes.NewBuffer(b2)
match := blush.NewExact("one", blush.Blue)
r := io.NopCloser(io.MultiReader(input1, input2))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
defer b.Close()
buf := &bytes.Buffer{}
n, err := buf.ReadFrom(b)
assert.NoError(t, err)
expectLen := len(b1) + len(b2) - len("one")*2 + len(match.String())*2
assert.EqualValues(t, expectLen, n)
expectStr := fmt.Sprintf("%s%s",
strings.Replace(string(b1), "one", match.String(), 1),
strings.Replace(string(b2), "one", match.String(), 1),
)
assert.EqualValues(t, expectStr, buf.String())
}
func testBlushReadHalfWay(t *testing.T) {
t.Parallel()
b1 := []byte("one for all\n")
b2 := []byte("all for one\n")
input1 := bytes.NewBuffer(b1)
input2 := bytes.NewBuffer(b2)
match := blush.NewExact("one", blush.Blue)
r := io.NopCloser(io.MultiReader(input1, input2))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
p := make([]byte, len(b1))
_, err := b.Read(p)
assert.NoError(t, err)
n, err := b.Read(p)
assert.Len(t, b1, n)
assert.NoError(t, err)
}
func testBlushReadOnClosed(t *testing.T) {
t.Parallel()
b1 := []byte("one for all\n")
b2 := []byte("all for one\n")
input1 := bytes.NewBuffer(b1)
input2 := bytes.NewBuffer(b2)
match := blush.NewExact("one", blush.Blue)
r := io.NopCloser(io.MultiReader(input1, input2))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
p := make([]byte, len(b1))
_, err := b.Read(p)
assert.NoError(t, err)
err = b.Close()
assert.NoError(t, err)
n, err := b.Read(p)
assert.True(t, errors.Is(err, blush.ErrClosed))
assert.Zero(t, n)
}
func testBlushReadLongOneLineText(t *testing.T) {
t.Parallel()
head := strings.Repeat("a", 10000)
tail := strings.Repeat("b", 10000)
input := bytes.NewBuffer([]byte(head + " FINDME " + tail))
match := blush.NewExact("FINDME", blush.Blue)
r := io.NopCloser(input)
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
p := make([]byte, 20)
_, err := b.Read(p)
assert.NoError(t, err)
err = b.Close()
assert.NoError(t, err)
n, err := b.Read(p)
assert.True(t, errors.Is(err, blush.ErrClosed))
assert.Zero(t, n)
}
func testBlushPrintName(t *testing.T) {
t.Parallel()
line1 := "line one\n"
line2 := "line two\n"
r1 := io.NopCloser(bytes.NewBuffer([]byte(line1)))
r2 := io.NopCloser(bytes.NewBuffer([]byte(line2)))
name1 := "reader1"
name2 := "reader2"
r, err := reader.NewMultiReader(
reader.WithReader(name1, r1),
reader.WithReader(name2, r2),
)
assert.NoError(t, err)
b := blush.Blush{
Reader: r,
Finders: []blush.Finder{blush.NewExact("line", blush.NoColour)},
WithFileName: true,
Drop: true,
}
buf := &bytes.Buffer{}
n, err := b.WriteTo(buf)
assert.NoError(t, err)
total := len(line1+line2+name1+name2) + len(blush.Separator)*2
assert.EqualValues(t, total, n)
s := strings.Split(buf.String(), "\n")
assert.Contains(t, s[0], name1)
assert.Contains(t, s[1], name2)
}
// testing stdin should not print the name
func testBlushStdinPrintName(t *testing.T) {
t.Parallel()
input := "line one"
oldStdin := os.Stdin
f, err := ioutil.TempFile(t.TempDir(), "blush_stdin")
assert.NoError(t, err)
defer func() {
os.Stdin = oldStdin
}()
os.Stdin = f
f.WriteString(input)
f.Seek(0, 0)
b := blush.Blush{
Reader: f,
Finders: []blush.Finder{blush.NewExact("line", blush.NoColour)},
WithFileName: true,
}
buf := &bytes.Buffer{}
_, err = b.WriteTo(buf)
assert.NoError(t, err)
assert.EqualValues(t, input, buf.String())
assert.NotContains(t, buf.String(), f.Name())
}
func testBlushPrintFilename(t *testing.T) {
t.Parallel()
p := t.TempDir()
f1, err := ioutil.TempFile(p, "blush_name")
assert.NoError(t, err)
f2, err := ioutil.TempFile(p, "blush_name")
assert.NoError(t, err)
line1 := "line one\n"
line2 := "line two\n"
f1.WriteString(line1)
f2.WriteString(line2)
tcs := []struct {
name string
withFilename bool
wantLen int
wantFilenames bool
}{
{"with filename", true, len(line1+line2+f1.Name()+f2.Name()) + len(blush.Separator)*2, true},
{"without filename", false, len(line1 + line2), false},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
r, err := reader.NewMultiReader(
reader.WithPaths([]string{p}, false),
)
assert.NoError(t, err)
b := blush.Blush{
Reader: r,
Finders: []blush.Finder{blush.NewExact("line", blush.NoColour)},
WithFileName: tc.withFilename,
Drop: true,
}
buf := &bytes.Buffer{}
n, err := b.WriteTo(buf)
assert.NoError(t, err)
assert.EqualValues(t, tc.wantLen, n)
notStr := "not"
if tc.wantFilenames {
notStr = ""
}
if strings.Contains(buf.String(), f1.Name()) != tc.wantFilenames {
t.Errorf("want `%s` %s in `%s`", f1.Name(), notStr, buf.String())
}
if strings.Contains(buf.String(), f2.Name()) != tc.wantFilenames {
t.Errorf("want `%s` %s in `%s`", f2.Name(), notStr, buf.String())
}
})
}
}
// reading with a small byte slice until the read is done.
func testBlushReadContiniously(t *testing.T) {
t.Parallel()
var (
ret []byte
p = make([]byte, 2)
count int
input = "one two three four\nfive six three seven"
)
match := blush.NewExact("three", blush.Blue)
r := io.NopCloser(bytes.NewBufferString(input))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
for {
if count > len(input) { // more that required
t.Errorf("didn't finish after %d reads: len = %d", count, len(input))
break
}
count++
_, err := b.Read(p)
if errors.Is(err, io.EOF) {
ret = append(ret, p...)
break
}
assert.NoError(t, err)
ret = append(ret, p...)
}
if c := strings.Count(string(ret), "three"); c != 2 {
t.Errorf("count %s = %d, want %d", "three", c, 2)
}
for _, s := range []string{"one", "two", "three", "four", "five", "six", "seven"} {
assert.Contains(t, string(ret), s)
}
}
func testBlushReadMiddleOfMatch(t *testing.T) {
t.Parallel()
var (
search = "aa this aa"
match = blush.NewExact("this", blush.Blue)
p = make([]byte, (len(search)+len(match.String()))/2)
ret []byte
)
r := io.NopCloser(bytes.NewBufferString(search))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
for i := 0; i < 2; i++ {
_, err := b.Read(p)
if err != nil && !errors.Is(err, io.EOF) {
t.Fatal(err)
}
ret = append(ret, p...)
}
assert.Contains(t, string(ret), "this")
}
func testBlushReadComplete(t *testing.T) {
t.Parallel()
input := "123456789"
match := blush.NewExact("1", blush.NoColour)
r := io.NopCloser(bytes.NewBufferString(input))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
p := make([]byte, 10)
n, err := b.Read(p)
assert.Len(t, input, n)
assert.True(t, errors.Is(err, io.EOF))
assert.EqualValues(t, input, string(bytes.Trim(p, "\x00")))
p = make([]byte, 4)
n, err = b.Read(p)
assert.Zero(t, n)
assert.True(t, errors.Is(err, io.EOF))
assert.Empty(t, string(bytes.Trim(p, "\x00")))
}
func testBlushReadPartComplete(t *testing.T) {
t.Parallel()
input := "123456789"
match := blush.NewExact("1", blush.NoColour)
r := io.NopCloser(bytes.NewBufferString(input))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
p := make([]byte, 3)
n, err := b.Read(p)
assert.EqualValues(t, 3, n)
assert.NoError(t, err)
assert.EqualValues(t, string(bytes.Trim(p, "\x00")), "123")
p = make([]byte, 6)
n, err = b.Read(p)
assert.NoError(t, err)
assert.EqualValues(t, 6, n)
assert.EqualValues(t, string(bytes.Trim(p, "\x00")), "456789")
}
func testBlushReadPartPartOver(t *testing.T) {
t.Parallel()
input := "123456789"
match := blush.NewExact("1", blush.NoColour)
r := io.NopCloser(bytes.NewBufferString(input))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
p := make([]byte, 3)
n, err := b.Read(p)
assert.NoError(t, err)
assert.EqualValues(t, 3, n)
assert.EqualValues(t, string(bytes.Trim(p, "\x00")), "123")
p = make([]byte, 3)
n, err = b.Read(p)
assert.EqualValues(t, 3, n)
assert.NoError(t, err)
assert.EqualValues(t, string(bytes.Trim(p, "\x00")), "456")
p = make([]byte, 10)
n, err = b.Read(p)
assert.EqualValues(t, 3, n)
assert.True(t, errors.Is(err, io.EOF))
assert.EqualValues(t, string(bytes.Trim(p, "\x00")), "789")
}
func testBlushReadMultiLine(t *testing.T) {
t.Parallel()
input := "line1\nline2\nline3\nline4\n"
match := blush.NewExact("l", blush.NoColour)
r := io.NopCloser(bytes.NewBufferString(input))
b := &blush.Blush{
Finders: []blush.Finder{match},
Reader: r,
}
tcs := []struct {
name string
length int
want string
wantLen int
wantErr error
}{
{"line1", 5, "line1", 5, nil},
{"\nli", 3, "\nli", 3, nil},
{"ne2\nline", 8, "ne2\nline", 8, nil},
{"3\nline4", 7, "3\nline4", 7, nil},
{"\n", 1, "\n", 1, nil},
{"finish", 10, "", 0, io.EOF},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
p := make([]byte, tc.length)
n, err := b.Read(p)
assert.True(t, errors.Is(err, tc.wantErr))
assert.EqualValues(t, tc.wantLen, n)
assert.EqualValues(t, tc.want, string(bytes.Trim(p, "\x00")))
})
}
}
func testBlushReadWriteToMode(t *testing.T) {
t.Parallel()
p := make([]byte, 1)
r := io.NopCloser(bytes.NewBufferString("input"))
b := &blush.Blush{
Finders: []blush.Finder{blush.NewExact("", blush.NoColour)},
Reader: r,
}
_, err := b.Read(p)
assert.NoError(t, err)
_, err = b.WriteTo(&bytes.Buffer{})
assert.True(t, errors.Is(err, blush.ErrReadWriteMix))
b = &blush.Blush{
Finders: []blush.Finder{blush.NewExact("", blush.NoColour)},
Reader: r,
}
_, err = b.WriteTo(&bytes.Buffer{})
assert.NoError(t, err)
_, err = b.Read(p)
assert.True(t, errors.Is(err, blush.ErrReadWriteMix))
}
================================================
FILE: blush/colour.go
================================================
package blush
import (
"fmt"
"strconv"
"strings"
)
// BgLevel is the colour value of R, G, or B when the colour is shown in the
// background.
const BgLevel = 70
// These are colour settings. NoRGB results in no colouring in the terminal.
var (
NoRGB = RGB{-1, -1, -1}
FgRed = RGB{255, 0, 0}
FgBlue = RGB{0, 0, 255}
FgGreen = RGB{0, 255, 0}
FgBlack = RGB{0, 0, 0}
FgWhite = RGB{255, 255, 255}
FgCyan = RGB{0, 255, 255}
FgMagenta = RGB{255, 0, 255}
FgYellow = RGB{255, 255, 0}
BgRed = RGB{BgLevel, 0, 0}
BgBlue = RGB{0, 0, BgLevel}
BgGreen = RGB{0, BgLevel, 0}
BgBlack = RGB{0, 0, 0}
BgWhite = RGB{BgLevel, BgLevel, BgLevel}
BgCyan = RGB{0, BgLevel, BgLevel}
BgMagenta = RGB{BgLevel, 0, BgLevel}
BgYellow = RGB{BgLevel, BgLevel, 0}
)
// Some stock colours. There will be no colouring when NoColour is used.
var (
NoColour = Colour{NoRGB, NoRGB}
Red = Colour{FgRed, NoRGB}
Blue = Colour{FgBlue, NoRGB}
Green = Colour{FgGreen, NoRGB}
Black = Colour{FgBlack, NoRGB}
White = Colour{FgWhite, NoRGB}
Cyan = Colour{FgCyan, NoRGB}
Magenta = Colour{FgMagenta, NoRGB}
Yellow = Colour{FgYellow, NoRGB}
)
// DefaultColour is the default colour if no colour is set via arguments.
var DefaultColour = Blue
// RGB represents colours that can be printed in terminals. R, G and B should be
// between 0 and 255.
type RGB struct {
R, G, B int
}
// Colour is a pair of RGB colours for foreground and background.
type Colour struct {
Foreground RGB
Background RGB
}
// Colourise wraps the input between colours.
func Colourise(input string, c Colour) string {
if c.Background == NoRGB && c.Foreground == NoRGB {
return input
}
var fg, bg string
if c.Foreground != NoRGB {
fg = foreground(c.Foreground)
}
if c.Background != NoRGB {
bg = background(c.Background)
}
return fg + bg + input + unformat()
}
func foreground(c RGB) string {
return fmt.Sprintf("\033[38;5;%dm", colour(c.R, c.G, c.B))
}
func background(c RGB) string {
return fmt.Sprintf("\033[48;5;%dm", colour(c.R, c.G, c.B))
}
func unformat() string {
return "\033[0m"
}
func colour(red, green, blue int) int {
return 16 + baseColor(red, 36) + baseColor(green, 6) + baseColor(blue, 1)
}
func baseColor(value, factor int) int {
return int(6*float64(value)/256) * factor
}
func colorFromArg(colour string) Colour {
if strings.HasPrefix(colour, "#") {
return hexColour(colour)
}
if grouping.MatchString(colour) {
if c := colourGroup(colour); c != NoColour {
return c
}
}
return stockColour(colour)
}
func colourGroup(colour string) Colour {
g := grouping.FindStringSubmatch(colour)
group, err := strconv.Atoi(g[2])
if err != nil {
return NoColour
}
c := stockColour(g[1])
switch group % 8 {
case 0:
c.Background = BgRed
case 1:
c.Background = BgBlue
case 2:
c.Background = BgGreen
case 3:
c.Background = BgBlack
case 4:
c.Background = BgWhite
case 5:
c.Background = BgCyan
case 6:
c.Background = BgMagenta
case 7:
c.Background = BgYellow
}
return c
}
func stockColour(colour string) Colour {
c := DefaultColour
switch colour {
case "r", "red":
c = Red
case "b", "blue":
c = Blue
case "g", "green":
c = Green
case "bl", "black":
c = Black
case "w", "white":
c = White
case "cy", "cyan":
c = Cyan
case "mg", "magenta":
c = Magenta
case "yl", "yellow":
c = Yellow
case "no-colour", "no-color": // nolint:misspell // it's ok.
c = NoColour
}
return c
}
func hexColour(colour string) Colour {
var r, g, b int
colour = strings.TrimPrefix(colour, "#")
switch len(colour) {
case 3:
c := strings.Split(colour, "")
r = getInt(c[0] + c[0])
g = getInt(c[1] + c[1])
b = getInt(c[2] + c[2])
case 6:
c := strings.Split(colour, "")
r = getInt(c[0] + c[1])
g = getInt(c[2] + c[3])
b = getInt(c[4] + c[5])
default:
return DefaultColour
}
for _, n := range []int{r, g, b} {
if n < 0 {
return DefaultColour
}
}
return Colour{RGB{R: r, G: g, B: b}, NoRGB}
}
// getInt returns a number between 0-255 from a hex code. If the hex is not
// between 00 and ff, it returns -1.
func getInt(hex string) int {
d, err := strconv.ParseInt("0x"+hex, 0, 64)
if err != nil || d > 255 || d < 0 {
return -99
}
return int(d)
}
================================================
FILE: blush/colour_test.go
================================================
package blush_test
import (
"strings"
"testing"
"github.com/alecthomas/assert"
"github.com/arsham/blush/blush"
)
func TestColourise(t *testing.T) {
t.Parallel()
input := "nswkgjTusmxWoiZLhZOGBG"
got := blush.Colourise(input, blush.NoColour)
assert.Contains(t, input, got)
assert.NotContains(t, "[38;", got)
assert.NotContains(t, "[48;", got)
assert.NotContains(t, "\033[0m", got)
c := blush.Colour{
Foreground: blush.FgGreen,
Background: blush.FgRed,
}
got = blush.Colourise(input, c)
assert.Contains(t, got, input)
assert.Contains(t, got, "[38;")
assert.Contains(t, got, "[48;")
assert.EqualValues(t, 1, strings.Count(got, "\033[0m"))
c = blush.Colour{
Foreground: blush.NoRGB,
Background: blush.FgRed,
}
got = blush.Colourise(input, c)
assert.Contains(t, got, input)
assert.NotContains(t, got, "[38;")
assert.Contains(t, got, "[48;")
assert.EqualValues(t, 1, strings.Count(got, "\033[0m"))
}
================================================
FILE: blush/doc.go
================================================
// Package blush reads from a given io.Reader line by line and looks for
// patterns.
//
// Blush struct has a Reader property which can be Stdin in case of it being
// shell's pipe, or any type that implements io.ReadCloser. If NoCut is set to
// true, it will show all lines despite being not matched. You cannot call
// Read() and WriteTo() on the same object. Blush will return ErrReadWriteMix on
// the second consequent call. The first time Read/WriteTo is called, it will
// start a goroutine and reads up to LineCache lines from Reader. If the Read()
// is in use, it starts a goroutine that reads up to CharCache bytes from the
// line cache and fills up the given buffer.
//
// The hex number should be in 3 or 6 part format (#aaaaaa or #aaa) and each
// part will be translated to a number value between 0 and 255 when creating the
// Colour instance. If any of hex parts are not between 00 and ff, it creates
// the DefaultColour value.
//
// Important Notes
//
// The Read() method could be slow in case of huge inspections. It is
// recommended to avoid it and use WriteTo() instead; io.Copy() can take care of
// that for you.
//
// When WriteTo() is called with an unavailable or un-writeable writer, there
// will be no further checks until it tries to write into it. If the Write
// encounters any errors regarding writes, it will return the amount if writes
// and stops its search.
//
// There always will be a newline after each read.
package blush
================================================
FILE: blush/errors.go
================================================
package blush
import "errors"
var (
// ErrNoWriter is returned if a nil object is passed to the WriteTo method.
ErrNoWriter = errors.New("no writer defined")
// ErrNoFinder is returned if there is no finder passed to Blush.
ErrNoFinder = errors.New("no finders defined")
// ErrClosed is returned if the reader is closed and you try to read from
// it.
ErrClosed = errors.New("reader already closed")
// ErrReadWriteMix is returned when the Read and WriteTo are called on the
// same object.
ErrReadWriteMix = errors.New("you cannot mix Read and WriteTo calls")
)
================================================
FILE: blush/find.go
================================================
package blush
import (
"fmt"
"regexp"
"strings"
)
var (
isRegExp = regexp.MustCompile(`[\^\$.\{\}\[\]\*\?]`)
// This is used for matching colour groups (b1, etc.).
grouping = regexp.MustCompile(`^([[:alpha:]]+)(\d+)$`)
)
// Finder finds texts based on a plain text or regexp logic. If it doesn't find
// any match, it will return an empty string. It might decorate the match with a
// given instruction.
type Finder interface {
Find(string) (string, bool)
}
// NewLocator returns a Rx object if search is a valid regexp, otherwise it
// returns Exact or Iexact. If insensitive is true, the match will be case
// insensitive. The colour argument can be in short form (b) or long form
// (blue). If it cannot find the colour, it will fall-back to DefaultColour. The
// colour also can be in hex format, which should be started with a pound sign
// (#666).
func NewLocator(colour, search string, insensitive bool) Finder {
c := colorFromArg(colour)
if !isRegExp.MatchString(search) {
if insensitive {
return NewIexact(search, c)
}
return NewExact(search, c)
}
decore := fmt.Sprintf("(%s)", search)
if insensitive {
decore = fmt.Sprintf("(?i)%s", decore)
if o, err := regexp.Compile(decore); err == nil {
return NewRx(o, c)
}
return NewIexact(search, c)
}
if o, err := regexp.Compile(decore); err == nil {
return NewRx(o, c)
}
return NewExact(search, c)
}
// Exact looks for the exact word in the string.
type Exact struct {
s string
colour Colour
}
// NewExact returns a new instance of the Exact.
func NewExact(s string, c Colour) Exact {
return Exact{
s: s,
colour: c,
}
}
// Find looks for the exact string. Any strings it finds will be decorated with
// the given Colour.
func (e Exact) Find(input string) (string, bool) {
if strings.Contains(input, e.s) {
return e.colourise(input, e.colour), true
}
return "", false
}
func (e Exact) colourise(input string, c Colour) string {
if c == NoColour {
return input
}
return strings.ReplaceAll(input, e.s, Colourise(e.s, c))
}
// Colour returns the Colour property.
func (e Exact) Colour() Colour {
return e.colour
}
// String will returned the colourised contents.
func (e Exact) String() string {
return e.colourise(e.s, e.colour)
}
// Iexact is like Exact but case insensitive.
type Iexact struct {
s string
colour Colour
}
// NewIexact returns a new instance of the Iexact.
func NewIexact(s string, c Colour) Iexact {
return Iexact{
s: s,
colour: c,
}
}
// Find looks for the exact string. Any strings it finds will be decorated with
// the given Colour.
func (i Iexact) Find(input string) (string, bool) {
if strings.Contains(strings.ToLower(input), strings.ToLower(i.s)) {
return i.colourise(input, i.colour), true
}
return "", false
}
func (i Iexact) colourise(input string, c Colour) string {
if c == NoColour {
return input
}
index := strings.Index(strings.ToLower(input), strings.ToLower(i.s))
end := len(i.s) + index
match := input[index:end]
return strings.ReplaceAll(input, match, Colourise(match, c))
}
// Colour returns the Colour property.
func (i Iexact) Colour() Colour {
return i.colour
}
// String will returned the colourised contents.
func (i Iexact) String() string {
return i.colourise(i.s, i.colour)
}
// Rx is the regexp implementation of the Locator.
type Rx struct {
*regexp.Regexp
colour Colour
}
// NewRx returns a new instance of the Rx.
func NewRx(r *regexp.Regexp, c Colour) Rx {
return Rx{
Regexp: r,
colour: c,
}
}
// Find looks for the string matching `r` regular expression. Any strings it
// finds will be decorated with the given Colour.
func (r Rx) Find(input string) (string, bool) {
if r.MatchString(input) {
return r.colourise(input, r.colour), true
}
return "", false
}
func (r Rx) colourise(input string, c Colour) string {
if c == NoColour {
return input
}
return r.ReplaceAllString(input, Colourise("$1", c))
}
// Colour returns the Colour property.
func (r Rx) Colour() Colour {
return r.colour
}
================================================
FILE: blush/find_test.go
================================================
package blush_test
import (
"fmt"
"regexp"
"testing"
"github.com/alecthomas/assert"
"github.com/arsham/blush/blush"
)
type colourer interface {
Colour() blush.Colour
}
// nolint:misspell // it's ok.
func TestNewLocatorColours(t *testing.T) {
t.Parallel()
tcs := []struct {
name string
colour string
want blush.Colour
}{
{"default", "", blush.DefaultColour},
{"garbage", "sdsds", blush.DefaultColour},
{"blue", "blue", blush.Blue},
{"blue short", "b", blush.Blue},
{"red", "red", blush.Red},
{"red short", "r", blush.Red},
{"blue", "blue", blush.Blue},
{"blue short", "b", blush.Blue},
{"green", "green", blush.Green},
{"green short", "g", blush.Green},
{"black", "black", blush.Black},
{"black short", "bl", blush.Black},
{"white", "white", blush.White},
{"white short", "w", blush.White},
{"cyan", "cyan", blush.Cyan},
{"cyan short", "cy", blush.Cyan},
{"magenta", "magenta", blush.Magenta},
{"magenta short", "mg", blush.Magenta},
{"yellow", "yellow", blush.Yellow},
{"yellow short", "yl", blush.Yellow},
{"no colour", "no-colour", blush.NoColour},
{"no colour american", "no-color", blush.NoColour},
{"hash 000", "#000", blush.Colour{Foreground: blush.RGB{R: 0, G: 0, B: 0}, Background: blush.NoRGB}},
{"hash 666", "#666", blush.Colour{Foreground: blush.RGB{R: 102, G: 102, B: 102}, Background: blush.NoRGB}},
{"hash 000000", "#000000", blush.Colour{Foreground: blush.RGB{R: 0, G: 0, B: 0}, Background: blush.NoRGB}},
{"hash 666666", "#666666", blush.Colour{Foreground: blush.RGB{R: 102, G: 102, B: 102}, Background: blush.NoRGB}},
{"hash FFF", "#FFF", blush.Colour{Foreground: blush.RGB{R: 255, G: 255, B: 255}, Background: blush.NoRGB}},
{"hash fff", "#fff", blush.Colour{Foreground: blush.RGB{R: 255, G: 255, B: 255}, Background: blush.NoRGB}},
{"hash ffffff", "#ffffff", blush.Colour{Foreground: blush.RGB{R: 255, G: 255, B: 255}, Background: blush.NoRGB}},
{"hash ababAB", "#ababAB", blush.Colour{Foreground: blush.RGB{R: 171, G: 171, B: 171}, Background: blush.NoRGB}},
{"hash hhhhhh", "#hhhhhh", blush.DefaultColour},
{"hash aaaaaaa", "#aaaaaaa", blush.DefaultColour},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
l := blush.NewLocator(tc.colour, "aaa", false)
c, ok := l.(colourer)
assert.True(t, ok)
assert.Equal(t, tc.want, c.Colour())
})
}
}
func TestNewLocatorExact(t *testing.T) {
t.Parallel()
l := blush.NewLocator("", "aaa", false)
assert.IsType(t, blush.Exact{}, l)
l = blush.NewLocator("", "*aaa", false)
assert.IsType(t, blush.Exact{}, l)
}
func TestNewLocatorIexact(t *testing.T) {
t.Parallel()
l := blush.NewLocator("", "aaa", true)
assert.IsType(t, blush.Iexact{}, l)
l = blush.NewLocator("", "*aaa", true)
assert.IsType(t, blush.Iexact{}, l)
}
func TestNewLocatorRx(t *testing.T) {
t.Parallel()
tcs := []struct {
name string
input string
matches []string
}{
{"empty", "^$", []string{""}},
{"starts with", "^aaa", []string{"aaa", "aaa sss"}},
{"ends with", "aaa$", []string{"aaa", "sss aaa"}},
{"with star", "blah blah.*", []string{"blah blah", "aa blah blah aa"}},
{"with curly brackets", "a{3}", []string{"aaa", "aa aaa aa"}},
{"with brackets", "[ab]", []string{"kjhadf", "kjlrbrlkj", "sdbsdha"}},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
l := blush.NewLocator("", tc.input, false)
assert.IsType(t, blush.Rx{}, l)
l = blush.NewLocator("", tc.input, false)
assert.IsType(t, blush.Rx{}, l)
})
}
}
func TestNewLocatorRxColours(t *testing.T) {
t.Parallel()
rx := blush.NewLocator("b", "a{3}", false)
want := "this " + blush.Colourise("aaa", blush.Blue) + "meeting"
got, ok := rx.Find("this aaameeting")
assert.Equal(t, want, got)
assert.True(t, ok)
}
func TestExactNotFound(t *testing.T) {
t.Parallel()
l := blush.NewExact("nooooo", blush.NoColour)
got, ok := l.Find("yessss")
assert.Empty(t, got)
assert.False(t, ok)
}
func TestExactFind(t *testing.T) {
t.Parallel()
l := blush.NewExact("nooooo", blush.NoColour)
got, ok := l.Find("yessss")
assert.Empty(t, got)
assert.False(t, ok)
tcs := []struct {
name string
search string
colour blush.Colour
input string
want string
wantOk bool
}{
{"exact no colour", "aaa", blush.NoColour, "aaa", "aaa", true},
{"exact not found", "aaaa", blush.NoColour, "aaa", "", false},
{"some parts no colour", "aaa", blush.NoColour, "bb aaa bb", "bb aaa bb", true},
{"exact blue", "aaa", blush.Blue, "aaa", blush.Colourise("aaa", blush.Blue), true},
{"some parts blue", "aaa", blush.Blue, "bb aaa bb", "bb " + blush.Colourise("aaa", blush.Blue) + " bb", true},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
l := blush.NewExact(tc.search, tc.colour)
got, ok := l.Find(tc.input)
assert.Equal(t, tc.want, got)
assert.Equal(t, tc.wantOk, ok)
})
}
}
func TestRxNotFound(t *testing.T) {
t.Parallel()
l := blush.NewRx(regexp.MustCompile("no{5}"), blush.NoColour)
got, ok := l.Find("yessss")
assert.Empty(t, got)
assert.False(t, ok)
}
func TestRxFind(t *testing.T) {
t.Parallel()
l := blush.NewRx(regexp.MustCompile("no{5}"), blush.NoColour)
got, ok := l.Find("yessss")
assert.Empty(t, got)
assert.False(t, ok)
tcs := []struct {
name string
search string
colour blush.Colour
input string
want string
wantOk bool
}{
{"exact no colour", "(^aaa$)", blush.NoColour, "aaa", "aaa", true},
{"exact not found", "(^aa$)", blush.NoColour, "aaa", "", false},
{"some parts no colour", "(aaa)", blush.NoColour, "bb aaa bb", "bb aaa bb", true},
{"some parts not matched", "(Aaa)", blush.NoColour, "bb aaa bb", "", false},
{"exact blue", "(aaa)", blush.Blue, "aaa", blush.Colourise("aaa", blush.Blue), true},
{"some parts blue", "(aaa)", blush.Blue, "bb aaa bb", "bb " + blush.Colourise("aaa", blush.Blue) + " bb", true},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
l := blush.NewRx(regexp.MustCompile(tc.search), tc.colour)
got, ok := l.Find(tc.input)
assert.Equal(t, tc.want, got)
assert.Equal(t, tc.wantOk, ok)
assert.Equal(t, tc.colour, l.Colour())
})
}
}
func TestIexactNotFound(t *testing.T) {
t.Parallel()
l := blush.NewIexact("nooooo", blush.NoColour)
got, ok := l.Find("yessss")
assert.Empty(t, got)
assert.False(t, ok)
}
func TestIexact(t *testing.T) {
t.Parallel()
tcs := []struct {
name string
search string
colour blush.Colour
input string
want string
wantOk bool
}{
{"exact no colour", "aaa", blush.NoColour, "aaa", "aaa", true},
{"exact not found", "aaaa", blush.NoColour, "aaa", "", false},
{"i exact no colour", "AAA", blush.NoColour, "aaa", "aaa", true},
{"some parts no colour", "aaa", blush.NoColour, "bb aaa bb", "bb aaa bb", true},
{"i some parts no colour", "AAA", blush.NoColour, "bb aaa bb", "bb aaa bb", true},
{"exact blue", "aaa", blush.Blue, "aaa", blush.Colourise("aaa", blush.Blue), true},
{"i exact blue", "AAA", blush.Blue, "aaa", blush.Colourise("aaa", blush.Blue), true},
{"some parts blue", "aaa", blush.Blue, "bb aaa bb", "bb " + blush.Colourise("aaa", blush.Blue) + " bb", true},
{"i some parts blue", "AAA", blush.Blue, "bb aaa bb", "bb " + blush.Colourise("aaa", blush.Blue) + " bb", true},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
l := blush.NewIexact(tc.search, tc.colour)
got, ok := l.Find(tc.input)
assert.Equal(t, tc.want, got)
assert.Equal(t, tc.wantOk, ok)
assert.Equal(t, tc.colour, l.Colour())
})
}
}
func TestRxInsensitiveFind(t *testing.T) {
t.Parallel()
tcs := []struct {
name string
search string
colour string
input string
want string
wantOk bool
}{
{"exact no colour", "^AAA$", "no-colour", "aaa", "aaa", true},
{"exact not found", "^AA$", "no-colour", "aaa", "", false},
{"some words no colour", `AAA*`, "no-colour", "bb aaa bb", "bb aaa bb", true},
{"exact blue", "^AAA$", "b", "aaa", blush.Colourise("aaa", blush.Blue), true},
{"default colour", "^AAA$", "", "aaa", blush.Colourise("aaa", blush.DefaultColour), true},
{"some words blue", "AAA?", "b", "bb aaa bb", "bb " + blush.Colourise("aaa", blush.Blue) + " bb", true},
{"some words blue long", "AAA?", "blue", "bb aaa bb", "bb " + blush.Colourise("aaa", blush.Blue) + " bb", true},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
l := blush.NewLocator(tc.colour, tc.search, true)
assert.IsType(t, blush.Rx{}, l)
got, ok := l.Find(tc.input)
assert.Equal(t, tc.want, got)
assert.Equal(t, tc.wantOk, ok)
})
}
rx := blush.NewLocator("b", "A{3}", true)
want := "this " + blush.Colourise("aaa", blush.Blue) + "meeting"
got, ok := rx.Find("this aaameeting")
assert.Equal(t, want, got)
assert.True(t, ok)
}
func TestColourGroup(t *testing.T) {
t.Parallel()
for i := 0; i < 100; i++ {
l := blush.NewLocator(fmt.Sprintf("b%d", i), "aaa", false)
e, ok := l.(blush.Exact)
assert.True(t, ok)
assert.Equal(t, blush.FgBlue, e.Colour().Foreground)
assert.NotEqual(t, blush.NoRGB, e.Colour().Background)
assert.NotEqual(t, e.Colour().Background, e.Colour().Foreground)
}
}
func TestColourNewGroup(t *testing.T) {
t.Parallel()
l := blush.NewLocator("b1", "aaa", false)
assert.IsType(t, blush.Exact{}, l)
e := l.(blush.Exact)
c1 := e.Colour()
l = blush.NewLocator("b1", "aaa", false)
e = l.(blush.Exact)
assert.EqualValues(t, c1, e.Colour())
l = blush.NewLocator("b2", "aaa", false)
e = l.(blush.Exact)
assert.Equal(t, blush.FgBlue, e.Colour().Foreground)
assert.NotEqual(t, c1, e.Colour())
assert.NotEqual(t, c1.Background, e.Colour().Background)
}
================================================
FILE: blush/helper_test.go
================================================
package blush_test
import "io"
// this file contains helpers for all tests in this package.
// In the testdata folder, there are three files. In each file there are 1 ONE,
// 2 TWO, 3 THREE and 4 FOURs. There is a line containing `LEAVEMEHERE` which
// does not collide with any of these numbers.
var leaveMeHere = "LEAVEMEHERE"
type nopCloser struct {
io.Reader
closeFunc func() error
}
func (n nopCloser) Close() error { return n.closeFunc() }
type badWriter struct {
writeFunc func([]byte) (int, error)
}
func (b *badWriter) Write(p []byte) (int, error) { return b.writeFunc(p) }
================================================
FILE: blush/testdata/d1/f1.txt
================================================
d1-f1 IgjZC oSlYrhXTWOHEp TOKEN-A dAThrGHHYVfYQxyCDAVm ONE
d1-f1 jqwvUPYQhouXPCk hHcxLvqporTHREEpepOVOpy TOKEN-B IeCbNDvXef
d1-f1 MqZyWlTHREEIFkdnrLvxC lRzgfxESRWV TOKEN-A IHI TOKEN-B AJCXcRBoTPuvYJh
d1-f1 MqZyWIFkdnrLvxC lRzgfxTWOYESRWV TOKEN-A IHI TOKEN-B AJCXcRBoTPuvYJh
d1-f1 JVKePHMQqe FOUR voIDKRbIrRpwIX FdOrOtFGkIEIlrQjp seWBh FLyaHCgJNLapx
d1-f1 UfFesVYPkInOGiCCDCDQop BOroDtUFOUR IqPKvmgQh cqvrdXgJDV QHyz txkaVWY rWLxIkNjWguH
d1-f1 hAQtogSFYXhDeWjWY AJPLeQuXjSoTxTTrUYP YKjoFOUR Tj
d1-f1 TfCEmfp lMXZJdFmTHREEZPdS HlfI Ex excepteur laborum eu irure in amet exercitation
d1-f1 cupidatat minim anim ut occaecat culpa non nostrud incididunt proident idFOUR.
d1-f1 LEAVEMEHERE
================================================
FILE: blush/testdata/d1/f2.txt
================================================
d1-f2 lIgjZC oSlYrhXHEp TOKEN-A dAThrGHHYVfYQxyCDAVm
d1-f2 ljqwvUPYQhouXPCk hHcxLvqporpepOVOpy TOKEN-B IeCbNDvXef
d1-f2 lMqZyWlIFkdnrLvxC lRzgfxYESRWV TOKEN-A IHI TOKEN-B AJCXcRBoTPuvYJh
d1-f2 Et enim amet dolor ONE occaecat minim ut dolore magna pariatur dolor cupidatat est
d1-f2 ullamco eiusmod laborum do esseFOUR mollit in ex excepteur veniam amet eu excepteur
d1-f2 mollit cupidatat voluptate tempor qui exercitation mollit sit incididunt incididunt aliquip ad
d1-f2 labore proident magna duis aute dolor irure esse eu THREEsunt nisi anim id occaecat
d1-f2 ad qui in ea do aliquaTWO tempor tempor ut id eu do ad aliqua dolor ut non ex in excepteur do dolore
d1-f2 enim exercitation irure dolor ea deserunt deserunt in commodo laboris aliqua labore duis laboris cillum in nisi ut consectetur duis
d1-f2 commodo culpa cillum ea dolore ad cillum voluptate dolore pariatur consequat non
d1-f2 dolore cillum labore dolore sed adipisicing pariatur officia est laborum cillum dolore in exercitation
d1-f2 ut veniam dolor ut proident eu ea id cupidatat laboris idTHREE pariatur consequat ea exercitation in dolor pariatur in
d1-f2 elit consectetur pariatur labore nostrud commodo aliquip ullamco pariatur ea mollit quis deserunt consectetur reprehenderit adipisicing qui
d1-f2 ex ut officia nulla adipiTWOsicing anim aute adipisicing deserunt cillum est mollFOURit incididunt FOURveniam eu ut ut non
d1-f2 eiusmod eu reprehenderit iTHREErureFOUR mollit sit veniam dolore non dolor do ea
d1-f2 laborum culpa laboris occaecat ut fugiat cillum officia est nostrud in tempor
d1-f2 sed fugiat sed sunt excepteur officia labore velit laborum commodo aliqua elit
d1-f2 nisi mollit laboris laboris voluptate commodo id excepteur.
================================================
FILE: blush/testdata/f1.txt
================================================
f1 IgjZC oSlYrhXHEp TOKEN-A dAThrGHHYVfYQxyCDAVm
f1 jqwvUPYQhouXPCk hHcxLvqporpepOVOpy TOKEN-B IeCbNDvXef
f1 MqZyWlIFkdnrLvxC lRzgfxYESRWV TOKEN-A IHI TOKEN-B AJCXcRBoTPuvYJh
f1 Sed et sint est irTWOure irure commodo magna vol uptate ex excepteur
f1 Sed et sint est irure irure commodo magna vol THREE uptate ex excepteur
f1 ex sit magn FOURa quis nisi amet officia veniam aliquip do adipisicing eiusmod.
f1 Ut exercitationONE proident duis ea ut culpa ea occaecat ex cupidatat minim officia pariatur eu in
f1 cillum consequat incididunt deserunt magna in cupidatat eiusmod ut
f1 esse aTHREEmet esse nisi cupidatat velit id non excepteur aTHREEnim.
f1 Esse excepteur sed in ex ad in laboris nulla culpa exercitation qui culpa quis
f1 esse parFOURiatur ametTWO fugiat ut nullaFOUR consectetur officia id non
f1 deserunt reprehenderit sed eu sit ullamco ea aliquip.
f1 FOUR
================================================
FILE: cmd/args.go
================================================
package cmd
import (
"os"
"path/filepath"
"strings"
"github.com/arsham/blush/blush"
)
// Note that hasArgs, setFinders and setPaths methods of args are designed to
// shrink the input as they go. Therefore the order of calls matters in some
// cases.
type args struct {
paths []string
matches []string
remaining []string
finders []blush.Finder
cut bool
noFilename bool
recursive bool
insensitive bool
stdin bool
}
// nolint:misspell // it's ok.
func newArgs(input ...string) (*args, error) {
a := &args{
matches: make([]string, 0),
remaining: input,
}
if a.hasArgs("--help") {
return nil, errShowHelp
}
a.recursive = a.hasArgs("-R")
a.cut = a.hasArgs("-d", "--drop")
a.noFilename = a.hasArgs("-h", "--no-filename")
a.insensitive = a.hasArgs("-i")
if stat, _ := os.Stdin.Stat(); (stat.Mode() & os.ModeCharDevice) == 0 {
a.stdin = true
} else if err := a.setPaths(); err != nil {
return nil, err
}
a.setFinders()
return a, nil
}
// hasArgs removes any occurring `args` argument.
func (a *args) hasArgs(args ...string) (found bool) {
remains := a.remaining
LOOP:
for _, arg := range args {
for i, ar := range a.remaining {
if ar == arg {
remains = append(remains[:i], remains[i+1:]...)
found = true
if len(remains) == 0 {
break LOOP
}
}
}
}
a.remaining = remains
return found
}
// setPaths starts from the end of the slice and removes any paths/globs/files
// it finds and put them in the paths property.
func (a *args) setPaths() error {
var (
foundOne bool
counter int
p, ret []string
input = a.remaining
)
// going backwards from the end.
input = flip(input)
// I don't like this label, but if we replace the `switch` statement with a
// regular if-then-else clause, it gets ugly and doesn't show its
// intentions. Order of cases in this switch matters.
LOOP:
for i, t := range input {
t = strings.Trim(t, " ")
if t == "" || inStringSlice(t, p) {
continue
}
m, err := filepath.Glob(t)
if err != nil {
return err
}
switch {
case len(input) > i+1 && strings.HasPrefix(input[i+1], "-"):
// In this case, the previous input was a flag argument, therefore
// it might have been a colouring command. That is why we are
// ignoring this item.
ret = append(ret, input[i:]...)
break LOOP
case len(m) > 0:
foundOne = true
p = append(p, t)
counter++
case foundOne:
// there is already a pattern found so we stop here.
ret = append(ret, input[i:]...)
break LOOP
}
}
if !foundOne {
return ErrNoFilesFound
}
// to return back in the same order.
ret = flip(ret)
// to keep the original user's preference.
p = flip(p)
a.remaining = ret
a.paths = p
return nil
}
func (a *args) setFinders() {
var lastColour string
a.finders = make([]blush.Finder, 0)
for _, token := range a.remaining {
if strings.HasPrefix(token, "-") {
lastColour = strings.TrimLeft(token, "-")
continue
}
l := blush.NewLocator(lastColour, token, a.insensitive)
a.finders = append(a.finders, l)
}
}
func flip(s []string) []string {
ret := make([]string, len(s))
max := len(s) - 1
for i, v := range s {
j := max - i
ret[j] = v
}
return ret
}
func inStringSlice(s string, haystack []string) bool {
for _, a := range haystack {
if a == s {
return true
}
}
return false
}
================================================
FILE: cmd/args_test.go
================================================
package cmd
import (
"errors"
"fmt"
"io/ioutil"
"path"
"testing"
"github.com/alecthomas/assert"
)
func TestArgs(t *testing.T) {
tcs := []struct {
name string
input []string
wantErr error
cut bool
noFilename bool
recursive bool
insensitive bool
}{
{name: "help", input: []string{"--help"}, wantErr: errShowHelp},
{name: "drop", input: []string{"--drop"}, cut: true},
{name: "drop short", input: []string{"-d"}, cut: true},
{name: "no filename", input: []string{"-h"}, noFilename: true},
{name: "no filename long", input: []string{"--no-filename"}, noFilename: true},
{name: "recursive", input: []string{"-R"}, recursive: true},
{name: "ins", input: []string{"-i"}, insensitive: true},
{name: "ins rec", input: []string{"-i", "-R"}, insensitive: true, recursive: true},
{name: "rec ins", input: []string{"-R", "-i"}, insensitive: true, recursive: true},
{
name: "rec ins nofile", input: []string{"-R", "-i", "-h"},
insensitive: true, recursive: true, noFilename: true,
},
{
name: "nofile rec ins", input: []string{"-h", "-R", "-i"},
insensitive: true, recursive: true, noFilename: true,
},
{
name: "nofile rec ins drop", input: []string{"-h", "-R", "-i", "-d"},
insensitive: true, recursive: true, noFilename: true, cut: true,
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tc.input = append(tc.input, "*")
a, err := newArgs(tc.input...)
if tc.wantErr != nil {
assert.True(t, errors.Is(err, tc.wantErr))
assert.Nil(t, a)
return
}
assert.NoError(t, err)
assert.NotNil(t, a)
assert.Equal(t, tc.cut, a.cut)
assert.Equal(t, tc.noFilename, a.noFilename)
assert.Equal(t, tc.recursive, a.recursive)
assert.Equal(t, tc.insensitive, a.insensitive)
})
}
}
func TestArgsPipe(t *testing.T) {
getPipe(t)
a, err := newArgs("something")
assert.NoError(t, err)
assert.NotNil(t, a)
assert.True(t, a.stdin)
}
func TestArgsPaths(t *testing.T) {
dir := t.TempDir()
f1, err := ioutil.TempFile(dir, "main")
assert.NoError(t, err)
f2, err := ioutil.TempFile(dir, "main")
assert.NoError(t, err)
tcs := []struct {
name string
input []string
wantPaths []string
wantErr bool
}{
{"not found", []string{"nowhere"}, []string{}, true},
{"only a file", []string{f1.Name()}, []string{f1.Name()}, false},
{"two files", []string{f1.Name(), f2.Name()}, []string{f1.Name(), f2.Name()}, false},
{"arg between two files", []string{f1.Name(), "-a", f2.Name()}, []string{}, true},
{"prefix file", []string{"a", f1.Name()}, []string{f1.Name()}, false},
{"prefix arg file", []string{"-r", f1.Name()}, []string{}, true},
{"file matches but is an argument", []string{"-r", f1.Name(), f2.Name()}, []string{f2.Name()}, false},
{"star dir", []string{path.Join(dir, "*")}, []string{path.Join(dir, "*")}, false},
{"stared dir", []string{dir + "*"}, []string{dir + "*"}, false},
{
"many prefixes",
[]string{"--#7ff", "main", "-g", "package", "-r", "a", path.Join(dir, "*")},
[]string{path.Join(dir, "*")},
false,
},
{
"many prefixes star",
[]string{"--#7ff", "main", "-g", "package", "-r", "a", dir + "*"},
[]string{dir + "*"},
false,
},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
a, err := newArgs(tc.input...)
if tc.wantErr {
assert.Error(t, err)
return
}
assert.NotNil(t, a)
assert.True(t, stringSliceEq(a.paths, tc.wantPaths))
})
}
}
func TestArgsHasArgs(t *testing.T) {
tcs := []struct {
input []string
args []string
want []string
wantOk bool
}{
{[]string{}, []string{""}, []string{}, false},
{[]string{}, []string{"-a"}, []string{}, false},
{[]string{}, []string{"-a", "-a"}, []string{}, false},
{[]string{"a"}, []string{"-a"}, []string{"a"}, false},
{[]string{"a"}, []string{"-a", "-a"}, []string{"a"}, false},
{[]string{"-a"}, []string{"-a"}, []string{}, true},
{[]string{"-a"}, []string{"-a", "-a"}, []string{}, true},
{[]string{"-a", "-b"}, []string{"-a"}, []string{"-b"}, true},
{[]string{"-a", "-c", "-b"}, []string{"-c"}, []string{"-a", "-b"}, true},
{[]string{"-a", "-c", "-b"}, []string{"-d"}, []string{"-a", "-c", "-b"}, false},
}
for i, tc := range tcs {
tc := tc
t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) {
getPipe(t)
a, err := newArgs(tc.input...)
assert.NoError(t, err)
ok := a.hasArgs(tc.args...)
assert.True(t, stringSliceEq(a.remaining, tc.want))
assert.EqualValues(t, tc.wantOk, ok)
})
}
}
================================================
FILE: cmd/doc.go
================================================
// Package cmd bootstraps the application.
//
// Main() reads the provided arguments from the command line and creates a
// blush.Blush instance. If there is any error, it will terminate the
// application with os.Exit(1), otherwise it then uses io.Copy() to write to
// standard output and exits normally.
//
// GetBlush() returns an error if no arguments are provided or it can't find all
// the passed files. Files should be last arguments, otherwise they are counted
// as matching strings. If there is no file passed, the input should come in
// from Stdin as a pipe.
//
// hasArgs(args ...string) function looks for args in input and if it finds it,
// it removes it and put the rest in the remaining slice.
//
// Notes
//
// We are not using the usual flag package because it cannot handle variables in
// the args and continues grouping of passed arguments.
package cmd
================================================
FILE: cmd/errors.go
================================================
package cmd
import "errors"
var (
// ErrNoInput is returned when the application doesn't receive any files as the
// last arguments or a stream of inputs from shell's pipe.
ErrNoInput = errors.New("no input provided")
// ErrNoFilesFound is returned when the files pattern passed to the application
// doesn't match any existing files.
ErrNoFilesFound = errors.New("no files found")
)
================================================
FILE: cmd/helper_test.go
================================================
package cmd_test
import (
"bytes"
"io/ioutil"
"os"
"reflect"
"strings"
"testing"
"github.com/alecthomas/assert"
"github.com/arsham/blush/blush"
)
var leaveMeHere = "LEAVEMEHERE"
type stdFile struct {
f *os.File
}
func (s *stdFile) String() string {
s.f.Seek(0, 0)
buf := new(bytes.Buffer)
buf.ReadFrom(s.f)
return buf.String()
}
func (s *stdFile) Close() error {
return s.f.Close()
}
func newStdFile(t *testing.T, name string) *stdFile {
t.Helper()
f, err := ioutil.TempFile(t.TempDir(), name)
if err != nil {
t.Fatal(err)
}
sf := &stdFile{f}
t.Cleanup(func() {
f.Close()
})
return sf
}
func setup(t *testing.T, args string) (stdout, stderr *stdFile) {
t.Helper()
oldArgs := os.Args
oldStdout := os.Stdout
oldStderr := os.Stderr
stdout = newStdFile(t, "stdout")
stderr = newStdFile(t, "stderr")
os.Stdout = stdout.f
os.Stderr = stderr.f
os.Args = []string{"blush"}
if len(args) > 1 {
os.Args = append(os.Args, strings.Split(args, " ")...)
}
t.Cleanup(func() {
os.Args = oldArgs
os.Stdout = oldStdout
os.Stderr = oldStderr
})
return stdout, stderr
}
func getPipe(t *testing.T) *os.File {
t.Helper()
file, err := ioutil.TempFile(t.TempDir(), "blush_pipe")
assert.NoError(t, err)
name := file.Name()
file.Close()
file, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR, os.ModeCharDevice|os.ModeDevice)
assert.NoError(t, err)
return file
}
func argsEqual(a, b []blush.Finder) bool {
isIn := func(a blush.Finder, haystack []blush.Finder) bool {
for _, b := range haystack {
if reflect.DeepEqual(a, b) {
return true
}
}
return false
}
for _, item := range b {
if !isIn(item, a) {
return false
}
}
return true
}
================================================
FILE: cmd/main.go
================================================
package cmd
import (
"errors"
"fmt"
"io"
"log"
"os"
"github.com/arsham/blush/blush"
"github.com/arsham/blush/internal/reader"
)
// Main reads the provided arguments from the command line and creates a
// blush.Blush instance.
func Main() {
b, err := GetBlush(os.Args)
if errors.Is(err, errShowHelp) {
fmt.Println(Usage)
return
}
if err != nil {
log.Fatalf("%s\n%s", err, Help)
return // this return statement should be here to support tests.
}
defer func() {
if err := b.Close(); err != nil {
log.Fatal(err)
}
}()
sig := make(chan os.Signal, 1)
WaitForSignal(sig, os.Exit)
if _, err := io.Copy(os.Stdout, b); err != nil {
log.Print(err)
}
}
// GetBlush returns an error if no arguments are provided or it can't find all
// the passed files in the input.
//
// Note
//
// The first argument will be dropped as it will be the application's name.
func GetBlush(input []string) (*blush.Blush, error) {
var (
r io.ReadCloser = os.Stdin
a *args
err error
)
if len(input) == 1 {
return nil, ErrNoInput
}
if a, err = newArgs(input[1:]...); err != nil {
return nil, err
}
if !a.stdin {
r, err = reader.NewMultiReader(reader.WithPaths(a.paths, a.recursive))
if err != nil {
return nil, err
}
}
return &blush.Blush{
Finders: a.finders,
Reader: r,
Drop: a.cut,
WithFileName: !a.noFilename,
}, nil
}
================================================
FILE: cmd/main_example_test.go
================================================
package cmd_test
import (
"fmt"
"github.com/arsham/blush/blush"
"github.com/arsham/blush/cmd"
)
type colourer interface {
Colour() blush.Colour
}
func ExampleGetBlush_red() {
input := []string{"blush", "--red", "term", "/"}
b, err := cmd.GetBlush(input)
fmt.Println("err == nil:", err == nil)
fmt.Println("Finders count:", len(b.Finders))
c := b.Finders[0].(colourer)
fmt.Println("Is red:", c.Colour() == blush.Red)
// Output:
// err == nil: true
// Finders count: 1
// Is red: true
}
func ExampleGetBlush_multiColour() {
input := []string{"-b", "term1", "-g", "term2", "/"}
b, err := cmd.GetBlush(input)
c1 := b.Finders[0].(colourer)
c2 := b.Finders[1].(colourer)
fmt.Println("err == nil:", err == nil)
fmt.Println("Finders count:", len(b.Finders))
fmt.Println("Is blue:", c1.Colour() == blush.Blue)
fmt.Println("Is green:", c2.Colour() == blush.Green)
// Output:
// err == nil: true
// Finders count: 2
// Is blue: true
// Is green: true
}
================================================
FILE: cmd/main_internal_test.go
================================================
package cmd
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/alecthomas/assert"
)
func getPipe(t *testing.T) *os.File {
t.Helper()
oldStdin := os.Stdin
file, err := ioutil.TempFile(t.TempDir(), "blush_pipe")
assert.NoError(t, err)
name := file.Name()
file.Close()
file, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR, os.ModeCharDevice|os.ModeDevice)
assert.NoError(t, err)
os.Stdin = file
t.Cleanup(func() {
os.Stdin = oldStdin
})
return file
}
func stringSliceEq(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
func TestFiles(t *testing.T) {
dir := t.TempDir()
f1, err := ioutil.TempFile(dir, "main")
if err != nil {
t.Fatal(err)
}
f2, err := ioutil.TempFile(dir, "main")
if err != nil {
t.Fatal(err)
}
tcs := []struct {
name string
input []string
wantRemaining []string
wantP []string
wantErr bool
}{
{"not found", []string{"nowhere"}, []string{}, []string{}, true},
{"only a file", []string{f1.Name()}, []string{}, []string{f1.Name()}, false},
{"two files", []string{f1.Name(), f2.Name()}, []string{}, []string{f1.Name(), f2.Name()}, false},
{"arg between two files", []string{f1.Name(), "-a", f2.Name()}, []string{f1.Name(), "-a", f2.Name()}, []string{}, true},
{"prefix file", []string{"a", f1.Name()}, []string{"a"}, []string{f1.Name()}, false},
{"prefix arg file", []string{"-r", f1.Name()}, []string{"-r", f1.Name()}, []string{}, true},
{"file matches but is an argument", []string{"-r", f1.Name(), f2.Name()}, []string{"-r", f1.Name()}, []string{f2.Name()}, false},
{
"star dir",
[]string{path.Join(dir, "*")},
[]string{},
[]string{path.Join(dir, "*")},
false,
},
{
"stared dir",
[]string{dir + "*"},
[]string{},
[]string{dir + "*"},
false,
},
{
"many prefixes",
[]string{"--#7ff", "main", "-g", "package", "-r", "a", path.Join(dir, "*")},
[]string{"--#7ff", "main", "-g", "package", "-r", "a"},
[]string{path.Join(dir, "*")},
false,
},
{
"many prefixes star",
[]string{"--#7ff", "main", "-g", "package", "-r", "a", dir + "*"},
[]string{"--#7ff", "main", "-g", "package", "-r", "a"},
[]string{dir + "*"},
false,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
a, err := newArgs(tc.input...)
if tc.wantErr {
if err == nil {
t.Error("err = nil, want error")
}
return
}
if !stringSliceEq(a.remaining, tc.wantRemaining) {
t.Errorf("files(%v): a.remaining = %v, want %v", tc.input, a.remaining, tc.wantRemaining)
}
if !stringSliceEq(a.paths, tc.wantP) {
t.Errorf("files(%v): a.paths = %v, want %v", tc.input, a.paths, tc.wantP)
}
})
}
}
================================================
FILE: cmd/main_test.go
================================================
package cmd_test
import (
"fmt"
"os"
"path"
"testing"
"github.com/alecthomas/assert"
"github.com/arsham/blush/blush"
"github.com/arsham/blush/cmd"
)
func TestMainHelp(t *testing.T) {
stdout, stderr := setup(t, "--help")
cmd.Main()
assert.Empty(t, stderr.String())
assert.Contains(t, stdout.String(), cmd.Usage)
}
func TestPipeInput(t *testing.T) {
oldStdin := os.Stdin
stdout, stderr := setup(t, "findme")
defer func() {
os.Stdin = oldStdin
}()
file := getPipe(t)
file.WriteString("you can findme here")
os.Stdin = file
file.Seek(0, 0)
cmd.Main()
assert.Empty(t, stderr.String())
assert.Contains(t, stdout.String(), "findme")
}
func TestMainMatch(t *testing.T) {
match := blush.Colourise("TOKEN", blush.Blue)
pwd, err := os.Getwd()
assert.NoError(t, err)
location := path.Join(pwd, "../blush/testdata")
tcs := []struct {
name string
input string
}{
{"exact sensitive", "-b TOKEN"},
{"exact insensitive", "-i -b TOKEN"},
{"regexp sensitive", "-b TOK[EN]{2}"},
{"regexp insensitive", "-i -b tok[en]{2}"},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
stdout, stderr := setup(t, fmt.Sprintf("%s %s", tc.input, location))
cmd.Main()
assert.Empty(t, stderr.String())
assert.NotEmpty(t, stdout.String())
assert.Contains(t, stdout.String(), match)
})
}
}
func TestMainMatchCut(t *testing.T) {
matches := []string{"TOKEN", "ONE", "TWO", "THREE", "FOUR"}
pwd, err := os.Getwd()
assert.NoError(t, err)
location := path.Join(pwd, "../blush/testdata")
stdout, stderr := setup(t, fmt.Sprintf("-b %s %s", leaveMeHere, location))
cmd.Main()
assert.Empty(t, stderr.String())
assert.NotEmpty(t, stdout.String())
for _, s := range matches {
assert.Contains(t, stdout.String(), s)
}
}
func TestNoFiles(t *testing.T) {
fileName := "test"
b, err := cmd.GetBlush([]string{fileName})
assert.Error(t, err)
assert.Nil(t, b)
}
func TestColourArgs(t *testing.T) {
aaa := "aaa"
bbb := "bbb"
tcs := []struct {
name string
input []string
want []blush.Finder
}{
{"empty", []string{"/"}, []blush.Finder{}},
{"1-default colour", []string{"aaa", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.DefaultColour),
}},
{"1-no colour", []string{"--no-colour", "aaa", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.NoColour),
}},
{"1-no colour american", []string{"--no-colour", "aaa", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.NoColour),
}},
{"1-colour", []string{"-b", "aaa", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Blue),
}},
{"1-colour long", []string{"--blue", "aaa", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Blue),
}},
{"2-default colour", []string{"aaa", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.DefaultColour),
blush.NewExact(bbb, blush.DefaultColour),
}},
{"2-no colour", []string{"--no-colour", "aaa", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.NoColour),
blush.NewExact(bbb, blush.NoColour),
}},
{"2-no colour american", []string{"--no-colour", "aaa", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.NoColour),
blush.NewExact(bbb, blush.NoColour),
}},
{"2-colour", []string{"-b", "aaa", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Blue),
blush.NewExact(bbb, blush.Blue),
}},
{"2-two colours", []string{"-b", "aaa", "-g", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Blue),
blush.NewExact(bbb, blush.Green),
}},
{"red", []string{"-r", "aaa", "--red", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Red),
blush.NewExact(bbb, blush.Red),
}},
{"green", []string{"-g", "aaa", "--green", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Green),
blush.NewExact(bbb, blush.Green),
}},
{"blue", []string{"-b", "aaa", "--blue", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Blue),
blush.NewExact(bbb, blush.Blue),
}},
{"white", []string{"-w", "aaa", "--white", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.White),
blush.NewExact(bbb, blush.White),
}},
{"black", []string{"-bl", "aaa", "--black", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Black),
blush.NewExact(bbb, blush.Black),
}},
{"cyan", []string{"-cy", "aaa", "--cyan", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Cyan),
blush.NewExact(bbb, blush.Cyan),
}},
{"magenta", []string{"-mg", "aaa", "--magenta", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Magenta),
blush.NewExact(bbb, blush.Magenta),
}},
{"yellow", []string{"-yl", "aaa", "--yellow", "bbb", "/"}, []blush.Finder{
blush.NewExact(aaa, blush.Yellow),
blush.NewExact(bbb, blush.Yellow),
}},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
input := append([]string{"blush"}, tc.input...)
b, err := cmd.GetBlush(input)
assert.NoError(t, err)
assert.NotNil(t, b)
assert.True(t, argsEqual(b.Finders, tc.want))
})
}
}
func TestWithFilename(t *testing.T) {
tcs := []struct {
name string
input []string
want bool
}{
{"with filename", []string{"blush", "/"}, true},
{"no filename", []string{"blush", "-h", "aaa", "/"}, false},
{"no filename long", []string{"blush", "--no-filename", "aaa", "/"}, false},
{"no filename both", []string{"blush", "-h", "--no-filename", "aaa", "/"}, false},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
b, err := cmd.GetBlush(tc.input)
assert.NoError(t, err)
assert.NotNil(t, b)
assert.Equal(t, tc.want, b.WithFileName)
})
}
}
================================================
FILE: cmd/signal.go
================================================
package cmd
import (
"os"
"os/signal"
"syscall"
)
// WaitForSignal calls exit with code 130 if receives an SIGINT or SIGTERM, 0 if
// SIGPIPE, and 1 otherwise.
func WaitForSignal(sig chan os.Signal, exit func(int)) {
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGPIPE)
go func() {
s := <-sig
switch s {
case syscall.SIGINT, syscall.SIGTERM:
exit(130) // Ctrl+c
case syscall.SIGPIPE:
exit(0)
}
exit(1)
}()
}
================================================
FILE: cmd/signal_test.go
================================================
package cmd_test
import (
"os"
"syscall"
"testing"
"time"
"github.com/alecthomas/assert"
"github.com/arsham/blush/cmd"
)
func TestCaptureSignals(t *testing.T) {
tcs := []struct {
name string
signal os.Signal
code int
}{
{"SIGINT", syscall.SIGINT, 130},
{"SIGTERM", syscall.SIGTERM, 130},
{"SIGPIPE", syscall.SIGPIPE, 0},
{"other", syscall.Signal(-1), 1},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
sig := make(chan os.Signal, 1)
code := make(chan int)
exit := func(c int) {
code <- c
}
cmd.WaitForSignal(sig, exit)
sig <- tc.signal
select {
case code := <-code:
assert.Equal(t, tc.code, code)
case <-time.After(500 * time.Millisecond):
t.Error("exit function wasn't called")
}
})
}
}
================================================
FILE: cmd/usage.go
================================================
package cmd
import "errors"
// These variables are used for showing help messages on command line.
var (
errShowHelp = errors.New("show errors")
Help = "Usage: blush [OPTION]... PATTERN [FILE]...\nTry 'blush --help' for more information."
// nolint:misspell // it's ok.
Usage = `Usage: blush [OPTION]... PATTERN [FILE]...
Colours:
-r, --red Match decorated with red colour. See Stock Colours section.
-r[G], --red[G] Matches are grouped with the group number.
Example: blush -b1 match filename
-#RGB, --#RGB Use user defined colour schemas.
Example: blush -#1eF match filename
-#RRGGBB, --#RRGGBB Same as -#RGB/--#RGB.
Pattern:
You can use simple pattern or regexp. If your pattern expands between
multiple words or has space in between, you should put them in quotations.
Stock Colours:
-r, --red
-g, --green
-b, --blue
-w, --white
-bl, --black
-yl, --yellow
-mg, --magenta
-cy, --cyan
Control arguments:
-d, --drop Drop unmatched lines.
-i Case insensitive match.
-h, --no-filename Suppress the prefixing of file names on output.
Multi match colouring:
blush -b match1 [match2]...: will colourise all matches with the same colour.
Using pipes:
cat FILE | blush -b match [-g match]...
`
)
================================================
FILE: configs/coverage.sh
================================================
#!/usr/bin/env bash
set -e
echo "" > coverage.txt
for d in $(go list ./... | grep -v vendor); do
go test -coverprofile=profile.out -covermode=atomic $d
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done
sed '/mode: atomic/d' ./coverage.txt > ./codacy_coverage.txt
================================================
FILE: doc.go
================================================
// Package blush searches for matches with colours.
//
// Usage
//
// $ blush -b "first search" -g "second one" -g "and another one" files/paths
// Any occurrence of "first search" will be in blue, "second one" and "and
// another one" are in green.
//
// Colouring Method
//
// With this method all texts are shown, but the matching words are coloured.
// You can activate this mode by providing "--colour" or "-C" argument.
//
// Piping
//
// Blush can also read from a pipe:
// $ cat FILENAME | blush -b "print in blue" -g "in green" -g "another green"
// $ cat FILENAME | blush "some text"
//
// Arguments
//
// +---------------+----------+------------------------------------------------+
// | Argument | Shortcut | Notes |
// +---------------+----------+------------------------------------------------+
// | --colour | -C | Colour, don't drop anything. |
// | N/A | -i | Case insensitive matching |
// | N/A | -R | Recursive |
// | --no-colour | N/A | Doesn't colourize matches. |
// | --no-filename | -h | Suppress the prefixing of file names on output |
// +---------------+----------+------------------------------------------------+
//
// File names or paths are matched from the end. Any argument that doesn't match
// any files or paths are considered as regular expression. If regular
// expressions are not followed by colouring arguments are coloured based on
// previously provided colour:
//
// $ blush -b match1 match3 FILENAME
//
// Please Note
//
// If no colour is provided, blush will choose blue. If you only provide
// file/path, it will print them out without colouring. If the matcher contains
// only alphabets and numbers, a non-regular expression is applied to search.
//
// Colour Groups
//
// You can provide a number for a colour argument to create a colour group:
//
// $ blush -b1 match1 -b2 match2 -b1 match3 FILENAME
//
// All matches will be shown as blue. But `match1` and `match3` will have a
// different background colour than `match2`. This means the numbers will create
// colour groups.
//
// You also can provide a colour with a series of grep requests:
//
// $ blush -b match1 match3 -g match2 FILENAME
//
// Colours
//
// You can choose a pre-defined colour, or pass it your own colour with a hash:
//
// +-----------+----------+
// | Argument | Shortcut |
// +-----------+----------+
// | --red | -r |
// | --green | -g |
// | --blue | -b |
// | --white | -w |
// | --black | -bl |
// | --yellow | -yl |
// | --magenta | -mg |
// | --cyan | -cy |
// | --#11bb22 | --#1b2 |
// +-----------+----------+
//
// Complex Grep
//
// You must put your complex grep into quotations:
//
// $ blush -b "^age: [0-9]+" FILENAME
package main
================================================
FILE: go.mod
================================================
module github.com/arsham/blush
go 1.18
require (
github.com/alecthomas/assert v1.0.0
github.com/google/go-cmp v0.5.7
github.com/pkg/errors v0.9.1
)
require (
github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/stretchr/testify v1.7.1 // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
)
================================================
FILE: go.sum
================================================
github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o=
github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY=
github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk=
github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
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/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: internal/reader/helper_test.go
================================================
package reader_test
import (
"io"
"os"
"path"
"testing"
"github.com/alecthomas/assert"
)
// this file contains helpers for all tests in this package.
type nopCloser struct {
io.Reader
closeFunc func() error
}
func (n nopCloser) Close() error { return n.closeFunc() }
type testCase struct {
name string
content string
}
func setup(t *testing.T, input []testCase) []string {
t.Helper()
dir := t.TempDir()
ret := make([]string, len(input))
for i, d := range input {
name := path.Join(dir, d.name)
base := path.Dir(name)
err := os.MkdirAll(base, os.ModePerm)
assert.NoError(t, err)
f, err := os.Create(name)
assert.NoError(t, err)
f.WriteString(d.content)
f.Close()
ret[i] = base
}
return ret
}
func inSlice(niddle string, haystack []string) bool {
for _, s := range haystack {
if s == niddle {
return true
}
}
return false
}
================================================
FILE: internal/reader/reader.go
================================================
package reader
import (
"io"
"os"
"github.com/arsham/blush/internal/tools"
"github.com/pkg/errors"
)
// ErrNoReader is returned if there is no reader defined.
var ErrNoReader = errors.New("no input")
// MultiReader holds one or more io.ReadCloser and reads their contents when
// Read() method is called in order. The reader is loaded lazily if it is a
// file to prevent the system going out of file descriptors.
type MultiReader struct {
currentName string
readers []*container
}
// NewMultiReader creates an instance of the MultiReader and passes it to all
// input functions.
func NewMultiReader(input ...Conf) (*MultiReader, error) {
m := &MultiReader{
readers: make([]*container, 0),
}
for _, c := range input {
if c == nil {
return nil, ErrNoReader
}
err := c(m)
if err != nil {
return nil, err
}
}
return m, nil
}
// Conf is used to configure the MultiReader.
type Conf func(*MultiReader) error
// WithReader adds the {name,r} reader to the MultiReader. If name is empty, the
// key will not be written in the output. You can provide as many empty names as
// you need.
func WithReader(name string, r io.ReadCloser) Conf {
return func(m *MultiReader) error {
if r == nil {
return errors.Wrap(ErrNoReader, "WithReader")
}
c := &container{
get: func() (io.ReadCloser, error) {
m.currentName = name
return r, nil
},
}
m.readers = append(m.readers, c)
return nil
}
}
// WithPaths searches through the path and adds any files it finds to the
// MultiReader. Each path will become its reader's name in the process. It
// returns an error if any of given files are not found. It ignores any files
// that cannot be read or opened.
func WithPaths(paths []string, recursive bool) Conf {
return func(m *MultiReader) error {
if paths == nil {
return errors.Wrap(ErrNoReader, "WithPaths: nil paths")
}
if len(paths) == 0 {
return errors.Wrap(ErrNoReader, "WithPaths: empty paths")
}
files, err := tools.Files(recursive, paths...)
if err != nil {
return errors.Wrap(err, "WithPaths")
}
for _, name := range files {
name := name
c := &container{
get: func() (io.ReadCloser, error) {
m.currentName = name
f, err := os.Open(name) // nolint:gosec // we need this.
return f, err
},
}
m.readers = append(m.readers, c)
}
return nil
}
}
// Read is almost the exact implementation of io.MultiReader but keeps track of
// reader names. It closes each reader once they report they are exhausted, and
// it will happen on the next read.
func (m *MultiReader) Read(b []byte) (n int, err error) {
for len(m.readers) > 0 {
if len(m.readers) == 1 {
if r, ok := m.readers[0].r.(*MultiReader); ok {
m.readers = r.readers
continue
}
}
n, err = m.readers[0].Read(b)
if errors.Is(err, io.EOF) {
err := m.readers[0].r.Close()
if err != nil {
return n, errors.Wrap(err, "MultiReader.Read")
}
c := &container{r: io.NopCloser(nil)}
m.readers[0] = c
m.readers = m.readers[1:]
}
if n > 0 || !errors.Is(err, io.EOF) {
if errors.Is(err, io.EOF) && len(m.readers) > 0 {
err = nil
}
return
}
}
m.currentName = ""
return 0, io.EOF
}
// Close does nothing.
func (m *MultiReader) Close() error { return nil }
// FileName returns the current reader's name.
func (m *MultiReader) FileName() string {
return m.currentName
}
// container takes care of opening the reader on demand. This is particularly
// useful when searching in thousands of files, because we want to open them on
// demand, otherwise the system gets out of file descriptors.
type container struct {
r io.ReadCloser
get func() (io.ReadCloser, error)
open bool
}
func (c *container) Read(b []byte) (int, error) {
if !c.open {
var err error
c.r, err = c.get()
if err != nil {
return 0, err
}
c.open = true
}
return c.r.Read(b)
}
================================================
FILE: internal/reader/reader_test.go
================================================
package reader_test
import (
"bytes"
"io"
"path"
"testing"
"github.com/alecthomas/assert"
"github.com/arsham/blush/internal/reader"
)
func TestWithReader(t *testing.T) {
t.Parallel()
m, err := reader.NewMultiReader(reader.WithReader("name", nil))
assert.Error(t, err)
assert.Nil(t, m)
r := io.NopCloser(&bytes.Buffer{})
m, err = reader.NewMultiReader(reader.WithReader("name", r))
assert.NoError(t, err)
assert.NotNil(t, m)
m, err = reader.NewMultiReader(reader.WithReader("", r))
assert.NoError(t, err)
assert.NotNil(t, m)
}
func TestWithReaderMultipleReadersClose(t *testing.T) {
t.Parallel()
var called []string
input1 := "afmBEswIRYosG7"
input2 := "UbMFeIFjvAhdA3sdT"
r1 := nopCloser{
Reader: bytes.NewBufferString(input1),
closeFunc: func() error {
called = append(called, "r1")
return nil
},
}
r2 := nopCloser{
Reader: bytes.NewBufferString(input2),
closeFunc: func() error {
called = append(called, "r2")
return nil
},
}
m, err := reader.NewMultiReader(reader.WithReader("r1", r1), reader.WithReader("r2", r2))
assert.NoError(t, err)
b := make([]byte, 100)
_, err = m.Read(b)
assert.NoError(t, err)
assert.EqualValues(t, input1, bytes.Trim(b, "\x00"))
_, err = m.Read(b)
assert.NoError(t, err)
assert.True(t, inSlice("r1", called))
assert.EqualValues(t, input2, bytes.Trim(b, "\x00"))
_, err = m.Read(b)
assert.EqualError(t, io.EOF, err.Error())
assert.True(t, inSlice("r2", called))
}
func TestWithReaderMultipleReadersError(t *testing.T) {
t.Parallel()
r := nopCloser{
Reader: &bytes.Buffer{},
closeFunc: func() error {
return nil
},
}
m, err := reader.NewMultiReader(reader.WithReader("r", r), nil)
assert.Error(t, err)
assert.Nil(t, m)
}
func TestWithPathsError(t *testing.T) {
t.Parallel()
tcs := []struct {
name string
input []string
}{
{"nil", nil},
{"empty", []string{}},
{"empty string", []string{""}},
{"not found", []string{"nomansland2987349237"}},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
input := reader.WithPaths(tc.input, true)
m, err := reader.NewMultiReader(input)
assert.Error(t, err)
assert.Nil(t, m)
})
}
}
func TestNewMultiReaderWithPaths(t *testing.T) {
t.Parallel()
var (
c1 = "VJSNS5IeLCtEB"
c2 = "kkNL8vGNJn"
c3 = "o6Ubb5Taj"
)
input := []testCase{
{"a/a.txt", c1},
{"a/b.txt", c2},
{"ab.txt", c3},
}
dirs := setup(t, input)
m, err := reader.NewMultiReader(reader.WithPaths(dirs, false))
assert.NoError(t, err)
assert.NotNil(t, m)
err = m.Close()
assert.NoError(t, err)
}
func TestMultiReaderReadOneReader(t *testing.T) {
t.Parallel()
input := "sdlksjdljfQYawl5OEEg"
r := io.NopCloser(bytes.NewBufferString(input))
m, err := reader.NewMultiReader(reader.WithReader("r", r))
assert.NoError(t, err)
assert.NotNil(t, m)
b := make([]byte, len(input))
n, err := m.Read(b)
assert.NoError(t, err)
assert.Equal(t, len(input), n)
assert.EqualValues(t, input, b)
n, err = m.Read(b)
assert.EqualError(t, err, io.EOF.Error())
assert.Zero(t, n)
}
func TestMultiReaderReadZeroBytes(t *testing.T) {
t.Parallel()
input := "3wAgvZ4bSfQYawl5OEEg"
r := io.NopCloser(bytes.NewBufferString(input))
m, err := reader.NewMultiReader(reader.WithReader("r", r))
assert.NoError(t, err)
assert.NotNil(t, m)
b := make([]byte, 0)
n, err := m.Read(b)
assert.NoError(t, err)
assert.Zero(t, n)
assert.Empty(t, b)
}
func TestMultiReaderReadOneReaderMoreSpace(t *testing.T) {
t.Parallel()
input := "3wAgvZ4bSfQYawl5OEEg"
r := io.NopCloser(bytes.NewBufferString(input))
m, err := reader.NewMultiReader(reader.WithReader("r", r))
assert.NoError(t, err)
assert.NotNil(t, m)
b := make([]byte, len(input)+1)
n, err := m.Read(b)
assert.NoError(t, err)
assert.EqualValues(t, len(input), n)
assert.EqualValues(t, input, bytes.Trim(b, "\x00"))
}
func TestMultiReaderReadMultipleReaders(t *testing.T) {
t.Parallel()
input := []string{"P5tyugWXFn", "b8YbUO7pMX3G8j4Bi"}
r1 := io.NopCloser(bytes.NewBufferString(input[0]))
r2 := io.NopCloser(bytes.NewBufferString(input[1]))
m, err := reader.NewMultiReader(
reader.WithReader("r1", r1),
reader.WithReader("r2", r2),
)
assert.NoError(t, err)
assert.NotNil(t, m)
tcs := []struct {
name string
b []byte
wantErr error
wantLen int
wantOut string
}{
{"r1", make([]byte, len(input[0])), nil, len(input[0]), input[0]},
{"r2", make([]byte, len(input[1])), nil, len(input[1]), input[1]},
{"nothing left", make([]byte, 10), io.EOF, 0, ""},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
n, err := m.Read(tc.b)
assert.Equal(t, err, tc.wantErr, "error")
assert.EqualValues(t, tc.wantLen, n)
assert.Equal(t, tc.wantOut, string(bytes.Trim(tc.b, "\x00")), "output")
})
}
}
func TestMultiReaderNames(t *testing.T) {
t.Parallel()
input := []string{"Mw0mxekLYOpXaKl8PVT", "1V5MjHUXYTPChW"}
r1 := io.NopCloser(bytes.NewBufferString(input[0]))
r2 := io.NopCloser(bytes.NewBufferString(input[1]))
m, err := reader.NewMultiReader(
reader.WithReader("r1", r1),
reader.WithReader("r2", r2),
)
assert.NoError(t, err)
assert.NotNil(t, m)
b := make([]byte, 100)
tcs := []struct {
name string
wantErr error
}{
{"r1", nil},
{"r2", nil},
{"", io.EOF},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
_, err := m.Read(b)
assert.Equal(t, err, tc.wantErr, "error")
assert.Equal(t, tc.name, m.FileName())
})
}
}
func TestNewMultiReaderWithPathsRead(t *testing.T) {
t.Parallel()
var (
c1 = "VJSNS5IeLCtEB"
c2 = "kkNL8vGNJn"
c3 = "o6Ubb5Taj"
)
input := []testCase{
{"a/a.txt", c1},
{"a/b.txt", c2},
{"ab.txt", c3},
}
dirs := setup(t, input)
w, err := reader.NewMultiReader(reader.WithPaths(dirs, false))
assert.NoError(t, err)
assert.NotNil(t, w)
t.Cleanup(func() {
err = w.Close()
assert.NoError(t, err)
})
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(w)
assert.NoError(t, err)
for _, s := range []string{c1, c2, c3} {
assert.Contains(t, buf.String(), s)
}
}
func TestNewMultiReaderRecursive(t *testing.T) {
t.Parallel()
var (
c1 = "1JQey4agQ3w9pqg3"
c2 = "7ToNRMgsOAR6A"
c3 = "EtOkn9C5zoH0Dla2rF9"
)
input := []testCase{
{"a/a.txt", c1},
{"a/b.txt", c2},
{"a/b/c.txt", c3},
}
dirs := setup(t, input)
base := path.Join(path.Dir(dirs[0]), "a")
w, err := reader.NewMultiReader(reader.WithPaths([]string{base}, true))
assert.NoError(t, err)
assert.NotNil(t, w)
t.Cleanup(func() {
err = w.Close()
assert.NoError(t, err)
})
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(w)
assert.NoError(t, err)
for _, s := range []string{c1, c2, c3} {
assert.Contains(t, buf.String(), s)
}
}
func TestNewMultiReaderNonRecursive(t *testing.T) {
t.Parallel()
var (
c1 = "DRAjfSq2y"
c2 = "ht3xCIQ"
c3 = "jPqPoAbMNb"
)
input := []testCase{
{"a/a.txt", c1},
{"a/b.txt", c2},
{"a/b/c.txt", c3},
}
dirs := setup(t, input)
base := path.Join(path.Dir(dirs[0]), "a")
w, err := reader.NewMultiReader(reader.WithPaths([]string{base}, false))
assert.NoError(t, err)
assert.NotNil(t, w)
t.Cleanup(func() {
err = w.Close()
assert.NoError(t, err)
})
buf := &bytes.Buffer{}
_, err = buf.ReadFrom(w)
assert.NoError(t, err)
for _, s := range []string{c1, c2} {
assert.Contains(t, buf.String(), s)
}
assert.NotContains(t, buf.String(), c3)
}
================================================
FILE: internal/tools/dir.go
================================================
// Package tools contains common tools used throughout this application.
package tools
import (
"errors"
"io"
"os"
"path"
"path/filepath"
)
// Files returns all files found in paths. If recursive is false, it only
// returns the immediate files in the paths.
func Files(recursive bool, paths ...string) ([]string, error) {
var (
fileList []string
fn = files
)
if recursive {
fn = rfiles
}
for _, p := range paths {
f, err := fn(p)
if err != nil {
return nil, err
}
fileList = append(fileList, f...)
}
if len(fileList) == 0 {
return nil, errors.New("no files found")
}
fileList = unique(fileList)
fileList = nonBinary(fileList)
return fileList, nil
}
func unique(fileList []string) []string {
var (
ret = make([]string, 0, len(fileList))
seen = make(map[string]struct{}, len(fileList))
)
for _, f := range fileList {
if _, ok := seen[f]; ok {
continue
}
seen[f] = struct{}{}
ret = append(ret, f)
}
return ret
}
func nonBinary(fileList []string) []string {
ret := make([]string, 0, len(fileList))
for _, f := range fileList {
if isPlainText(f) {
ret = append(ret, f)
}
}
return ret
}
func rfiles(location string) ([]string, error) {
fileList := []string{}
err := filepath.Walk(location, func(location string, f os.FileInfo, err error) error {
if os.IsPermission(err) {
return nil
}
if err != nil {
return err
}
if !f.IsDir() {
fileList = append(fileList, location)
}
return nil
})
if err != nil {
return nil, err
}
return fileList, nil
}
func files(location string) ([]string, error) {
if s, err := os.Stat(location); err == nil && !s.IsDir() {
return []string{location}, nil
}
files, err := os.ReadDir(location)
if err != nil {
return nil, err
}
fileList := []string{}
for _, f := range files {
if !f.IsDir() {
p := path.Join(location, f.Name())
fileList = append(fileList, p)
}
}
return fileList, nil
}
// TODO: we should ignore the line in search stage instead.
func isPlainText(name string) bool {
f, err := os.Open(name) // nolint:gosec // this is required.
if err != nil {
return false
}
defer f.Close() // nolint:errcheck,gosec // not required.
header := make([]byte, 512)
_, err = f.Read(header)
if err != nil && !errors.Is(err, io.EOF) {
return false
}
return IsPlainText(string(header))
}
================================================
FILE: internal/tools/dir_example_test.go
================================================
package tools_test
import (
"github.com/arsham/blush/internal/tools"
)
func ExampleFiles() {
tools.Files(true, "~/Documents", "/tmp")
// Or
dirs := []string{"~/Documents", "/tmp"}
tools.Files(false, dirs...)
}
================================================
FILE: internal/tools/dir_test.go
================================================
package tools_test
import (
"image"
"image/png"
"io/ioutil"
"os"
"path"
"sort"
"strings"
"testing"
"github.com/alecthomas/assert"
"github.com/arsham/blush/internal/tools"
"github.com/google/go-cmp/cmp"
)
func stringSliceEq(t *testing.T, a, b []string) {
t.Helper()
sort.Strings(a)
sort.Strings(b)
if diff := cmp.Diff(a, b); diff != "" {
t.Errorf("(-want +got):\\n%s", diff)
}
}
func inSlice(niddle string, haystack []string) bool {
for _, s := range haystack {
if s == niddle {
return true
}
}
return false
}
func setup(t *testing.T, count int) (dirs, expect []string) {
t.Helper()
ret := make(map[string]struct{})
tmp := t.TempDir()
files := []struct {
dir string
count int
}{
{"a", count}, // keep this here.
{"a/b/c", count}, // this one is in the above folder, keep!
{"abc", count}, // this one is outside.
{"f", 0}, // this should not be matched.
}
for _, f := range files {
l := path.Join(tmp, f.dir)
err := os.MkdirAll(l, os.ModePerm)
assert.NoError(t, err)
for i := 0; i < f.count; i++ {
f, err := ioutil.TempFile(l, "file_")
assert.NoError(t, err)
ret[path.Dir(f.Name())] = struct{}{}
expect = append(expect, f.Name())
f.WriteString("test")
}
}
for d := range ret {
dirs = append(dirs, d)
}
sort.Strings(dirs)
return dirs, expect
}
func TestFilesError(t *testing.T) {
t.Parallel()
f, err := tools.Files(false)
assert.Nil(t, f)
assert.Error(t, err)
f, err = tools.Files(false, "/path to heaven")
assert.Error(t, err)
assert.Nil(t, f)
}
func TestFiles(t *testing.T) {
t.Parallel()
dirs, expect := setup(t, 10)
f, err := tools.Files(false, dirs...)
assert.NoError(t, err)
assert.NotNil(t, f)
stringSliceEq(t, expect, f)
// the a and abc should match, a/b/c should not
f, err = tools.Files(false, dirs[0], dirs[2])
assert.NoError(t, err)
if len(f) != 20 { // all files in `a` and `abc`
t.Errorf("len(f) = %d, want %d: %v", len(f), 20, f)
}
}
func TestFilesOnSingleFile(t *testing.T) {
t.Parallel()
file, err := ioutil.TempFile(t.TempDir(), "blush_tools")
assert.NoError(t, err)
name := file.Name()
f, err := tools.Files(true, name)
assert.NoError(t, err)
if len(f) != 1 {
t.Fatalf("len(f) = %d, want 1", len(f))
}
if f[0] != name {
t.Errorf("f[0] = %s, want %s", f[0], name)
}
f, err = tools.Files(false, name)
assert.NoError(t, err)
if len(f) != 1 {
t.Fatalf("len(f) = %d, want 1", len(f))
}
if f[0] != name {
t.Errorf("f[0] = %s, want %s", f[0], name)
}
}
func TestFilesRecursive(t *testing.T) {
t.Parallel()
f, err := tools.Files(true, "/path to heaven")
assert.Error(t, err)
assert.Nil(t, f)
dirs, expect := setup(t, 10)
f, err = tools.Files(true, dirs...)
assert.NoError(t, err)
assert.NotNil(t, f)
stringSliceEq(t, expect, f)
f, err = tools.Files(true, dirs[0]) // expecting `a`
assert.NoError(t, err)
if len(f) != 20 { // all files in `a`
t.Errorf("len(f) = %d, want %d: %v", len(f), 20, f)
}
}
func setupUnpermissioned(t *testing.T) (rootDir, fileB string) {
t.Helper()
// creates this structure:
// /tmp
// /a <--- 0222 perm
// /aaa.txt
// /b <--- 0777 perm
// /bbb.txt
rootDir = t.TempDir()
dirA := path.Join(rootDir, "a")
dirB := path.Join(rootDir, "b")
dirs := []struct {
dir, file string
}{
{dirA, "aaa.txt"},
{dirB, "bbb.txt"},
}
for _, d := range dirs {
err := os.Mkdir(d.dir, 0o777)
assert.NoError(t, err)
name := path.Join(d.dir, d.file)
_, err = os.Create(name)
assert.NoError(t, err)
}
err := os.Chmod(dirA, 0o222)
assert.NoError(t, err)
fileB = path.Join(dirB, "bbb.txt")
t.Cleanup(func() {
err := os.Chmod(dirA, 0o777)
assert.NoError(t, err)
})
return rootDir, fileB
}
func TestIgnoreNontPermissionedFolders(t *testing.T) {
t.Parallel()
rootDir, fileB := setupUnpermissioned(t)
f, err := tools.Files(true, rootDir)
assert.NoError(t, err)
assert.NotNil(t, f)
expect := []string{
fileB,
}
stringSliceEq(t, expect, f)
}
// first returned string is text format, second is binary.
func setupBinaryFile(t *testing.T) (str, name string) {
t.Helper()
dir := t.TempDir()
txt, err := os.Create(path.Join(dir, "txt"))
assert.NoError(t, err)
img := image.NewRGBA(image.Rect(0, 0, 1, 1))
binary, err := os.Create(path.Join(dir, "binary"))
assert.NoError(t, err)
txt.WriteString("aaaaa")
png.Encode(binary, img)
return txt.Name(), binary.Name()
}
func TestIgnoreNonTextFiles(t *testing.T) {
t.Parallel()
txt, binary := setupBinaryFile(t)
paths := path.Dir(txt)
got, err := tools.Files(false, paths)
assert.NoError(t, err)
assert.True(t, inSlice(txt, got))
assert.False(t, inSlice(binary, got))
}
func TestUnPrintableButTextContents(t *testing.T) {
t.Parallel()
tcs := []struct {
name string
input string
wantLen int
}{
{"null", string(rune(0)), 1},
{"space", " ", 1},
{"return", "\r", 1},
{"line feed", "\n", 1},
{"tab", "\t", 1},
{"mix", "\na\tbbb\n\n\n\t\t\n \t \n \r\nsjdk", 1},
{"one", string(rune(1)), 0},
{"bell", "\b", 0},
{"bell in middle", "a\bc", 0},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
file, err := ioutil.TempFile(t.TempDir(), "blush_text")
assert.NoError(t, err)
file.WriteString(tc.input)
got, err := tools.Files(false, file.Name())
assert.NoError(t, err)
assert.Len(t, got, tc.wantLen, strings.Join(got, "\n"))
})
}
}
func TestFilesIgnoreDirs(t *testing.T) {
t.Parallel()
dir := t.TempDir()
p := path.Join(dir, "a")
err := os.MkdirAll(p, 0o777)
assert.NoError(t, err)
file, err := ioutil.TempFile(dir, "b")
assert.NoError(t, err)
g, err := tools.Files(true, dir)
assert.NoError(t, err)
assert.NotNil(t, g)
assert.False(t, inSlice(p, g))
assert.True(t, inSlice(file.Name(), g))
}
================================================
FILE: internal/tools/strings.go
================================================
package tools
import (
"unicode"
)
// IsPlainText returns false if at least one of the runes in the input is not
// represented as a plain text in a file. Null is an exception.
func IsPlainText(input string) bool {
for _, r := range input {
switch r {
case 0, '\n', '\t', '\r':
continue
}
if r > unicode.MaxASCII || !unicode.IsPrint(r) {
return false
}
}
return true
}
================================================
FILE: internal/tools/strings_test.go
================================================
package tools_test
import (
"fmt"
"testing"
"github.com/alecthomas/assert"
"github.com/arsham/blush/internal/tools"
)
func TestIsPlainText(t *testing.T) {
t.Parallel()
tcs := []struct {
name string
input string
want bool
}{
{"null", fmt.Sprintf("%d", 0), true},
{"space", " ", true},
{"return", "\r", true},
{"line feed", "\n", true},
{"tab", "\t", true},
{"bell", "\b", false},
{"mix", "\n\n \r\nsjdk", true},
{"1", "\x01", false},
{"zero in middle", "n\x00b", true},
{"bell in middle", "a\bc", false},
}
for _, tc := range tcs {
tc := tc
t.Run(tc.name, func(t *testing.T) {
got := tools.IsPlainText(tc.input)
assert.Equal(t, tc.want, got)
})
}
}
================================================
FILE: main.go
================================================
package main
import "github.com/arsham/blush/cmd"
func main() {
cmd.Main()
}
gitextract_ym2oh4bj/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── go.yml ├── .gitignore ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── blush/ │ ├── benchmarks_test.go │ ├── blush.go │ ├── blush_example_test.go │ ├── blush_test.go │ ├── colour.go │ ├── colour_test.go │ ├── doc.go │ ├── errors.go │ ├── find.go │ ├── find_test.go │ ├── helper_test.go │ └── testdata/ │ ├── d1/ │ │ ├── f1.txt │ │ └── f2.txt │ └── f1.txt ├── cmd/ │ ├── args.go │ ├── args_test.go │ ├── doc.go │ ├── errors.go │ ├── helper_test.go │ ├── main.go │ ├── main_example_test.go │ ├── main_internal_test.go │ ├── main_test.go │ ├── signal.go │ ├── signal_test.go │ └── usage.go ├── configs/ │ └── coverage.sh ├── doc.go ├── go.mod ├── go.sum ├── internal/ │ ├── reader/ │ │ ├── helper_test.go │ │ ├── reader.go │ │ └── reader_test.go │ └── tools/ │ ├── dir.go │ ├── dir_example_test.go │ ├── dir_test.go │ ├── strings.go │ └── strings_test.go └── main.go
SYMBOL INDEX (197 symbols across 27 files)
FILE: blush/benchmarks_test.go
function BenchmarkColourise (line 14) | func BenchmarkColourise(b *testing.B) {
function BenchmarkNewLocator (line 46) | func BenchmarkNewLocator(b *testing.B) {
function BenchmarkFind (line 80) | func BenchmarkFind(b *testing.B) {
type benchCase (line 150) | type benchCase struct
function BenchmarkBlush (line 157) | func BenchmarkBlush(b *testing.B) {
function benchmarkRead (line 312) | func benchmarkRead(b *testing.B, bc benchCase) {
function benchmarkWriteTo (line 332) | func benchmarkWriteTo(b *testing.B, bc benchCase) {
FILE: blush/blush.go
type mode (line 10) | type mode
constant Separator (line 14) | Separator = ": "
constant DefaultLineCache (line 17) | DefaultLineCache = 50
constant DefaultCharCache (line 21) | DefaultCharCache = 1000
constant readMode (line 23) | readMode mode = iota
constant writeToMode (line 24) | writeToMode
type Blush (line 33) | type Blush struct
method Read (line 50) | func (b *Blush) Read(p []byte) (n int, err error) {
method WriteTo (line 75) | func (b *Blush) WriteTo(w io.Writer) (int64, error) {
method setup (line 100) | func (b *Blush) setup(m mode) error {
method decorate (line 124) | func (b *Blush) decorate(input string) (string, bool) {
method readLines (line 136) | func (b *Blush) readLines() {
method transfer (line 153) | func (b *Blush) transfer() {
method Close (line 163) | func (b *Blush) Close() error {
function lookInto (line 170) | func lookInto(f []Finder, line string) (string, bool) {
function fileName (line 182) | func fileName(r io.Reader) string {
FILE: blush/blush_example_test.go
function ExampleBlush (line 11) | func ExampleBlush() {
function ExampleBlush_Read (line 21) | func ExampleBlush_Read() {
function ExampleBlush_Read_inDetails (line 32) | func ExampleBlush_Read_inDetails() {
function ExampleBlush_WriteTo (line 58) | func ExampleBlush_WriteTo() {
function ExampleBlush_WriteTo_copy (line 79) | func ExampleBlush_WriteTo_copy() {
function ExampleBlush_WriteTo_multiReader (line 100) | func ExampleBlush_WriteTo_multiReader() {
function ExampleBlush_WriteTo_multiReaderInDetails (line 115) | func ExampleBlush_WriteTo_multiReaderInDetails() {
FILE: blush/blush_test.go
function TestBlush (line 19) | func TestBlush(t *testing.T) {
function testBlushWriteTo (line 36) | func testBlushWriteTo(t *testing.T) {
function testBlushWriteToErrors (line 46) | func testBlushWriteToErrors(t *testing.T) {
function testBlushWriteToNoMatch (line 83) | func testBlushWriteToNoMatch(t *testing.T) {
function testBlushWriteToMatch (line 102) | func testBlushWriteToMatch(t *testing.T) {
function testBlushWriteToMatchDrop (line 109) | func testBlushWriteToMatchDrop(t *testing.T) {
function testBlushWriteToMatchColour (line 134) | func testBlushWriteToMatchColour(t *testing.T) {
function testBlushWriteToMatchCountColour (line 158) | func testBlushWriteToMatchCountColour(t *testing.T) {
function testBlushWriteToColour (line 202) | func testBlushWriteToColour(t *testing.T) {
function testBlushWriteToColourNoCutMode (line 234) | func testBlushWriteToColourNoCutMode(t *testing.T) {
function testBlushWriteToMultipleMatchInOneLine (line 266) | func testBlushWriteToMultipleMatchInOneLine(t *testing.T) {
function testBlushClosesReader (line 297) | func testBlushClosesReader(t *testing.T) {
function testBlushRead (line 316) | func testBlushRead(t *testing.T) {
function testBlushReadOneStream (line 325) | func testBlushReadOneStream(t *testing.T) {
function testBlushReadTwoStreams (line 360) | func testBlushReadTwoStreams(t *testing.T) {
function testBlushReadHalfWay (line 386) | func testBlushReadHalfWay(t *testing.T) {
function testBlushReadOnClosed (line 406) | func testBlushReadOnClosed(t *testing.T) {
function testBlushReadLongOneLineText (line 429) | func testBlushReadLongOneLineText(t *testing.T) {
function testBlushPrintName (line 450) | func testBlushPrintName(t *testing.T) {
function testBlushStdinPrintName (line 481) | func testBlushStdinPrintName(t *testing.T) {
function testBlushPrintFilename (line 506) | func testBlushPrintFilename(t *testing.T) {
function testBlushReadContiniously (line 559) | func testBlushReadContiniously(t *testing.T) {
function testBlushReadMiddleOfMatch (line 595) | func testBlushReadMiddleOfMatch(t *testing.T) {
function testBlushReadComplete (line 619) | func testBlushReadComplete(t *testing.T) {
function testBlushReadPartComplete (line 640) | func testBlushReadPartComplete(t *testing.T) {
function testBlushReadPartPartOver (line 662) | func testBlushReadPartPartOver(t *testing.T) {
function testBlushReadMultiLine (line 690) | func testBlushReadMultiLine(t *testing.T) {
function testBlushReadWriteToMode (line 725) | func testBlushReadWriteToMode(t *testing.T) {
FILE: blush/colour.go
constant BgLevel (line 11) | BgLevel = 70
type RGB (line 52) | type RGB struct
type Colour (line 57) | type Colour struct
function Colourise (line 63) | func Colourise(input string, c Colour) string {
function foreground (line 78) | func foreground(c RGB) string {
function background (line 82) | func background(c RGB) string {
function unformat (line 86) | func unformat() string {
function colour (line 90) | func colour(red, green, blue int) int {
function baseColor (line 94) | func baseColor(value, factor int) int {
function colorFromArg (line 98) | func colorFromArg(colour string) Colour {
function colourGroup (line 110) | func colourGroup(colour string) Colour {
function stockColour (line 138) | func stockColour(colour string) Colour {
function hexColour (line 163) | func hexColour(colour string) Colour {
function getInt (line 190) | func getInt(hex string) int {
FILE: blush/colour_test.go
function TestColourise (line 11) | func TestColourise(t *testing.T) {
FILE: blush/find.go
type Finder (line 18) | type Finder interface
function NewLocator (line 28) | func NewLocator(colour, search string, insensitive bool) Finder {
type Exact (line 53) | type Exact struct
method Find (line 68) | func (e Exact) Find(input string) (string, bool) {
method colourise (line 75) | func (e Exact) colourise(input string, c Colour) string {
method Colour (line 83) | func (e Exact) Colour() Colour {
method String (line 88) | func (e Exact) String() string {
function NewExact (line 59) | func NewExact(s string, c Colour) Exact {
type Iexact (line 93) | type Iexact struct
method Find (line 108) | func (i Iexact) Find(input string) (string, bool) {
method colourise (line 115) | func (i Iexact) colourise(input string, c Colour) string {
method Colour (line 126) | func (i Iexact) Colour() Colour {
method String (line 131) | func (i Iexact) String() string {
function NewIexact (line 99) | func NewIexact(s string, c Colour) Iexact {
type Rx (line 136) | type Rx struct
method Find (line 151) | func (r Rx) Find(input string) (string, bool) {
method colourise (line 158) | func (r Rx) colourise(input string, c Colour) string {
method Colour (line 166) | func (r Rx) Colour() Colour {
function NewRx (line 142) | func NewRx(r *regexp.Regexp, c Colour) Rx {
FILE: blush/find_test.go
type colourer (line 12) | type colourer interface
function TestNewLocatorColours (line 17) | func TestNewLocatorColours(t *testing.T) {
function TestNewLocatorExact (line 69) | func TestNewLocatorExact(t *testing.T) {
function TestNewLocatorIexact (line 77) | func TestNewLocatorIexact(t *testing.T) {
function TestNewLocatorRx (line 85) | func TestNewLocatorRx(t *testing.T) {
function TestNewLocatorRxColours (line 111) | func TestNewLocatorRxColours(t *testing.T) {
function TestExactNotFound (line 120) | func TestExactNotFound(t *testing.T) {
function TestExactFind (line 128) | func TestExactFind(t *testing.T) {
function TestRxNotFound (line 160) | func TestRxNotFound(t *testing.T) {
function TestRxFind (line 168) | func TestRxFind(t *testing.T) {
function TestIexactNotFound (line 202) | func TestIexactNotFound(t *testing.T) {
function TestIexact (line 210) | func TestIexact(t *testing.T) {
function TestRxInsensitiveFind (line 242) | func TestRxInsensitiveFind(t *testing.T) {
function TestColourGroup (line 279) | func TestColourGroup(t *testing.T) {
function TestColourNewGroup (line 291) | func TestColourNewGroup(t *testing.T) {
FILE: blush/helper_test.go
type nopCloser (line 13) | type nopCloser struct
method Close (line 18) | func (n nopCloser) Close() error { return n.closeFunc() }
type badWriter (line 20) | type badWriter struct
method Write (line 24) | func (b *badWriter) Write(p []byte) (int, error) { return b.writeFunc(...
FILE: cmd/args.go
type args (line 14) | type args struct
method hasArgs (line 50) | func (a *args) hasArgs(args ...string) (found bool) {
method setPaths (line 70) | func (a *args) setPaths() error {
method setFinders (line 124) | func (a *args) setFinders() {
function newArgs (line 27) | func newArgs(input ...string) (*args, error) {
function flip (line 137) | func flip(s []string) []string {
function inStringSlice (line 147) | func inStringSlice(s string, haystack []string) bool {
FILE: cmd/args_test.go
function TestArgs (line 13) | func TestArgs(t *testing.T) {
function TestArgsPipe (line 66) | func TestArgsPipe(t *testing.T) {
function TestArgsPaths (line 74) | func TestArgsPaths(t *testing.T) {
function TestArgsHasArgs (line 123) | func TestArgsHasArgs(t *testing.T) {
FILE: cmd/helper_test.go
type stdFile (line 17) | type stdFile struct
method String (line 21) | func (s *stdFile) String() string {
method Close (line 28) | func (s *stdFile) Close() error {
function newStdFile (line 32) | func newStdFile(t *testing.T, name string) *stdFile {
function setup (line 45) | func setup(t *testing.T, args string) (stdout, stderr *stdFile) {
function getPipe (line 69) | func getPipe(t *testing.T) *os.File {
function argsEqual (line 81) | func argsEqual(a, b []blush.Finder) bool {
FILE: cmd/main.go
function Main (line 16) | func Main() {
function GetBlush (line 44) | func GetBlush(input []string) (*blush.Blush, error) {
FILE: cmd/main_example_test.go
type colourer (line 10) | type colourer interface
function ExampleGetBlush_red (line 14) | func ExampleGetBlush_red() {
function ExampleGetBlush_multiColour (line 28) | func ExampleGetBlush_multiColour() {
FILE: cmd/main_internal_test.go
function getPipe (line 12) | func getPipe(t *testing.T) *os.File {
function stringSliceEq (line 29) | func stringSliceEq(a, b []string) bool {
function TestFiles (line 41) | func TestFiles(t *testing.T) {
FILE: cmd/main_test.go
function TestMainHelp (line 14) | func TestMainHelp(t *testing.T) {
function TestPipeInput (line 21) | func TestPipeInput(t *testing.T) {
function TestMainMatch (line 36) | func TestMainMatch(t *testing.T) {
function TestMainMatchCut (line 64) | func TestMainMatchCut(t *testing.T) {
function TestNoFiles (line 79) | func TestNoFiles(t *testing.T) {
function TestColourArgs (line 86) | func TestColourArgs(t *testing.T) {
function TestWithFilename (line 176) | func TestWithFilename(t *testing.T) {
FILE: cmd/signal.go
function WaitForSignal (line 11) | func WaitForSignal(sig chan os.Signal, exit func(int)) {
FILE: cmd/signal_test.go
function TestCaptureSignals (line 13) | func TestCaptureSignals(t *testing.T) {
FILE: internal/reader/helper_test.go
type nopCloser (line 14) | type nopCloser struct
method Close (line 19) | func (n nopCloser) Close() error { return n.closeFunc() }
type testCase (line 21) | type testCase struct
function setup (line 26) | func setup(t *testing.T, input []testCase) []string {
function inSlice (line 45) | func inSlice(niddle string, haystack []string) bool {
FILE: internal/reader/reader.go
type MultiReader (line 17) | type MultiReader struct
method Read (line 96) | func (m *MultiReader) Read(b []byte) (n int, err error) {
method Close (line 126) | func (m *MultiReader) Close() error { return nil }
method FileName (line 129) | func (m *MultiReader) FileName() string {
function NewMultiReader (line 24) | func NewMultiReader(input ...Conf) (*MultiReader, error) {
type Conf (line 41) | type Conf
function WithReader (line 46) | func WithReader(name string, r io.ReadCloser) Conf {
function WithPaths (line 66) | func WithPaths(paths []string, recursive bool) Conf {
type container (line 136) | type container struct
method Read (line 142) | func (c *container) Read(b []byte) (int, error) {
FILE: internal/reader/reader_test.go
function TestWithReader (line 13) | func TestWithReader(t *testing.T) {
function TestWithReaderMultipleReadersClose (line 29) | func TestWithReaderMultipleReadersClose(t *testing.T) {
function TestWithReaderMultipleReadersError (line 66) | func TestWithReaderMultipleReadersError(t *testing.T) {
function TestWithPathsError (line 79) | func TestWithPathsError(t *testing.T) {
function TestNewMultiReaderWithPaths (line 102) | func TestNewMultiReaderWithPaths(t *testing.T) {
function TestMultiReaderReadOneReader (line 123) | func TestMultiReaderReadOneReader(t *testing.T) {
function TestMultiReaderReadZeroBytes (line 142) | func TestMultiReaderReadZeroBytes(t *testing.T) {
function TestMultiReaderReadOneReaderMoreSpace (line 157) | func TestMultiReaderReadOneReaderMoreSpace(t *testing.T) {
function TestMultiReaderReadMultipleReaders (line 171) | func TestMultiReaderReadMultipleReaders(t *testing.T) {
function TestMultiReaderNames (line 205) | func TestMultiReaderNames(t *testing.T) {
function TestNewMultiReaderWithPathsRead (line 235) | func TestNewMultiReaderWithPathsRead(t *testing.T) {
function TestNewMultiReaderRecursive (line 265) | func TestNewMultiReaderRecursive(t *testing.T) {
function TestNewMultiReaderNonRecursive (line 298) | func TestNewMultiReaderNonRecursive(t *testing.T) {
FILE: internal/tools/dir.go
function Files (line 14) | func Files(recursive bool, paths ...string) ([]string, error) {
function unique (line 38) | func unique(fileList []string) []string {
function nonBinary (line 53) | func nonBinary(fileList []string) []string {
function rfiles (line 63) | func rfiles(location string) ([]string, error) {
function files (line 83) | func files(location string) ([]string, error) {
function isPlainText (line 102) | func isPlainText(name string) bool {
FILE: internal/tools/dir_example_test.go
function ExampleFiles (line 7) | func ExampleFiles() {
FILE: internal/tools/dir_test.go
function stringSliceEq (line 18) | func stringSliceEq(t *testing.T, a, b []string) {
function inSlice (line 27) | func inSlice(niddle string, haystack []string) bool {
function setup (line 36) | func setup(t *testing.T, count int) (dirs, expect []string) {
function TestFilesError (line 70) | func TestFilesError(t *testing.T) {
function TestFiles (line 80) | func TestFiles(t *testing.T) {
function TestFilesOnSingleFile (line 97) | func TestFilesOnSingleFile(t *testing.T) {
function TestFilesRecursive (line 122) | func TestFilesRecursive(t *testing.T) {
function setupUnpermissioned (line 142) | func setupUnpermissioned(t *testing.T) (rootDir, fileB string) {
function TestIgnoreNontPermissionedFolders (line 176) | func TestIgnoreNontPermissionedFolders(t *testing.T) {
function setupBinaryFile (line 189) | func setupBinaryFile(t *testing.T) (str, name string) {
function TestIgnoreNonTextFiles (line 206) | func TestIgnoreNonTextFiles(t *testing.T) {
function TestUnPrintableButTextContents (line 216) | func TestUnPrintableButTextContents(t *testing.T) {
function TestFilesIgnoreDirs (line 245) | func TestFilesIgnoreDirs(t *testing.T) {
FILE: internal/tools/strings.go
function IsPlainText (line 9) | func IsPlainText(input string) bool {
FILE: internal/tools/strings_test.go
function TestIsPlainText (line 11) | func TestIsPlainText(t *testing.T) {
FILE: main.go
function main (line 5) | func main() {
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (152K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 593,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\n**Describe the bug**\nA clear and concise descriptio"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 560,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? "
},
{
"path": ".github/workflows/go.yml",
"chars": 1618,
"preview": "name: Continues Integration\n\non:\n push:\n branches:\n - master\n - feature/*\n - bugfix/*\n pull_request:"
},
{
"path": ".gitignore",
"chars": 213,
"preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Ou"
},
{
"path": ".golangci.yml",
"chars": 2987,
"preview": "service:\n golangci-lint-version: 1.45.x\n\nlinters-settings:\n funlen:\n lines: 100\n statements: 50\n cyclop:\n sk"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3227,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 1695,
"preview": "# Contributing\n\n1. [Dependencies](#dependencies)\n2. [Testing](#testing)\n3. [Benchmarking](#benchmarking)\n4. [Pull Reques"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2018 Arsham Shirvani\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "Makefile",
"chars": 4061,
"preview": "help: ## Show help messages.\n\t@grep -E '^[0-9a-zA-Z_-]+:(.*?## .*)?$$' $(MAKEFILE_LIST) | sed 's/^Makefile://' | awk 'BE"
},
{
"path": "PULL_REQUEST_TEMPLATE.md",
"chars": 279,
"preview": "### Description\nPlease explain the changes you made here.\n\n### Checklist\n- [ ] Code compiles correctly\n- [ ] Created tes"
},
{
"path": "README.md",
"chars": 5484,
"preview": "# Blush\n\n[](https://pkg.go.dev/github.com/arsham/dbtools)"
},
{
"path": "blush/benchmarks_test.go",
"chars": 9481,
"preview": "package blush_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/arsham/blush/blush\"\n"
},
{
"path": "blush/blush.go",
"chars": 4127,
"preview": "package blush\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\n\t\"github.com/arsham/blush/internal/reader\"\n)\n\ntype mode int\n\nconst (\n\t// Separat"
},
{
"path": "blush/blush_example_test.go",
"chars": 4144,
"preview": "package blush_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/arsham/blush/blush\"\n)\n\nfunc ExampleBlush() {\n\tf := blus"
},
{
"path": "blush/blush_test.go",
"chars": 19408,
"preview": "package blush_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github."
},
{
"path": "blush/colour.go",
"chars": 4277,
"preview": "package blush\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// BgLevel is the colour value of R, G, or B when the colour is "
},
{
"path": "blush/colour_test.go",
"chars": 932,
"preview": "package blush_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\"\n\t\"github.com/arsham/blush/blush\"\n)\n\n"
},
{
"path": "blush/doc.go",
"chars": 1470,
"preview": "// Package blush reads from a given io.Reader line by line and looks for\n// patterns.\n//\n// Blush struct has a Reader pr"
},
{
"path": "blush/errors.go",
"chars": 578,
"preview": "package blush\n\nimport \"errors\"\n\nvar (\n\t// ErrNoWriter is returned if a nil object is passed to the WriteTo method.\n\tErrN"
},
{
"path": "blush/find.go",
"chars": 4033,
"preview": "package blush\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar (\n\tisRegExp = regexp.MustCompile(`[\\^\\$.\\{\\}\\[\\]\\*\\?]`)\n\t// T"
},
{
"path": "blush/find_test.go",
"chars": 9713,
"preview": "package blush_test\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\"\n\t\"github.com/arsham/blush/blus"
},
{
"path": "blush/helper_test.go",
"chars": 594,
"preview": "package blush_test\n\nimport \"io\"\n\n// this file contains helpers for all tests in this package.\n\n// In the testdata folder"
},
{
"path": "blush/testdata/d1/f1.txt",
"chars": 685,
"preview": "d1-f1 IgjZC oSlYrhXTWOHEp TOKEN-A dAThrGHHYVfYQxyCDAVm ONE\nd1-f1 jqwvUPYQhouXPCk hHcxLvqporTHREEpepOVOpy TOKEN-B IeCbNDv"
},
{
"path": "blush/testdata/d1/f2.txt",
"chars": 1720,
"preview": "d1-f2 lIgjZC oSlYrhXHEp TOKEN-A dAThrGHHYVfYQxyCDAVm\nd1-f2 ljqwvUPYQhouXPCk hHcxLvqporpepOVOpy TOKEN-B IeCbNDvXef\nd1-f2 "
},
{
"path": "blush/testdata/f1.txt",
"chars": 873,
"preview": "f1 IgjZC oSlYrhXHEp TOKEN-A dAThrGHHYVfYQxyCDAVm\nf1 jqwvUPYQhouXPCk hHcxLvqporpepOVOpy TOKEN-B IeCbNDvXef\nf1 MqZyWlIFkdn"
},
{
"path": "cmd/args.go",
"chars": 3372,
"preview": "package cmd\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/arsham/blush/blush\"\n)\n\n// Note that hasArgs, setFi"
},
{
"path": "cmd/args_test.go",
"chars": 4560,
"preview": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\"\n)\n\nfunc TestArgs"
},
{
"path": "cmd/doc.go",
"chars": 878,
"preview": "// Package cmd bootstraps the application.\n//\n// Main() reads the provided arguments from the command line and creates a"
},
{
"path": "cmd/errors.go",
"chars": 393,
"preview": "package cmd\n\nimport \"errors\"\n\nvar (\n\t// ErrNoInput is returned when the application doesn't receive any files as the\n\t//"
},
{
"path": "cmd/helper_test.go",
"chars": 1701,
"preview": "package cmd_test\n\nimport (\n\t\"bytes\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\""
},
{
"path": "cmd/main.go",
"chars": 1386,
"preview": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/arsham/blush/blush\"\n\t\"github.com/arsham/blush/in"
},
{
"path": "cmd/main_example_test.go",
"chars": 976,
"preview": "package cmd_test\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/arsham/blush/blush\"\n\t\"github.com/arsham/blush/cmd\"\n)\n\ntype colourer inte"
},
{
"path": "cmd/main_internal_test.go",
"chars": 2792,
"preview": "package cmd\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\"\n)\n\nfunc getPipe(t *testing."
},
{
"path": "cmd/main_test.go",
"chars": 5581,
"preview": "package cmd_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\"\n\t\"github.com/arsham/blush/bl"
},
{
"path": "cmd/signal.go",
"chars": 450,
"preview": "package cmd\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n)\n\n// WaitForSignal calls exit with code 130 if receives an SIGINT o"
},
{
"path": "cmd/signal_test.go",
"chars": 782,
"preview": "package cmd_test\n\nimport (\n\t\"os\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/alecthomas/assert\"\n\t\"github.com/arsham/blus"
},
{
"path": "cmd/usage.go",
"chars": 1366,
"preview": "package cmd\n\nimport \"errors\"\n\n// These variables are used for showing help messages on command line.\nvar (\n\terrShowHelp "
},
{
"path": "configs/coverage.sh",
"chars": 327,
"preview": "#!/usr/bin/env bash\n\nset -e\necho \"\" > coverage.txt\n\nfor d in $(go list ./... | grep -v vendor); do\n go test -coverpro"
},
{
"path": "doc.go",
"chars": 2968,
"preview": "// Package blush searches for matches with colours.\n//\n// Usage\n//\n// $ blush -b \"first search\" -g \"second one\" -g \"and"
},
{
"path": "go.mod",
"chars": 496,
"preview": "module github.com/arsham/blush\n\ngo 1.18\n\nrequire (\n\tgithub.com/alecthomas/assert v1.0.0\n\tgithub.com/google/go-cmp v0.5.7"
},
{
"path": "go.sum",
"chars": 3386,
"preview": "github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o=\ngithub.com/alecthomas/assert v1.0.0/"
},
{
"path": "internal/reader/helper_test.go",
"chars": 877,
"preview": "package reader_test\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\"\n)\n\n// this file contains h"
},
{
"path": "internal/reader/reader.go",
"chars": 3887,
"preview": "package reader\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/arsham/blush/internal/tools\"\n\t\"github.com/pkg/errors\"\n)\n\n// ErrNoRead"
},
{
"path": "internal/reader/reader_test.go",
"chars": 7400,
"preview": "package reader_test\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\"\n\t\"github.com/arsham/blu"
},
{
"path": "internal/tools/dir.go",
"chars": 2346,
"preview": "// Package tools contains common tools used throughout this application.\npackage tools\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t"
},
{
"path": "internal/tools/dir_example_test.go",
"chars": 217,
"preview": "package tools_test\n\nimport (\n\t\"github.com/arsham/blush/internal/tools\"\n)\n\nfunc ExampleFiles() {\n\ttools.Files(true, \"~/Do"
},
{
"path": "internal/tools/dir_test.go",
"chars": 5776,
"preview": "package tools_test\n\nimport (\n\t\"image\"\n\t\"image/png\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.co"
},
{
"path": "internal/tools/strings.go",
"chars": 391,
"preview": "package tools\n\nimport (\n\t\"unicode\"\n)\n\n// IsPlainText returns false if at least one of the runes in the input is not\n// r"
},
{
"path": "internal/tools/strings_test.go",
"chars": 701,
"preview": "package tools_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/alecthomas/assert\"\n\t\"github.com/arsham/blush/internal/tools"
},
{
"path": "main.go",
"chars": 80,
"preview": "package main\n\nimport \"github.com/arsham/blush/cmd\"\n\nfunc main() {\n\tcmd.Main()\n}\n"
}
]
About this extraction
This page contains the full source code of the arsham/blush GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (133.7 KB), approximately 43.7k tokens, and a symbol index with 197 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.