arguments.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import collections
  2. from argparse import ArgumentParser
  3. from borgmatic.config import collect
  4. SUBPARSER_ALIASES = {
  5. 'init': ['--init', '-I'],
  6. 'prune': ['--prune', '-p'],
  7. 'create': ['--create', '-C'],
  8. 'check': ['--check', '-k'],
  9. 'extract': ['--extract', '-x'],
  10. 'mount': ['--mount', '-m'],
  11. 'umount': ['--umount', '-u'],
  12. 'restore': ['--restore', '-r'],
  13. 'list': ['--list', '-l'],
  14. 'info': ['--info', '-i'],
  15. }
  16. def parse_subparser_arguments(unparsed_arguments, subparsers):
  17. '''
  18. Given a sequence of arguments, and a subparsers object as returned by
  19. argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
  20. parsing all arguments. This allows common arguments like "--repository" to be shared across
  21. multiple subparsers.
  22. Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
  23. '''
  24. arguments = collections.OrderedDict()
  25. remaining_arguments = list(unparsed_arguments)
  26. alias_to_subparser_name = {
  27. alias: subparser_name
  28. for subparser_name, aliases in SUBPARSER_ALIASES.items()
  29. for alias in aliases
  30. }
  31. for subparser_name, subparser in subparsers.choices.items():
  32. if subparser_name not in remaining_arguments:
  33. continue
  34. canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name)
  35. # If a parsed value happens to be the same as the name of a subparser, remove it from the
  36. # remaining arguments. This prevents, for instance, "check --only extract" from triggering
  37. # the "extract" subparser.
  38. parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
  39. for value in vars(parsed).values():
  40. if isinstance(value, str):
  41. if value in subparsers.choices:
  42. remaining_arguments.remove(value)
  43. elif isinstance(value, list):
  44. for item in value:
  45. if item in subparsers.choices:
  46. remaining_arguments.remove(item)
  47. arguments[canonical_name] = parsed
  48. # If no actions are explicitly requested, assume defaults: prune, create, and check.
  49. if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
  50. for subparser_name in ('prune', 'create', 'check'):
  51. subparser = subparsers.choices[subparser_name]
  52. parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
  53. arguments[subparser_name] = parsed
  54. return arguments
  55. def parse_global_arguments(unparsed_arguments, top_level_parser, subparsers):
  56. '''
  57. Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
  58. object as returned by argparse.ArgumentParser().add_subparsers(), parse and return any global
  59. arguments as a parsed argparse.Namespace instance.
  60. '''
  61. # Ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
  62. # are global arguments.
  63. remaining_arguments = list(unparsed_arguments)
  64. present_subparser_names = set()
  65. for subparser_name, subparser in subparsers.choices.items():
  66. if subparser_name not in remaining_arguments:
  67. continue
  68. present_subparser_names.add(subparser_name)
  69. unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
  70. # If no actions are explicitly requested, assume defaults: prune, create, and check.
  71. if (
  72. not present_subparser_names
  73. and '--help' not in unparsed_arguments
  74. and '-h' not in unparsed_arguments
  75. ):
  76. for subparser_name in ('prune', 'create', 'check'):
  77. subparser = subparsers.choices[subparser_name]
  78. unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
  79. # Remove the subparser names themselves.
  80. for subparser_name in present_subparser_names:
  81. if subparser_name in remaining_arguments:
  82. remaining_arguments.remove(subparser_name)
  83. return top_level_parser.parse_args(remaining_arguments)
  84. def parse_arguments(*unparsed_arguments):
  85. '''
  86. Given command-line arguments with which this script was invoked, parse the arguments and return
  87. them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance.
  88. '''
  89. config_paths = collect.get_default_config_paths(expand_home=True)
  90. unexpanded_config_paths = collect.get_default_config_paths(expand_home=False)
  91. global_parser = ArgumentParser(add_help=False)
  92. global_group = global_parser.add_argument_group('global arguments')
  93. global_group.add_argument(
  94. '-c',
  95. '--config',
  96. nargs='*',
  97. dest='config_paths',
  98. default=config_paths,
  99. help='Configuration filenames or directories, defaults to: {}'.format(
  100. ' '.join(unexpanded_config_paths)
  101. ),
  102. )
  103. global_group.add_argument(
  104. '--excludes',
  105. dest='excludes_filename',
  106. help='Deprecated in favor of exclude_patterns within configuration',
  107. )
  108. global_group.add_argument(
  109. '-n',
  110. '--dry-run',
  111. dest='dry_run',
  112. action='store_true',
  113. help='Go through the motions, but do not actually write to any repositories',
  114. )
  115. global_group.add_argument(
  116. '-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
  117. )
  118. global_group.add_argument(
  119. '-v',
  120. '--verbosity',
  121. type=int,
  122. choices=range(-1, 3),
  123. default=0,
  124. help='Display verbose progress to the console (from only errors to very verbose: -1, 0, 1, or 2)',
  125. )
  126. global_group.add_argument(
  127. '--syslog-verbosity',
  128. type=int,
  129. choices=range(-1, 3),
  130. default=0,
  131. 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',
  132. )
  133. global_group.add_argument(
  134. '--log-file-verbosity',
  135. type=int,
  136. choices=range(-1, 3),
  137. default=0,
  138. 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',
  139. )
  140. global_group.add_argument(
  141. '--log-file',
  142. type=str,
  143. default=None,
  144. help='Write log messages to this file instead of syslog',
  145. )
  146. global_group.add_argument(
  147. '--override',
  148. metavar='SECTION.OPTION=VALUE',
  149. nargs='+',
  150. dest='overrides',
  151. help='One or more configuration file options to override with specified values',
  152. )
  153. global_group.add_argument(
  154. '--version',
  155. dest='version',
  156. default=False,
  157. action='store_true',
  158. help='Display installed version number of borgmatic and exit',
  159. )
  160. top_level_parser = ArgumentParser(
  161. description='''
  162. Simple, configuration-driven backup software for servers and workstations. If none of
  163. the action options are given, then borgmatic defaults to: prune, create, and check
  164. archives.
  165. ''',
  166. parents=[global_parser],
  167. )
  168. subparsers = top_level_parser.add_subparsers(
  169. title='actions',
  170. metavar='',
  171. help='Specify zero or more actions. Defaults to prune, create, and check. Use --help with action for details:',
  172. )
  173. init_parser = subparsers.add_parser(
  174. 'init',
  175. aliases=SUBPARSER_ALIASES['init'],
  176. help='Initialize an empty Borg repository',
  177. description='Initialize an empty Borg repository',
  178. add_help=False,
  179. )
  180. init_group = init_parser.add_argument_group('init arguments')
  181. init_group.add_argument(
  182. '-e',
  183. '--encryption',
  184. dest='encryption_mode',
  185. help='Borg repository encryption mode',
  186. required=True,
  187. )
  188. init_group.add_argument(
  189. '--append-only',
  190. dest='append_only',
  191. action='store_true',
  192. help='Create an append-only repository',
  193. )
  194. init_group.add_argument(
  195. '--storage-quota',
  196. dest='storage_quota',
  197. help='Create a repository with a fixed storage quota',
  198. )
  199. init_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  200. prune_parser = subparsers.add_parser(
  201. 'prune',
  202. aliases=SUBPARSER_ALIASES['prune'],
  203. help='Prune archives according to the retention policy',
  204. description='Prune archives according to the retention policy',
  205. add_help=False,
  206. )
  207. prune_group = prune_parser.add_argument_group('prune arguments')
  208. prune_group.add_argument(
  209. '--stats',
  210. dest='stats',
  211. default=False,
  212. action='store_true',
  213. help='Display statistics of archive',
  214. )
  215. prune_group.add_argument(
  216. '--files',
  217. dest='files',
  218. default=False,
  219. action='store_true',
  220. help='Show file details and stats at verbosity 1',
  221. )
  222. prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  223. create_parser = subparsers.add_parser(
  224. 'create',
  225. aliases=SUBPARSER_ALIASES['create'],
  226. help='Create archives (actually perform backups)',
  227. description='Create archives (actually perform backups)',
  228. add_help=False,
  229. )
  230. create_group = create_parser.add_argument_group('create arguments')
  231. create_group.add_argument(
  232. '--progress',
  233. dest='progress',
  234. default=False,
  235. action='store_true',
  236. help='Display progress for each file as it is processed',
  237. )
  238. create_group.add_argument(
  239. '--stats',
  240. dest='stats',
  241. default=False,
  242. action='store_true',
  243. help='Display statistics of archive',
  244. )
  245. create_group.add_argument(
  246. '--files',
  247. dest='files',
  248. default=False,
  249. action='store_true',
  250. help='Show file details and stats at verbosity 1',
  251. )
  252. create_group.add_argument(
  253. '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
  254. )
  255. create_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  256. check_parser = subparsers.add_parser(
  257. 'check',
  258. aliases=SUBPARSER_ALIASES['check'],
  259. help='Check archives for consistency',
  260. description='Check archives for consistency',
  261. add_help=False,
  262. )
  263. check_group = check_parser.add_argument_group('check arguments')
  264. check_group.add_argument(
  265. '--repair',
  266. dest='repair',
  267. default=False,
  268. action='store_true',
  269. help='Attempt to repair any inconsistencies found (experimental and only for interactive use)',
  270. )
  271. check_group.add_argument(
  272. '--only',
  273. metavar='CHECK',
  274. choices=('repository', 'archives', 'data', 'extract'),
  275. dest='only',
  276. action='append',
  277. help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks; can specify flag multiple times',
  278. )
  279. check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  280. extract_parser = subparsers.add_parser(
  281. 'extract',
  282. aliases=SUBPARSER_ALIASES['extract'],
  283. help='Extract files from a named archive to the current directory',
  284. description='Extract a named archive to the current directory',
  285. add_help=False,
  286. )
  287. extract_group = extract_parser.add_argument_group('extract arguments')
  288. extract_group.add_argument(
  289. '--repository',
  290. help='Path of repository to extract, defaults to the configured repository if there is only one',
  291. )
  292. extract_group.add_argument('--archive', help='Name of archive to extract', required=True)
  293. extract_group.add_argument(
  294. '--path',
  295. '--restore-path',
  296. metavar='PATH',
  297. nargs='+',
  298. dest='paths',
  299. help='Paths to extract from archive, defaults to the entire archive',
  300. )
  301. extract_group.add_argument(
  302. '--destination',
  303. metavar='PATH',
  304. dest='destination',
  305. help='Directory to extract files into, defaults to the current directory',
  306. )
  307. extract_group.add_argument(
  308. '--progress',
  309. dest='progress',
  310. default=False,
  311. action='store_true',
  312. help='Display progress for each file as it is processed',
  313. )
  314. extract_group.add_argument(
  315. '-h', '--help', action='help', help='Show this help message and exit'
  316. )
  317. mount_parser = subparsers.add_parser(
  318. 'mount',
  319. aliases=SUBPARSER_ALIASES['mount'],
  320. help='Mount files from a named archive as a FUSE filesystem',
  321. description='Mount a named archive as a FUSE filesystem',
  322. add_help=False,
  323. )
  324. mount_group = mount_parser.add_argument_group('mount arguments')
  325. mount_group.add_argument(
  326. '--repository',
  327. help='Path of repository to use, defaults to the configured repository if there is only one',
  328. )
  329. mount_group.add_argument('--archive', help='Name of archive to mount')
  330. mount_group.add_argument(
  331. '--mount-point',
  332. metavar='PATH',
  333. dest='mount_point',
  334. help='Path where filesystem is to be mounted',
  335. required=True,
  336. )
  337. mount_group.add_argument(
  338. '--path',
  339. metavar='PATH',
  340. nargs='+',
  341. dest='paths',
  342. help='Paths to mount from archive, defaults to the entire archive',
  343. )
  344. mount_group.add_argument(
  345. '--foreground',
  346. dest='foreground',
  347. default=False,
  348. action='store_true',
  349. help='Stay in foreground until ctrl-C is pressed',
  350. )
  351. mount_group.add_argument('--options', dest='options', help='Extra Borg mount options')
  352. mount_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  353. umount_parser = subparsers.add_parser(
  354. 'umount',
  355. aliases=SUBPARSER_ALIASES['umount'],
  356. help='Unmount a FUSE filesystem that was mounted with "borgmatic mount"',
  357. description='Unmount a mounted FUSE filesystem',
  358. add_help=False,
  359. )
  360. umount_group = umount_parser.add_argument_group('umount arguments')
  361. umount_group.add_argument(
  362. '--mount-point',
  363. metavar='PATH',
  364. dest='mount_point',
  365. help='Path of filesystem to unmount',
  366. required=True,
  367. )
  368. umount_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  369. restore_parser = subparsers.add_parser(
  370. 'restore',
  371. aliases=SUBPARSER_ALIASES['restore'],
  372. help='Restore database dumps from a named archive',
  373. description='Restore database dumps from a named archive. (To extract files instead, use "borgmatic extract".)',
  374. add_help=False,
  375. )
  376. restore_group = restore_parser.add_argument_group('restore arguments')
  377. restore_group.add_argument(
  378. '--repository',
  379. help='Path of repository to restore from, defaults to the configured repository if there is only one',
  380. )
  381. restore_group.add_argument('--archive', help='Name of archive to restore from', required=True)
  382. restore_group.add_argument(
  383. '--database',
  384. metavar='NAME',
  385. nargs='+',
  386. dest='databases',
  387. 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',
  388. )
  389. restore_group.add_argument(
  390. '--progress',
  391. dest='progress',
  392. default=False,
  393. action='store_true',
  394. help='Display progress for each database dump file as it is extracted from archive',
  395. )
  396. restore_group.add_argument(
  397. '-h', '--help', action='help', help='Show this help message and exit'
  398. )
  399. list_parser = subparsers.add_parser(
  400. 'list',
  401. aliases=SUBPARSER_ALIASES['list'],
  402. help='List archives',
  403. description='List archives or the contents of an archive',
  404. add_help=False,
  405. )
  406. list_group = list_parser.add_argument_group('list arguments')
  407. list_group.add_argument(
  408. '--repository',
  409. help='Path of repository to list, defaults to the configured repository if there is only one',
  410. )
  411. list_group.add_argument('--archive', help='Name of archive to list')
  412. list_group.add_argument(
  413. '--path',
  414. metavar='PATH',
  415. nargs='+',
  416. dest='paths',
  417. help='Paths to list from archive, defaults to the entire archive',
  418. )
  419. list_group.add_argument(
  420. '--short', default=False, action='store_true', help='Output only archive or path names'
  421. )
  422. list_group.add_argument('--format', help='Format for file listing')
  423. list_group.add_argument(
  424. '--json', default=False, action='store_true', help='Output results as JSON'
  425. )
  426. list_group.add_argument(
  427. '-P', '--prefix', help='Only list archive names starting with this prefix'
  428. )
  429. list_group.add_argument(
  430. '-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
  431. )
  432. list_group.add_argument(
  433. '--successful',
  434. default=False,
  435. action='store_true',
  436. help='Only list archive names of successful (non-checkpoint) backups',
  437. )
  438. list_group.add_argument(
  439. '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
  440. )
  441. list_group.add_argument(
  442. '--first', metavar='N', help='List first N archives after other filters are applied'
  443. )
  444. list_group.add_argument(
  445. '--last', metavar='N', help='List last N archives after other filters are applied'
  446. )
  447. list_group.add_argument(
  448. '-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
  449. )
  450. list_group.add_argument(
  451. '--exclude-from', metavar='FILENAME', help='Exclude paths from exclude file, one per line'
  452. )
  453. list_group.add_argument('--pattern', help='Include or exclude paths matching a pattern')
  454. list_group.add_argument(
  455. '--patterns-from',
  456. metavar='FILENAME',
  457. help='Include or exclude paths matching patterns from pattern file, one per line',
  458. )
  459. list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  460. info_parser = subparsers.add_parser(
  461. 'info',
  462. aliases=SUBPARSER_ALIASES['info'],
  463. help='Display summary information on archives',
  464. description='Display summary information on archives',
  465. add_help=False,
  466. )
  467. info_group = info_parser.add_argument_group('info arguments')
  468. info_group.add_argument(
  469. '--repository',
  470. help='Path of repository to show info for, defaults to the configured repository if there is only one',
  471. )
  472. info_group.add_argument('--archive', help='Name of archive to show info for')
  473. info_group.add_argument(
  474. '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
  475. )
  476. info_group.add_argument(
  477. '-P', '--prefix', help='Only show info for archive names starting with this prefix'
  478. )
  479. info_group.add_argument(
  480. '-a',
  481. '--glob-archives',
  482. metavar='GLOB',
  483. help='Only show info for archive names matching this glob',
  484. )
  485. info_group.add_argument(
  486. '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
  487. )
  488. info_group.add_argument(
  489. '--first',
  490. metavar='N',
  491. help='Show info for first N archives after other filters are applied',
  492. )
  493. info_group.add_argument(
  494. '--last', metavar='N', help='Show info for first N archives after other filters are applied'
  495. )
  496. info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  497. arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
  498. arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
  499. if arguments['global'].excludes_filename:
  500. raise ValueError(
  501. 'The --excludes option has been replaced with exclude_patterns in configuration'
  502. )
  503. if 'init' in arguments and arguments['global'].dry_run:
  504. raise ValueError('The init action cannot be used with the --dry-run option')
  505. if 'list' in arguments and arguments['list'].glob_archives and arguments['list'].successful:
  506. raise ValueError('The --glob-archives and --successful options cannot be used together')
  507. if (
  508. 'list' in arguments
  509. and 'info' in arguments
  510. and arguments['list'].json
  511. and arguments['info'].json
  512. ):
  513. raise ValueError('With the --json option, list and info actions cannot be used together')
  514. return arguments