arguments.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923
  1. import collections
  2. from argparse import Action, ArgumentParser
  3. from borgmatic.config import collect
  4. SUBPARSER_ALIASES = {
  5. 'rcreate': ['init', '-I'],
  6. 'prune': ['-p'],
  7. 'compact': [],
  8. 'create': ['-C'],
  9. 'check': ['-k'],
  10. 'extract': ['-x'],
  11. 'export-tar': [],
  12. 'mount': ['-m'],
  13. 'umount': ['-u'],
  14. 'restore': ['-r'],
  15. 'rlist': [],
  16. 'list': ['-l'],
  17. 'rinfo': [],
  18. 'info': ['-i'],
  19. 'transfer': [],
  20. 'break-lock': [],
  21. 'borg': [],
  22. }
  23. def parse_subparser_arguments(unparsed_arguments, subparsers):
  24. '''
  25. Given a sequence of arguments and a dict from subparser name to argparse.ArgumentParser
  26. instance, give each requested action's subparser a shot at parsing all arguments. This allows
  27. common arguments like "--repository" to be shared across multiple subparsers.
  28. Return the result as a tuple of (a dict mapping from subparser name to a parsed namespace of
  29. arguments, a list of remaining arguments not claimed by any subparser).
  30. '''
  31. arguments = collections.OrderedDict()
  32. remaining_arguments = list(unparsed_arguments)
  33. alias_to_subparser_name = {
  34. alias: subparser_name
  35. for subparser_name, aliases in SUBPARSER_ALIASES.items()
  36. for alias in aliases
  37. }
  38. # If the "borg" action is used, skip all other subparsers. This avoids confusion like
  39. # "borg list" triggering borgmatic's own list action.
  40. if 'borg' in unparsed_arguments:
  41. subparsers = {'borg': subparsers['borg']}
  42. for argument in remaining_arguments:
  43. canonical_name = alias_to_subparser_name.get(argument, argument)
  44. subparser = subparsers.get(canonical_name)
  45. if not subparser:
  46. continue
  47. # If a parsed value happens to be the same as the name of a subparser, remove it from the
  48. # remaining arguments. This prevents, for instance, "check --only extract" from triggering
  49. # the "extract" subparser.
  50. parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
  51. for value in vars(parsed).values():
  52. if isinstance(value, str):
  53. if value in subparsers:
  54. remaining_arguments.remove(value)
  55. elif isinstance(value, list):
  56. for item in value:
  57. if item in subparsers:
  58. remaining_arguments.remove(item)
  59. arguments[canonical_name] = parsed
  60. # If no actions are explicitly requested, assume defaults.
  61. if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
  62. for subparser_name in ('create', 'prune', 'compact', 'check'):
  63. subparser = subparsers[subparser_name]
  64. parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
  65. arguments[subparser_name] = parsed
  66. remaining_arguments = list(unparsed_arguments)
  67. # Now ask each subparser, one by one, to greedily consume arguments.
  68. for subparser_name, subparser in subparsers.items():
  69. if subparser_name not in arguments.keys():
  70. continue
  71. subparser = subparsers[subparser_name]
  72. unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
  73. # Special case: If "borg" is present in the arguments, consume all arguments after (+1) the
  74. # "borg" action.
  75. if 'borg' in arguments:
  76. borg_options_index = remaining_arguments.index('borg') + 1
  77. arguments['borg'].options = remaining_arguments[borg_options_index:]
  78. remaining_arguments = remaining_arguments[:borg_options_index]
  79. # Remove the subparser names themselves.
  80. for subparser_name, subparser in subparsers.items():
  81. if subparser_name in remaining_arguments:
  82. remaining_arguments.remove(subparser_name)
  83. return (arguments, remaining_arguments)
  84. class Extend_action(Action):
  85. '''
  86. An argparse action to support Python 3.8's "extend" action in older versions of Python.
  87. '''
  88. def __call__(self, parser, namespace, values, option_string=None):
  89. items = getattr(namespace, self.dest, None)
  90. if items:
  91. items.extend(values)
  92. else:
  93. setattr(namespace, self.dest, list(values))
  94. def make_parsers():
  95. '''
  96. Build a top-level parser and its subparsers and return them as a tuple.
  97. '''
  98. config_paths = collect.get_default_config_paths(expand_home=True)
  99. unexpanded_config_paths = collect.get_default_config_paths(expand_home=False)
  100. global_parser = ArgumentParser(add_help=False)
  101. global_parser.register('action', 'extend', Extend_action)
  102. global_group = global_parser.add_argument_group('global arguments')
  103. global_group.add_argument(
  104. '-c',
  105. '--config',
  106. nargs='*',
  107. dest='config_paths',
  108. default=config_paths,
  109. help=f"Configuration filenames or directories, defaults to: {' '.join(unexpanded_config_paths)}",
  110. )
  111. global_group.add_argument(
  112. '--excludes',
  113. dest='excludes_filename',
  114. help='Deprecated in favor of exclude_patterns within configuration',
  115. )
  116. global_group.add_argument(
  117. '-n',
  118. '--dry-run',
  119. dest='dry_run',
  120. action='store_true',
  121. help='Go through the motions, but do not actually write to any repositories',
  122. )
  123. global_group.add_argument(
  124. '-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
  125. )
  126. global_group.add_argument(
  127. '-v',
  128. '--verbosity',
  129. type=int,
  130. choices=range(-1, 3),
  131. default=0,
  132. help='Display verbose progress to the console (from only errors to very verbose: -1, 0, 1, or 2)',
  133. )
  134. global_group.add_argument(
  135. '--syslog-verbosity',
  136. type=int,
  137. choices=range(-1, 3),
  138. default=0,
  139. help='Log verbose progress to syslog (from only errors to very verbose: -1, 0, 1, or 2). Ignored when console is interactive or --log-file is given',
  140. )
  141. global_group.add_argument(
  142. '--log-file-verbosity',
  143. type=int,
  144. choices=range(-1, 3),
  145. default=0,
  146. help='Log verbose progress to log file (from only errors to very verbose: -1, 0, 1, or 2). Only used when --log-file is given',
  147. )
  148. global_group.add_argument(
  149. '--monitoring-verbosity',
  150. type=int,
  151. choices=range(-1, 3),
  152. default=0,
  153. help='Log verbose progress to monitoring integrations that support logging (from only errors to very verbose: -1, 0, 1, or 2)',
  154. )
  155. global_group.add_argument(
  156. '--log-file',
  157. type=str,
  158. help='Write log messages to this file instead of syslog',
  159. )
  160. global_group.add_argument(
  161. '--log-file-format',
  162. type=str,
  163. help='Log format string used for log messages written to the log file',
  164. )
  165. global_group.add_argument(
  166. '--override',
  167. metavar='SECTION.OPTION=VALUE',
  168. nargs='+',
  169. dest='overrides',
  170. action='extend',
  171. help='One or more configuration file options to override with specified values',
  172. )
  173. global_group.add_argument(
  174. '--no-environment-interpolation',
  175. dest='resolve_env',
  176. action='store_false',
  177. help='Do not resolve environment variables in configuration file',
  178. )
  179. global_group.add_argument(
  180. '--bash-completion',
  181. default=False,
  182. action='store_true',
  183. help='Show bash completion script and exit',
  184. )
  185. global_group.add_argument(
  186. '--fish-completion',
  187. default=False,
  188. action='store_true',
  189. help='Show fish completion script and exit',
  190. )
  191. global_group.add_argument(
  192. '--version',
  193. dest='version',
  194. default=False,
  195. action='store_true',
  196. help='Display installed version number of borgmatic and exit',
  197. )
  198. top_level_parser = ArgumentParser(
  199. description='''
  200. Simple, configuration-driven backup software for servers and workstations. If none of
  201. the action options are given, then borgmatic defaults to: create, prune, compact, and
  202. check.
  203. ''',
  204. parents=[global_parser],
  205. )
  206. subparsers = top_level_parser.add_subparsers(
  207. title='actions',
  208. metavar='',
  209. help='Specify zero or more actions. Defaults to create, prune, compact, and check. Use --help with action for details:',
  210. )
  211. rcreate_parser = subparsers.add_parser(
  212. 'rcreate',
  213. aliases=SUBPARSER_ALIASES['rcreate'],
  214. help='Create a new, empty Borg repository',
  215. description='Create a new, empty Borg repository',
  216. add_help=False,
  217. )
  218. rcreate_group = rcreate_parser.add_argument_group('rcreate arguments')
  219. rcreate_group.add_argument(
  220. '-e',
  221. '--encryption',
  222. dest='encryption_mode',
  223. help='Borg repository encryption mode',
  224. required=True,
  225. )
  226. rcreate_group.add_argument(
  227. '--source-repository',
  228. '--other-repo',
  229. metavar='KEY_REPOSITORY',
  230. help='Path to an existing Borg repository whose key material should be reused (Borg 2.x+ only)',
  231. )
  232. rcreate_group.add_argument(
  233. '--repository',
  234. help='Path of the new repository to create (must be already specified in a borgmatic configuration file), defaults to the configured repository if there is only one',
  235. )
  236. rcreate_group.add_argument(
  237. '--copy-crypt-key',
  238. action='store_true',
  239. help='Copy the crypt key used for authenticated encryption from the source repository, defaults to a new random key (Borg 2.x+ only)',
  240. )
  241. rcreate_group.add_argument(
  242. '--append-only',
  243. action='store_true',
  244. help='Create an append-only repository',
  245. )
  246. rcreate_group.add_argument(
  247. '--storage-quota',
  248. help='Create a repository with a fixed storage quota',
  249. )
  250. rcreate_group.add_argument(
  251. '--make-parent-dirs',
  252. action='store_true',
  253. help='Create any missing parent directories of the repository directory',
  254. )
  255. rcreate_group.add_argument(
  256. '-h', '--help', action='help', help='Show this help message and exit'
  257. )
  258. transfer_parser = subparsers.add_parser(
  259. 'transfer',
  260. aliases=SUBPARSER_ALIASES['transfer'],
  261. help='Transfer archives from one repository to another, optionally upgrading the transferred data (Borg 2.0+ only)',
  262. description='Transfer archives from one repository to another, optionally upgrading the transferred data (Borg 2.0+ only)',
  263. add_help=False,
  264. )
  265. transfer_group = transfer_parser.add_argument_group('transfer arguments')
  266. transfer_group.add_argument(
  267. '--repository',
  268. help='Path of existing destination repository to transfer archives to, defaults to the configured repository if there is only one',
  269. )
  270. transfer_group.add_argument(
  271. '--source-repository',
  272. help='Path of existing source repository to transfer archives from',
  273. required=True,
  274. )
  275. transfer_group.add_argument(
  276. '--archive',
  277. help='Name of single archive to transfer (or "latest"), defaults to transferring all archives',
  278. )
  279. transfer_group.add_argument(
  280. '--upgrader',
  281. help='Upgrader type used to convert the transferred data, e.g. "From12To20" to upgrade data from Borg 1.2 to 2.0 format, defaults to no conversion',
  282. )
  283. transfer_group.add_argument(
  284. '--progress',
  285. default=False,
  286. action='store_true',
  287. help='Display progress as each archive is transferred',
  288. )
  289. transfer_group.add_argument(
  290. '-a',
  291. '--match-archives',
  292. '--glob-archives',
  293. metavar='PATTERN',
  294. help='Only transfer archives with names matching this pattern',
  295. )
  296. transfer_group.add_argument(
  297. '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
  298. )
  299. transfer_group.add_argument(
  300. '--first',
  301. metavar='N',
  302. help='Only transfer first N archives after other filters are applied',
  303. )
  304. transfer_group.add_argument(
  305. '--last', metavar='N', help='Only transfer last N archives after other filters are applied'
  306. )
  307. transfer_group.add_argument(
  308. '-h', '--help', action='help', help='Show this help message and exit'
  309. )
  310. prune_parser = subparsers.add_parser(
  311. 'prune',
  312. aliases=SUBPARSER_ALIASES['prune'],
  313. help='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)',
  314. description='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)',
  315. add_help=False,
  316. )
  317. prune_group = prune_parser.add_argument_group('prune arguments')
  318. prune_group.add_argument(
  319. '--repository',
  320. help='Path of specific existing repository to prune (must be already specified in a borgmatic configuration file)',
  321. )
  322. prune_group.add_argument(
  323. '--stats',
  324. dest='stats',
  325. default=False,
  326. action='store_true',
  327. help='Display statistics of archive',
  328. )
  329. prune_group.add_argument(
  330. '--list', dest='list_archives', action='store_true', help='List archives kept/pruned'
  331. )
  332. prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  333. compact_parser = subparsers.add_parser(
  334. 'compact',
  335. aliases=SUBPARSER_ALIASES['compact'],
  336. help='Compact segments to free space (Borg 1.2+, borgmatic 1.5.23+ only)',
  337. description='Compact segments to free space (Borg 1.2+, borgmatic 1.5.23+ only)',
  338. add_help=False,
  339. )
  340. compact_group = compact_parser.add_argument_group('compact arguments')
  341. compact_group.add_argument(
  342. '--repository',
  343. help='Path of specific existing repository to compact (must be already specified in a borgmatic configuration file)',
  344. )
  345. compact_group.add_argument(
  346. '--progress',
  347. dest='progress',
  348. default=False,
  349. action='store_true',
  350. help='Display progress as each segment is compacted',
  351. )
  352. compact_group.add_argument(
  353. '--cleanup-commits',
  354. dest='cleanup_commits',
  355. default=False,
  356. action='store_true',
  357. help='Cleanup commit-only 17-byte segment files left behind by Borg 1.1 (flag in Borg 1.2 only)',
  358. )
  359. compact_group.add_argument(
  360. '--threshold',
  361. type=int,
  362. dest='threshold',
  363. help='Minimum saved space percentage threshold for compacting a segment, defaults to 10',
  364. )
  365. compact_group.add_argument(
  366. '-h', '--help', action='help', help='Show this help message and exit'
  367. )
  368. create_parser = subparsers.add_parser(
  369. 'create',
  370. aliases=SUBPARSER_ALIASES['create'],
  371. help='Create an archive (actually perform a backup)',
  372. description='Create an archive (actually perform a backup)',
  373. add_help=False,
  374. )
  375. create_group = create_parser.add_argument_group('create arguments')
  376. create_group.add_argument(
  377. '--repository',
  378. help='Path of specific existing repository to backup to (must be already specified in a borgmatic configuration file)',
  379. )
  380. create_group.add_argument(
  381. '--progress',
  382. dest='progress',
  383. default=False,
  384. action='store_true',
  385. help='Display progress for each file as it is backed up',
  386. )
  387. create_group.add_argument(
  388. '--stats',
  389. dest='stats',
  390. default=False,
  391. action='store_true',
  392. help='Display statistics of archive',
  393. )
  394. create_group.add_argument(
  395. '--list', '--files', dest='list_files', action='store_true', help='Show per-file details'
  396. )
  397. create_group.add_argument(
  398. '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
  399. )
  400. create_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  401. check_parser = subparsers.add_parser(
  402. 'check',
  403. aliases=SUBPARSER_ALIASES['check'],
  404. help='Check archives for consistency',
  405. description='Check archives for consistency',
  406. add_help=False,
  407. )
  408. check_group = check_parser.add_argument_group('check arguments')
  409. check_group.add_argument(
  410. '--repository',
  411. help='Path of specific existing repository to check (must be already specified in a borgmatic configuration file)',
  412. )
  413. check_group.add_argument(
  414. '--progress',
  415. dest='progress',
  416. default=False,
  417. action='store_true',
  418. help='Display progress for each file as it is checked',
  419. )
  420. check_group.add_argument(
  421. '--repair',
  422. dest='repair',
  423. default=False,
  424. action='store_true',
  425. help='Attempt to repair any inconsistencies found (for interactive use)',
  426. )
  427. check_group.add_argument(
  428. '--only',
  429. metavar='CHECK',
  430. choices=('repository', 'archives', 'data', 'extract'),
  431. dest='only',
  432. action='append',
  433. help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks (subject to configured frequency, can specify flag multiple times)',
  434. )
  435. check_group.add_argument(
  436. '--force',
  437. default=False,
  438. action='store_true',
  439. help='Ignore configured check frequencies and run checks unconditionally',
  440. )
  441. check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  442. extract_parser = subparsers.add_parser(
  443. 'extract',
  444. aliases=SUBPARSER_ALIASES['extract'],
  445. help='Extract files from a named archive to the current directory',
  446. description='Extract a named archive to the current directory',
  447. add_help=False,
  448. )
  449. extract_group = extract_parser.add_argument_group('extract arguments')
  450. extract_group.add_argument(
  451. '--repository',
  452. help='Path of repository to extract, defaults to the configured repository if there is only one',
  453. )
  454. extract_group.add_argument(
  455. '--archive', help='Name of archive to extract (or "latest")', required=True
  456. )
  457. extract_group.add_argument(
  458. '--path',
  459. '--restore-path',
  460. metavar='PATH',
  461. nargs='+',
  462. dest='paths',
  463. help='Paths to extract from archive, defaults to the entire archive',
  464. )
  465. extract_group.add_argument(
  466. '--destination',
  467. metavar='PATH',
  468. dest='destination',
  469. help='Directory to extract files into, defaults to the current directory',
  470. )
  471. extract_group.add_argument(
  472. '--strip-components',
  473. type=lambda number: number if number == 'all' else int(number),
  474. metavar='NUMBER',
  475. help='Number of leading path components to remove from each extracted path or "all" to strip all leading path components. Skip paths with fewer elements',
  476. )
  477. extract_group.add_argument(
  478. '--progress',
  479. dest='progress',
  480. default=False,
  481. action='store_true',
  482. help='Display progress for each file as it is extracted',
  483. )
  484. extract_group.add_argument(
  485. '-h', '--help', action='help', help='Show this help message and exit'
  486. )
  487. export_tar_parser = subparsers.add_parser(
  488. 'export-tar',
  489. aliases=SUBPARSER_ALIASES['export-tar'],
  490. help='Export an archive to a tar-formatted file or stream',
  491. description='Export an archive to a tar-formatted file or stream',
  492. add_help=False,
  493. )
  494. export_tar_group = export_tar_parser.add_argument_group('export-tar arguments')
  495. export_tar_group.add_argument(
  496. '--repository',
  497. help='Path of repository to export from, defaults to the configured repository if there is only one',
  498. )
  499. export_tar_group.add_argument(
  500. '--archive', help='Name of archive to export (or "latest")', required=True
  501. )
  502. export_tar_group.add_argument(
  503. '--path',
  504. metavar='PATH',
  505. nargs='+',
  506. dest='paths',
  507. help='Paths to export from archive, defaults to the entire archive',
  508. )
  509. export_tar_group.add_argument(
  510. '--destination',
  511. metavar='PATH',
  512. dest='destination',
  513. help='Path to destination export tar file, or "-" for stdout (but be careful about dirtying output with --verbosity or --list)',
  514. required=True,
  515. )
  516. export_tar_group.add_argument(
  517. '--tar-filter', help='Name of filter program to pipe data through'
  518. )
  519. export_tar_group.add_argument(
  520. '--list', '--files', dest='list_files', action='store_true', help='Show per-file details'
  521. )
  522. export_tar_group.add_argument(
  523. '--strip-components',
  524. type=int,
  525. metavar='NUMBER',
  526. dest='strip_components',
  527. help='Number of leading path components to remove from each exported path. Skip paths with fewer elements',
  528. )
  529. export_tar_group.add_argument(
  530. '-h', '--help', action='help', help='Show this help message and exit'
  531. )
  532. mount_parser = subparsers.add_parser(
  533. 'mount',
  534. aliases=SUBPARSER_ALIASES['mount'],
  535. help='Mount files from a named archive as a FUSE filesystem',
  536. description='Mount a named archive as a FUSE filesystem',
  537. add_help=False,
  538. )
  539. mount_group = mount_parser.add_argument_group('mount arguments')
  540. mount_group.add_argument(
  541. '--repository',
  542. help='Path of repository to use, defaults to the configured repository if there is only one',
  543. )
  544. mount_group.add_argument('--archive', help='Name of archive to mount (or "latest")')
  545. mount_group.add_argument(
  546. '--mount-point',
  547. metavar='PATH',
  548. dest='mount_point',
  549. help='Path where filesystem is to be mounted',
  550. required=True,
  551. )
  552. mount_group.add_argument(
  553. '--path',
  554. metavar='PATH',
  555. nargs='+',
  556. dest='paths',
  557. help='Paths to mount from archive, defaults to the entire archive',
  558. )
  559. mount_group.add_argument(
  560. '--foreground',
  561. dest='foreground',
  562. default=False,
  563. action='store_true',
  564. help='Stay in foreground until ctrl-C is pressed',
  565. )
  566. mount_group.add_argument('--options', dest='options', help='Extra Borg mount options')
  567. mount_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  568. umount_parser = subparsers.add_parser(
  569. 'umount',
  570. aliases=SUBPARSER_ALIASES['umount'],
  571. help='Unmount a FUSE filesystem that was mounted with "borgmatic mount"',
  572. description='Unmount a mounted FUSE filesystem',
  573. add_help=False,
  574. )
  575. umount_group = umount_parser.add_argument_group('umount arguments')
  576. umount_group.add_argument(
  577. '--mount-point',
  578. metavar='PATH',
  579. dest='mount_point',
  580. help='Path of filesystem to unmount',
  581. required=True,
  582. )
  583. umount_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  584. restore_parser = subparsers.add_parser(
  585. 'restore',
  586. aliases=SUBPARSER_ALIASES['restore'],
  587. help='Restore database dumps from a named archive',
  588. description='Restore database dumps from a named archive. (To extract files instead, use "borgmatic extract".)',
  589. add_help=False,
  590. )
  591. restore_group = restore_parser.add_argument_group('restore arguments')
  592. restore_group.add_argument(
  593. '--repository',
  594. help='Path of repository to restore from, defaults to the configured repository if there is only one',
  595. )
  596. restore_group.add_argument(
  597. '--archive', help='Name of archive to restore from (or "latest")', required=True
  598. )
  599. restore_group.add_argument(
  600. '--database',
  601. metavar='NAME',
  602. nargs='+',
  603. dest='databases',
  604. help="Names of databases to restore from archive, defaults to all databases. Note that any databases to restore must be defined in borgmatic's configuration",
  605. )
  606. restore_group.add_argument(
  607. '--schema',
  608. metavar='NAME',
  609. nargs='+',
  610. dest='schemas',
  611. help='Names of schemas to restore from the database, defaults to all schemas. Schemas are only supported for PostgreSQL and MongoDB databases',
  612. )
  613. restore_group.add_argument(
  614. '-h', '--help', action='help', help='Show this help message and exit'
  615. )
  616. rlist_parser = subparsers.add_parser(
  617. 'rlist',
  618. aliases=SUBPARSER_ALIASES['rlist'],
  619. help='List repository',
  620. description='List the archives in a repository',
  621. add_help=False,
  622. )
  623. rlist_group = rlist_parser.add_argument_group('rlist arguments')
  624. rlist_group.add_argument(
  625. '--repository',
  626. help='Path of repository to list, defaults to the configured repositories',
  627. )
  628. rlist_group.add_argument(
  629. '--short', default=False, action='store_true', help='Output only archive names'
  630. )
  631. rlist_group.add_argument('--format', help='Format for archive listing')
  632. rlist_group.add_argument(
  633. '--json', default=False, action='store_true', help='Output results as JSON'
  634. )
  635. rlist_group.add_argument(
  636. '-P', '--prefix', help='Deprecated. Only list archive names starting with this prefix'
  637. )
  638. rlist_group.add_argument(
  639. '-a',
  640. '--match-archives',
  641. '--glob-archives',
  642. metavar='PATTERN',
  643. help='Only list archive names matching this pattern',
  644. )
  645. rlist_group.add_argument(
  646. '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
  647. )
  648. rlist_group.add_argument(
  649. '--first', metavar='N', help='List first N archives after other filters are applied'
  650. )
  651. rlist_group.add_argument(
  652. '--last', metavar='N', help='List last N archives after other filters are applied'
  653. )
  654. rlist_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  655. list_parser = subparsers.add_parser(
  656. 'list',
  657. aliases=SUBPARSER_ALIASES['list'],
  658. help='List archive',
  659. description='List the files in an archive or search for a file across archives',
  660. add_help=False,
  661. )
  662. list_group = list_parser.add_argument_group('list arguments')
  663. list_group.add_argument(
  664. '--repository',
  665. help='Path of repository containing archive to list, defaults to the configured repositories',
  666. )
  667. list_group.add_argument('--archive', help='Name of the archive to list (or "latest")')
  668. list_group.add_argument(
  669. '--path',
  670. metavar='PATH',
  671. nargs='+',
  672. dest='paths',
  673. help='Paths or patterns to list from a single selected archive (via "--archive"), defaults to listing the entire archive',
  674. )
  675. list_group.add_argument(
  676. '--find',
  677. metavar='PATH',
  678. nargs='+',
  679. dest='find_paths',
  680. help='Partial paths or patterns to search for and list across multiple archives',
  681. )
  682. list_group.add_argument(
  683. '--short', default=False, action='store_true', help='Output only path names'
  684. )
  685. list_group.add_argument('--format', help='Format for file listing')
  686. list_group.add_argument(
  687. '--json', default=False, action='store_true', help='Output results as JSON'
  688. )
  689. list_group.add_argument(
  690. '-P', '--prefix', help='Deprecated. Only list archive names starting with this prefix'
  691. )
  692. list_group.add_argument(
  693. '-a',
  694. '--match-archives',
  695. '--glob-archives',
  696. metavar='PATTERN',
  697. help='Only list archive names matching this pattern',
  698. )
  699. list_group.add_argument(
  700. '--successful',
  701. default=True,
  702. action='store_true',
  703. help='Deprecated; no effect. Newer versions of Borg shows successful (non-checkpoint) archives by default.',
  704. )
  705. list_group.add_argument(
  706. '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
  707. )
  708. list_group.add_argument(
  709. '--first', metavar='N', help='List first N archives after other filters are applied'
  710. )
  711. list_group.add_argument(
  712. '--last', metavar='N', help='List last N archives after other filters are applied'
  713. )
  714. list_group.add_argument(
  715. '-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
  716. )
  717. list_group.add_argument(
  718. '--exclude-from', metavar='FILENAME', help='Exclude paths from exclude file, one per line'
  719. )
  720. list_group.add_argument('--pattern', help='Include or exclude paths matching a pattern')
  721. list_group.add_argument(
  722. '--patterns-from',
  723. metavar='FILENAME',
  724. help='Include or exclude paths matching patterns from pattern file, one per line',
  725. )
  726. list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  727. rinfo_parser = subparsers.add_parser(
  728. 'rinfo',
  729. aliases=SUBPARSER_ALIASES['rinfo'],
  730. help='Show repository summary information such as disk space used',
  731. description='Show repository summary information such as disk space used',
  732. add_help=False,
  733. )
  734. rinfo_group = rinfo_parser.add_argument_group('rinfo arguments')
  735. rinfo_group.add_argument(
  736. '--repository',
  737. help='Path of repository to show info for, defaults to the configured repository if there is only one',
  738. )
  739. rinfo_group.add_argument(
  740. '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
  741. )
  742. rinfo_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  743. info_parser = subparsers.add_parser(
  744. 'info',
  745. aliases=SUBPARSER_ALIASES['info'],
  746. help='Show archive summary information such as disk space used',
  747. description='Show archive summary information such as disk space used',
  748. add_help=False,
  749. )
  750. info_group = info_parser.add_argument_group('info arguments')
  751. info_group.add_argument(
  752. '--repository',
  753. help='Path of repository containing archive to show info for, defaults to the configured repository if there is only one',
  754. )
  755. info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")')
  756. info_group.add_argument(
  757. '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
  758. )
  759. info_group.add_argument(
  760. '-P',
  761. '--prefix',
  762. help='Deprecated. Only show info for archive names starting with this prefix',
  763. )
  764. info_group.add_argument(
  765. '-a',
  766. '--match-archives',
  767. '--glob-archives',
  768. metavar='PATTERN',
  769. help='Only show info for archive names matching this pattern',
  770. )
  771. info_group.add_argument(
  772. '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
  773. )
  774. info_group.add_argument(
  775. '--first',
  776. metavar='N',
  777. help='Show info for first N archives after other filters are applied',
  778. )
  779. info_group.add_argument(
  780. '--last', metavar='N', help='Show info for last N archives after other filters are applied'
  781. )
  782. info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  783. break_lock_parser = subparsers.add_parser(
  784. 'break-lock',
  785. aliases=SUBPARSER_ALIASES['break-lock'],
  786. help='Break the repository and cache locks left behind by Borg aborting',
  787. description='Break Borg repository and cache locks left behind by Borg aborting',
  788. add_help=False,
  789. )
  790. break_lock_group = break_lock_parser.add_argument_group('break-lock arguments')
  791. break_lock_group.add_argument(
  792. '--repository',
  793. help='Path of repository to break the lock for, defaults to the configured repository if there is only one',
  794. )
  795. break_lock_group.add_argument(
  796. '-h', '--help', action='help', help='Show this help message and exit'
  797. )
  798. borg_parser = subparsers.add_parser(
  799. 'borg',
  800. aliases=SUBPARSER_ALIASES['borg'],
  801. help='Run an arbitrary Borg command',
  802. description="Run an arbitrary Borg command based on borgmatic's configuration",
  803. add_help=False,
  804. )
  805. borg_group = borg_parser.add_argument_group('borg arguments')
  806. borg_group.add_argument(
  807. '--repository',
  808. help='Path of repository to pass to Borg, defaults to the configured repositories',
  809. )
  810. borg_group.add_argument('--archive', help='Name of archive to pass to Borg (or "latest")')
  811. borg_group.add_argument(
  812. '--',
  813. metavar='OPTION',
  814. dest='options',
  815. nargs='+',
  816. help='Options to pass to Borg, command first ("create", "list", etc). "--" is optional. To specify the repository or the archive, you must use --repository or --archive instead of providing them here.',
  817. )
  818. borg_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  819. return top_level_parser, subparsers
  820. def parse_arguments(*unparsed_arguments):
  821. '''
  822. Given command-line arguments with which this script was invoked, parse the arguments and return
  823. them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance.
  824. '''
  825. top_level_parser, subparsers = make_parsers()
  826. arguments, remaining_arguments = parse_subparser_arguments(
  827. unparsed_arguments, subparsers.choices
  828. )
  829. arguments['global'] = top_level_parser.parse_args(remaining_arguments)
  830. if arguments['global'].excludes_filename:
  831. raise ValueError(
  832. 'The --excludes flag has been replaced with exclude_patterns in configuration.'
  833. )
  834. if 'create' in arguments and arguments['create'].list_files and arguments['create'].progress:
  835. raise ValueError(
  836. 'With the create action, only one of --list (--files) and --progress flags can be used.'
  837. )
  838. if (
  839. ('list' in arguments and 'rinfo' in arguments and arguments['list'].json)
  840. or ('list' in arguments and 'info' in arguments and arguments['list'].json)
  841. or ('rinfo' in arguments and 'info' in arguments and arguments['rinfo'].json)
  842. ):
  843. raise ValueError('With the --json flag, multiple actions cannot be used together.')
  844. if (
  845. 'transfer' in arguments
  846. and arguments['transfer'].archive
  847. and arguments['transfer'].match_archives
  848. ):
  849. raise ValueError(
  850. 'With the transfer action, only one of --archive and --match-archives flags can be used.'
  851. )
  852. if 'list' in arguments and (arguments['list'].prefix and arguments['list'].match_archives):
  853. raise ValueError(
  854. 'With the list action, only one of --prefix or --match-archives flags can be used.'
  855. )
  856. if 'rlist' in arguments and (arguments['rlist'].prefix and arguments['rlist'].match_archives):
  857. raise ValueError(
  858. 'With the rlist action, only one of --prefix or --match-archives flags can be used.'
  859. )
  860. if 'info' in arguments and (
  861. (arguments['info'].archive and arguments['info'].prefix)
  862. or (arguments['info'].archive and arguments['info'].match_archives)
  863. or (arguments['info'].prefix and arguments['info'].match_archives)
  864. ):
  865. raise ValueError(
  866. 'With the info action, only one of --archive, --prefix, or --match-archives flags can be used.'
  867. )
  868. return arguments