diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg
index ac4f67b..a9e856e 100644
--- a/assets/coverage-badge.svg
+++ b/assets/coverage-badge.svg
@@ -11,7 +11,7 @@
coverage
coverage
- 19.0%
- 19.0%
+ 19.3%
+ 19.3%
diff --git a/internal/shutils/helpers/files_find.go b/internal/shutils/helpers/files_find.go
new file mode 100644
index 0000000..dcdea12
--- /dev/null
+++ b/internal/shutils/helpers/files_find.go
@@ -0,0 +1,208 @@
+// 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"
+
+ "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)
+
+ slog.Debug("find-files", "pattern", searchPath)
+
+ matches, err := filepath.Glob(searchPath)
+ if err != nil {
+ slog.Warn("find-files: invalid glob pattern", "pattern", globPattern, "error", err)
+ continue
+ }
+
+ for _, match := range matches {
+ info, err := os.Lstat(match)
+ if err != nil {
+ continue
+ }
+
+ if info.Mode()&os.ModeSymlink != 0 {
+ continue
+ }
+
+ if info.IsDir() {
+ err := filepath.Walk(match, func(walkPath string, walkInfo os.FileInfo, walkErr error) error {
+ if walkErr != nil {
+ return nil
+ }
+ if walkInfo.Mode()&os.ModeSymlink != 0 {
+ return nil
+ }
+
+ relPath, err := makeRelativePath(hc.Dir, walkPath)
+ if err != nil {
+ return nil
+ }
+ foundFiles = append(foundFiles, relPath)
+ return nil
+ })
+ if err != nil {
+ slog.Warn("find-files: error walking directory", "path", match, "error", err)
+ continue
+ }
+ } else {
+ relPath, err := makeRelativePath(hc.Dir, 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..e4cf862 100644
--- a/internal/shutils/helpers/helpers_internal_test.go
+++ b/internal/shutils/helpers/helpers_internal_test.go
@@ -214,3 +214,70 @@ files-find-lang ` + tc.args
})
}
}
+
+func TestFindFiles(t *testing.T) {
+ tests := []testCase{
+ {
+ name: "All dirs",
+ dirsToCreate: []string{
+ "usr/share/locale/ru/LC_MESSAGES",
+ "usr/share/locale/tr/LC_MESSAGES",
+ },
+ 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",
+ },
+ 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",
+ },
+ args: "\"/usr/share/locale/*/LC_MESSAGES/*.mo\"",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ tempDir, err := os.MkdirTemp("", "test-files-find-lang")
+ 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)
+ }
+
+ 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)
+ })
+ }
+}