From 25d001c1c930038ed89bd2573c19460bc087c4b0 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Thu, 19 Jun 2025 09:03:37 +0000 Subject: [PATCH] fix: add find-files (#109) closes #96 Reviewed-on: https://gitea.plemya-x.ru/Plemya-x/ALR/pulls/109 Co-authored-by: Maxim Slipenko Co-committed-by: Maxim Slipenko --- assets/coverage-badge.svg | 4 +- go.mod | 2 +- go.sum | 9 +- internal/logger/hclog.go | 2 + internal/shutils/helpers/files_find.go | 178 ++++++++++++++++++ internal/shutils/helpers/helpers.go | 111 +---------- .../shutils/helpers/helpers_internal_test.go | 107 ++++++++++- 7 files changed, 289 insertions(+), 124 deletions(-) create mode 100644 internal/shutils/helpers/files_find.go diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg index ac4f67b..ffdf7ae 100644 --- a/assets/coverage-badge.svg +++ b/assets/coverage-badge.svg @@ -11,7 +11,7 @@ coverage coverage - 19.0% - 19.0% + 19.4% + 19.4% diff --git a/go.mod b/go.mod index cab744e..3668d8a 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/PuerkitoBio/purell v1.2.0 github.com/alecthomas/chroma/v2 v2.9.1 + github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/caarlos0/env v3.5.0+incompatible github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbletea v1.2.4 @@ -22,7 +23,6 @@ require ( github.com/hashicorp/go-hclog v0.14.1 github.com/hashicorp/go-plugin v1.6.3 github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 - github.com/jmoiron/sqlx v1.3.5 github.com/leonelquinteros/gotext v1.7.0 github.com/mattn/go-isatty v0.0.20 github.com/mholt/archiver/v4 v4.0.0-alpha.8 diff --git a/go.sum b/go.sum index b0d5f39..760ee31 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM= github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8= github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY= @@ -156,7 +158,6 @@ github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -251,8 +252,6 @@ github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3m github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -281,9 +280,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQiI8Ht/8= github.com/leonelquinteros/gotext v1.7.0/go.mod h1:qJdoQuERPpccw7L70uoU+K/BvTfRBHYsisCQyFLXyvw= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= @@ -302,7 +298,6 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= diff --git a/internal/logger/hclog.go b/internal/logger/hclog.go index d991ab8..f638ab6 100644 --- a/internal/logger/hclog.go +++ b/internal/logger/hclog.go @@ -65,6 +65,8 @@ func (a *HCLoggerAdapter) Log(level hclog.Level, msg string, args ...interface{} var chLogLevel chLog.Level if msg == "plugin process exited" || strings.HasPrefix(msg, "[ERR] plugin: stream copy 'stderr' error") || + strings.HasPrefix(msg, "[WARN] error closing client during Kill") || + strings.HasPrefix(msg, "[WARN] plugin failed to exit gracefully") || strings.HasPrefix(msg, "[DEBUG] plugin") { chLogLevel = chLog.DebugLevel } else { diff --git a/internal/shutils/helpers/files_find.go b/internal/shutils/helpers/files_find.go new file mode 100644 index 0000000..2ef0df1 --- /dev/null +++ b/internal/shutils/helpers/files_find.go @@ -0,0 +1,178 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package helpers + +import ( + "fmt" + "log/slog" + "os" + "path" + "path/filepath" + + "github.com/bmatcuk/doublestar/v4" + "mvdan.cc/sh/v3/interp" +) + +func matchNamePattern(name, pattern string) bool { + matched, err := filepath.Match(pattern, name) + if err != nil { + return false + } + return matched +} + +func validateDir(dirPath, commandName string) error { + info, err := os.Stat(dirPath) + if err != nil { + return fmt.Errorf("%s: %w", commandName, err) + } + if !info.IsDir() { + return fmt.Errorf("%s: %s is not a directory", commandName, dirPath) + } + return nil +} + +func outputFiles(hc interp.HandlerContext, files []string) { + for _, file := range files { + fmt.Fprintln(hc.Stdout, file) + } +} + +func makeRelativePath(basePath, fullPath string) (string, error) { + relPath, err := filepath.Rel(basePath, fullPath) + if err != nil { + return "", err + } + return "./" + relPath, nil +} + +func filesFindLangCmd(hc interp.HandlerContext, cmd string, args []string) error { + namePattern := "*.mo" + if len(args) > 0 { + namePattern = args[0] + ".mo" + } + + localePath := "./usr/share/locale/" + realPath := path.Join(hc.Dir, localePath) + + if err := validateDir(realPath, "files-find-lang"); err != nil { + return err + } + + var langFiles []string + err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { + relPath, relErr := makeRelativePath(hc.Dir, p) + if relErr != nil { + return relErr + } + langFiles = append(langFiles, relPath) + } + return nil + }) + if err != nil { + return fmt.Errorf("files-find-lang: %w", err) + } + + outputFiles(hc, langFiles) + return nil +} + +func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error { + namePattern := "*" + if len(args) > 0 { + namePattern = args[0] + } + + docPath := "./usr/share/doc/" + docRealPath := path.Join(hc.Dir, docPath) + + if err := validateDir(docRealPath, "files-find-doc"); err != nil { + return err + } + + var docFiles []string + + entries, err := os.ReadDir(docRealPath) + if err != nil { + return fmt.Errorf("files-find-doc: %w", err) + } + + for _, entry := range entries { + if matchNamePattern(entry.Name(), namePattern) { + targetPath := filepath.Join(docRealPath, entry.Name()) + targetInfo, err := os.Stat(targetPath) + if err != nil { + return fmt.Errorf("files-find-doc: %w", err) + } + if targetInfo.IsDir() { + err := filepath.Walk(targetPath, func(subPath string, subInfo os.FileInfo, subErr error) error { + if subErr != nil { + return subErr + } + relPath, err := makeRelativePath(hc.Dir, subPath) + if err != nil { + return err + } + docFiles = append(docFiles, relPath) + return nil + }) + if err != nil { + return fmt.Errorf("files-find-doc: %w", err) + } + } + } + } + + outputFiles(hc, docFiles) + return nil +} + +func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error { + if len(args) == 0 { + return fmt.Errorf("find-files: at least one glob pattern is required") + } + + var foundFiles []string + + for _, globPattern := range args { + searchPath := path.Join(hc.Dir, globPattern) + + basepath, pattern := doublestar.SplitPattern(searchPath) + fsys := os.DirFS(basepath) + matches, err := doublestar.Glob(fsys, pattern, doublestar.WithNoFollow()) + if err != nil { + slog.Warn("find-files: invalid glob pattern", "pattern", globPattern, "error", err) + continue + } + + for _, match := range matches { + relPath, err := makeRelativePath(hc.Dir, path.Join(basepath, match)) + if err != nil { + continue + } + foundFiles = append(foundFiles, relPath) + } + } + + outputFiles(hc, foundFiles) + return nil +} diff --git a/internal/shutils/helpers/helpers.go b/internal/shutils/helpers/helpers.go index 7a4bd36..89a04a6 100644 --- a/internal/shutils/helpers/helpers.go +++ b/internal/shutils/helpers/helpers.go @@ -24,7 +24,6 @@ import ( "fmt" "io" "os" - "path" "path/filepath" "strconv" "strings" @@ -57,6 +56,7 @@ var Helpers = handlers.ExecFuncs{ "install-library": installLibraryCmd, "git-version": gitVersionCmd, + "files-find": filesFindCmd, "files-find-lang": filesFindLangCmd, "files-find-doc": filesFindDocCmd, } @@ -65,6 +65,7 @@ var Helpers = handlers.ExecFuncs{ // that don't modify any state var Restricted = handlers.ExecFuncs{ "git-version": gitVersionCmd, + "files-find": filesFindCmd, "files-find-lang": filesFindLangCmd, "files-find-doc": filesFindDocCmd, } @@ -265,114 +266,6 @@ func gitVersionCmd(hc interp.HandlerContext, cmd string, args []string) error { return nil } -func filesFindLangCmd(hc interp.HandlerContext, cmd string, args []string) error { - namePattern := "*.mo" - if len(args) > 0 { - namePattern = args[0] + ".mo" - } - - localePath := "./usr/share/locale/" - realPath := path.Join(hc.Dir, localePath) - - info, err := os.Stat(realPath) - if err != nil { - return fmt.Errorf("files-find-lang: %w", err) - } - if !info.IsDir() { - return fmt.Errorf("files-find-lang: %s is not a directory", localePath) - } - - var langFiles []string - err = filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { - relPath, relErr := filepath.Rel(hc.Dir, p) - if relErr != nil { - return relErr - } - langFiles = append(langFiles, "./"+relPath) - } - return nil - }) - if err != nil { - return fmt.Errorf("files-find-lang: %w", err) - } - - for _, file := range langFiles { - fmt.Fprintln(hc.Stdout, file) - } - - return nil -} - -func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error { - namePattern := "*" - if len(args) > 0 { - namePattern = args[0] - } - - docPath := "./usr/share/doc/" - docRealPath := path.Join(hc.Dir, docPath) - - info, err := os.Stat(docRealPath) - if err != nil { - return fmt.Errorf("files-find-doc: %w", err) - } - if !info.IsDir() { - return fmt.Errorf("files-find-doc: %s is not a directory", docPath) - } - - var docFiles []string - - entries, err := os.ReadDir(docRealPath) - if err != nil { - return err - } - for _, entry := range entries { - if matchNamePattern(entry.Name(), namePattern) { - targetPath := filepath.Join(docRealPath, entry.Name()) - targetInfo, err := os.Stat(targetPath) - if err != nil { - return err - } - if targetInfo.IsDir() { - err := filepath.Walk(targetPath, func(subPath string, subInfo os.FileInfo, subErr error) error { - relPath, err := filepath.Rel(hc.Dir, subPath) - if err != nil { - return err - } - docFiles = append(docFiles, "./"+relPath) - return nil - }) - if err != nil { - return err - } - } - } - } - - if err != nil { - return fmt.Errorf("files-find-doc: %w", err) - } - - for _, file := range docFiles { - fmt.Fprintln(hc.Stdout, file) - } - - return nil -} - -func matchNamePattern(name, pattern string) bool { - matched, err := filepath.Match(pattern, name) - if err != nil { - return false - } - return matched -} - func helperInstall(from, to string, perms os.FileMode) error { err := os.MkdirAll(filepath.Dir(to), 0o755) if err != nil { diff --git a/internal/shutils/helpers/helpers_internal_test.go b/internal/shutils/helpers/helpers_internal_test.go index d6599a3..1f25d1d 100644 --- a/internal/shutils/helpers/helpers_internal_test.go +++ b/internal/shutils/helpers/helpers_internal_test.go @@ -31,12 +31,18 @@ import ( "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers" ) +type symlink struct { + linkPath string + targetPath string +} + type testCase struct { - name string - dirsToCreate []string - filesToCreate []string - expectedOutput []string - args string + name string + dirsToCreate []string + filesToCreate []string + expectedOutput []string + symlinksToCreate []symlink + args string } func TestFindFilesDoc(t *testing.T) { @@ -214,3 +220,94 @@ files-find-lang ` + tc.args }) } } + +func TestFindFiles(t *testing.T) { + tests := []testCase{ + { + name: "With file and dir symlinks", + dirsToCreate: []string{ + "usr/share/locale/ru/LC_MESSAGES", + "usr/share/locale/tr/LC_MESSAGES", + "opt/app", + "opt/app/internal", + }, + filesToCreate: []string{ + "usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo", + "usr/share/locale/ru/LC_MESSAGES/yandex-disk-indicator.mo", + "usr/share/locale/tr/LC_MESSAGES/yandex-disk.mo", + "opt/app/internal/test", + }, + symlinksToCreate: []symlink{ + { + linkPath: "/opt/app/etc", + targetPath: "/etc", + }, + }, + expectedOutput: []string{ + "./usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo", + "./usr/share/locale/ru/LC_MESSAGES/yandex-disk-indicator.mo", + "./usr/share/locale/tr/LC_MESSAGES/yandex-disk.mo", + "./opt/app/etc", + "./opt/app/internal", + "./opt/app/internal/test", + }, + args: "\"/usr/share/locale/*/LC_MESSAGES/*.mo\" \"/opt/app/**/*\"", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tempDir, err := os.MkdirTemp("", "test-files-find") + assert.NoError(t, err) + defer os.RemoveAll(tempDir) + + for _, dir := range tc.dirsToCreate { + dirPath := filepath.Join(tempDir, dir) + err := os.MkdirAll(dirPath, 0o755) + assert.NoError(t, err) + } + + for _, file := range tc.filesToCreate { + filePath := filepath.Join(tempDir, file) + err := os.WriteFile(filePath, []byte("test content"), 0o644) + assert.NoError(t, err) + } + + for _, sl := range tc.symlinksToCreate { + linkFullPath := filepath.Join(tempDir, sl.linkPath) + targetFullPath := sl.targetPath + + // make sure parent dir exists + err := os.MkdirAll(filepath.Dir(linkFullPath), 0o755) + assert.NoError(t, err) + + err = os.Symlink(targetFullPath, linkFullPath) + assert.NoError(t, err) + } + + helpers := handlers.ExecFuncs{ + "files-find": filesFindCmd, + } + buf := &bytes.Buffer{} + runner, err := interp.New( + interp.Dir(tempDir), + interp.StdIO(os.Stdin, buf, os.Stderr), + interp.ExecHandler(helpers.ExecHandler(interp.DefaultExecHandler(1000))), + ) + assert.NoError(t, err) + + scriptContent := ` +shopt -s globstar +files-find ` + tc.args + + script, err := syntax.NewParser().Parse(strings.NewReader(scriptContent), "") + assert.NoError(t, err) + + err = runner.Run(context.Background(), script) + assert.NoError(t, err) + + contents := strings.Fields(strings.TrimSpace(buf.String())) + assert.ElementsMatch(t, tc.expectedOutput, contents) + }) + } +}