diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b444581..bb3dd9c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,13 @@ updates: directory: "/" # Location of package manifests schedule: interval: "daily" + + # Maintain dependencies for GitHub Actions + # These would open PR, these PR would be tested with the CI + # They will have to be merged manually by a maintainer + - package-ecosystem: github-actions + directory: / + open-pull-requests-limit: 10 # avoid spam, if no one reacts + schedule: + interval: weekly + time: '11:00' \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7e66af8..bd12bd4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,19 +10,28 @@ jobs: test: strategy: matrix: - go-version: [1.20.x, 1.21.x] + go-version: [1.20.x, 1.21.x, 1.22.x, 1.23.x, 1.24.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Build run: go build -v ./... - name: Test - run: go test -race -cover -covermode=atomic -coverprofile=coverage.out ./... - - uses: codecov/codecov-action@v1 + run: go test -race ./... + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: 'stable' + - name: Coverage + run: go test -race -cover -covermode=atomic -coverprofile=coverage.txt ./... + - uses: codecov/codecov-action@v5 with: - file: ./coverage.out - verbose: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml index 75a8592..d22fd92 100644 --- a/.github/workflows/codacy.yml +++ b/.github/workflows/codacy.yml @@ -35,12 +35,12 @@ jobs: runs-on: ubuntu-latest steps: # Checkout the repository to the GitHub Actions runner - - name: Checkout code - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - - name: Run Codacy Analysis CLI - uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b + - name: Codacy Analysis CLI + uses: codacy/codacy-analysis-cli-action@v4 with: # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository # You can also omit the token and run the tools that support default configurations @@ -55,6 +55,6 @@ jobs: # Upload the SARIF file generated in the previous step - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index b0dedc4..b64f34f 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -14,7 +14,7 @@ jobs: dependency-review: runs-on: ubuntu-latest steps: - - name: 'Checkout Repository' - uses: actions/checkout@v3 - - name: 'Dependency Review' - uses: actions/dependency-review-action@v3 + - name: Checkout + uses: actions/checkout@v4 + - name: Dependency Review + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index fb4817e..390813e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -12,16 +12,18 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 + - name: checkout-action + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 with: - go-version: '1.21' - cache: false + go-version: 'stable' - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: + uses: golangci/golangci-lint-action@v7 + # with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.55 + # version: v1.62 # Optional: working directory, useful for monorepos # working-directory: somedir @@ -30,5 +32,5 @@ jobs: # args: --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. - only-new-issues: false + # only-new-issues: false diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e542a73..ab03aa5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,14 +12,14 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - run: git fetch --force --tags - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: stable - - uses: goreleaser/goreleaser-action@v4 + - uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest diff --git a/.golangci.yml b/.golangci.yml index 836dabb..7e63691 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,59 +1,68 @@ -# run: -# # timeout for analysis, e.g. 30s, 5m, default is 1m -# timeout: 5m - +version: "2" linters: - disable-all: true + default: none enable: - bodyclose + - copyloopvar - dogsled - goconst - gocritic - - gofmt - - goimports - gosec - - gosimple - govet - ineffassign - misspell - prealloc - - exportloopref - revive - staticcheck - - stylecheck - - typecheck + - thelper - unconvert - unparam - unused - - misspell - wsl - -issues: - exclude-rules: - - text: "Use of weak random number generator" - linters: - - gosec - - text: "comment on exported var" - linters: - - golint - - text: "don't use an underscore in package name" - linters: - - golint - - text: "ST1003:" - linters: - - stylecheck - # FIXME: Disabled until golangci-lint updates stylecheck with this fix: - # https://github.com/dominikh/go-tools/issues/389 - - text: "ST1016:" - linters: - - stylecheck - -linters-settings: - dogsled: - max-blank-identifiers: 3 - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - -run: - tests: false + settings: + dogsled: + max-blank-identifiers: 3 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - gosec + text: Use of weak random number generator + - linters: + - golint + text: comment on exported var + - linters: + - golint + text: don't use an underscore in package name + - linters: + - staticcheck + text: 'ST1003:' + - linters: + - wsl + path: _test.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - goimports + settings: + gci: + sections: + - standard + - default + - localmodule + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index e8eda60..6d84e87 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,76 +1,134 @@ + # 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, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community 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 +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or 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 +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a - professional setting + professional setting -## Our Responsibilities +## Enforcement 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. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -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. +Community leaders 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, and will communicate reasons for moderation +decisions when appropriate. ## 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. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at alessio@debian.org. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and 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. +reported to the community leaders responsible for enforcement at +[alessio AT debian DOT org][contact]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -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. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -[homepage]: https://www.contributor-covenant.org +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations +[contact]: mailto:alessio_AT_debian_DOT_org diff --git a/Makefile b/Makefile index f92895c..dd15c6e 100644 --- a/Makefile +++ b/Makefile @@ -5,21 +5,30 @@ VERSION := $(shell git describe) all: build -build: +build-stamp: go build -a -v + touch $@ +build: build-stamp -install: - go install ./cmd/escargs +install-stamp: build + go install -v \ + -ldflags="X 'main.version=$(VERSION)'" \ + ./cmd/escargs + touch $@ +install: install-stamp escargs: build go build -v \ - -ldflags="-X 'main.version=$(VERSION)'" \ - ./cmd/escargs + -ldflags="-X 'main.version=$(VERSION)'" \ + ./cmd/escargs clean: - rm -rfv escargs + rm -f escargs + +distclean: clean + rm -f build-stamp install-stamp uninstall: - rm -v $(shell go env GOPATH)/bin/escargs + rm -fv $(shell go env GOPATH)/bin/escargs -.PHONY: build clean install uninstall +.PHONY: clean distclean install uninstall diff --git a/README.md b/README.md index 910bb25..84ca374 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ +# shellescape + ![Build](https://github.com/alessio/shellescape/workflows/Build/badge.svg) [![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/alessio/shellescape?tab=overview) [![sourcegraph](https://sourcegraph.com/github.com/alessio/shellescape/-/badge.svg)](https://sourcegraph.com/github.com/alessio/shellescape) [![codecov](https://codecov.io/gh/alessio/shellescape/branch/master/graph/badge.svg)](https://codecov.io/gh/alessio/shellescape) -[![Coverage](https://gocover.io/_badge/github.com/alessio/shellescape)](https://gocover.io/github.com/alessio/shellescape) [![Go Report Card](https://goreportcard.com/badge/github.com/alessio/shellescape)](https://goreportcard.com/report/github.com/alessio/shellescape) -# shellescape Escape arbitrary strings for safe use as command line arguments. + ## Contents of the package This package provides the `shellescape.Quote()` function that returns a @@ -32,7 +33,9 @@ import ( func main() { fmt.Printf("ls -l %s\n", os.Args[1]) } + ``` + _[See in Go Playground](https://play.golang.org/p/Wj2WoUfH_d)_ Especially when creating pipeline of commands which might end up being @@ -48,14 +51,16 @@ import ( "fmt" "os" - "gopkg.in/alessio/shellescape.v1" + "al.essio.dev/pkg/shellescape" ) func main() { fmt.Printf("ls -l %s\n", shellescape.Quote(os.Args[1])) } ``` -_[See in Go Playground](https://play.golang.org/p/HJ_CXgSrmp)_ + +_[See in Go Playground](https://go.dev/play/p/GeguukpSUTk)_ ## The escargs utility -__escargs__ reads lines from the standard input and prints shell-escaped versions. Unlinke __xargs__, blank lines on the standard input are not discarded. + +__escargs__ reads lines from the standard input and prints shell-escaped versions. Unlike __xargs__, blank lines on the standard input are not discarded. diff --git a/cmd/escargs/escargs.go b/cmd/escargs/escargs.go index 6df6602..324144a 100644 --- a/cmd/escargs/escargs.go +++ b/cmd/escargs/escargs.go @@ -1,17 +1,16 @@ // escargs reads lines from the standard input and prints shell-escaped -// versions. Unlinke xargs, blank lines on the standard input are not +// versions. Unlike xargs, blank lines on the standard input are not // discarded. package main import ( "bufio" - "bytes" "flag" "fmt" "log" "os" - "github.com/alessio/shellescape" + "al.essio.dev/pkg/shellescape" ) var ( @@ -63,7 +62,7 @@ func main() { } if nullSeparator { - scanner.Split(splitNullTerminatedItems) + scanner.Split(shellescape.ScanTokens) } for scanner.Scan() { @@ -82,25 +81,6 @@ func main() { } } -func splitNullTerminatedItems(data []byte, atEOF bool) (advance int, token []byte, err error) { - // Return nothing if at end of file and no data passed. - if atEOF && len(data) == 0 { - return 0, nil, nil - } - - // Find the index of the input of a null character. - if i := bytes.IndexByte(data, '\x00'); i >= 0 { - return i + 1, data[0:i], nil - } - // If we're at EOF, we have a final, non-terminated line. Return it. - if atEOF { - return len(data), data, nil - } - - // Request more data. - return 0, nil, nil -} - func usage() { usageString := `Usage: escargs [-0ad] Escape arbitrary strings for safe use as command line arguments. @@ -113,5 +93,5 @@ Options:` func outputVersion() { fmt.Fprintf(os.Stderr, "escargs version %s\n", version) - fmt.Fprintln(os.Stderr, "Copyright (C) 2020-2023 Alessio Treglia ") + fmt.Fprintln(os.Stderr, "Copyright (C) 2020-2024 Alessio Treglia ") } diff --git a/example_test.go b/example_test.go index d9cb344..615ab5f 100644 --- a/example_test.go +++ b/example_test.go @@ -1,11 +1,13 @@ package shellescape_test import ( + "bufio" "fmt" "strings" - "github.com/alessio/shellescape" "github.com/google/shlex" + + "al.essio.dev/pkg/shellescape" ) func ExampleQuote() { @@ -79,6 +81,7 @@ func ExampleQuoteCommand() { fmt.Println("lastSplit[1]:", lastSplit[1]) fmt.Println("lastSplit[2]:", lastSplit[2]) + // Output: // unsafe: ls -l myfile; rm -rf / // command: ls -l 'myfile; rm -rf /' // splitCommand: [ls -l myfile; rm -rf /] @@ -100,3 +103,18 @@ func ExampleStripUnsafe() { // safe: "printable!" #$%^characters '' 12321312" // unsafe: these runes shall be removed: } + +func ExampleScanTokens() { + words := "'tis\x00but\x00a\x00scratch!\x00" + scanner := bufio.NewScanner(strings.NewReader(words)) + + scanner.Split(shellescape.ScanTokens) + for scanner.Scan() { + fmt.Println(scanner.Text()) + } + // Output: + // 'tis + // but + // a + // scratch! +} diff --git a/go.mod b/go.mod index 6a43f1a..f46d761 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/alessio/shellescape +module al.essio.dev/pkg/shellescape go 1.18 diff --git a/shellescape.go b/shellescape.go index dc34a55..fa1caf6 100644 --- a/shellescape.go +++ b/shellescape.go @@ -6,7 +6,7 @@ POSIX shells. The original Python package which this work was inspired by can be found at https://pypi.python.org/pypi/shellescape. */ -package shellescape // "import gopkg.in/alessio/shellescape.v1" +package shellescape // "import al.essio.dev/pkg/shellescape" /* The functionality provided by shellescape.Quote could be helpful @@ -15,6 +15,7 @@ be appended to/used in the context of shell programs' command line arguments. */ import ( + "bytes" "regexp" "strings" "unicode" @@ -64,3 +65,24 @@ func StripUnsafe(s string) string { return -1 }, s) } + +// ScanTokens is a split function for a bufio.Scanner that returns each word of text, stripped +// of amy trailing end-of-text empty byte. +func ScanTokens(data []byte, atEOF bool) (advance int, token []byte, err error) { + // Return nothing if at end-of-file and no data passed. + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + // Find the index of the input of a null character. + if i := bytes.IndexByte(data, '\x00'); i >= 0 { + return i + 1, data[0:i], nil + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + + // Request more data. + return 0, nil, nil +} diff --git a/shellescape_test.go b/shellescape_test.go index 41ae19c..3f8b629 100644 --- a/shellescape_test.go +++ b/shellescape_test.go @@ -1,12 +1,16 @@ package shellescape_test import ( + "bufio" + "bytes" "testing" - "github.com/alessio/shellescape" + "al.essio.dev/pkg/shellescape" ) func assertEqual(t *testing.T, s, expected string) { + t.Helper() + if s != expected { t.Fatalf("%q (expected: %q)", s, expected) } @@ -80,3 +84,22 @@ func TestStripUnsafe(t *testing.T) { }) } } + +func TestScanTokens(t *testing.T) { + data := [][]byte{[]byte("foo"), []byte("bar"), []byte("baz")} + buf := bytes.NewBuffer(bytes.Join(data, []byte{'\x00'})) + want := []string{"foo", "bar", "baz"} + + scanner := bufio.NewScanner(buf) + scanner.Split(shellescape.ScanTokens) + + for i := 0; scanner.Scan(); i++ { + if got := scanner.Text(); got != want[i] { + t.Errorf("scanner.Text() = %v, want %v", got, want[i]) + } + } + + if err := scanner.Err(); err != nil { + t.Errorf("scanner.Err() = %v, want nil", err) + } +}