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 ``` 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 [![PkgGoDev](https://pkg.go.dev/badge/github.com/arsham/dbtools)](https://pkg.go.dev/github.com/arsham/dbtools) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/arsham/dbtools) [![Build Status](https://github.com/arsham/dbtools/actions/workflows/go.yml/badge.svg)](https://github.com/arsham/dbtools/actions/workflows/go.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Coverage Status](https://codecov.io/gh/arsham/blush/branch/master/graph/badge.svg)](https://codecov.io/gh/arsham/blush) [![Go Report Card](https://goreportcard.com/badge/github.com/arsham/blush)](https://goreportcard.com/report/github.com/arsham/blush) With Blush, you can highlight matches with any colours of your choice. ![1](https://user-images.githubusercontent.com/428611/164768864-e9713ac3-0097-4435-8bcb-577dbf7b9931.png) 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. ![2](https://user-images.githubusercontent.com/428611/164768874-bf687313-c103-449b-bb57-6fdcea51fc5d.png) ### Dropping Unmatched By default, unmatched lines are not dropped. But you can use the `-d` flag to drop them: ![3](https://user-images.githubusercontent.com/428611/164768875-c9aa3e47-7db0-454f-8a55-1e2bff332c69.png) ## 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 ``` ![4](https://user-images.githubusercontent.com/428611/164768879-f9b73b2c-b6bb-4cf5-a98a-e51535fa554a.png) ### 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 ``` ![5](https://user-images.githubusercontent.com/428611/164768882-5ce57477-e9d5-4170-ac10-731e9391cbee.png) 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). ![6](https://user-images.githubusercontent.com/428611/164768883-154b4fd9-946f-43eb-b3f5-ede6027c3eda.png) ## Complex Grep You must put your complex grep into quotations: ```bash $ blush -b "^age: [0-9]+" FILENAME ``` ![7](https://user-images.githubusercontent.com/428611/164768886-5b94b8fa-77e2-4617-80f2-040edce18660.png) ## 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: // 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: // 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: // 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: // 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() }