Skip to content

Repository files navigation

Hi there 👋

This package is code for parsing the text output from rsync, for extracting key numbers of the output for updating views and log in

ParseRsyncOutput

A Swift package for parsing rsync command output and extracting synchronization statistics, file counts, transfer sizes, and performance metrics.

Features

  • 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

Requirements

  • Swift 5.9+
  • macOS 13.0+ / iOS 16.0+
  • Foundation framework
  • OSLog for logging

Usage

Basic Parsing

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)")}}

Parsing OpenRsync Output

// 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")}}

Using Formatted Properties

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)")

SwiftUI Integration

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)}}}}}}

Error Handling

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)")}}

Data Structures

NumbersOnly

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
}

ParseResult

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
}

VersionRsync

Enum for specifying rsync version:

publicenumVersionRsync{case ver3 // rsync 3.x
case openrsync // rsync 2.x or openrsync
}

Error Types

RsyncParseError

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
}

API Reference

ParseRsyncOutput

Main parser class (requires @MainActor):

Initialization

publicinit(_ preparedoutputfromrsync:[String], _ rsyncversion:VersionRsync)

Properties

  • numbersonly: NumbersOnly? - Parsed numerical statistics
  • stats: String? - Formatted performance summary
  • errors: [RsyncParseError] - Array of parsing errors
  • warnings: [String] - Array of warning messages
  • parseResult: ParseResult - Complete result with status

Formatted Properties

Formatted display helpers:

  • formatted_filestransferred: String
  • formatted_numberoffiles: String
  • formatted_totalfilesize: String
  • formatted_totaldirectories: String
  • formatted_numberoffiles_totaldirectories: String
  • formatted_numberofcreatedfiles: String
  • formatted_numberofdeletedfiles: String
  • formatted_totaltransferredfilessize: String - Automatic human-readable byte units (bytes, KB, MB, GB)

Methods

  • rsyncver3(stringnumbersonly:) - Parse rsync 3.x output
  • rsyncver2(stringnumbersonly:) - Parse rsync 2.x/openrsync output
  • returnIntNumber(_:) -> [Int] - Extract integers from string
  • returnDoubleNumber(_:) -> [Double] - Extract doubles from string

Parsing Details

Internals (Maintainers)

  • 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 in ExtractFieldsDataV2 for symmetry.
  • validateV3Fields(_:) and validateV2Fields(_:) perform count checks on their respective structs before parsing, emitting missingRequiredField with aggregated names.
  • parseV3Fields(_:) and rsyncver2(_:) 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.

Rsync 3.x Output Format

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

Rsync 2.x/OpenRsync Format

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

Statistics Calculation

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

Best Practices

  1. Always check parseResult.isSuccess before accessing numbersonly
  2. Handle warnings - they indicate partial parsing issues but don't prevent use
  3. Use formatted properties for display to ensure proper localization
  4. Specify correct version - use .ver3 for modern rsync, .openrsync for older versions
  5. Log errors - parsing errors are automatically logged via OSLog

Example: Complete Workflow

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)")}}}

License

MIT

Author

Thomas Evensen

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages