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.
156 lines
3.5 KiB
Go
156 lines
3.5 KiB
Go
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 ','
|
||
}
|