#!/usr/bin/env php handleTest($argc, $argv), 'transform' => handleTransform($argc, $argv), 'validate' => handleValidate($argc, $argv), 'auto-import' => handleAutoImport($argc, $argv), 'help', '-h', '--help' => showHelp(), default => throw new Exception("Unbekanntes Kommando: $command"), }; } catch (Exception $e) { fwrite(STDERR, "\n❌ ERROR: " . $e->getMessage() . "\n\n"); exit(1); } // ============================================================================ // COMMAND HANDLERS // ============================================================================ /** * Zeige Hilfe und Verwendungsanleitung */ function showHelp(): void { echo <<<'HELP' ╔════════════════════════════════════════════════════════════════════════════╗ ║ Firefly Import Preprocessor - Kommandozeilen-Tool ║ ║ ║ ║ Ein schlankes PHP 8 Tool zur Transformation von UBS E-Banking Exporten ║ ║ in ein Firefly III kompatibles Format. ║ ╚════════════════════════════════════════════════════════════════════════════╝ VERWENDUNG: transformer [command] [options] KOMMANDOS: test [input] [config] [options] Testet die Transformation mit limitierter Zeilenzahl Optionen: --rows=N Nur N Zeilen verarbeiten (Standard: 10) --output=FILE, -o Ergebnis auch in Datei schreiben Beispiel: transformer test ubs-export.csv config.json --rows=5 transformer test ubs-export.csv config.json -o test-output.csv transform [input] [config] [options] Transformiert eine komplette CSV-Datei Optionen: --output=FILE, -o Output-Pfad (Standard: input-transformed.csv) --no-import Nicht automatisch in Firefly III importieren Beispiel: transformer transform ubs-export.csv config.json transformer transform ubs-export.csv config.json -o import.csv validate [config] [options] Validiert die Konfigurationsdatei Optionen: --strict Strikte Validierung (empfohlen) Beispiel: transformer validate config.json transformer validate config.json --strict auto-import [config] [options] Überwacht Quellverzeichnis und verarbeitet neue Dateien Optionen: --watch Kontinuierliche Überwachung (Daemon-Modus) --interval=SEC Prüfintervall in Sekunden (Standard: 60) --dry-run Zeigt was gemacht würde (keine echte Verarbeitung) Beispiel: transformer auto-import config.json transformer auto-import config.json --watch --interval=30 help, -h, --help Zeige diese Hilfe GLOBALE OPTIONEN: --debug, -d Aktiviere Debug-Modus (detaillierte Ausgaben) INSTALLATION: 1. PHP 8.1+ muss installiert sein php --version 2. Autoloader-Setup (wähle eins): Option A: Mit Composer (empfohlen) composer install Option B: Manuell - Dateien in Verzeichnisstruktur: ff-imp-preprocessor/ ├── bin/transformer.php ├── src/*.php └── config/config.json 3. Ausführbar machen: chmod +x bin/transformer.php 4. Konfiguration anpassen: cp config/config.example.json config/config.json nano config/config.json BEISPIELE: # Transformation mit Test (erste 5 Zeilen) ./bin/transformer test data/ubs-export.csv config/config.json --rows=5 # Komplette Transformation ./bin/transformer transform data/ubs-export.csv config/config.json \ --output=output/firefly-import.csv # Konfiguration validieren ./bin/transformer validate config/config.json --strict # Auto-Import mit Überwachung starten ./bin/transformer auto-import config/config.json --watch # Nur nächste Datei verarbeiten ./bin/transformer auto-import config/config.json KONFIGURATION: Die config.json muss folgende Struktur haben: { "metadata": { "extractionRules": {...} }, "csvStructure": { "delimiter": ";", ... }, "columnTransformations": { ... }, "fireflyImport": { "apiUrl": "...", "apiKey": "..." }, "directories": { "source": "./import/source", "output": "./import/output", "archive": "./import/archive", "error": "./import/error" } } DOKUMENTATION: Siehe README.md und UBS_Transformer_Guide.md für vollständige Dokumentation LIZENZ: MIT License HELP; } /** * Expandiert ~ zu absolutem Home-Verzeichnis und löst relative Pfade auf */ function expandPath(string $path): string { if (str_starts_with($path, '~/') || $path === '~') { $home = getenv('HOME') ?: posix_getpwuid(posix_getuid())['dir']; $path = $home . substr($path, 1); } // Relative Pfade gegen cwd auflösen (ohne realpath, damit nicht-existierende Dirs erlaubt sind) if (!str_starts_with($path, '/')) { $path = getcwd() . '/' . $path; } return $path; } /** * Parse CLI-Optionen in assoziatives Array */ function parseOptions(array $argv, int $startIndex = 0): array { $options = []; for ($i = $startIndex; $i < count($argv); $i++) { if (strpos($argv[$i], '--') === 0) { $parts = explode('=', substr($argv[$i], 2), 2); $options[$parts[0]] = $parts[1] ?? true; } elseif (strpos($argv[$i], '-') === 0 && strlen($argv[$i]) > 1) { $options[substr($argv[$i], 1)] = true; } } return $options; } /** * Teste Transformation mit begrenzter Zeilenzahl */ function handleTest($argc, $argv): void { if ($argc < 4) { throw new Exception("Usage: transformer test [input-file] [config-file] [options]"); } $inputFile = $argv[2]; $configFile = $argv[3]; $options = parseOptions($argv, 4); $debug = isset($options['debug']) || isset($options['d']); $maxRows = isset($options['rows']) ? (int)$options['rows'] : 10; $outputFile = $options['output'] ?? $options['o'] ?? null; if (!file_exists($inputFile)) { throw new Exception("Input-Datei nicht gefunden: $inputFile"); } if (!file_exists($configFile)) { throw new Exception("Konfigurationsdatei nicht gefunden: $configFile"); } echo "\n📊 TEST-MODUS: Verarbeite max. $maxRows Zeilen\n"; echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; $configLoader = new ConfigurationLoader($configFile); $config = $configLoader->load(); $engine = new TransformerEngine($configLoader, $debug); $result = $engine->transform($inputFile, $maxRows); // Ausgabe Statistiken echo "\n✅ STATISTIKEN:\n"; echo " Verarbeitete Zeilen: " . $result['rowsProcessed'] . "\n"; echo " Metadaten extrahiert: " . count($result['metadata'] ?? []) . "\n"; echo " Output-Spalten: " . $result['outputColumns'] . "\n"; if (!empty($result['metadata'])) { echo "\n📋 EXTRAHIERTE METADATEN:\n"; foreach ($result['metadata'] as $key => $value) { $display = substr($value, 0, 50); if (strlen($value) > 50) { $display .= "..."; } echo " $key: $display\n"; } } if (!empty($result['sampleData'])) { $sampleCount = min(5, count($result['sampleData'])); echo "\n📝 BEISPIEL-DATEN ($sampleCount Zeilen):\n"; foreach (array_slice($result['sampleData'], 0, $sampleCount) as $index => $row) { echo " Zeile " . ($index + 1) . ": "; foreach ($row as $col => $value) { $val = substr($value, 0, 30); if (strlen($value) > 30) { $val .= "..."; } //if (!is_int($col)) { echo "$col=$val | "; //} } echo "\n"; } } if ($outputFile) { echo "\n💾 Output-Datei: $outputFile\n"; } echo "\n✅ Test erfolgreich!\n\n"; } /** * Transformiere komplette CSV-Datei */ function handleTransform($argc, $argv): void { if ($argc < 4) { throw new Exception("Usage: transformer transform [input-file] [config-file] [options]"); } $inputFile = $argv[2]; $configFile = $argv[3]; $options = parseOptions($argv, 4); $debug = isset($options['debug']) || isset($options['d']); $outputFile = $options['output'] ?? $options['o'] ?? null; if (!file_exists($inputFile)) { throw new Exception("Input-Datei nicht gefunden: $inputFile"); } if (!file_exists($configFile)) { throw new Exception("Konfigurationsdatei nicht gefunden: $configFile"); } echo "\n🚀 TRANSFORMATION STARTEN\n"; echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; $configLoader = new ConfigurationLoader($configFile); $configLoader->load(); // --output überschreibt Zielverzeichnis und Dateiname aus der Konfiguration if ($outputFile !== null) { $outputFile = expandPath($outputFile); $configLoader->set('directories.output', dirname($outputFile)); $configLoader->set('csvStructure.outputFilename', basename($outputFile)); } $engine = new TransformerEngine($configLoader, $debug); $result = $engine->transform($inputFile); echo "✅ Transformation erfolgreich!\n"; echo " Output-Datei: " . ($result['outputFile'] ?? 'N/A') . "\n"; echo " Zeilen transformiert: " . ($result['rowsProcessed'] ?? 0) . "\n"; echo "\n✅ Fertig!\n\n"; } /** * Validiere Konfigurationsdatei */ function handleValidate($argc, $argv): void { if ($argc < 3) { throw new Exception("Usage: transformer validate [config-file] [options]"); } $configFile = $argv[2]; $options = parseOptions($argv, 3); $strict = isset($options['strict']); if (!file_exists($configFile)) { throw new Exception("Konfigurationsdatei nicht gefunden: $configFile"); } echo "\n✔️ KONFIGURATION VALIDIEREN\n"; echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; $configLoader = new ConfigurationLoader($configFile); try { $config = $configLoader->load(); // Basis-Validierung echo "✅ JSON-Format valide\n"; $required = ['metadata', 'csvStructure', 'columnTransformations']; $missing = []; foreach ($required as $key) { if (isset($config[$key])) { echo "✅ Abschnitt '$key' vorhanden\n"; } else { echo "⚠️ Abschnitt '$key' fehlt\n"; $missing[] = $key; } } // Firefly-Validierung if (isset($config['fireflyImport'])) { echo "✅ Firefly III Konfiguration vorhanden\n"; if (empty($config['fireflyImport']['apiUrl'])) { echo "⚠️ Firefly III API-URL fehlt\n"; if ($strict) { throw new Exception("Strikte Validierung: Firefly III API-URL erforderlich"); } } if (empty($config['fireflyImport']['apiKey'])) { echo "⚠️ Firefly III API-Key fehlt\n"; if ($strict) { throw new Exception("Strikte Validierung: Firefly III API-Key erforderlich"); } } } else { echo "⚠️ Firefly III Konfiguration nicht vorhanden (optional)\n"; } // Verzeichnisse-Validierung if (isset($config['directories'])) { echo "✅ Verzeichnisse konfiguriert\n"; $dirs = ['source', 'output', 'archive', 'error']; foreach ($dirs as $dir) { if (!empty($config['directories'][$dir])) { echo " ✅ $dir: " . $config['directories'][$dir] . "\n"; } } } if (empty($missing)) { echo "\n✅ Konfiguration ist VALIDE!\n\n"; } else { if ($strict) { throw new Exception("Strikte Validierung: " . count($missing) . " erforderliche Abschnitte fehlen"); } echo "\n⚠️ Konfiguration hat Warnungen aber ist funktional\n\n"; } } catch (Exception $e) { throw new Exception("Validierungsfehler: " . $e->getMessage()); } } /** * Auto-Import mit Verzeichnis-Überwachung */ function handleAutoImport($argc, $argv): void { if ($argc < 3) { throw new Exception("Usage: transformer auto-import [config-file] [options]"); } $configFile = $argv[2]; $options = parseOptions($argv, 3); $debug = isset($options['debug']) || isset($options['d']); if (!file_exists($configFile)) { throw new Exception("Konfigurationsdatei nicht gefunden: $configFile"); } $configLoader = new ConfigurationLoader($configFile); $config = $configLoader->load(); $sourceDir = $config['directories']['source'] ?? './import/source'; $outputDir = $config['directories']['output'] ?? './import/output'; $archiveDir = $config['directories']['archive'] ?? './import/archive'; $errorDir = $config['directories']['error'] ?? './import/error'; $dryRun = isset($options['dry-run']); $watch = isset($options['watch']); $interval = isset($options['interval']) ? (int)$options['interval'] : 60; // Verzeichnisse erstellen foreach ([$sourceDir, $outputDir, $archiveDir, $errorDir] as $dir) { if (!is_dir($dir)) { mkdir($dir, 0755, true); } } echo "\n🔍 AUTO-IMPORT GESTARTET\n"; echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; echo " Quellverzeichnis: $sourceDir\n"; echo " Output-Verzeichnis: $outputDir\n"; echo " Archiv-Verzeichnis: $archiveDir\n"; if ($watch) { echo " Mode: WATCH (kontinuierlich)\n"; echo " Intervall: {$interval}s\n"; } else { echo " Mode: EINMALIG\n"; } if ($dryRun) { echo " Dry-Run: JA (keine echten Operationen)\n"; } echo "\n"; if ($watch) { echo "⏳ Drücke Ctrl+C zum Beenden.\n\n"; while (true) { processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun, $debug); sleep($interval); } } else { processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun, $debug); } } /** * Verarbeite Verzeichnis mit CSV-Dateien */ function processImportDirectory($sourceDir, $outputDir, $archiveDir, $errorDir, $config, $configFile, $dryRun = false, $debug = false): void { if (!is_dir($sourceDir)) { return; } $files = glob($sourceDir . '/*.csv'); if (empty($files)) { return; } foreach ($files as $file) { $basename = basename($file); try { echo "📄 Verarbeite: $basename ... "; $configLoader = new ConfigurationLoader($configFile); $config = $configLoader->load(); $engine = new TransformerEngine($configLoader, $debug); $outputFile = $outputDir . '/' . str_replace('.csv', '-transformed.csv', $basename); if (!$dryRun) { $result = $engine->transform($file); $outputFile = $result['outputFile'] ?? $outputFile; // Archiviere Original-Datei $archiveFile = $archiveDir . '/' . $basename; if (!rename($file, $archiveFile)) { throw new Exception("Konnte nicht archivieren"); } // Firefly Import if (!empty($config['fireflyImport'])) { $importer = new FireflyImporter($config['fireflyImport']); $importer->import($outputFile); } } echo "✅\n"; } catch (Exception $e) { echo "❌ " . $e->getMessage() . "\n"; if (!$dryRun) { // Verschiebe zu Error-Verzeichnis $errorFile = $errorDir . '/' . $basename; @rename($file, $errorFile); } } } }