fix: add find-files (#109)
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Update alr-git / changelog (push) Successful in 31s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Update alr-git / changelog (push) Successful in 31s
				
			closes #96 Reviewed-on: #109 Co-authored-by: Maxim Slipenko <no-reply@maxim.slipenko.com> Co-committed-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
This commit is contained in:
		
							
								
								
									
										178
									
								
								internal/shutils/helpers/files_find.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								internal/shutils/helpers/files_find.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| 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 | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user