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)
+ })
+ }
+}