2
0

arguments.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  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. 'list': ['--list', '-l'],
  11. 'info': ['--info', '-i'],
  12. }
  13. def parse_subparser_arguments(unparsed_arguments, subparsers):
  14. '''
  15. Given a sequence of arguments, and a subparsers object as returned by
  16. argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
  17. parsing all arguments. This allows common arguments like "--repository" to be shared across
  18. multiple subparsers.
  19. Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
  20. '''
  21. arguments = collections.OrderedDict()
  22. remaining_arguments = list(unparsed_arguments)
  23. alias_to_subparser_name = {
  24. alias: subparser_name
  25. for subparser_name, aliases in SUBPARSER_ALIASES.items()
  26. for alias in aliases
  27. }
  28. for subparser_name, subparser in subparsers.choices.items():
  29. if subparser_name not in remaining_arguments:
  30. continue
  31. canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name)
  32. # If a parsed value happens to be the same as the name of a subparser, remove it from the
  33. # remaining arguments. This prevents, for instance, "check --only extract" from triggering
  34. # the "extract" subparser.
  35. parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
  36. for value in vars(parsed).values():
  37. if isinstance(value, str):
  38. if value in subparsers.choices:
  39. remaining_arguments.remove(value)
  40. elif isinstance(value, list):
  41. for item in value:
  42. if item in subparsers.choices:
  43. remaining_arguments.remove(item)
  44. arguments[canonical_name] = parsed
  45. # If no actions are explicitly requested, assume defaults: prune, create, and check.
  46. if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
  47. for subparser_name in ('prune', 'create', 'check'):
  48. subparser = subparsers.choices[subparser_name]
  49. parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
  50. arguments[subparser_name] = parsed
  51. return arguments
  52. def parse_global_arguments(unparsed_arguments, top_level_parser, subparsers):
  53. '''
  54. Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
  55. object as returned by argparse.ArgumentParser().add_subparsers(), parse and return any global
  56. arguments as a parsed argparse.Namespace instance.
  57. '''
  58. # Ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
  59. # are global arguments.
  60. remaining_arguments = list(unparsed_arguments)
  61. present_subparser_names = set()
  62. for subparser_name, subparser in subparsers.choices.items():
  63. if subparser_name not in remaining_arguments:
  64. continue
  65. present_subparser_names.add(subparser_name)
  66. unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
  67. # If no actions are explicitly requested, assume defaults: prune, create, and check.
  68. if (
  69. not present_subparser_names
  70. and '--help' not in unparsed_arguments
  71. and '-h' not in unparsed_arguments
  72. ):
  73. for subparser_name in ('prune', 'create', 'check'):
  74. subparser = subparsers.choices[subparser_name]
  75. unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
  76. # Remove the subparser names themselves.
  77. for subparser_name in present_subparser_names:
  78. if subparser_name in remaining_arguments:
  79. remaining_arguments.remove(subparser_name)
  80. return top_level_parser.parse_args(remaining_arguments)
  81. def parse_arguments(*unparsed_arguments):
  82. '''
  83. Given command-line arguments with which this script was invoked, parse the arguments and return
  84. them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance.
  85. '''
  86. config_paths = collect.get_default_config_paths()
  87. global_parser = ArgumentParser(add_help=False)
  88. global_group = global_parser.add_argument_group('global arguments')
  89. global_group.add_argument(
  90. '-c',
  91. '--config',
  92. nargs='*',
  93. dest='config_paths',
  94. default=config_paths,
  95. help='Configuration filenames or directories, defaults to: {}'.format(
  96. ' '.join(config_paths)
  97. ),
  98. )
  99. global_group.add_argument(
  100. '--excludes',
  101. dest='excludes_filename',
  102. help='Deprecated in favor of exclude_patterns within configuration',
  103. )
  104. global_group.add_argument(
  105. '-n',
  106. '--dry-run',
  107. dest='dry_run',
  108. action='store_true',
  109. help='Go through the motions, but do not actually write to any repositories',
  110. )
  111. global_group.add_argument(
  112. '-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
  113. )
  114. global_group.add_argument(
  115. '-v',
  116. '--verbosity',
  117. type=int,
  118. choices=range(0, 3),
  119. default=0,
  120. help='Display verbose progress to the console (from none to lots: 0, 1, or 2)',
  121. )
  122. global_group.add_argument(
  123. '--syslog-verbosity',
  124. type=int,
  125. choices=range(0, 3),
  126. default=0,
  127. help='Display verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive',
  128. )
  129. global_group.add_argument(
  130. '--log-file',
  131. type=str,
  132. default=None,
  133. help='Write log messages to this file instead of concole and syslog',
  134. )
  135. global_group.add_argument(
  136. '--version',
  137. dest='version',
  138. default=False,
  139. action='store_true',
  140. help='Display installed version number of borgmatic and exit',
  141. )
  142. top_level_parser = ArgumentParser(
  143. description='''
  144. A simple wrapper script for the Borg backup software that creates and prunes backups.
  145. If none of the action options are given, then borgmatic defaults to: prune, create, and
  146. check archives.
  147. ''',
  148. parents=[global_parser],
  149. )
  150. subparsers = top_level_parser.add_subparsers(
  151. title='actions',
  152. metavar='',
  153. help='Specify zero or more actions. Defaults to prune, create, and check. Use --help with action for details:',
  154. )
  155. init_parser = subparsers.add_parser(
  156. 'init',
  157. aliases=SUBPARSER_ALIASES['init'],
  158. help='Initialize an empty Borg repository',
  159. description='Initialize an empty Borg repository',
  160. add_help=False,
  161. )
  162. init_group = init_parser.add_argument_group('init arguments')
  163. init_group.add_argument(
  164. '-e',
  165. '--encryption',
  166. dest='encryption_mode',
  167. help='Borg repository encryption mode',
  168. required=True,
  169. )
  170. init_group.add_argument(
  171. '--append-only',
  172. dest='append_only',
  173. action='store_true',
  174. help='Create an append-only repository',
  175. )
  176. init_group.add_argument(
  177. '--storage-quota',
  178. dest='storage_quota',
  179. help='Create a repository with a fixed storage quota',
  180. )
  181. init_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  182. prune_parser = subparsers.add_parser(
  183. 'prune',
  184. aliases=SUBPARSER_ALIASES['prune'],
  185. help='Prune archives according to the retention policy',
  186. description='Prune archives according to the retention policy',
  187. add_help=False,
  188. )
  189. prune_group = prune_parser.add_argument_group('prune arguments')
  190. prune_group.add_argument(
  191. '--stats',
  192. dest='stats',
  193. default=False,
  194. action='store_true',
  195. help='Display statistics of archive',
  196. )
  197. prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  198. create_parser = subparsers.add_parser(
  199. 'create',
  200. aliases=SUBPARSER_ALIASES['create'],
  201. help='Create archives (actually perform backups)',
  202. description='Create archives (actually perform backups)',
  203. add_help=False,
  204. )
  205. create_group = create_parser.add_argument_group('create arguments')
  206. create_group.add_argument(
  207. '--progress',
  208. dest='progress',
  209. default=False,
  210. action='store_true',
  211. help='Display progress for each file as it is processed',
  212. )
  213. create_group.add_argument(
  214. '--stats',
  215. dest='stats',
  216. default=False,
  217. action='store_true',
  218. help='Display statistics of archive',
  219. )
  220. create_group.add_argument(
  221. '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
  222. )
  223. create_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  224. check_parser = subparsers.add_parser(
  225. 'check',
  226. aliases=SUBPARSER_ALIASES['check'],
  227. help='Check archives for consistency',
  228. description='Check archives for consistency',
  229. add_help=False,
  230. )
  231. check_group = check_parser.add_argument_group('check arguments')
  232. check_group.add_argument(
  233. '--only',
  234. metavar='CHECK',
  235. choices=('repository', 'archives', 'data', 'extract'),
  236. dest='only',
  237. action='append',
  238. help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks; can specify flag multiple times',
  239. )
  240. check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  241. extract_parser = subparsers.add_parser(
  242. 'extract',
  243. aliases=SUBPARSER_ALIASES['extract'],
  244. help='Extract a named archive to the current directory',
  245. description='Extract a named archive to the current directory',
  246. add_help=False,
  247. )
  248. extract_group = extract_parser.add_argument_group('extract arguments')
  249. extract_group.add_argument(
  250. '--repository',
  251. help='Path of repository to extract, defaults to the configured repository if there is only one',
  252. )
  253. extract_group.add_argument('--archive', help='Name of archive to operate on', required=True)
  254. extract_group.add_argument(
  255. '--restore-path',
  256. nargs='+',
  257. dest='restore_paths',
  258. help='Paths to restore from archive, defaults to the entire archive',
  259. )
  260. extract_group.add_argument(
  261. '--progress',
  262. dest='progress',
  263. default=False,
  264. action='store_true',
  265. help='Display progress for each file as it is processed',
  266. )
  267. extract_group.add_argument(
  268. '-h', '--help', action='help', help='Show this help message and exit'
  269. )
  270. list_parser = subparsers.add_parser(
  271. 'list',
  272. aliases=SUBPARSER_ALIASES['list'],
  273. help='List archives',
  274. description='List archives or the contents of an archive',
  275. add_help=False,
  276. )
  277. list_group = list_parser.add_argument_group('list arguments')
  278. list_group.add_argument(
  279. '--repository',
  280. help='Path of repository to list, defaults to the configured repository if there is only one',
  281. )
  282. list_group.add_argument('--archive', help='Name of archive to list')
  283. list_group.add_argument(
  284. '--short', default=False, action='store_true', help='Output only archive or path names'
  285. )
  286. list_group.add_argument('--format', help='Format for file listing')
  287. list_group.add_argument(
  288. '--json', default=False, action='store_true', help='Output results as JSON'
  289. )
  290. list_group.add_argument(
  291. '-P', '--prefix', help='Only list archive names starting with this prefix'
  292. )
  293. list_group.add_argument(
  294. '-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
  295. )
  296. list_group.add_argument(
  297. '--successful',
  298. default=False,
  299. action='store_true',
  300. help='Only list archive names of successful (non-checkpoint) backups',
  301. )
  302. list_group.add_argument(
  303. '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
  304. )
  305. list_group.add_argument(
  306. '--first', metavar='N', help='List first N archives after other filters are applied'
  307. )
  308. list_group.add_argument(
  309. '--last', metavar='N', help='List last N archives after other filters are applied'
  310. )
  311. list_group.add_argument(
  312. '-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
  313. )
  314. list_group.add_argument(
  315. '--exclude-from', metavar='FILENAME', help='Exclude paths from exclude file, one per line'
  316. )
  317. list_group.add_argument('--pattern', help='Include or exclude paths matching a pattern')
  318. list_group.add_argument(
  319. '--patterns-from',
  320. metavar='FILENAME',
  321. help='Include or exclude paths matching patterns from pattern file, one per line',
  322. )
  323. list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  324. info_parser = subparsers.add_parser(
  325. 'info',
  326. aliases=SUBPARSER_ALIASES['info'],
  327. help='Display summary information on archives',
  328. description='Display summary information on archives',
  329. add_help=False,
  330. )
  331. info_group = info_parser.add_argument_group('info arguments')
  332. info_group.add_argument(
  333. '--repository',
  334. help='Path of repository to show info for, defaults to the configured repository if there is only one',
  335. )
  336. info_group.add_argument('--archive', help='Name of archive to show info for')
  337. info_group.add_argument(
  338. '--json', dest='json', default=False, action='store_true', help='Output results as JSON'
  339. )
  340. info_group.add_argument(
  341. '-P', '--prefix', help='Only show info for archive names starting with this prefix'
  342. )
  343. info_group.add_argument(
  344. '-a',
  345. '--glob-archives',
  346. metavar='GLOB',
  347. help='Only show info for archive names matching this glob',
  348. )
  349. info_group.add_argument(
  350. '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
  351. )
  352. info_group.add_argument(
  353. '--first',
  354. metavar='N',
  355. help='Show info for first N archives after other filters are applied',
  356. )
  357. info_group.add_argument(
  358. '--last', metavar='N', help='Show info for first N archives after other filters are applied'
  359. )
  360. info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
  361. arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
  362. arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
  363. if arguments['global'].excludes_filename:
  364. raise ValueError(
  365. 'The --excludes option has been replaced with exclude_patterns in configuration'
  366. )
  367. if 'init' in arguments and arguments['global'].dry_run:
  368. raise ValueError('The init action cannot be used with the --dry-run option')
  369. if 'list' in arguments and arguments['list'].glob_archives and arguments['list'].successful:
  370. raise ValueError('The --glob-archives and --successful options cannot be used together')
  371. if (
  372. 'list' in arguments
  373. and 'info' in arguments
  374. and arguments['list'].json
  375. and arguments['info'].json
  376. ):
  377. raise ValueError('With the --json option, list and info actions cannot be used together')
  378. return arguments