Eric Bower
·
2026-05-31
1// Package rsyncopts implements a parser for command-line options that
2// implements a subset of popt(3) semantics; just enough to parse typical
3// rsync(1) invocations without the advanced popt features like aliases
4// or option prefix matching (not --del, only --delete).
5//
6// If we encounter arguments that rsync(1) parses differently compared to this
7// package, then this package should be adjusted to match rsync(1).
8package rsyncopts
9
10import (
11 "errors"
12 "fmt"
13 "log/slog"
14 "math"
15 "slices"
16 "strconv"
17 "strings"
18 "syscall"
19 "unicode"
20)
21
22const (
23 OPT_SERVER = 1000 + iota
24 OPT_DAEMON
25 OPT_SENDER
26 OPT_EXCLUDE
27 OPT_EXCLUDE_FROM
28 OPT_FILTER
29 OPT_COMPARE_DEST
30 OPT_COPY_DEST
31 OPT_LINK_DEST
32 OPT_HELP
33 OPT_INCLUDE
34 OPT_INCLUDE_FROM
35 OPT_MODIFY_WINDOW
36 OPT_MIN_SIZE
37 OPT_CHMOD
38 OPT_READ_BATCH
39 OPT_WRITE_BATCH
40 OPT_ONLY_WRITE_BATCH
41 OPT_MAX_SIZE
42 OPT_NO_D
43 OPT_APPEND
44 OPT_NO_ICONV
45 OPT_INFO
46 OPT_DEBUG
47 OPT_BLOCK_SIZE
48 OPT_USERMAP
49 OPT_GROUPMAP
50 OPT_CHOWN
51 OPT_BWLIMIT
52 OPT_STDERR
53 OPT_OLD_COMPRESS
54 OPT_NEW_COMPRESS
55 OPT_NO_COMPRESS
56 OPT_OLD_ARGS
57 OPT_STOP_AFTER
58 OPT_STOP_AT
59 OPT_REFUSED_BASE = 9000
60)
61
62type infoLevel int
63
64const (
65 INFO_BACKUP infoLevel = iota
66 INFO_COPY
67 INFO_DEL
68 INFO_FLIST
69 INFO_MISC
70 INFO_MOUNT
71 INFO_NAME
72 INFO_NONREG
73 INFO_PROGRESS
74 INFO_REMOVE
75 INFO_SKIP
76 INFO_STATS
77 INFO_SYMSAFE
78 COUNT_INFO
79)
80
81// NewOptions returns an Options struct with all options initialized to their
82// default values. Note that ParseArguments will set some options (that default
83// to -1) based on the encountered command-line flags and built-in rules.
84func NewOptions() *Options {
85 return &Options{
86 msgs2stderr: 2, // Default: send errors to stderr for local & remote-shell transfers
87 output_motd: 1,
88 human_readable: 1,
89 allow_inc_recurse: 1,
90 xfer_dirs: -1,
91 relative_paths: -1,
92 implied_dirs: 1,
93 max_delete: math.MinInt32,
94 whole_file: -1,
95 do_compression_level: math.MinInt32,
96 rsync_path: "rsync",
97 default_af_hint: syscall.AF_INET6,
98 blocking_io: -1,
99 protocol_version: 27,
100 }
101}
102
103// GokrazyOptions contains additional command-line flags, prefixed with
104// gokr. (like --gokr.modulemap) to not clash with rsync flag names.
105type GokrazyOptions struct {
106 Config string
107 Listen string
108 MonitoringListen string
109 AnonSSHListen string
110 ModuleMap string
111}
112
113func (o *GokrazyOptions) table() []poptOption {
114 return []poptOption{
115 /* longName, shortName, argInfo, arg, val */
116 {"gokr.config", "", POPT_ARG_STRING, &o.Config, 0},
117 {"gokr.listen", "", POPT_ARG_STRING, &o.Listen, 0},
118 {"gokr.monitoring_listen", "", POPT_ARG_STRING, &o.MonitoringListen, 0},
119 {"gokr.anonssh_listen", "", POPT_ARG_STRING, &o.AnonSSHListen, 0},
120 {"gokr.modulemap", "", POPT_ARG_STRING, &o.ModuleMap, 0},
121 }
122}
123
124type Options struct {
125 Gokrazy GokrazyOptions
126
127 // not directly referenced in the table, but used in the special case code.
128 do_compression int
129 info [COUNT_INFO]uint16
130 local_server int
131
132 // order matches long_options order
133 verbose int
134 msgs2stderr int
135 quiet int
136 output_motd int
137 do_stats int
138 human_readable int
139 dry_run int
140 recurse int
141 allow_inc_recurse int
142 xfer_dirs int
143 preserve_perms int
144 preserve_executability int
145 preserve_acls int
146 preserve_xattrs int
147 preserve_mtimes int
148 preserve_atimes int
149 open_noatime int
150 preserve_crtimes int
151 omit_dir_times int
152 omit_link_times int
153 modify_window int
154 am_root int // 0 = normal, 1 = root, 2 = --super, -1 = --fake-super
155 preserve_uid int
156 preserve_gid int
157 preserve_devices int
158 copy_devices int
159 write_devices int
160 preserve_specials int
161 preserve_links int
162 copy_links int
163 copy_unsafe_links int
164 safe_symlinks int
165 munge_symlinks int
166 copy_dirlinks int
167 keep_dirlinks int
168 preserve_hard_links int
169 relative_paths int
170 implied_dirs int
171 ignore_times int
172 size_only int
173 one_file_system int
174 update_only int
175 ignore_non_existing int
176 ignore_existing int
177 max_size_arg string
178 min_size_arg string
179 max_alloc_arg string
180 sparse_files int
181 preallocate_files int
182 inplace int
183 append_mode int
184 delete_during int
185 delete_mode int
186 delete_before int
187 delete_after int
188 delete_excluded int
189 missing_args int // 0 = FERROR_XFER, 1 = ignore, 2 = delete
190 remove_source_files int
191 force_delete int
192 ignore_errors int
193 max_delete int
194 cvs_exclude int
195 // If 1, send the whole file as literal data rather than trying to create an
196 // incremental diff.
197 // If -1, then look at whether we're local or remote and go by that.
198 // See also disable_deltas_p()
199 whole_file int
200 always_checksum int
201 checksum_choice string
202 fuzzy_basis int
203 compress_choice string
204 skip_compress string
205 do_compression_level int
206 do_progress int
207 keep_partial int
208 partial_dir string
209 delay_updates int
210 prune_empty_dirs int
211 logfile_name string
212 logfile_format string
213 stdout_format string
214 itemize_changes int
215 bwlimit_arg string
216 bwlimit int
217 make_backups int
218 backup_dir string
219 backup_suffix string
220 list_only int
221 batch_name string
222 files_from string
223 eol_nulls int
224 old_style_args int // intentionally set to 0; unsupported
225 protect_args int // intentionally set to 0; currently unsupported
226 trust_sender int
227 numeric_ids int
228 io_timeout int
229 connect_timeout int
230 do_fsync int
231 shell_cmd string
232 rsync_path string
233 tmpdir string
234 iconv_opt string
235 default_af_hint int
236 allow_8bit_chars int
237 mkpath_dest_arg int
238 use_qsort int
239 copy_as string
240 bind_address string // numeric IPv4 or IPv6, or a hostname
241 rsync_port int
242 sockopts string
243 password_file string
244 early_input_file string
245 blocking_io int
246 outbuf_mode string
247 protocol_version int
248 checksum_seed int
249 am_server int
250 am_sender int
251 am_daemon int
252
253 daemon_bwlimit int
254 config_file string
255 daemon_opt int
256 no_detach int
257}
258
259type priority int
260
261const (
262 DEFAULT_PRIORITY priority = iota
263 HELP_PRIORITY
264 USER_PRIORITY
265 LIMIT_PRIORITY
266)
267
268const (
269 W_CLI = 1 << iota
270 W_SRV
271 W_SND
272 W_REC
273)
274
275type output struct {
276 name string
277 where int
278 help string
279}
280
281var infoWords = [...]output{
282 {"BACKUP", W_REC, "Mention files backed up"},
283 {"COPY", W_REC, "Mention files copied locally on the receiving side"},
284 {"DEL", W_REC, "Mention deletions on the receiving side"},
285 {"FLIST", W_CLI, "Mention file-list receiving/sending (levels 1-2)"},
286 {"MISC", W_SND | W_REC, "Mention miscellaneous information (levels 1-2)"},
287 {"MOUNT", W_SND | W_REC, "Mention mounts that were found or skipped"},
288 {"NAME", W_SND | W_REC, "Mention 1) updated file/dir names, 2) unchanged names"},
289 {"NONREG", W_REC, "Mention skipped non-regular files (default 1, 0 disables)"},
290 {"PROGRESS", W_CLI, "Mention 1) per-file progress or 2) total transfer progress"},
291 {"REMOVE", W_SND, "Mention files removed on the sending side"},
292 {"SKIP", W_REC, "Mention files skipped due to transfer overrides (levels 1-2)"},
293 {"STATS", W_CLI | W_SRV, "Mention statistics at end of run (levels 1-3)"},
294 {"SYMSAFE", W_SND | W_REC, "Mention symlinks that are unsafe"},
295}
296
297func parseOutputWords(words []output, levels []uint16, str string, prio priority) {
298Level:
299 for s := range strings.SplitSeq(str, ",") {
300 if strings.TrimSpace(s) == "" {
301 continue
302 }
303 trimmed := strings.TrimRightFunc(s, unicode.IsNumber)
304 lev := 1
305 if len(trimmed) < len(s) {
306 var err error
307 lev, err = strconv.Atoi(s[len(trimmed):])
308 if err != nil {
309 continue
310 }
311 }
312 trimmed = strings.ToLower(trimmed)
313 all := false
314 switch trimmed {
315 case "none":
316 lev = 0
317 case "all":
318 all = true
319 }
320 for j := range words {
321 word := words[j]
322 if strings.ToLower(word.name) == trimmed || all {
323 levels[j] = uint16(lev)
324 if !all {
325 continue Level
326 }
327 }
328 }
329 }
330}
331
332func (o *Options) setOutputVerbosity(prio priority) {
333 // debugVerbosity is reserved for future use with debug logging levels.
334 // debugVerbosity := [...]string{
335 // "",
336 // "",
337 // "BIND,CMD,CONNECT,DEL,DELTASUM,DUP,FILTER,FLIST,ICONV",
338 // "ACL,BACKUP,CONNECT2,DELTASUM2,DEL2,EXIT,FILTER2,FLIST2,FUZZY,GENR,OWN,RECV,SEND,TIME",
339 // "CMD2,DELTASUM3,DEL3,EXIT2,FLIST3,ICONV2,OWN2,PROTO,TIME2",
340 // "CHDIR,DELTASUM4,FLIST4,FUZZY2,HASH,HLINK",
341 // }
342 infoVerbosity := [...]string{
343 "NONREG",
344 "COPY,DEL,FLIST,MISC,NAME,STATS,SYMSAFE",
345 "BACKUP,MISC2,MOUNT,NAME2,REMOVE,SKIP",
346 }
347 for j := 0; j <= o.verbose; j++ {
348 if j < len(infoVerbosity) {
349 parseOutputWords(infoWords[:], o.info[:], infoVerbosity[j], prio)
350 }
351 // TODO: enable debug verbosity when debug logging is implemented
352 // if j < len(debugVerbosity) {
353 // parseOutputWords(debugWords[:], o.debug[:], debugVerbosity[j], prio)
354 // }
355 }
356}
357
358func (o *Options) Help() string {
359 return ""
360}
361
362func (o *Options) ShellCommand() string { return o.shell_cmd }
363func (o *Options) UpdateOnly() bool { return o.update_only != 0 }
364func (o *Options) DryRun() bool { return o.dry_run != 0 }
365func (o *Options) PreserveLinks() bool { return o.preserve_links != 0 }
366func (o *Options) PreserveUid() bool { return o.preserve_uid != 0 }
367func (o *Options) PreserveGid() bool { return o.preserve_gid != 0 }
368func (o *Options) PreserveDevices() bool { return o.preserve_devices != 0 }
369func (o *Options) PreserveMTimes() bool { return o.preserve_mtimes != 0 }
370func (o *Options) PreservePerms() bool { return o.preserve_perms != 0 }
371func (o *Options) PreserveSpecials() bool { return o.preserve_specials != 0 }
372func (o *Options) PreserveHardLinks() bool { return o.preserve_hard_links != 0 }
373func (o *Options) Recurse() bool { return o.recurse != 0 }
374func (o *Options) Verbose() bool { return o.verbose != 0 }
375func (o *Options) DeleteMode() bool { return o.delete_mode != 0 }
376func (o *Options) Sender() bool { return o.am_sender != 0 }
377func (o *Options) SetSender() { o.am_sender = 1 }
378func (o *Options) LocalServer() bool { return o.local_server != 0 }
379func (o *Options) SetLocalServer() { o.local_server = 1 }
380func (o *Options) Server() bool { return o.am_server != 0 }
381func (o *Options) Daemon() bool { return o.am_daemon != 0 }
382func (o *Options) ConnectTimeoutSeconds() int { return o.connect_timeout }
383func (o *Options) AlwaysChecksum() bool { return o.always_checksum != 0 }
384func (o *Options) Compress() bool { return o.do_compression != 0 }
385func (o *Options) CompressChoice() string { return o.compress_choice }
386func (o *Options) CompressLevel() int { return o.do_compression_level }
387func (o *Options) IgnoreTimes() bool { return o.ignore_times == 1 }
388func (o *Options) SizeOnly() bool { return o.size_only == 1 }
389
390func (o *Options) daemonTable() []poptOption {
391 return []poptOption{
392 /* longName, shortName, argInfo, arg, val */
393 {"help", "", POPT_ARG_NONE, nil, OPT_HELP},
394 {"address", "", POPT_ARG_STRING, &o.bind_address, 0},
395 {"bwlimit", "", POPT_ARG_INT, &o.daemon_bwlimit, 0},
396 {"config", "", POPT_ARG_STRING, &o.config_file, 0},
397 {"daemon", "", POPT_ARG_NONE, &o.daemon_opt, 0},
398 {"dparam", "M", POPT_ARG_STRING, nil, 'M'},
399 {"ipv4", "4", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET},
400 {"ipv6", "6", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET6},
401 {"detach", "", POPT_ARG_VAL, &o.no_detach, 0},
402 {"no-detach", "", POPT_ARG_VAL, &o.no_detach, 1},
403 {"log-file", "", POPT_ARG_STRING, &o.logfile_name, 0},
404 {"log-file-format", "", POPT_ARG_STRING, &o.logfile_format, 0},
405 {"port", "", POPT_ARG_INT, &o.rsync_port, 0},
406 {"sockopts", "", POPT_ARG_STRING, &o.sockopts, 0},
407 {"protocol", "", POPT_ARG_INT, &o.protocol_version, 0},
408 {"server", "", POPT_ARG_NONE, &o.am_server, 0},
409 {"temp-dir", "T", POPT_ARG_STRING, &o.tmpdir, 0},
410 {"verbose", "v", POPT_ARG_NONE, 0, 'v'},
411 {"no-verbose", "", POPT_ARG_VAL, &o.verbose, 0},
412 {"no-v", "", POPT_ARG_VAL, &o.verbose, 0},
413 {"help", "h", POPT_ARG_NONE, 0, 'h'},
414 }
415}
416
417func (o *Options) table() []poptOption {
418 return []poptOption{
419 /* longName, shortName, argInfo, arg, val */
420 {"help", "", POPT_ARG_NONE, nil, OPT_HELP},
421 {"version", "V", POPT_ARG_NONE, nil, 'V'},
422 {"verbose", "v", POPT_ARG_NONE, nil, 'v'},
423 {"no-verbose", "", POPT_ARG_VAL, &o.verbose, 0},
424 {"no-v", "", POPT_ARG_VAL, &o.verbose, 0},
425 {"info", "", POPT_ARG_STRING, nil, OPT_INFO},
426 {"debug", "", POPT_ARG_STRING, nil, OPT_DEBUG},
427 {"stderr", "", POPT_ARG_STRING, nil, OPT_STDERR},
428 {"msgs2stderr", "", POPT_ARG_VAL, &o.msgs2stderr, 1},
429 {"no-msgs2stderr", "", POPT_ARG_VAL, &o.msgs2stderr, 0},
430 {"quiet", "q", POPT_ARG_NONE, nil, 'q'},
431 {"motd", "", POPT_ARG_VAL, &o.output_motd, 1},
432 {"no-motd", "", POPT_ARG_VAL, &o.output_motd, 0},
433 {"stats", "", POPT_ARG_NONE, &o.do_stats, 0},
434 {"human-readable", "h", POPT_ARG_NONE, nil, 'h'},
435 {"no-human-readable", "", POPT_ARG_VAL, &o.human_readable, 0},
436 {"no-h", "", POPT_ARG_VAL, &o.human_readable, 0},
437 {"dry-run", "n", POPT_ARG_NONE, &o.dry_run, 0},
438 {"archive", "a", POPT_ARG_NONE, nil, 'a'},
439 {"recursive", "r", POPT_ARG_VAL, &o.recurse, 2},
440 {"no-recursive", "", POPT_ARG_VAL, &o.recurse, 0},
441 {"no-r", "", POPT_ARG_VAL, &o.recurse, 0},
442 {"inc-recursive", "", POPT_ARG_VAL, &o.allow_inc_recurse, 1},
443 {"no-inc-recursive", "", POPT_ARG_VAL, &o.allow_inc_recurse, 0},
444 {"i-r", "", POPT_ARG_VAL, &o.allow_inc_recurse, 1},
445 {"no-i-r", "", POPT_ARG_VAL, &o.allow_inc_recurse, 0},
446 {"dirs", "d", POPT_ARG_VAL, &o.xfer_dirs, 2},
447 {"no-dirs", "", POPT_ARG_VAL, &o.xfer_dirs, 0},
448 {"no-d", "", POPT_ARG_VAL, &o.xfer_dirs, 0},
449 {"old-dirs", "", POPT_ARG_VAL, &o.xfer_dirs, 4},
450 {"old-d", "", POPT_ARG_VAL, &o.xfer_dirs, 4},
451 {"perms", "p", POPT_ARG_VAL, &o.preserve_perms, 1},
452 {"no-perms", "", POPT_ARG_VAL, &o.preserve_perms, 0},
453 {"no-p", "", POPT_ARG_VAL, &o.preserve_perms, 0},
454 {"executability", "E", POPT_ARG_NONE, &o.preserve_executability, 0},
455 {"acls", "A", POPT_ARG_NONE, nil, 'A'},
456 {"no-acls", "", POPT_ARG_VAL, &o.preserve_acls, 0},
457 {"no-A", "", POPT_ARG_VAL, &o.preserve_acls, 0},
458 {"xattrs", "X", POPT_ARG_NONE, nil, 'X'},
459 {"no-xattrs", "", POPT_ARG_VAL, &o.preserve_xattrs, 0},
460 {"no-X", "", POPT_ARG_VAL, &o.preserve_xattrs, 0},
461 {"times", "t", POPT_ARG_VAL, &o.preserve_mtimes, 1},
462 {"no-times", "", POPT_ARG_VAL, &o.preserve_mtimes, 0},
463 {"no-t", "", POPT_ARG_VAL, &o.preserve_mtimes, 0},
464 {"atimes", "U", POPT_ARG_NONE, nil, 'U'},
465 {"no-atimes", "", POPT_ARG_VAL, &o.preserve_atimes, 0},
466 {"no-U", "", POPT_ARG_VAL, &o.preserve_atimes, 0},
467 {"open-noatime", "", POPT_ARG_VAL, &o.open_noatime, 1},
468 {"no-open-noatime", "", POPT_ARG_VAL, &o.open_noatime, 0},
469 {"crtimes", "N", POPT_ARG_NONE, &o.preserve_crtimes, 1}, // refused
470 {"no-crtimes", "", POPT_ARG_VAL, &o.preserve_crtimes, 0},
471 {"no-N", "", POPT_ARG_VAL, &o.preserve_crtimes, 0},
472 {"omit-dir-times", "O", POPT_ARG_VAL, &o.omit_dir_times, 1},
473 {"no-omit-dir-times", "", POPT_ARG_VAL, &o.omit_dir_times, 0},
474 {"no-O", "", POPT_ARG_VAL, &o.omit_dir_times, 0},
475 {"omit-link-times", "J", POPT_ARG_VAL, &o.omit_link_times, 1},
476 {"no-omit-link-times", "", POPT_ARG_VAL, &o.omit_link_times, 0},
477 {"no-J", "", POPT_ARG_VAL, &o.omit_link_times, 0},
478 {"modify-window", "@", POPT_ARG_INT, &o.modify_window, OPT_MODIFY_WINDOW},
479 {"super", "", POPT_ARG_VAL, &o.am_root, 2},
480 {"no-super", "", POPT_ARG_VAL, &o.am_root, 0},
481 {"fake-super", "", POPT_ARG_VAL, &o.am_root, -1},
482 {"owner", "o", POPT_ARG_VAL, &o.preserve_uid, 1},
483 {"no-owner", "", POPT_ARG_VAL, &o.preserve_uid, 0},
484 {"no-o", "", POPT_ARG_VAL, &o.preserve_uid, 0},
485 {"group", "g", POPT_ARG_VAL, &o.preserve_gid, 1},
486 {"no-group", "", POPT_ARG_VAL, &o.preserve_gid, 0},
487 {"no-g", "", POPT_ARG_VAL, &o.preserve_gid, 0},
488 {"", "D", POPT_ARG_NONE, nil, 'D'},
489 {"no-D", "", POPT_ARG_NONE, nil, OPT_NO_D},
490 {"devices", "", POPT_ARG_VAL, &o.preserve_devices, 1},
491 {"no-devices", "", POPT_ARG_VAL, &o.preserve_devices, 0},
492 {"copy-devices", "", POPT_ARG_NONE, &o.copy_devices, 0},
493 {"write-devices", "", POPT_ARG_VAL, &o.write_devices, 1},
494 {"no-write-devices", "", POPT_ARG_VAL, &o.write_devices, 0},
495 {"specials", "", POPT_ARG_VAL, &o.preserve_specials, 1},
496 {"no-specials", "", POPT_ARG_VAL, &o.preserve_specials, 0},
497 {"links", "l", POPT_ARG_VAL, &o.preserve_links, 1},
498 {"no-links", "", POPT_ARG_VAL, &o.preserve_links, 0},
499 {"no-l", "", POPT_ARG_VAL, &o.preserve_links, 0},
500 {"copy-links", "L", POPT_ARG_NONE, &o.copy_links, 0},
501 {"copy-unsafe-links", "", POPT_ARG_NONE, &o.copy_unsafe_links, 0},
502 {"safe-links", "", POPT_ARG_NONE, &o.safe_symlinks, 0},
503 {"munge-links", "", POPT_ARG_VAL, &o.munge_symlinks, 1},
504 {"no-munge-links", "", POPT_ARG_VAL, &o.munge_symlinks, 0},
505 {"copy-dirlinks", "k", POPT_ARG_NONE, &o.copy_dirlinks, 0},
506 {"keep-dirlinks", "K", POPT_ARG_NONE, &o.keep_dirlinks, 0},
507 {"hard-links", "H", POPT_ARG_NONE, nil, 'H'},
508 {"no-hard-links", "", POPT_ARG_VAL, &o.preserve_hard_links, 0},
509 {"no-H", "", POPT_ARG_VAL, &o.preserve_hard_links, 0},
510 {"relative", "R", POPT_ARG_VAL, &o.relative_paths, 1},
511 {"no-relative", "", POPT_ARG_VAL, &o.relative_paths, 0},
512 {"no-R", "", POPT_ARG_VAL, &o.relative_paths, 0},
513 {"implied-dirs", "", POPT_ARG_VAL, &o.implied_dirs, 1},
514 {"no-implied-dirs", "", POPT_ARG_VAL, &o.implied_dirs, 0},
515 {"i-d", "", POPT_ARG_VAL, &o.implied_dirs, 1},
516 {"no-i-d", "", POPT_ARG_VAL, &o.implied_dirs, 0},
517 {"chmod", "", POPT_ARG_STRING, nil, OPT_CHMOD},
518 {"ignore-times", "I", POPT_ARG_NONE, &o.ignore_times, 0},
519 {"size-only", "", POPT_ARG_NONE, &o.size_only, 0},
520 {"one-file-system", "x", POPT_ARG_NONE, nil, 'x'},
521 {"no-one-file-system", "", POPT_ARG_VAL, &o.one_file_system, 0},
522 {"no-x", "", POPT_ARG_VAL, &o.one_file_system, 0},
523 {"update", "u", POPT_ARG_NONE, &o.update_only, 0},
524 {"existing", "", POPT_ARG_NONE, &o.ignore_non_existing, 0},
525 {"ignore-non-existing", "", POPT_ARG_NONE, &o.ignore_non_existing, 0},
526 {"ignore-existing", "", POPT_ARG_NONE, &o.ignore_existing, 0},
527 {"max-size", "", POPT_ARG_STRING, &o.max_size_arg, OPT_MAX_SIZE},
528 {"min-size", "", POPT_ARG_STRING, &o.min_size_arg, OPT_MIN_SIZE},
529 {"max-alloc", "", POPT_ARG_STRING, &o.max_alloc_arg, 0},
530 {"sparse", "S", POPT_ARG_VAL, &o.sparse_files, 1},
531 {"no-sparse", "", POPT_ARG_VAL, &o.sparse_files, 0},
532 {"no-S", "", POPT_ARG_VAL, &o.sparse_files, 0},
533 {"preallocate", "", POPT_ARG_NONE, &o.preallocate_files, 0},
534 {"inplace", "", POPT_ARG_VAL, &o.inplace, 1},
535 {"no-inplace", "", POPT_ARG_VAL, &o.inplace, 0},
536 {"append", "", POPT_ARG_NONE, nil, OPT_APPEND},
537 {"append-verify", "", POPT_ARG_VAL, &o.append_mode, 2},
538 {"no-append", "", POPT_ARG_VAL, &o.append_mode, 0},
539 {"del", "", POPT_ARG_NONE, &o.delete_during, 0},
540 {"delete", "", POPT_ARG_NONE, &o.delete_mode, 0},
541 {"delete-before", "", POPT_ARG_NONE, &o.delete_before, 0},
542 {"delete-during", "", POPT_ARG_VAL, &o.delete_during, 1},
543 {"delete-delay", "", POPT_ARG_VAL, &o.delete_during, 2},
544 {"delete-after", "", POPT_ARG_NONE, &o.delete_after, 0},
545 {"delete-excluded", "", POPT_ARG_NONE, &o.delete_excluded, 0},
546 {"delete-missing-args", "", POPT_BIT_SET, &o.missing_args, 2},
547 {"ignore-missing-args", "", POPT_BIT_SET, &o.missing_args, 1},
548 {"remove-sent-files", "", POPT_ARG_VAL, &o.remove_source_files, 2}, /* deprecated */
549 {"remove-source-files", "", POPT_ARG_VAL, &o.remove_source_files, 1},
550 {"force", "", POPT_ARG_VAL, &o.force_delete, 1},
551 {"no-force", "", POPT_ARG_VAL, &o.force_delete, 0},
552 {"ignore-errors", "", POPT_ARG_VAL, &o.ignore_errors, 1},
553 {"no-ignore-errors", "", POPT_ARG_VAL, &o.ignore_errors, 0},
554 {"max-delete", "", POPT_ARG_INT, &o.max_delete, 0},
555 {"", "F", POPT_ARG_NONE, nil, 'F'},
556 {"filter", "f", POPT_ARG_STRING, nil, OPT_FILTER},
557 {"exclude", "", POPT_ARG_STRING, nil, OPT_EXCLUDE},
558 {"include", "", POPT_ARG_STRING, nil, OPT_INCLUDE},
559 {"exclude-from", "", POPT_ARG_STRING, nil, OPT_EXCLUDE_FROM},
560 {"include-from", "", POPT_ARG_STRING, nil, OPT_INCLUDE_FROM},
561 {"cvs-exclude", "C", POPT_ARG_NONE, &o.cvs_exclude, 0},
562 {"whole-file", "W", POPT_ARG_VAL, &o.whole_file, 1},
563 {"no-whole-file", "", POPT_ARG_VAL, &o.whole_file, 0},
564 {"no-W", "", POPT_ARG_VAL, &o.whole_file, 0},
565 {"checksum", "c", POPT_ARG_VAL, &o.always_checksum, 1},
566 {"no-checksum", "", POPT_ARG_VAL, &o.always_checksum, 0},
567 {"no-c", "", POPT_ARG_VAL, &o.always_checksum, 0},
568 {"checksum-choice", "", POPT_ARG_STRING, &o.checksum_choice, 0},
569 {"cc", "", POPT_ARG_STRING, &o.checksum_choice, 0},
570 {"block-size", "B", POPT_ARG_STRING, nil, OPT_BLOCK_SIZE},
571 {"compare-dest", "", POPT_ARG_STRING, nil, OPT_COMPARE_DEST},
572 {"copy-dest", "", POPT_ARG_STRING, nil, OPT_COPY_DEST},
573 {"link-dest", "", POPT_ARG_STRING, nil, OPT_LINK_DEST},
574 {"fuzzy", "y", POPT_ARG_NONE, nil, 'y'},
575 {"no-fuzzy", "", POPT_ARG_VAL, &o.fuzzy_basis, 0},
576 {"no-y", "", POPT_ARG_VAL, &o.fuzzy_basis, 0},
577 {"compress", "z", POPT_ARG_NONE, nil, 'z'},
578 {"old-compress", "", POPT_ARG_NONE, nil, OPT_OLD_COMPRESS},
579 {"new-compress", "", POPT_ARG_NONE, nil, OPT_NEW_COMPRESS},
580 {"no-compress", "", POPT_ARG_NONE, nil, OPT_NO_COMPRESS},
581 {"no-z", "", POPT_ARG_NONE, nil, OPT_NO_COMPRESS},
582 {"compress-choice", "", POPT_ARG_STRING, &o.compress_choice, 0},
583 {"zc", "", POPT_ARG_STRING, &o.compress_choice, 0},
584 {"skip-compress", "", POPT_ARG_STRING, &o.skip_compress, 0},
585 {"compress-level", "", POPT_ARG_INT, &o.do_compression_level, 0},
586 {"zl", "", POPT_ARG_INT, &o.do_compression_level, 0},
587 {"", "P", POPT_ARG_NONE, nil, 'P'},
588 {"progress", "", POPT_ARG_VAL, &o.do_progress, 1},
589 {"no-progress", "", POPT_ARG_VAL, &o.do_progress, 0},
590 {"partial", "", POPT_ARG_VAL, &o.keep_partial, 1},
591 {"no-partial", "", POPT_ARG_VAL, &o.keep_partial, 0},
592 {"partial-dir", "", POPT_ARG_STRING, &o.partial_dir, 0},
593 {"delay-updates", "", POPT_ARG_VAL, &o.delay_updates, 1},
594 {"no-delay-updates", "", POPT_ARG_VAL, &o.delay_updates, 0},
595 {"prune-empty-dirs", "m", POPT_ARG_VAL, &o.prune_empty_dirs, 1},
596 {"no-prune-empty-dirs", "", POPT_ARG_VAL, &o.prune_empty_dirs, 0},
597 {"no-m", "", POPT_ARG_VAL, &o.prune_empty_dirs, 0},
598 {"log-file", "", POPT_ARG_STRING, &o.logfile_name, 0},
599 {"log-file-format", "", POPT_ARG_STRING, &o.logfile_format, 0},
600 {"out-format", "", POPT_ARG_STRING, &o.stdout_format, 0},
601 {"log-format", "", POPT_ARG_STRING, &o.stdout_format, 0}, /* DEPRECATED */
602 {"itemize-changes", "i", POPT_ARG_NONE, nil, 'i'},
603 {"no-itemize-changes", "", POPT_ARG_VAL, &o.itemize_changes, 0},
604 {"no-i", "", POPT_ARG_VAL, &o.itemize_changes, 0},
605 {"bwlimit", "", POPT_ARG_STRING, &o.bwlimit_arg, OPT_BWLIMIT},
606 {"no-bwlimit", "", POPT_ARG_VAL, &o.bwlimit, 0},
607 {"backup", "b", POPT_ARG_VAL, &o.make_backups, 1},
608 {"no-backup", "", POPT_ARG_VAL, &o.make_backups, 0},
609 {"backup-dir", "", POPT_ARG_STRING, &o.backup_dir, 0},
610 {"suffix", "", POPT_ARG_STRING, &o.backup_suffix, 0},
611 {"list-only", "", POPT_ARG_VAL, &o.list_only, 2},
612 {"read-batch", "", POPT_ARG_STRING, &o.batch_name, OPT_READ_BATCH},
613 {"write-batch", "", POPT_ARG_STRING, &o.batch_name, OPT_WRITE_BATCH},
614 {"only-write-batch", "", POPT_ARG_STRING, &o.batch_name, OPT_ONLY_WRITE_BATCH},
615 {"files-from", "", POPT_ARG_STRING, &o.files_from, 0},
616 {"from0", "0", POPT_ARG_VAL, &o.eol_nulls, 1},
617 {"no-from0", "", POPT_ARG_VAL, &o.eol_nulls, 0},
618 {"old-args", "", POPT_ARG_NONE, nil, OPT_OLD_ARGS},
619 {"no-old-args", "", POPT_ARG_VAL, &o.old_style_args, 0},
620 {"secluded-args", "s", POPT_ARG_VAL, &o.protect_args, 1},
621 {"no-secluded-args", "", POPT_ARG_VAL, &o.protect_args, 0},
622 {"protect-args", "", POPT_ARG_VAL, &o.protect_args, 1},
623 {"no-protect-args", "", POPT_ARG_VAL, &o.protect_args, 0},
624 {"no-s", "", POPT_ARG_VAL, &o.protect_args, 0},
625 {"trust-sender", "", POPT_ARG_VAL, &o.trust_sender, 1},
626 {"numeric-ids", "", POPT_ARG_VAL, &o.numeric_ids, 1},
627 {"no-numeric-ids", "", POPT_ARG_VAL, &o.numeric_ids, 0},
628 {"usermap", "", POPT_ARG_STRING, nil, OPT_USERMAP},
629 {"groupmap", "", POPT_ARG_STRING, nil, OPT_GROUPMAP},
630 {"chown", "", POPT_ARG_STRING, nil, OPT_CHOWN},
631 {"timeout", "", POPT_ARG_INT, &o.io_timeout, 0},
632 {"no-timeout", "", POPT_ARG_VAL, &o.io_timeout, 0},
633 {"contimeout", "", POPT_ARG_INT, &o.connect_timeout, 0},
634 {"no-contimeout", "", POPT_ARG_VAL, &o.connect_timeout, 0},
635 {"fsync", "", POPT_ARG_NONE, &o.do_fsync, 0},
636 {"stop-after", "", POPT_ARG_STRING, nil, OPT_STOP_AFTER},
637 {"time-limit", "", POPT_ARG_STRING, nil, OPT_STOP_AFTER}, /* earlier stop-after name */
638 {"stop-at", "", POPT_ARG_STRING, nil, OPT_STOP_AT},
639 {"rsh", "e", POPT_ARG_STRING, &o.shell_cmd, 0},
640 {"rsync-path", "", POPT_ARG_STRING, &o.rsync_path, 0},
641 {"temp-dir", "T", POPT_ARG_STRING, &o.tmpdir, 0},
642 {"iconv", "", POPT_ARG_STRING, &o.iconv_opt, 0},
643 {"no-iconv", "", POPT_ARG_NONE, nil, OPT_NO_ICONV},
644 {"ipv4", "4", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET},
645 {"ipv6", "6", POPT_ARG_VAL, &o.default_af_hint, syscall.AF_INET6},
646 {"8-bit-output", "8", POPT_ARG_VAL, &o.allow_8bit_chars, 1},
647 {"no-8-bit-output", "", POPT_ARG_VAL, &o.allow_8bit_chars, 0},
648 {"no-8", "", POPT_ARG_VAL, &o.allow_8bit_chars, 0},
649 {"mkpath", "", POPT_ARG_VAL, &o.mkpath_dest_arg, 1},
650 {"no-mkpath", "", POPT_ARG_VAL, &o.mkpath_dest_arg, 0},
651 {"qsort", "", POPT_ARG_NONE, &o.use_qsort, 0},
652 {"copy-as", "", POPT_ARG_STRING, &o.copy_as, 0},
653 {"address", "", POPT_ARG_STRING, &o.bind_address, 0},
654 {"port", "", POPT_ARG_INT, &o.rsync_port, 0},
655 {"sockopts", "", POPT_ARG_STRING, &o.sockopts, 0},
656 {"password-file", "", POPT_ARG_STRING, &o.password_file, 0},
657 {"early-input", "", POPT_ARG_STRING, &o.early_input_file, 0},
658 {"blocking-io", "", POPT_ARG_VAL, &o.blocking_io, 1},
659 {"no-blocking-io", "", POPT_ARG_VAL, &o.blocking_io, 0},
660 {"outbuf", "", POPT_ARG_STRING, &o.outbuf_mode, 0},
661 {"remote-option", "M", POPT_ARG_STRING, nil, 'M'},
662 {"protocol", "", POPT_ARG_INT, &o.protocol_version, 0},
663 {"checksum-seed", "", POPT_ARG_INT, &o.checksum_seed, 0},
664 {"server", "", POPT_ARG_NONE, nil, OPT_SERVER},
665 {"sender", "", POPT_ARG_NONE, nil, OPT_SENDER},
666 /* All the following options switch us into daemon-mode option-parsing. */
667 {"config", "", POPT_ARG_STRING, nil, OPT_DAEMON},
668 {"daemon", "", POPT_ARG_NONE, nil, OPT_DAEMON},
669 {"dparam", "", POPT_ARG_STRING, nil, OPT_DAEMON},
670 {"detach", "", POPT_ARG_NONE, nil, OPT_DAEMON},
671 {"no-detach", "", POPT_ARG_NONE, nil, OPT_DAEMON},
672 }
673}
674
675var errNotYetImplemented = errors.New("option not yet implemented in gokrazy/rsync")
676
677// rsync/options.c:parse_arguments.
678func ParseArguments(args []string, gokrazyTable bool) (*Context, error) {
679 // NOTE: We do not implement support for refusing options per rsyncd.conf
680 // here, as we have our own configuration file.
681
682 version_opt_cnt := 0
683
684 opts := NewOptions()
685 table := opts.table()
686 if gokrazyTable {
687 // We need to make the --gokr.* flags known, otherwise the first parsing
688 // attempt fails and the daemon mode parsing is never run.
689 table = slices.Concat(opts.Gokrazy.table(), table)
690 }
691 pc := Context{
692 Options: opts,
693 table: table,
694 args: args,
695 }
696
697 for {
698 opt, err := pc.poptGetNextOpt()
699 if err != nil {
700 return nil, err
701 }
702 if opt == -1 {
703 break // done
704 }
705 // Most options are handled by poptGetNextOpt, only special cases
706 // are returned and handled here.
707 switch opt {
708 case 'V':
709 version_opt_cnt++
710
711 case OPT_SERVER:
712 opts.am_server = 1
713
714 case OPT_SENDER:
715 if opts.am_server == 0 {
716 return nil, fmt.Errorf("--sender only allowed with --server")
717 }
718 opts.am_sender = 1
719
720 case OPT_DAEMON:
721 // Parse the whole command-line using the daemon options table.
722 table := opts.daemonTable()
723 if gokrazyTable {
724 table = slices.Concat(opts.Gokrazy.table(), table)
725 }
726 pc := Context{
727 Options: opts,
728 table: table,
729 args: args,
730 }
731
732 for {
733 opt, err := pc.poptGetNextOpt()
734 if err != nil {
735 return nil, err
736 }
737 if opt == -1 {
738 break // done
739 }
740 // Most options are handled by poptGetNextOpt, only special cases
741 // are returned and handled here.
742 switch opt {
743 case 'M':
744 return nil, errNotYetImplemented
745 case 'v':
746 opts.verbose++
747 default:
748 return nil, fmt.Errorf("unhandled special case opt: %v", opt)
749 }
750 }
751
752 opts.am_daemon = 1
753
754 return &pc, nil
755
756 case OPT_FILTER,
757 OPT_EXCLUDE,
758 OPT_INCLUDE,
759 OPT_INCLUDE_FROM,
760 OPT_EXCLUDE_FROM:
761 return nil, errNotYetImplemented
762
763 case 'a':
764 if opts.recurse == 0 {
765 opts.recurse = 1
766 }
767 opts.preserve_links = 1
768 opts.preserve_perms = 1
769 opts.preserve_mtimes = 1
770 opts.preserve_gid = 1
771 opts.preserve_uid = 1
772 opts.preserve_devices = 1
773 opts.preserve_specials = 1
774
775 case 'D':
776 opts.preserve_devices = 1
777 opts.preserve_specials = 1
778
779 case OPT_NO_D:
780 opts.preserve_devices = 0
781 opts.preserve_specials = 0
782
783 case 'h':
784 opts.human_readable++
785
786 case 'H':
787 opts.preserve_hard_links = 1
788
789 case 'i':
790 opts.itemize_changes++
791
792 case 'U':
793 opts.preserve_atimes++
794 if opts.preserve_atimes > 1 {
795 opts.open_noatime = 1
796 }
797
798 case 'v':
799 opts.verbose++
800
801 case 'y':
802 return nil, errNotYetImplemented
803
804 case 'q':
805 opts.quiet++
806
807 case 'x':
808 opts.one_file_system++
809
810 case 'F':
811 return nil, errNotYetImplemented
812
813 case 'P':
814 opts.do_progress = 1
815 opts.keep_partial = 1
816
817 case 'z':
818 opts.do_compression++
819
820 case OPT_OLD_COMPRESS:
821 opts.compress_choice = "zlib"
822
823 case OPT_NEW_COMPRESS:
824 opts.compress_choice = "zlibx"
825
826 case OPT_NO_COMPRESS:
827 opts.do_compression = 0
828 opts.compress_choice = ""
829
830 case OPT_OLD_ARGS:
831 return nil, errNotYetImplemented
832
833 case 'M': // --remote-option
834 return nil, errNotYetImplemented
835
836 case OPT_WRITE_BATCH,
837 OPT_ONLY_WRITE_BATCH,
838 OPT_READ_BATCH:
839 return nil, errNotYetImplemented
840
841 case OPT_BLOCK_SIZE:
842 return nil, errNotYetImplemented
843
844 case OPT_MAX_SIZE, // (needs parse_size_arg)
845 OPT_MIN_SIZE,
846 OPT_BWLIMIT:
847 return nil, errNotYetImplemented
848
849 case OPT_APPEND:
850 return nil, errNotYetImplemented
851
852 case OPT_LINK_DEST,
853 OPT_COPY_DEST,
854 OPT_COMPARE_DEST:
855 return nil, errNotYetImplemented
856
857 case OPT_CHMOD: // (needs parse_chmod):
858 return nil, errNotYetImplemented
859
860 case OPT_INFO:
861 parseOutputWords(infoWords[:], opts.info[:], pc.poptGetOptArg(), USER_PRIORITY)
862
863 case OPT_DEBUG:
864 // TODO: plumb the debug level that make sense for our implementation
865 slog.Info("TODO: set debug level", "to", pc.poptGetOptArg())
866
867 case OPT_USERMAP,
868 OPT_GROUPMAP,
869 OPT_CHOWN:
870 return nil, errNotYetImplemented
871
872 case 'A':
873 return nil, fmt.Errorf("ACLs are not supported by gokrazy/rsync")
874
875 case 'X':
876 opts.preserve_xattrs++
877
878 case OPT_STOP_AFTER,
879 OPT_STOP_AT,
880 OPT_STDERR:
881 return nil, errNotYetImplemented
882
883 default:
884 return nil, fmt.Errorf("unhandled special case opt: %v", opt)
885 }
886 }
887
888 // rsync/options.c line 1973 and following set option defaults based on
889 // other options
890
891 opts.setOutputVerbosity(DEFAULT_PRIORITY)
892
893 if opts.recurse != 0 {
894 opts.xfer_dirs = 1
895 }
896 if opts.xfer_dirs < 0 {
897 if opts.list_only != 0 {
898 opts.xfer_dirs = 1
899 } else {
900 opts.xfer_dirs = 0
901 }
902 }
903
904 if opts.relative_paths < 0 {
905 if opts.files_from != "" {
906 opts.relative_paths = 1
907 } else {
908 opts.relative_paths = 0
909 }
910 }
911
912 if opts.relative_paths == 0 {
913 opts.implied_dirs = 0
914 }
915
916 // NOTE: This simplification means that even if we ignore POPT_ARGFLAG_OR
917 // and store ints without regards for bit sets, we get the same result.
918 // Nevertheless, we support bit to be future-proof as new options are added.
919 if opts.missing_args == 3 {
920 // simplify if both options were specified
921 opts.missing_args = 2
922 }
923
924 if opts.backup_suffix == "" && opts.backup_dir == "" {
925 opts.backup_suffix = "~"
926 }
927
928 if opts.backup_dir != "" {
929 opts.make_backups = 1 // --backup-dir implies --backup
930 }
931
932 if opts.do_progress != 0 /* && !opts.am_server */ {
933 if opts.info[INFO_NAME] == 0 {
934 opts.info[INFO_NAME] = 1
935 }
936 }
937
938 if opts.info[INFO_NAME] >= 1 && opts.stdout_format == "" {
939 opts.stdout_format = "%n%L"
940 }
941
942 return &pc, nil
943}