csv2excel/main.go
Oliver Jakoubek 88aa81ff46 fix: make -o flag robust against PowerShell argument splitting
PowerShell splits `-o=file.xlsx` into `-o=file` + `.xlsx` as separate
args. Fix: auto-append .xlsx if output has no extension, and filter
out .xlsx files from input args with a helpful warning.
2026-03-05 12:05:12 +01:00

156 lines
3.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
// PowerShell kann "-o=testdatei.xlsx" in "-o=testdatei" + ".xlsx" splitten
if filepath.Ext(*out) == "" {
*out += ".xlsx"
}
files := flag.Args()
if len(files) == 0 {
fmt.Fprintln(os.Stderr, "Verwendung: csv2excel [flags] datei1.csv datei2.csv ...")
fmt.Fprintln(os.Stderr, "Tipp: In PowerShell Leerzeichen statt = verwenden: -o ausgabe.xlsx")
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
var csvFiles []string
for _, f := range files {
if strings.EqualFold(filepath.Ext(f), ".xlsx") {
fmt.Fprintf(os.Stderr, "Warnung: %s übersprungen Excel-Datei als Input? Meintest du: -o=%s\n", f, f)
continue
}
csvFiles = append(csvFiles, f)
}
files = csvFiles
if len(files) == 0 {
return fmt.Errorf("keine CSV-Dateien zum Verarbeiten")
}
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 ','
}