diff --git a/cora.go b/cora.go index 80ce17e..20672a2 100644 --- a/cora.go +++ b/cora.go @@ -1,22 +1,16 @@ package main import ( - "bufio" "fmt" - "io" "io/fs" - "log" "os" "path/filepath" "strings" + "github.com/shaharia-lab/cora/pkg/concatenator" "github.com/spf13/cobra" ) -const ( - defaultBufferSize = 64 * 1024 // 64KB -) - func main() { if err := newRootCmd().Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) @@ -34,7 +28,7 @@ func newRootCmd() *cobra.Command { RunE: cfg.run, } - rootCmd.Flags().StringVarP(&cfg.SourceDirectory, "source", "s", "", "Source directory to concatenate files from") + rootCmd.Flags().StringVarP(&cfg.SourceDirectory, "source", "s", "", "Source directory to Concatenate files from") rootCmd.Flags().StringVarP(&cfg.OutputFile, "output", "o", "", "Output file to write concatenated files to") rootCmd.Flags().StringSliceVarP(&cfg.ExcludePatterns, "exclude", "e", nil, "Glob patterns to exclude") rootCmd.Flags().StringSliceVarP(&cfg.IncludePatterns, "include", "i", nil, "Glob patterns to include") @@ -60,9 +54,9 @@ func (cfg *config) run(cmd *cobra.Command, args []string) error { return err } - debugLog := newDebugLog(cfg.EnableDebugging) + debugLog := concatenator.NewDebugLog(cfg.EnableDebugging) w := newWalker(cfg.SourceDirectory, cfg.ExcludePatterns, cfg.IncludePatterns, debugLog) - c := newConcatenator(cfg.OutputFile, cfg.Separator, cfg.PathPrefix, debugLog) + c := concatenator.NewConcatenation(cfg.OutputFile, cfg.Separator, cfg.PathPrefix, debugLog) return cfg.process(w, c) } @@ -77,14 +71,14 @@ func (cfg *config) validate() error { return nil } -func (cfg *config) process(w *walker, c *concatenator) error { +func (cfg *config) process(w *walker, c *concatenator.Concatenator) error { filePaths, err := w.walk() if err != nil { return fmt.Errorf("failed to walk directory: %w", err) } - if err := c.concatenate(filePaths); err != nil { - return fmt.Errorf("failed to concatenate files: %w", err) + if err := c.Concatenate(filePaths); err != nil { + return fmt.Errorf("failed to Concatenate files: %w", err) } return nil @@ -94,10 +88,10 @@ type walker struct { sourceDirectory string excludePatterns []string includePatterns []string - debugLog *debugLog + debugLog *concatenator.DebugLog } -func newWalker(sourceDirectory string, excludePatterns, includePatterns []string, debugLog *debugLog) *walker { +func newWalker(sourceDirectory string, excludePatterns, includePatterns []string, debugLog *concatenator.DebugLog) *walker { return &walker{ sourceDirectory: sourceDirectory, excludePatterns: excludePatterns, @@ -120,7 +114,7 @@ func (w *walker) walk() ([]string, error) { } if excluded { - w.debugLog.print(fmt.Sprintf("Excluding %s", path)) + w.debugLog.Print(fmt.Sprintf("Excluding %s", path)) if d.IsDir() { return filepath.SkipDir } @@ -135,13 +129,13 @@ func (w *walker) walk() ([]string, error) { } if included { files = append(files, path) - w.debugLog.print(fmt.Sprintf("Including %s", path)) + w.debugLog.Print(fmt.Sprintf("Including %s", path)) } else { - w.debugLog.print(fmt.Sprintf("Skipping %s (not in include patterns)", path)) + w.debugLog.Print(fmt.Sprintf("Skipping %s (not in include patterns)", path)) } } else { files = append(files, path) - w.debugLog.print(fmt.Sprintf("Including %s", path)) + w.debugLog.Print(fmt.Sprintf("Including %s", path)) } } @@ -151,90 +145,6 @@ func (w *walker) walk() ([]string, error) { return files, err } -type concatenator struct { - outputPath string - separator []byte - debugLog *debugLog - pathPrefix []byte -} - -func newConcatenator(outputPath, separator, pathPrefix string, debugLog *debugLog) *concatenator { - return &concatenator{ - outputPath: outputPath, - separator: []byte(separator), - pathPrefix: []byte(pathPrefix), - debugLog: debugLog, - } -} - -func (c *concatenator) concatenate(filePaths []string) error { - if err := os.MkdirAll(filepath.Dir(c.outputPath), 0755); err != nil { - return fmt.Errorf("failed to create output directory: %w", err) - } - - outFile, err := os.Create(c.outputPath) - if err != nil { - return fmt.Errorf("failed to create output file: %w", err) - } - defer outFile.Close() - - writer := bufio.NewWriterSize(outFile, defaultBufferSize) - defer writer.Flush() - - for i, filePath := range filePaths { - if i > 0 { - if _, err := writer.Write(c.separator); err != nil { - return fmt.Errorf("failed to write separator: %w", err) - } - } - - if err := c.writeFileHeader(writer, filePath); err != nil { - return err - } - - if err := c.appendFileContent(writer, filePath); err != nil { - return err - } - - if _, err := writer.Write([]byte{'\n'}); err != nil { - return fmt.Errorf("failed to write newline after file content: %w", err) - } - } - - return nil -} - -func (c *concatenator) writeFileHeader(writer *bufio.Writer, filePath string) error { - if _, err := writer.Write(c.pathPrefix); err != nil { - return fmt.Errorf("failed to write path prefix: %w", err) - } - - if _, err := writer.WriteString(filePath); err != nil { - return fmt.Errorf("failed to write file path: %w", err) - } - - if _, err := writer.Write([]byte{'\n'}); err != nil { - return fmt.Errorf("failed to write newline after file path: %w", err) - } - - return nil -} - -func (c *concatenator) appendFileContent(writer *bufio.Writer, filePath string) error { - file, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open file %s: %w", filePath, err) - } - defer file.Close() - - _, err = io.Copy(writer, file) - if err != nil { - return fmt.Errorf("failed to copy content from %s: %w", filePath, err) - } - - return nil -} - func matchesGlob(rootPath, filePath string, patterns []string) (bool, error) { relPath, err := filepath.Rel(rootPath, filePath) if err != nil { @@ -267,21 +177,3 @@ func matchesGlob(rootPath, filePath string, patterns []string) (bool, error) { return false, nil } - -type debugLog struct { - enabled bool -} - -func newDebugLog(enabled bool) *debugLog { - return &debugLog{ - enabled: enabled, - } -} - -func (d *debugLog) print(message string) { - if !d.enabled { - return - } - - log.Println(message) -} diff --git a/cora_test.go b/cora_test.go index c93a8cb..b8759bf 100644 --- a/cora_test.go +++ b/cora_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "testing" + "github.com/shaharia-lab/cora/pkg/concatenator" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -26,9 +27,9 @@ func TestConcatenator(t *testing.T) { require.NoError(t, err) } - debugLog := newDebugLog(false) - concatenator := newConcatenator(outputFile, "\n---\n", "File: ", debugLog) - err := concatenator.concatenate(inputFiles) + debugLog := concatenator.NewDebugLog(false) + conc := concatenator.NewConcatenation(outputFile, "\n---\n", "File: ", debugLog) + err := conc.Concatenate(inputFiles) require.NoError(t, err) content, err := os.ReadFile(outputFile) @@ -60,9 +61,9 @@ func TestConcatenatorLargeFiles(t *testing.T) { require.NoError(t, err) } - debugLog := newDebugLog(false) - concatenator := newConcatenator(outputFile, "\n", "File: ", debugLog) - err := concatenator.concatenate(inputFiles) + debugLog := concatenator.NewDebugLog(false) + conc := concatenator.NewConcatenation(outputFile, "\n", "File: ", debugLog) + err := conc.Concatenate(inputFiles) require.NoError(t, err) stat, err := os.Stat(outputFile) @@ -85,7 +86,7 @@ func TestWalker(t *testing.T) { root := t.TempDir() createTestFiles(t, root) - debugLog := newDebugLog(false) + debugLog := concatenator.NewDebugLog(false) tests := []struct { name string @@ -207,14 +208,14 @@ func TestDebugLog(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - debugLog := newDebugLog(tt.enabled) + debugLog := concatenator.NewDebugLog(tt.enabled) // Capture log output var buf bytes.Buffer log.SetOutput(&buf) defer log.SetOutput(os.Stderr) - debugLog.print(tt.message) + debugLog.Print(tt.message) if tt.enabled { assert.Contains(t, buf.String(), tt.message) diff --git a/pkg/concatenator/concatenator.go b/pkg/concatenator/concatenator.go new file mode 100644 index 0000000..ec05ad1 --- /dev/null +++ b/pkg/concatenator/concatenator.go @@ -0,0 +1,119 @@ +package concatenator + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path/filepath" +) + +const ( + defaultBufferSize = 64 * 1024 // 64KB +) + +// Concatenator concatenates files into a single file. +type Concatenator struct { + outputPath string + separator []byte + debugLog *DebugLog + pathPrefix []byte +} + +// NewConcatenation creates a new Concatenator. +func NewConcatenation(outputPath, separator, pathPrefix string, debugLog *DebugLog) *Concatenator { + return &Concatenator{ + outputPath: outputPath, + separator: []byte(separator), + pathPrefix: []byte(pathPrefix), + debugLog: debugLog, + } +} + +// Concatenate concatenates the files specified by filePaths into a single file. +func (c *Concatenator) Concatenate(filePaths []string) error { + if err := os.MkdirAll(filepath.Dir(c.outputPath), 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + outFile, err := os.Create(c.outputPath) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer outFile.Close() + + writer := bufio.NewWriterSize(outFile, defaultBufferSize) + defer writer.Flush() + + for i, filePath := range filePaths { + if i > 0 { + if _, err := writer.Write(c.separator); err != nil { + return fmt.Errorf("failed to write separator: %w", err) + } + } + + if err := c.writeFileHeader(writer, filePath); err != nil { + return err + } + + if err := c.appendFileContent(writer, filePath); err != nil { + return err + } + + if _, err := writer.Write([]byte{'\n'}); err != nil { + return fmt.Errorf("failed to write newline after file content: %w", err) + } + } + + return nil +} + +func (c *Concatenator) appendFileContent(writer *bufio.Writer, filePath string) error { + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", filePath, err) + } + defer file.Close() + + _, err = io.Copy(writer, file) + if err != nil { + return fmt.Errorf("failed to copy content from %s: %w", filePath, err) + } + + return nil +} + +func (c *Concatenator) writeFileHeader(writer *bufio.Writer, filePath string) error { + if _, err := writer.Write(c.pathPrefix); err != nil { + return fmt.Errorf("failed to write path prefix: %w", err) + } + + if _, err := writer.WriteString(filePath); err != nil { + return fmt.Errorf("failed to write file path: %w", err) + } + + if _, err := writer.Write([]byte{'\n'}); err != nil { + return fmt.Errorf("failed to write newline after file path: %w", err) + } + + return nil +} + +type DebugLog struct { + enabled bool +} + +func NewDebugLog(enabled bool) *DebugLog { + return &DebugLog{ + enabled: enabled, + } +} + +func (d *DebugLog) Print(message string) { + if !d.enabled { + return + } + + log.Println(message) +}