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
let rsyncOutput = [
"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
let parser = ParseRsyncOutput(rsyncOutput, .ver3)
// Check parsing success
if parser.parseResult.isSuccess {
print("✓ Parsing successful")
if let numbers = parser.numbersonly {
print("Files: \(numbers.numberoffiles)")
print("Transferred: \(numbers.filestransferred)")
print("Created: \(numbers.numberofcreatedfiles)")
print("Deleted: \(numbers.numberofdeletedfiles)")
print("Changes needed: \(numbers.datatosynchronize)")
}
if let stats = parser.stats {
print("Summary: \(stats)")
// Output: "42 files : 123.5 MB in 20.03 seconds"
}
} else {
print("✗ Parsing failed")
for error in parser.parseResult.errors {
print("Error: \(error.localizedDescription)")
}
}// For rsync 2.x or openrsync
let parser = ParseRsyncOutput(rsyncOutput, .openrsync)
if parser.parseResult.isSuccess {
// Access parsed data
if let 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:
let parser = 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
struct SyncResultView: View {
let parser: ParseRsyncOutput
var body: some View {
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)
if let 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)
}
}
}
}
if let stats = parser.stats {
Section("Performance") {
Text(stats)
.font(.caption)
.foregroundStyle(.secondary)
}
}
} else {
Section("Errors") {
ForEach(parser.parseResult.errors, id: \.localizedDescription) { error in
Label(error.localizedDescription, systemImage: "xmark.circle")
.foregroundStyle(.red)
}
}
}
if parser.parseResult.hasWarnings {
Section("Warnings") {
ForEach(parser.parseResult.warnings, id: \.self) { warning in
Label(warning, systemImage: "exclamationmark.triangle")
.foregroundStyle(.orange)
.font(.caption)
}
}
}
}
}
}let parser = ParseRsyncOutput(rsyncOutput, .ver3)
// Check for errors
if !parser.parseResult.isSuccess {
for error in 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 {
for warning in parser.parseResult.warnings {
print("⚠️ \(warning)")
}
}Complete parsed statistics from rsync output:
public struct NumbersOnly {
public var numberoffiles: Int // Number of regular files
public var totaldirectories: Int // Number of directories
public var totalfilesize: Double // Total size in bytes
public var filestransferred: Int // Files actually transferred
public var totaltransferredfilessize: Double // Size of transferred files
public var numberofcreatedfiles: Int // Newly created files
public var numberofdeletedfiles: Int // Deleted files
public var datatosynchronize: Bool // True if changes exist
}Wrapper containing parsing results and status:
public struct ParseResult {
public let numbersonly: NumbersOnly? // Parsed statistics
public let stats: String? // Formatted summary
public let errors: [RsyncParseError] // Parsing errors
public let warnings: [String] // Non-fatal warnings
public var isSuccess: Bool // True if no errors
public var hasWarnings: Bool // True if warnings exist
}Enum for specifying rsync version:
public enum VersionRsync {
case ver3 // rsync 3.x
case openrsync // rsync 2.x or openrsync
}public enum RsyncParseError: 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):
public init(_ 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
@MainActor
func processSyncResults(_ output: [String]) {
let parser = ParseRsyncOutput(output, .ver3)
guard parser.parseResult.isSuccess else {
print("Parsing failed:")
parser.parseResult.errors.forEach { print(" - \($0.localizedDescription)") }
return
}
guard let 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()
if let 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