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..b1f2bc4 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, } @@ -265,114 +265,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) + }) + } +}