This package is code for parsing the text output from rsync, for extracting key numbers of the output for updating views and log in
A Swift package for parsing rsync command output and extracting synchronization statistics, file counts, transfer sizes, and performance metrics.
- Multi-Version Support: Parse output from both rsync 3.x and rsync 2.x/openrsync
- Comprehensive Statistics: Extract file counts, sizes, transfer metrics, and sync status
- Error Handling: Robust error detection and reporting with detailed error messages
- Warning System: Non-fatal warnings for partial parsing issues
- Formatted Output: Pre-formatted strings ready for UI display
- Type-Safe Parsing: Strongly-typed data structures for parsed results
- Struct-Based Validation: Shared field bundles (
ExtractFieldsDataV2/ExtractFieldsDataV3) keep rsync 2.x and 3.x parsing aligned with consistent error reporting - Performance Metrics: Automatic calculation of transfer speed and duration
- Swift 5.9+
- macOS 13.0+ / iOS 16.0+
- Foundation framework
- OSLog for logging
import ParseRsyncOutput
// Sample rsync output lines
letrsyncOutput=["Number of files: 1,234 (reg: 1,100, dir: 134)","Number of created files: 42","Number of deleted files: 15","Total file size: 1,234,567,890 bytes","Total transferred file size: 123,456,789 bytes","files transferred: 42","sent 123456 bytes received 789012 bytes 45678.00 bytes/sec"]
// Parse for rsync 3.x
letparser=ParseRsyncOutput(rsyncOutput,.ver3)
// Check parsing success
if parser.parseResult.isSuccess {print("✓ Parsing successful")iflet numbers = parser.numbersonly {print("Files: \(numbers.numberoffiles)")print("Transferred: \(numbers.filestransferred)")print("Created: \(numbers.numberofcreatedfiles)")print("Deleted: \(numbers.numberofdeletedfiles)")print("Changes needed: \(numbers.datatosynchronize)")}iflet stats = parser.stats {print("Summary: \(stats)")
// Output: "42 files : 123.5 MB in 20.03 seconds"
}}else{print("✗ Parsing failed")forerrorin parser.parseResult.errors {print("Error: \(error.localizedDescription)")}}// For rsync 2.x or openrsync
letparser=ParseRsyncOutput(rsyncOutput,.openrsync)if parser.parseResult.isSuccess {
// Access parsed data
iflet numbers = parser.numbersonly {print("Synced \(numbers.filestransferred) files")print("Total size: \(numbers.totalfilesize) bytes")}}The parser provides pre-formatted strings ideal for displaying in UIs:
letparser=ParseRsyncOutput(rsyncOutput,.ver3)
// Use formatted properties directly
print("Files: \(parser.formatted_numberoffiles)")print("Directories: \(parser.formatted_totaldirectories)")print("Total Size: \(parser.formatted_totalfilesize)")print("Transferred: \(parser.formatted_filestransferred)")print("Transferred Size: \(parser.formatted_totaltransferredfilessize)")print("Created: \(parser.formatted_numberofcreatedfiles)")print("Deleted: \(parser.formatted_numberofdeletedfiles)")print("Files + Dirs: \(parser.formatted_numberoffiles_totaldirectories)")import SwiftUI
import ParseRsyncOutput
structSyncResultView:View{letparser:ParseRsyncOutputvarbody:someView{Form{if parser.parseResult.isSuccess {Section("Transfer Summary"){LabeledContent("Files Transferred", value: parser.formatted_filestransferred)LabeledContent("Total Files", value: parser.formatted_numberoffiles)LabeledContent("Directories", value: parser.formatted_totaldirectories)LabeledContent("Total Size", value: parser.formatted_totalfilesize)LabeledContent("Transferred Size", value: parser.formatted_totaltransferredfilessize)}Section("Changes"){LabeledContent("Created", value: parser.formatted_numberofcreatedfiles)LabeledContent("Deleted", value: parser.formatted_numberofdeletedfiles)iflet numbers = parser.numbersonly {HStack{Text("Sync Status")Spacer()if numbers.datatosynchronize {Label("Changes Detected", systemImage:"exclamationmark.circle").foregroundStyle(.orange)}else{Label("Up to Date", systemImage:"checkmark.circle").foregroundStyle(.green)}}}}iflet stats = parser.stats {Section("Performance"){Text(stats).font(.caption).foregroundStyle(.secondary)}}}else{Section("Errors"){ForEach(parser.parseResult.errors, id: \.localizedDescription){ error inLabel(error.localizedDescription, systemImage:"xmark.circle").foregroundStyle(.red)}}}if parser.parseResult.hasWarnings {Section("Warnings"){ForEach(parser.parseResult.warnings, id: \.self){ warning inLabel(warning, systemImage:"exclamationmark.triangle").foregroundStyle(.orange).font(.caption)}}}}}}letparser=ParseRsyncOutput(rsyncOutput,.ver3)
// Check for errors
if !parser.parseResult.isSuccess {forerrorin parser.parseResult.errors {switch error {case.missingRequiredField(let field):print("Missing: \(field)")case.invalidNumberFormat(let field,let value):print("Invalid number in \(field): \(value)")case.invalidOutputFormat(let details):print("Format error: \(details)")case.incompleteSummaryLine:print("Incomplete summary line")case.divisionByZero:print("Invalid bytes/sec value")case.unsupportedVersion:print("Unsupported rsync version")}}}
// Check for warnings (non-fatal)
if parser.parseResult.hasWarnings {forwarningin parser.parseResult.warnings {print("⚠️ \(warning)")}}Complete parsed statistics from rsync output:
publicstructNumbersOnly{publicvarnumberoffiles:Int // Number of regular files
publicvartotaldirectories:Int // Number of directories
publicvartotalfilesize:Double // Total size in bytes
publicvarfilestransferred:Int // Files actually transferred
publicvartotaltransferredfilessize:Double // Size of transferred files
publicvarnumberofcreatedfiles:Int // Newly created files
publicvarnumberofdeletedfiles:Int // Deleted files
publicvardatatosynchronize:Bool // True if changes exist
}Wrapper containing parsing results and status:
publicstructParseResult{publicletnumbersonly:NumbersOnly? // Parsed statistics
publicletstats:String? // Formatted summary
publicleterrors:[RsyncParseError] // Parsing errors
publicletwarnings:[String] // Non-fatal warnings
publicvarisSuccess:Bool // True if no errors
publicvarhasWarnings:Bool // True if warnings exist
}Enum for specifying rsync version:
publicenumVersionRsync{case ver3 // rsync 3.x
case openrsync // rsync 2.x or openrsync
}publicenumRsyncParseError:Error,LocalizedError{case missingRequiredField(String) // Required field not found
case invalidNumberFormat(field:String, value:String) // Cannot parse number
case invalidOutputFormat(String) // Malformed output
case incompleteSummaryLine // Incomplete sent/received line
case divisionByZero // Invalid bytes/sec
case unsupportedVersion // Unknown rsync version
}Main parser class (requires @MainActor):
publicinit(_ preparedoutputfromrsync:[String], _ rsyncversion:VersionRsync)numbersonly: NumbersOnly?- Parsed numerical statisticsstats: String?- Formatted performance summaryerrors: [RsyncParseError]- Array of parsing errorswarnings: [String]- Array of warning messagesparseResult: ParseResult- Complete result with status
Formatted display helpers:
formatted_filestransferred: Stringformatted_numberoffiles: Stringformatted_totalfilesize: Stringformatted_totaldirectories: Stringformatted_numberoffiles_totaldirectories: Stringformatted_numberofcreatedfiles: Stringformatted_numberofdeletedfiles: Stringformatted_totaltransferredfilessize: String- Automatic human-readable byte units (bytes, KB, MB, GB)
rsyncver3(stringnumbersonly:)- Parse rsync 3.x outputrsyncver2(stringnumbersonly:)- Parse rsync 2.x/openrsync outputreturnIntNumber(_:) -> [Int]- Extract integers from stringreturnDoubleNumber(_:) -> [Double]- Extract doubles from string
extractSummaryLine(_:)picks the single sent/received/bytes line and flags missing or duplicate cases.extractFieldsV3(_:)returns grouped v3 fields; v2 uses the same arrays but is wrapped inExtractFieldsDataV2for symmetry.validateV3Fields(_:)andvalidateV2Fields(_:)perform count checks on their respective structs before parsing, emittingmissingRequiredFieldwith aggregated names.parseV3Fields(_:)andrsyncver2(_:)convert strings to numbers; invalid formats are caught early with typed errors.calculateStats(_:stringnumbersonly:numbersonly:)builds the human-readable summary and guards against divide-by-zero or malformed values.
The parser expects these lines in rsync 3.x output:
Number of files: 1,234 (reg: 1,100, dir: 134)
Number of created files: 42
Number of deleted files: 15
Total file size: 1,234,567,890 bytes
Total transferred file size: 123,456,789 bytes
files transferred: 42
sent 123456 bytes received 789012 bytes 45678.00 bytes/sec
For older versions:
Number of files: 1,234
Total file size: 1,234,567,890 bytes
Total transferred file size: 123,456,789 bytes
files transferred: 42
sent 123456 bytes received 789012 bytes 45678.00 bytes/sec
The stats property provides a formatted summary:
"42 files : 123.5 MB in 20.03 seconds"
Calculated as:
- Files: Number of transferred files
- Size: Total bytes sent formatted automatically as bytes, KB, MB, or GB
- Time: Total bytes sent / bytes per second
- Always check
parseResult.isSuccessbefore accessingnumbersonly - Handle warnings - they indicate partial parsing issues but don't prevent use
- Use formatted properties for display to ensure proper localization
- Specify correct version - use
.ver3for modern rsync,.openrsyncfor older versions - Log errors - parsing errors are automatically logged via OSLog
import ParseRsyncOutput
@MainActorfunc processSyncResults(_ output:[String]){letparser=ParseRsyncOutput(output,.ver3)guard parser.parseResult.isSuccess else{print("Parsing failed:")
parser.parseResult.errors.forEach{print(" - \($0.localizedDescription)")}return}guardlet numbers = parser.numbersonly else{print("No statistics available")return}
// Display results
print("=== Sync Results ===")print("Files: \(parser.formatted_numberoffiles)")print("Directories: \(parser.formatted_totaldirectories)")print("Total Size: \(parser.formatted_totalfilesize)")print()print("Transferred: \(parser.formatted_filestransferred)")print("Created: \(parser.formatted_numberofcreatedfiles)")print("Deleted: \(parser.formatted_numberofdeletedfiles)")print()iflet stats = parser.stats {print("Performance: \(stats)")}print()if numbers.datatosynchronize {print("⚠️ Changes detected - sync needed")}else{print("✓ Everything up to date")}
// Show warnings if any
if parser.parseResult.hasWarnings {print("\nWarnings:")
parser.parseResult.warnings.forEach{print(" - \($0)")}}}MIT
Thomas Evensen