diff --git a/.github/workflows/ci-and-release.yml b/.github/workflows/ci-and-release.yml index a5a1e87..814a680 100644 --- a/.github/workflows/ci-and-release.yml +++ b/.github/workflows/ci-and-release.yml @@ -2,7 +2,7 @@ name: CI and Release on: push: - branches: + branches: - '**' tags: - 'v*' @@ -24,7 +24,10 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.24' + + - name: Tidy modules + run: go mod tidy - name: Run tests run: go test -v ./... @@ -46,7 +49,10 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.24' + + - name: Tidy modules + run: go mod tidy - name: Run tests run: go test -v ./... @@ -82,7 +88,7 @@ jobs: sha256sum * > checksums.txt - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: | dist/* @@ -90,5 +96,4 @@ jobs: draft: false prerelease: false env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/cli.go b/cli.go index b7b0601..eb09a1d 100644 --- a/cli.go +++ b/cli.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "runtime/pprof" "slices" ) @@ -16,6 +17,18 @@ func RunCLI(ctx context.Context, args []string, stdin io.Reader, stdout io.Write return fmt.Errorf("bad parameters: %w", err) } + if params.CPUProfile != "" { + f, err := os.Create(params.CPUProfile) + if err != nil { + return fmt.Errorf("could not create CPU profile: %w", err) + } + defer f.Close() + if err := pprof.StartCPUProfile(f); err != nil { + return fmt.Errorf("could not start CPU profile: %w", err) + } + defer pprof.StopCPUProfile() + } + replacer, err := replacer(params) if err != nil { return fmt.Errorf("cannot parse template: %w", err) diff --git a/cmd/patt/main.go b/cmd/patt/main.go index 7655e07..6d14a82 100644 --- a/cmd/patt/main.go +++ b/cmd/patt/main.go @@ -7,6 +7,8 @@ import ( "patt" ) +var version = "dev" + func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) defer stop() @@ -18,4 +20,4 @@ func exitIfErr(err error) { _, _ = os.Stderr.WriteString(err.Error() + "\n") os.Exit(1) } -} +} \ No newline at end of file diff --git a/cmd/profiling/main.go b/cmd/profiling/main.go deleted file mode 100644 index f824836..0000000 --- a/cmd/profiling/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "context" - "log" - "os" - "patt" - "runtime" - "runtime/pprof" -) - -func main() { - - // CPU profiling - f, err := os.Create("cpu.prof") - if err != nil { - log.Fatal(err) - } - defer f.Close() - - if err := pprof.StartCPUProfile(f); err != nil { - log.Fatal(err) - } - defer pprof.StopCPUProfile() - - args := []string{"patt", "[<_> <_>] [error] <_>", "", "./testdata/Apache_500MB.log"} - err = patt.RunCLI(context.Background(), args, os.Stdin, os.Stdout) - if err != nil { - log.Fatal(err) - } - - f, err = os.Create("mem.prof") - if err != nil { - log.Fatal(err) - } - defer f.Close() - - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.Fatal(err) - } -} diff --git a/params.go b/params.go index 28dbe04..12d9d73 100644 --- a/params.go +++ b/params.go @@ -11,10 +11,12 @@ type CLIParams struct { ReplaceTemplate string InputFiles []string Keep bool + CPUProfile string } // ParseCLIParams parses flags + positional args // +// // patt [flags] search_pattern [[more_search ...] replace_pattern] // [-- file1 [file2 ...]] // @@ -49,6 +51,10 @@ func ParseCLIParams(argsWithFlags []string) (CLIParams, error) { } cmd.Flags().BoolVarP(&out.Keep, "keep", "k", false, "print non‑matching lines") + cmd.Flags().StringVar(&out.CPUProfile, "cpu-profile", "", "write cpu profile to file") + if err := cmd.Flags().MarkHidden("cpu-profile"); err != nil { + return out, err + } if err := cmd.ParseFlags(argsWithFlags); err != nil { return out, err diff --git a/params_test.go b/params_test.go index a7d7270..51c9f16 100644 --- a/params_test.go +++ b/params_test.go @@ -79,6 +79,16 @@ func TestParseCLIParams_NoErrors(t *testing.T) { ReplaceTemplate: "template", }, }, + { + name: "cpu profile flag", + args: []string{"--cpu-profile=cpu.pprof", "pattern", "replacement", "--", "input.txt"}, + want: CLIParams{ + SearchPatterns: []string{"pattern"}, + ReplaceTemplate: "replacement", + InputFiles: []string{"input.txt"}, + CPUProfile: "cpu.pprof", + }, + }, } for _, tt := range tests { diff --git a/pattern.go b/pattern.go index 46f2ed2..ceb7f1f 100644 --- a/pattern.go +++ b/pattern.go @@ -147,4 +147,4 @@ func (m *MultiReplacer) Match(line []byte) bool { func (m *MultiReplacer) Replace(line []byte) []byte { return m.replacers[m.lastMatchedIx].Replace(line) -} +} \ No newline at end of file diff --git a/pattern/pattern.go b/pattern/pattern.go index 5e5aad9..01c7e92 100644 --- a/pattern/pattern.go +++ b/pattern/pattern.go @@ -12,8 +12,9 @@ var ( ) type Matcher struct { - e expr - names []string + e expr + names []string + longestLiteral []byte } func New(in string) (*Matcher, error) { @@ -24,9 +25,18 @@ func New(in string) (*Matcher, error) { if err := e.validate(); err != nil { return nil, err } + var longestLiteral []byte + for _, n := range e { + if l, ok := n.(literals); ok { + if len(l) > len(longestLiteral) { + longestLiteral = l + } + } + } return &Matcher{ - e: e, - names: e.captures(), + e: e, + names: e.captures(), + longestLiteral: longestLiteral, }, nil } @@ -41,7 +51,15 @@ func ParseLineFilter(in []byte) (*Matcher, error) { if err = e.validateNoConsecutiveCaptures(); err != nil { return nil, err } - return &Matcher{e: e}, nil + var longestLiteral []byte + for _, n := range e { + if l, ok := n.(literals); ok { + if len(l) > len(longestLiteral) { + longestLiteral = l + } + } + } + return &Matcher{e: e, longestLiteral: longestLiteral}, nil } func ParseLiterals(in string) ([][]byte, error) { @@ -137,9 +155,10 @@ func (m *Matcher) Names() []string { } func (m *Matcher) Test(in []byte) bool { - if len(in) == 0 || len(m.e) == 0 { - // An empty line can only match an empty pattern. - return len(in) == 0 && len(m.e) == 0 + if len(m.longestLiteral) > 0 { + if !bytes.Contains(in, m.longestLiteral) { + return false + } } var off int for i := range m.e { @@ -158,6 +177,10 @@ func (m *Matcher) Test(in []byte) bool { } off += j + len(lit) } + if len(in) == 0 || len(m.e) == 0 { + // An empty line can only match an empty pattern. + return len(in) == 0 && len(m.e) == 0 + } // If we end up on a literal, we only consider the test successful if // the remaining input is empty. Otherwise, if we end up on a capture, // the remainder (the captured text) must not be empty. @@ -170,4 +193,4 @@ func (m *Matcher) Test(in []byte) bool { _, reqRem := m.e[len(m.e)-1].(capture) hasRem := off != len(in) return reqRem == hasRem -} +} \ No newline at end of file