id = "rsync"
name = "rsync"
homepage = "https://rsync.samba.org"
description = "rsync is an open source utility that provides fast incremental file transfer."
executables = ["rsync"]
platforms = ["macos", "linux"]
since = "2.6.9"
version_from = { command = ["rsync", "--version"] }
[dotfiles]
paths = ["~/.popt"]
exclude = []
secret = []
side_effect = false
[commands]
format = 1
features = ["variant_conditions"]
label = "Synchronize one source"
description = "One source to one destination. Directory contents: src/ copies children; directory itself: src copies the directory. SSH checks run only when requested. Daemon URLs, two remote ends, custom remote commands and passwords are outside this form."
quality = "curated"
base = "fig:rsync"
required_capabilities = ["rsync.ssh-protect-args"]
parser = { end_of_options = "--", interleave = "permute", long_equals = true, short_attached = true, short_bundles = true }
[[commands.source]]
id = "rsync-manpage"
kind = "curated"
url = "https://download.samba.org/pub/rsync/rsync.1"
revision = "2026-10-27"
evidence = "Archive, recursive, compress, verbose, exclude, delete, dry-run, itemize-changes, one local/SSH source and destination. -s protects remote arguments and requires both rsync versions at least 3.0.0."
[[commands.field]]
id = "archive"
label = "Archive / preserve attributes"
description = "Archive is a group of preservation options (-rlptgoD), not just recursion. It stays one --archive flag."
type = "boolean"
option = ["--archive", "-a"]
sources = ["rsync-manpage"]
[[commands.field]]
id = "recursive"
label = "Recurse into directories"
type = "boolean"
option = ["--recursive", "-r"]
sources = ["rsync-manpage"]
[[commands.field]]
id = "compress"
label = "Compress during transfer"
type = "boolean"
option = ["--compress", "-z"]
sources = ["rsync-manpage"]
[[commands.field]]
id = "verbose"
label = "Verbosity"
type = "count"
option = ["--verbose", "-v"]
sources = ["rsync-manpage"]
[[commands.field]]
id = "exclude"
label = "Exclude patterns (ordered)"
description = "Literal patterns passed to rsync in this order; Otty never expands globs."
type = "string"
option = ["--exclude"]
multiple = true
sources = ["rsync-manpage"]
[[commands.field]]
id = "delete"
label = "Delete extra destination files"
description = "Deletes destination entries not present in the source. Review a dry run first."
type = "boolean"
option = ["--delete"]
effect = "delete"
sources = ["rsync-manpage"]
[[commands.field]]
id = "dry_run"
label = "Dry run / preview only"
type = "boolean"
option = ["--dry-run", "-n"]
sources = ["rsync-manpage"]
[[commands.field]]
id = "itemize_changes"
label = "Itemize changes"
type = "boolean"
option = ["--itemize-changes", "-i"]
sources = ["rsync-manpage"]
[[commands.field]]
id = "protect_args"
label = "Protect SSH arguments"
description = "Always includes -s for SSH endpoints; both rsync installations must support it."
fixed = ["-s"]
enabled_when = { any = [{ field = "source", op = "variant_is", value = "ssh" }, { field = "destination", op = "variant_is", value = "ssh" }] }
sources = ["rsync-manpage"]
[[commands.field]]
id = "source"
label = "Source"
description = "src/ transfers its contents; src transfers the directory itself. Keep the trailing slash exactly as intended."
position = 0
min = 1
sources = ["rsync-manpage"]
[[commands.field.variant]]
id = "ssh"
label = "SSH"
format = "{host}:{path}"
parts = [
{ id = "host", type = "string", provider = "builtin:ssh.hosts", validation = { min_length = 1, forbid_chars = "/:@ \t\r\n" } },
{ id = "path", type = "string", validation = { min_length = 1 } },
]
[[commands.field.variant]]
id = "local"
label = "Local"
format = "{value}"
parts = [{ id = "value", type = "path", path = { kind = "any", role = "input", must_exist = true, trailing_slash = "meaningful", transforms = ["dot_slash_if_dash", "dot_slash_if_colon"] } }]
[[commands.field]]
id = "destination"
label = "Destination"
position = 1
min = 0
sources = ["rsync-manpage"]
[[commands.field.variant]]
id = "ssh"
label = "SSH"
format = "{host}:{path}"
parts = [
{ id = "host", type = "string", provider = "builtin:ssh.hosts", validation = { min_length = 1, forbid_chars = "/:@ \t\r\n" } },
{ id = "path", type = "string", validation = { min_length = 1 } },
]
[[commands.field.variant]]
id = "local"
label = "Local"
format = "{value}"
parts = [{ id = "value", type = "path", path = { kind = "any", role = "output", must_exist = false, transforms = ["dot_slash_if_dash", "dot_slash_if_colon"] } }]
[[commands.constraint]]
kind = "at_most_one"
fields = ["source", "destination"]
when = { all = [{ field = "source", op = "variant_is", value = "ssh" }, { field = "destination", op = "variant_is", value = "ssh" }] }
message = "Only one endpoint may use SSH."
[[commands.constraint]]
kind = "at_most_one"
fields = ["source", "destination"]
when = { all = [{ task = "push-ssh" }, { not = { field = "destination", op = "variant_is", value = "ssh" } }] }
message = "The Push to SSH task requires an SSH destination."
[[commands.task]]
id = "preview"
label = "Preview synchronization"
set = { archive = [true], dry_run = [true], itemize_changes = [true] }
lock = ["dry_run"]
fields = ["source", "destination", "archive", "delete", "exclude", "verbose"]
required = ["source", "destination"]
[[commands.task]]
id = "local-backup"
label = "Local backup"
set = { archive = [true] }
lock = ["archive"]
fields = ["source", "destination", "delete", "dry_run", "exclude", "verbose"]
required = ["source", "destination"]
[[commands.task]]
id = "push-ssh"
label = "Push to SSH"
description = "Choose the SSH variant for the destination, then explicitly check both rsync installations before inserting."
set = { archive = [true], compress = [true] }
lock = ["compress"]
fields = ["source", "destination", "archive", "delete", "dry_run", "exclude"]
required = ["source", "destination"]
[[commands.group]]
id = "basic"
label = "Transfer"
fields = ["source", "destination", "archive", "recursive", "compress", "dry_run"]
[[commands.group]]
id = "advanced"
label = "Advanced"
fields = ["verbose", "exclude", "delete", "itemize_changes", "protect_args"]