package main import ( "encoding/csv" "flag" "fmt" "io" "os" "path/filepath" "strings" "github.com/xuri/excelize/v2" "golang.org/x/text/encoding/charmap" "golang.org/x/text/transform" "code.beautifulmachines.dev/jakoubek/csv2excel/internal/version" ) func main() { sep := flag.String("sep", "auto", "Trennzeichen: auto, ',', ';', '\\t'") enc := flag.String("enc", "utf8", "Encoding: utf8, windows1252") out := flag.String("o", "output.xlsx", "Ausgabedatei") showVersion := flag.Bool("version", false, "Version anzeigen") flag.Parse() if *showVersion { fmt.Printf("csv2excel %s (commit %s, built %s)\n", version.Version, version.Commit, version.BuildDate) os.Exit(0) } files := flag.Args() if len(files) == 0 { fmt.Fprintln(os.Stderr, "Verwendung: csv2xlsx [flags] datei1.csv datei2.csv ...") os.Exit(1) } if err := run(files, *out, *sep, *enc); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run(files []string, out, sep, enc string) error { xlsx := excelize.NewFile() firstSheet := true for _, path := range files { sheetName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) // Excelize: max 31 Zeichen, keine Sonderzeichen if len(sheetName) > 31 { sheetName = sheetName[:31] } f, err := os.Open(path) if err != nil { fmt.Fprintf(os.Stderr, "Fehler beim Öffnen %s: %v\n", path, err) continue } defer f.Close() var reader io.Reader = f if enc == "windows1252" { reader = transform.NewReader(f, charmap.Windows1252.NewDecoder()) } delimiter := detectDelimiter(path, sep) csvReader := csv.NewReader(reader) csvReader.Comma = delimiter csvReader.LazyQuotes = true // tolerant bei kaputten CSV-Dateien if firstSheet { xlsx.SetSheetName("Sheet1", sheetName) firstSheet = false } else { xlsx.NewSheet(sheetName) } row := 1 for { record, err := csvReader.Read() if err == io.EOF { break } if err != nil { fmt.Fprintf(os.Stderr, "Warnung Zeile %d in %s: %v\n", row, path, err) continue } for col, val := range record { cell, _ := excelize.CoordinatesToCellName(col+1, row) xlsx.SetCellValue(sheetName, cell, val) } row++ } fmt.Printf("✓ %s → Reiter \"%s\" (%d Zeilen)\n", path, sheetName, row-1) } if err := xlsx.SaveAs(out); err != nil { return fmt.Errorf("Fehler beim Speichern: %w", err) } fmt.Printf("✅ Gespeichert: %s\n", out) return nil } func detectDelimiter(path, hint string) rune { switch hint { case ";": return ';' case "\t": return '\t' case ",": return ',' } // auto-detect: erste Zeile lesen und zählen f, err := os.Open(path) if err != nil { return ';' } defer f.Close() buf := make([]byte, 4096) n, _ := f.Read(buf) line := string(buf[:n]) if idx := strings.Index(line, "\n"); idx > 0 { line = line[:idx] } sc := strings.Count(line, ";") cc := strings.Count(line, ",") tc := strings.Count(line, "\t") if tc >= sc && tc >= cc { return '\t' } if sc >= cc { return ';' } return ',' }