Coverage for src\derivepassphrase\cli.py: 100.000%

466 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-23 12:17 +0200

1# SPDX-FileCopyrightText: 2025 Marco Ricci <software@the13thletter.info> 

2# 

3# SPDX-License-Identifier: Zlib 

4 

5# ruff: noqa: TRY400 

6 

7"""Command-line interface for derivepassphrase.""" 

8 

9from __future__ import annotations 

10 

11import base64 

12import collections 

13import contextlib 

14import json 

15import logging 

16import os 

17from typing import ( 

18 TYPE_CHECKING, 

19 Final, 

20 Literal, 

21 NoReturn, 

22 TextIO, 

23 cast, 

24) 

25 

26import click 

27import click.shell_completion 

28from typing_extensions import ( 

29 Any, 

30) 

31 

32from derivepassphrase import _internals, _types, exporter, vault 

33from derivepassphrase._internals import cli_helpers, cli_machinery 

34from derivepassphrase._internals import cli_messages as _msg 

35 

36if TYPE_CHECKING: 

37 from collections.abc import Sequence 

38 from collections.abc import Set as AbstractSet 

39 from contextlib import AbstractContextManager 

40 

41__all__ = ('derivepassphrase',) 

42 

43PROG_NAME = _internals.PROG_NAME 

44VERSION = _internals.VERSION 

45 

46 

47@click.group( 

48 context_settings={ 

49 'help_option_names': ['-h', '--help'], 

50 'ignore_unknown_options': True, 

51 'allow_interspersed_args': False, 

52 }, 

53 epilog=_msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EPILOG_01), 

54 invoke_without_command=True, 

55 cls=cli_machinery.TopLevelCLIEntryPoint, 

56 help=( 

57 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_01), 

58 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_02), 

59 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_03), 

60 ), 

61) 

62@cli_machinery.version_option( 

63 cli_machinery.derivepassphrase_version_option_callback 

64) 

65@cli_machinery.color_forcing_pseudo_option 

66@cli_machinery.standard_logging_options 

67@click.pass_context 

68def derivepassphrase(ctx: click.Context, /) -> None: 

69 """Derive a strong passphrase, deterministically, from a master secret. 

70 

71 This is a [`click`][CLICK]-powered command-line interface function, 

72 and not intended for programmatic use. See the derivepassphrase(1) 

73 manpage for full documentation of the interface. (See also 

74 [`click.testing.CliRunner`][] for controlled, programmatic 

75 invocation.) 

76 

77 [CLICK]: https://pypi.org/package/click/ 

78 

79 """ 

80 # TODO(the-13th-letter): Turn this callback into a no-op in v1.0. 

81 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands 

82 deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') 1:-;.q=/?W9+wO

83 if ctx.invoked_subcommand is None: 1:-;.q=/?W9+wO

84 deprecation.warning( 1O

85 _msg.TranslatedString( 

86 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED 

87 ), 

88 extra={'color': ctx.color}, 

89 ) 

90 # See definition of click.Group.invoke, non-chained case. 

91 with ctx: 1O

92 sub_ctx = derivepassphrase_vault.make_context( 1O

93 'vault', ctx.args, parent=ctx 

94 ) 

95 with sub_ctx: 1O

96 return derivepassphrase_vault.invoke(sub_ctx) 1O

97 return None 1:-;.q=/?W9+w

98 

99 

100# Exporter 

101# ======== 

102 

103 

104@derivepassphrase.group( 

105 'export', 

106 context_settings={ 

107 'help_option_names': ['-h', '--help'], 

108 'ignore_unknown_options': True, 

109 'allow_interspersed_args': False, 

110 }, 

111 invoke_without_command=True, 

112 cls=cli_machinery.DefaultToVaultGroup, 

113 help=( 

114 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_01), 

115 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_02), 

116 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_03), 

117 ), 

118) 

119@cli_machinery.version_option(cli_machinery.export_version_option_callback) 

120@cli_machinery.color_forcing_pseudo_option 

121@cli_machinery.standard_logging_options 

122@click.pass_context 

123def derivepassphrase_export(ctx: click.Context, /) -> None: 

124 """Export a foreign configuration to standard output. 

125 

126 This is a [`click`][CLICK]-powered command-line interface function, 

127 and not intended for programmatic use. See the 

128 derivepassphrase-export(1) manpage for full documentation of the 

129 interface. (See also [`click.testing.CliRunner`][] for controlled, 

130 programmatic invocation.) 

131 

132 [CLICK]: https://pypi.org/package/click/ 

133 

134 """ 

135 # TODO(the-13th-letter): Turn this callback into a no-op in v1.0. 

136 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes/#v1.0-implied-subcommands 

137 deprecation = logging.getLogger(f'{PROG_NAME}.deprecation') 1-./9+

138 if ctx.invoked_subcommand is None: 1-./9+

139 deprecation.warning( 1+

140 _msg.TranslatedString( 

141 _msg.WarnMsgTemplate.V10_SUBCOMMAND_REQUIRED 

142 ), 

143 extra={'color': ctx.color}, 

144 ) 

145 # See definition of click.Group.invoke, non-chained case. 

146 with ctx: 1+

147 sub_ctx = derivepassphrase_export_vault.make_context( 1+

148 'vault', ctx.args, parent=ctx 

149 ) 

150 # Constructing the subcontext above will usually already 

151 # lead to a click.UsageError, so this block typically won't 

152 # actually be called. 

153 with sub_ctx: # pragma: no cover 

154 return derivepassphrase_export_vault.invoke(sub_ctx) 1+

155 return None 1-./9

156 

157 

158@derivepassphrase_export.command( 

159 'vault', 

160 context_settings={'help_option_names': ['-h', '--help']}, 

161 cls=cli_machinery.CommandWithHelpGroups, 

162 help=( 

163 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_01), 

164 _msg.TranslatedString( 

165 _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_02, 

166 path_metavar=_msg.TranslatedString( 

167 _msg.Label.EXPORT_VAULT_METAVAR_PATH, 

168 ), 

169 ), 

170 _msg.TranslatedString( 

171 _msg.Label.DERIVEPASSPHRASE_EXPORT_VAULT_03, 

172 path_metavar=_msg.TranslatedString( 

173 _msg.Label.EXPORT_VAULT_METAVAR_PATH, 

174 ), 

175 ), 

176 ), 

177) 

178@click.option( 

179 '-f', 

180 '--format', 

181 'formats', 

182 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT), 

183 multiple=True, 

184 default=('v0.3', 'v0.2', 'storeroom'), 

185 type=click.Choice(['v0.2', 'v0.3', 'storeroom']), 

186 help=_msg.TranslatedString( 

187 _msg.Label.EXPORT_VAULT_FORMAT_HELP_TEXT, 

188 defaults_hint=_msg.TranslatedString( 

189 _msg.Label.EXPORT_VAULT_FORMAT_DEFAULTS_HELP_TEXT, 

190 ), 

191 metavar=_msg.TranslatedString( 

192 _msg.Label.EXPORT_VAULT_FORMAT_METAVAR_FMT, 

193 ), 

194 ), 

195 cls=cli_machinery.StandardOption, 

196) 

197@click.option( 

198 '-k', 

199 '--key', 

200 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_KEY_METAVAR_K), 

201 help=_msg.TranslatedString( 

202 _msg.Label.EXPORT_VAULT_KEY_HELP_TEXT, 

203 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_KEY_METAVAR_K), 

204 defaults_hint=_msg.TranslatedString( 

205 _msg.Label.EXPORT_VAULT_KEY_DEFAULTS_HELP_TEXT, 

206 ), 

207 ), 

208 cls=cli_machinery.StandardOption, 

209) 

210@cli_machinery.version_option( 

211 cli_machinery.export_vault_version_option_callback 

212) 

213@cli_machinery.color_forcing_pseudo_option 

214@cli_machinery.standard_logging_options 

215@click.argument( 

216 'path', 

217 metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_METAVAR_PATH), 

218 required=True, 

219 shell_complete=cli_helpers.shell_complete_path, 

220) 

221@click.pass_context 

222def derivepassphrase_export_vault( 

223 ctx: click.Context, 

224 /, 

225 *, 

226 path: str | bytes | os.PathLike[str] | None, 

227 formats: Sequence[Literal['v0.2', 'v0.3', 'storeroom']] = (), 

228 key: str | bytes | None = None, 

229) -> None: 

230 """Export a vault-native configuration to standard output. 

231 

232 This is a [`click`][CLICK]-powered command-line interface function, 

233 and not intended for programmatic use. See the 

234 derivepassphrase-export-vault(1) manpage for full documentation of 

235 the interface. (See also [`click.testing.CliRunner`][] for 

236 controlled, programmatic invocation.) 

237 

238 [CLICK]: https://pypi.org/package/click/ 

239 

240 """ 

241 logger = logging.getLogger(PROG_NAME) 19'(#)$*%,!

242 if path in {'VAULT_PATH', b'VAULT_PATH'}: 19'(#)$*%,!

243 path = None 19'#!

244 if isinstance(key, str): # pragma: no branch 19'(#)$*%,!

245 key = key.encode('utf-8') 1(#

246 for fmt in formats: 19'(#)$*%,!

247 try: 19'(#)$*%,!

248 config = exporter.export_vault_config_data(path, key, format=fmt) 19'(#)$*%,!

249 except ( 1)$*%!

250 IsADirectoryError, 

251 NotADirectoryError, 

252 exporter.NotAVaultConfigError, 

253 RuntimeError, 

254 ): 

255 logger.info( 1$%

256 _msg.TranslatedString( 

257 _msg.InfoMsgTemplate.CANNOT_LOAD_AS_VAULT_CONFIG, 

258 path=path or exporter.get_vault_path(), 

259 fmt=fmt, 

260 ), 

261 extra={'color': ctx.color}, 

262 ) 

263 continue 1$%

264 except OSError as exc: 1)*!

265 logger.error( 1)*

266 _msg.TranslatedString( 

267 _msg.ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG_OSERROR, 

268 path=path, 

269 error=exc.strerror, 

270 filename=exc.filename, 

271 ).maybe_without_filename(), 

272 extra={'color': ctx.color}, 

273 ) 

274 ctx.exit(1) 1)*

275 except ModuleNotFoundError: 1!

276 logger.error( 1!

277 _msg.TranslatedString( 

278 _msg.ErrMsgTemplate.MISSING_MODULE, 

279 module='cryptography', 

280 ), 

281 extra={'color': ctx.color}, 

282 ) 

283 logger.info( 1!

284 _msg.TranslatedString( 

285 _msg.InfoMsgTemplate.PIP_INSTALL_EXTRA, 

286 extra_name='export', 

287 ), 

288 extra={'color': ctx.color}, 

289 ) 

290 ctx.exit(1) 1!

291 else: 

292 if not _types.is_vault_config(config): 19'(#,

293 logger.error( 1,

294 _msg.TranslatedString( 

295 _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG, 

296 config=config, 

297 ), 

298 extra={'color': ctx.color}, 

299 ) 

300 ctx.exit(1) 1,

301 click.echo( 19'(#

302 json.dumps( 

303 config, ensure_ascii=False, indent=2, sort_keys=True 

304 ), 

305 color=ctx.color, 

306 ) 

307 break 19'(#

308 else: 

309 logger.error( 1$%

310 _msg.TranslatedString( 

311 _msg.ErrMsgTemplate.CANNOT_PARSE_AS_VAULT_CONFIG, 

312 path=path, 

313 ).maybe_without_filename(), 

314 extra={'color': ctx.color}, 

315 ) 

316 ctx.exit(1) 1$%

317 

318 

319class _VaultContext: # noqa: PLR0904 

320 """The context for the "vault" command-line interface. 

321 

322 This context object -- wrapping a [`click.Context`][] object -- 

323 encapsulates a single call to the `derivepassphrase vault` 

324 command-line. Although fully documented, this class is an 

325 implementation detail of the `derivepassphrase vault` command-line 

326 and should not be instantiated directly by users or API clients. 

327 

328 Attributes: 

329 logger: 

330 The logger used for warnings and error messages. 

331 deprecation: 

332 The logger used for deprecation warnings. 

333 ctx: 

334 The underlying [`click.Context`][] from which the 

335 command-line settings and parameter values are queried. 

336 all_ops: 

337 A list of operations supported by the CLI, in the 

338 order that they are queried in the `click` context. The 

339 final entry is the default operation, and does not 

340 correspond to a `click` parameter; all others are `click` 

341 parameter names. 

342 readwrite_ops: 

343 A set of operations which modify the `derivepassphrase` 

344 state. All other operations are read-only. 

345 options_in_group: 

346 A mapping of option group names to lists of known options 

347 from this group. Used during the validation of the command 

348 line. 

349 params_by_str: 

350 A mapping of option names (long names, short names, etc.) to 

351 option objects. Used during the validation of the command 

352 line. 

353 

354 """ 

355 

356 logger: Final = logging.getLogger(PROG_NAME) 

357 """""" 

358 deprecation: Final = logging.getLogger(PROG_NAME + '.deprecation') 

359 """""" 

360 ctx: Final[click.Context] 

361 """""" 

362 all_ops: tuple[str, ...] = ( 

363 'delete_service_settings', 

364 'delete_globals', 

365 'clear_all_settings', 

366 'import_settings', 

367 'export_settings', 

368 'store_config_only', 

369 # The default op "derive_passphrase" must be last! 

370 'derive_passphrase', 

371 ) 

372 readwrite_ops: AbstractSet[str] = frozenset({ 

373 'delete_service_settings', 

374 'delete_globals', 

375 'clear_all_settings', 

376 'import_settings', 

377 'store_config_only', 

378 }) 

379 options_in_group: dict[type[click.Option], list[click.Option]] 

380 params_by_str: dict[str, click.Parameter] 

381 

382 def __init__(self, ctx: click.Context, /) -> None: 

383 """Initialize the vault context. 

384 

385 Args: 

386 ctx: 

387 The underlying [`click.Context`][] from which the 

388 command-line settings and parameter values are queried. 

389 

390 """ # noqa: DOC501 

391 self.ctx = ctx 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

392 self.params_by_str = {} 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

393 self.options_in_group = {} 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

394 for param in ctx.command.params: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

395 if isinstance(param, click.Option): 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

396 group: type[click.Option] 

397 known_option_groups = [ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

398 cli_machinery.PassphraseGenerationOption, 

399 cli_machinery.ConfigurationOption, 

400 cli_machinery.StorageManagementOption, 

401 cli_machinery.LoggingOption, 

402 cli_machinery.CompatibilityOption, 

403 cli_machinery.StandardOption, 

404 ] 

405 if isinstance(param, cli_machinery.OptionGroupOption): 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

406 for class_ in known_option_groups: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

407 if isinstance(param, class_): 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

408 group = class_ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

409 break 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

410 else: 

411 raise AssertionError( # noqa: TRY003 

412 f'Unknown option group for {param!r}' # noqa: EM102 

413 ) 

414 else: 

415 group = click.Option 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

416 self.options_in_group.setdefault(group, []).append(param) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

417 self.params_by_str[param.human_readable_name] = param 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

418 for name in param.opts + param.secondary_opts: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

419 self.params_by_str[name] = param 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

420 

421 def is_param_set(self, param: click.Parameter, /) -> bool: 

422 """Return true if the parameter is set.""" 

423 return bool(self.ctx.params.get(param.human_readable_name)) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

424 

425 def option_name(self, param: click.Parameter | str, /) -> str: 

426 """Return the option name of a parameter. 

427 

428 Annoyingly, `param.human_readable_name` contains the *function* 

429 parameter name, not the list of option names. *Those* are 

430 stashed in the `.opts` and `.secondary_opts` attributes, which 

431 are visible in the `.to_info_dict()` output, but not otherwise 

432 documented. We return the shortest one among the long-form 

433 option names. 

434 

435 Args: 

436 param: The parameter whose option name is requested. 

437 

438 Raises: 

439 ValueError: The parameter has no long-form option names. 

440 

441 """ 

442 param = self.params_by_str[param] if isinstance(param, str) else param 18

443 names = [param.human_readable_name, *param.opts, *param.secondary_opts] 18

444 option_names = [n for n in names if n.startswith('--')] 18

445 return min(option_names, key=len) 18

446 

447 def check_incompatible_options( 

448 self, 

449 param1: click.Parameter | str, 

450 param2: click.Parameter | str, 

451 ) -> None: 

452 """Raise an error if the two options are incompatible. 

453 

454 Raises: 

455 click.BadOptionUsage: The given options are incompatible. 

456 

457 """ 

458 param1 = ( 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

459 self.params_by_str[param1] if isinstance(param1, str) else param1 

460 ) 

461 param2 = ( 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

462 self.params_by_str[param2] if isinstance(param2, str) else param2 

463 ) 

464 if param1 == param2: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

465 return 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

466 if not self.is_param_set(param1): 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

467 return 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

468 if self.is_param_set(param2): 1adqABpbf8zYZ17W360ntDPxuKQRSsmlcTr24JwUV5ghEeyvikjoC

469 param1_str = self.option_name(param1) 18

470 param2_str = self.option_name(param2) 18

471 raise click.BadOptionUsage( 18

472 param1_str, 

473 str( 

474 _msg.TranslatedString( 

475 _msg.ErrMsgTemplate.PARAMS_MUTUALLY_EXCLUSIVE, 

476 param1=param1_str, 

477 param2=param2_str, 

478 ) 

479 ), 

480 ctx=self.ctx, 

481 ) 

482 return 1adqABpbf8zYZ17W360ntDPxuKQRSsmlcTr24JwUV5ghEeyvikjoC

483 

484 def err(self, msg: Any, /, **kwargs: Any) -> NoReturn: # noqa: ANN401 

485 """Log an error, then abort the function call. 

486 

487 We ensure that color handling is done properly before the error 

488 is logged. 

489 

490 """ 

491 stacklevel = kwargs.pop('stacklevel', 1) 1YZ1360nDPxuQRSmlTr24Jko

492 stacklevel += 1 1YZ1360nDPxuQRSmlTr24Jko

493 extra = kwargs.pop('extra', {}) 1YZ1360nDPxuQRSmlTr24Jko

494 extra.setdefault('color', self.ctx.color) 1YZ1360nDPxuQRSmlTr24Jko

495 self.logger.error(msg, stacklevel=stacklevel, extra=extra, **kwargs) 1YZ1360nDPxuQRSmlTr24Jko

496 self.ctx.exit(1) 1YZ1360nDPxuQRSmlTr24Jko

497 

498 def warning(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401 

499 """Log a warning. 

500 

501 We ensure that color handling is done properly before the 

502 warning is logged. 

503 

504 """ 

505 stacklevel = kwargs.pop('stacklevel', 1) 1qpfzJVghEeyvijC

506 stacklevel += 1 1qpfzJVghEeyvijC

507 extra = kwargs.pop('extra', {}) 1qpfzJVghEeyvijC

508 extra.setdefault('color', self.ctx.color) 1qpfzJVghEeyvijC

509 self.logger.warning(msg, stacklevel=stacklevel, extra=extra, **kwargs) 1qpfzJVghEeyvijC

510 

511 def deprecation_warning(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401 

512 """Log a deprecation warning. 

513 

514 We ensure that color handling is done properly before the 

515 warning is logged. 

516 

517 """ 

518 stacklevel = kwargs.pop('stacklevel', 1) 1UV

519 stacklevel += 1 1UV

520 extra = kwargs.pop('extra', {}) 1UV

521 extra.setdefault('color', self.ctx.color) 1UV

522 self.deprecation.warning( 1UV

523 msg, stacklevel=stacklevel, extra=extra, **kwargs 

524 ) 

525 

526 def deprecation_info(self, msg: Any, /, **kwargs: Any) -> None: # noqa: ANN401 

527 """Log a deprecation info message. 

528 

529 We ensure that color handling is done properly before the 

530 warning is logged. 

531 

532 """ 

533 stacklevel = kwargs.pop('stacklevel', 1) 1U

534 stacklevel += 1 1U

535 extra = kwargs.pop('extra', {}) 1U

536 extra.setdefault('color', self.ctx.color) 1U

537 self.deprecation.info( 1U

538 msg, stacklevel=stacklevel, extra=extra, **kwargs 

539 ) 

540 

541 def get_config(self) -> _types.VaultConfig: 

542 """Return the vault configuration stored on disk. 

543 

544 If no configuration is stored, return an empty configuration. 

545 

546 If only a v0.1-style configuration is found, attempt to migrate 

547 it. This will be removed in v1.0. 

548 

549 """ 

550 try: 1adqABHLMIpbfz7W360ntDPxuKQRSXNsmlcrJwOUV5ghEeyGvFikjoC

551 return cli_helpers.load_config() 1adqABHLMIpbfz7W360ntDPxuKQRSXNsmlcrJwOUV5ghEeyGvFikjoC

552 except FileNotFoundError: 1qABW360XNsmlwOUV

553 # TODO(the-13th-letter): Return the empty default 

554 # configuration directly. 

555 # https://the13thletter.info/derivepassphrase/latest/upgrade-notes.html#v1.0-old-settings-file 

556 try: 1qABW0XNsmlwOUV

557 backup_config, exc = cli_helpers.migrate_and_load_old_config() 1qABW0XNsmlwOUV

558 except FileNotFoundError: 1qABW0XNsmlwO

559 return {'services': {}} 1qABW0XNsmlwO

560 old_name = cli_helpers.config_filename( 1UV

561 subsystem='old settings.json' 

562 ).name 

563 new_name = cli_helpers.config_filename(subsystem='vault').name 1UV

564 self.deprecation_warning( 1UV

565 _msg.TranslatedString( 

566 _msg.WarnMsgTemplate.V01_STYLE_CONFIG, 

567 old=old_name, 

568 new=new_name, 

569 ), 

570 ) 

571 if isinstance(exc, OSError): 1UV

572 self.warning( 1V

573 _msg.TranslatedString( 

574 _msg.WarnMsgTemplate.FAILED_TO_MIGRATE_CONFIG, 

575 path=new_name, 

576 error=exc.strerror, 

577 filename=exc.filename, 

578 ).maybe_without_filename(), 

579 ) 

580 else: 

581 self.deprecation_info( 1U

582 _msg.TranslatedString( 

583 _msg.InfoMsgTemplate.SUCCESSFULLY_MIGRATED, 

584 path=new_name, 

585 ), 

586 ) 

587 return backup_config 1UV

588 except OSError as exc: 136

589 self.err( 16

590 _msg.TranslatedString( 

591 _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS, 

592 error=exc.strerror, 

593 filename=exc.filename, 

594 ).maybe_without_filename(), 

595 ) 

596 except Exception as exc: # noqa: BLE001 13

597 self.err( 13

598 _msg.TranslatedString( 

599 _msg.ErrMsgTemplate.CANNOT_LOAD_VAULT_SETTINGS, 

600 error=str(exc), 

601 filename=None, 

602 ).maybe_without_filename(), 

603 exc_info=exc, 

604 ) 

605 

606 def put_config(self, config: _types.VaultConfig, /) -> None: 

607 """Store the given vault configuration to disk.""" 

608 try: 1adqpbfztxusmlc5ghEeyvikj

609 cli_helpers.save_config(config) 1adqpbfztxusmlc5ghEeyvikj

610 except OSError as exc: 1xuml

611 self.err( 1xm

612 _msg.TranslatedString( 

613 _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS, 

614 error=exc.strerror, 

615 filename=exc.filename, 

616 ).maybe_without_filename(), 

617 ) 

618 except Exception as exc: # noqa: BLE001 1ul

619 self.err( 1ul

620 _msg.TranslatedString( 

621 _msg.ErrMsgTemplate.CANNOT_STORE_VAULT_SETTINGS, 

622 error=str(exc), 

623 filename=None, 

624 ).maybe_without_filename(), 

625 exc_info=exc, 

626 ) 

627 

628 def get_user_config(self) -> dict[str, Any]: 

629 """Return the global user configuration stored on disk. 

630 

631 If no configuration is stored, return an empty configuration. 

632 

633 """ 

634 try: 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlcTr24JwOghEeyGvFikjoC

635 return cli_helpers.load_user_config() 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlcTr24JwOghEeyGvFikjoC

636 except FileNotFoundError: 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsml24JwOghEeyGvFikjoC

637 return {} 1adqABHLMIpbfzYZ1ntDPxuKQRSXNsmlJwOghEeyGvFikjoC

638 except OSError as exc: 124

639 self.err( 14

640 _msg.TranslatedString( 

641 _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG, 

642 error=exc.strerror, 

643 filename=exc.filename, 

644 ).maybe_without_filename(), 

645 ) 

646 except Exception as exc: # noqa: BLE001 12

647 self.err( 12

648 _msg.TranslatedString( 

649 _msg.ErrMsgTemplate.CANNOT_LOAD_USER_CONFIG, 

650 error=str(exc), 

651 filename=None, 

652 ).maybe_without_filename(), 

653 exc_info=exc, 

654 ) 

655 

656 def validate_command_line(self) -> None: # noqa: C901,PLR0912 

657 """Check for missing/extra arguments and conflicting options. 

658 

659 Raises: 

660 click.UsageError: 

661 The command-line is invalid because of missing or extra 

662 arguments, or because of conflicting options. The error 

663 message contains further details. 

664 

665 """ 

666 param: click.Parameter 

667 self.check_incompatible_options('--phrase', '--key') 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

668 for group in ( 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

669 cli_machinery.ConfigurationOption, 

670 cli_machinery.StorageManagementOption, 

671 ): 

672 for opt in self.options_in_group[group]: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

673 if opt not in { 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

674 self.params_by_str['--config'], 

675 self.params_by_str['--notes'], 

676 }: 

677 for other_opt in self.options_in_group[ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

678 cli_machinery.PassphraseGenerationOption 

679 ]: 

680 self.check_incompatible_options(opt, other_opt) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

681 

682 for group in ( 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

683 cli_machinery.ConfigurationOption, 

684 cli_machinery.StorageManagementOption, 

685 ): 

686 for opt in self.options_in_group[group]: 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

687 for other_opt in self.options_in_group[ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

688 cli_machinery.ConfigurationOption 

689 ]: 

690 if {opt, other_opt} != { 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

691 self.params_by_str['--config'], 

692 self.params_by_str['--notes'], 

693 }: 

694 self.check_incompatible_options(opt, other_opt) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

695 for other_opt in self.options_in_group[ 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

696 cli_machinery.StorageManagementOption 

697 ]: 

698 self.check_incompatible_options(opt, other_opt) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

699 service: str | None = self.ctx.params['service'] 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

700 service_metavar = _msg.TranslatedString( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

701 _msg.Label.VAULT_METAVAR_SERVICE 

702 ) 

703 sv_or_global_options = self.options_in_group[ 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

704 cli_machinery.PassphraseGenerationOption 

705 ] 

706 for param in sv_or_global_options: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

707 if self.is_param_set(param) and not ( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

708 service is not None 

709 or self.is_param_set(self.params_by_str['--config']) 

710 ): 

711 err_msg = _msg.TranslatedString( 1b

712 _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE_OR_CONFIG, 

713 param=param.opts[0], 

714 service_metavar=service_metavar, 

715 ) 

716 raise click.UsageError(str(err_msg)) 1b

717 sv_options = [ 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

718 self.params_by_str['--notes'], 

719 self.params_by_str['--delete'], 

720 ] 

721 for param in sv_options: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

722 if self.is_param_set(param) and not service is not None: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

723 err_msg = _msg.TranslatedString( 1b

724 _msg.ErrMsgTemplate.PARAMS_NEEDS_SERVICE, 

725 param=param.opts[0], 

726 service_metavar=service_metavar, 

727 ) 

728 raise click.UsageError(str(err_msg)) 1b

729 no_sv_options = [ 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

730 self.params_by_str['--delete-globals'], 

731 self.params_by_str['--clear'], 

732 *self.options_in_group[cli_machinery.StorageManagementOption], 

733 ] 

734 for param in no_sv_options: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

735 if self.is_param_set(param) and service is not None: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

736 err_msg = _msg.TranslatedString( 1b

737 _msg.ErrMsgTemplate.PARAMS_NO_SERVICE, 

738 param=param.opts[0], 

739 service_metavar=service_metavar, 

740 ) 

741 raise click.UsageError(str(err_msg)) 1b

742 

743 def get_mutex( 

744 self, 

745 op: str, 

746 ) -> AbstractContextManager[AbstractContextManager | None]: 

747 """Return a mutex for accessing the configuration on disk. 

748 

749 The mutex is a context manager, and will lock out other threads 

750 and processes attempting to access the configuration in an 

751 incompatible manner. 

752 

753 Returns: 

754 If the requested operation is a read-only operation, return 

755 a no-op mutex. (Concurrent reads are always allowed, even 

756 in the presence of writers.) Otherwise, for read-write 

757 operations, return an actual mutex. 

758 

759 """ 

760 return ( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

761 cli_helpers.configuration_mutex() 

762 if op in self.readwrite_ops 

763 else contextlib.nullcontext() 

764 ) 

765 

766 def dispatch_op(self) -> None: 

767 """Dispatch to the handler matching the command-line call. 

768 

769 Also issue any appropriate warnings about the command-line, 

770 e.g., incompatibilities with vault(1) or ineffective options. 

771 

772 """ 

773 service_metavar = _msg.TranslatedString( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

774 _msg.Label.VAULT_METAVAR_SERVICE 

775 ) 

776 if self.ctx.params['service'] == '': # noqa: PLC1901 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

777 self.warning( 1f

778 _msg.TranslatedString( 

779 _msg.WarnMsgTemplate.EMPTY_SERVICE_NOT_SUPPORTED, 

780 service_metavar=service_metavar, 

781 ), 

782 ) 

783 if self.ctx.params.get('edit_notes') and not self.ctx.params.get( 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

784 'store_config_only' 

785 ): 

786 self.warning( 1C

787 _msg.TranslatedString( 

788 _msg.WarnMsgTemplate.EDITING_NOTES_BUT_NOT_STORING_CONFIG, 

789 service_metavar=service_metavar, 

790 ), 

791 ) 

792 op: str 

793 for candidate_op in self.all_ops[:-1]: 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

794 if self.ctx.params.get(candidate_op): 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

795 op = candidate_op 1adqpbfzYZ17W360ntDPxuKQRSsmlcTrJUV5ghEeyvikjo

796 break 1adqpbfzYZ17W360ntDPxuKQRSsmlcTrJUV5ghEeyvikjo

797 else: 

798 op = self.all_ops[-1] 1ABHLMIbXNcr24wOGFC

799 with self.get_mutex(op): 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

800 op_func = getattr(self, 'run_op_' + op) 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

801 return op_func() 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

802 

803 def run_op_delete_service_settings(self) -> None: 

804 """Delete settings for a specific service.""" 

805 service = self.ctx.params['service'] 1ad5

806 assert service is not None 1ad5

807 configuration = self.get_config() 1ad5

808 if service in configuration['services']: 1ad5

809 del configuration['services'][service] 1ad5

810 self.put_config(configuration) 1ad5

811 

812 def run_op_delete_globals(self) -> None: 

813 """Delete the global settings.""" 

814 configuration = self.get_config() 1adb5

815 if 'global' in configuration: 1adb5

816 del configuration['global'] 1adb5

817 self.put_config(configuration) 1adb5

818 

819 def run_op_clear_all_settings(self) -> None: 

820 """Clear all settings.""" 

821 self.put_config({'services': {}}) 1adb5hEey

822 

823 def run_op_import_settings(self) -> None: # noqa: C901,PLR0912 

824 """Import settings from a given file. 

825 

826 Issue multiple warnings, if appropriate, e.g. for Unicode 

827 normalization issues with stored passphrases or conflicting 

828 stored passphrases and keys. Respect the `--overwrite-config` 

829 and `--merge-config` options when writing the imported 

830 configuration to disk. 

831 

832 """ 

833 service_metavar = _msg.TranslatedString( 1adqbfzYZ1cTghEeyv

834 _msg.Label.VAULT_METAVAR_SERVICE 

835 ) 

836 user_config = self.get_user_config() 1adqbfzYZ1cTghEeyv

837 import_settings = self.ctx.params['import_settings'] 1adqbfzYZ1cTghEeyv

838 overwrite_config = self.ctx.params['overwrite_config'] 1adqbfzYZ1cTghEeyv

839 try: 1adqbfzYZ1cTghEeyv

840 # TODO(the-13th-letter): keep track of auto-close; try 

841 # os.dup if feasible 

842 infile = cast( 1adqbfzYZ1cTghEeyv

843 'TextIO', 

844 ( 

845 import_settings 

846 if hasattr(import_settings, 'close') 

847 else click.open_file(os.fspath(import_settings), 'rt') 

848 ), 

849 ) 

850 # Don't specifically catch TypeError or ValueError here if 

851 # the passed-in fileobj is not a readable text stream. This 

852 # will never happen on the command-line (thanks to `click`), 

853 # and for programmatic use, our caller may want accurate 

854 # error information. 

855 with infile: 1adqbfzYZcTghEeyv

856 maybe_config = json.load(infile) 1adqbfzYZcTghEeyv

857 except json.JSONDecodeError as exc: 1Z1

858 self.err( 1Z

859 _msg.TranslatedString( 

860 _msg.ErrMsgTemplate.CANNOT_DECODEIMPORT_VAULT_SETTINGS, 

861 error=exc, 

862 ) 

863 ) 

864 except OSError as exc: 11

865 self.err( 11

866 _msg.TranslatedString( 

867 _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS, 

868 error=exc.strerror, 

869 filename=exc.filename, 

870 ).maybe_without_filename() 

871 ) 

872 cleaned = _types.clean_up_falsy_vault_config_values(maybe_config) 1adqbfzYcTghEeyv

873 if not _types.is_vault_config(maybe_config): 1adqbfzYcTghEeyv

874 self.err( 1Y

875 _msg.TranslatedString( 

876 _msg.ErrMsgTemplate.CANNOT_IMPORT_VAULT_SETTINGS, 

877 error=_msg.TranslatedString( 

878 _msg.ErrMsgTemplate.INVALID_VAULT_CONFIG, 

879 config=maybe_config, 

880 ), 

881 filename=None, 

882 ).maybe_without_filename() 

883 ) 

884 assert cleaned is not None 1adqbfzcTghEeyv

885 for step in cleaned: 1adqbfzcTghEeyv

886 # These are never fatal errors, because the semantics of 

887 # vault upon encountering these settings are ill-specified, 

888 # but not ill-defined. 

889 if step.action == 'replace': 1eyv

890 self.warning( 1eyv

891 _msg.TranslatedString( 

892 _msg.WarnMsgTemplate.STEP_REPLACE_INVALID_VALUE, 

893 old=json.dumps(step.old_value), 

894 path=_types.json_path(step.path), 

895 new=json.dumps(step.new_value), 

896 ), 

897 ) 

898 else: 

899 self.warning( 1v

900 _msg.TranslatedString( 

901 _msg.WarnMsgTemplate.STEP_REMOVE_INEFFECTIVE_VALUE, 

902 path=_types.json_path(step.path), 

903 old=json.dumps(step.old_value), 

904 ), 

905 ) 

906 if '' in maybe_config['services']: 1adqbfzcTghEeyv

907 self.warning( 1qf

908 _msg.TranslatedString( 

909 _msg.WarnMsgTemplate.EMPTY_SERVICE_SETTINGS_INACCESSIBLE, 

910 service_metavar=service_metavar, 

911 PROG_NAME=PROG_NAME, 

912 ), 

913 ) 

914 for service_name in sorted(maybe_config['services'].keys()): 1adqbfzcTghEeyv

915 if not cli_helpers.is_completable_item(service_name): 1adqfzcTgeyv

916 self.warning( 1g

917 _msg.TranslatedString( 

918 _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE, 

919 service=service_name, 

920 ), 

921 ) 

922 try: 1adqbfzcTghEeyv

923 cli_helpers.check_for_misleading_passphrase( 1adqbfzcTghEeyv

924 ('global',), 

925 cast('dict[str, Any]', maybe_config.get('global', {})), 

926 main_config=user_config, 

927 ctx=self.ctx, 

928 ) 

929 for key, value in maybe_config['services'].items(): 1adqbfzcTghEeyv

930 cli_helpers.check_for_misleading_passphrase( 1adqfzcTgeyv

931 ('services', key), 

932 cast('dict[str, Any]', value), 

933 main_config=user_config, 

934 ctx=self.ctx, 

935 ) 

936 except AssertionError as exc: 1T

937 self.err( 1T

938 _msg.TranslatedString( 

939 _msg.ErrMsgTemplate.INVALID_USER_CONFIG, 

940 error=exc, 

941 filename=None, 

942 ).maybe_without_filename(), 

943 ) 

944 global_obj = maybe_config.get('global', {}) 1adqbfzcghEeyv

945 has_key = _types.js_truthiness(global_obj.get('key')) 1adqbfzcghEeyv

946 has_phrase = _types.js_truthiness(global_obj.get('phrase')) 1adqbfzcghEeyv

947 if has_key and has_phrase: 1adqbfzcghEeyv

948 self.warning( 1zhEv

949 _msg.TranslatedString( 

950 _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE, 

951 ), 

952 ) 

953 for service_name, service_obj in maybe_config['services'].items(): 1adqbfzcghEeyv

954 has_key = _types.js_truthiness( 1adqfzcgeyv

955 service_obj.get('key') 

956 ) or _types.js_truthiness(global_obj.get('key')) 

957 has_phrase = _types.js_truthiness( 1adqfzcgeyv

958 service_obj.get('phrase') 

959 ) or _types.js_truthiness(global_obj.get('phrase')) 

960 if has_key and has_phrase: 1adqfzcgeyv

961 self.warning( 1zyv

962 _msg.TranslatedString( 

963 _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE, 

964 service=json.dumps(service_name), 

965 ), 

966 ) 

967 if overwrite_config: 1adqbfzcghEeyv

968 self.put_config(maybe_config) 1ad

969 else: 

970 configuration = self.get_config() 1adqbfzcghEeyv

971 merged_config: collections.ChainMap[str, Any] = ( 1adqbfzcghEeyv

972 collections.ChainMap( 

973 { 

974 'services': collections.ChainMap( 

975 maybe_config['services'], 

976 configuration['services'], 

977 ), 

978 }, 

979 {'global': maybe_config['global']} 

980 if 'global' in maybe_config 

981 else {}, 

982 {'global': configuration['global']} 

983 if 'global' in configuration 

984 else {}, 

985 ) 

986 ) 

987 new_config: Any = { 1adqbfzcghEeyv

988 k: dict(v) if isinstance(v, collections.ChainMap) else v 

989 for k, v in sorted(merged_config.items()) 

990 } 

991 assert _types.is_vault_config(new_config) 1adqbfzcghEeyv

992 self.put_config(new_config) 1adqbfzcghEeyv

993 

994 def run_op_export_settings(self) -> None: 

995 """Export settings to a given file. 

996 

997 Respect the `--export-as` option when writing the exported 

998 configuration to the file. 

999 

1000 """ 

1001 export_settings = self.ctx.params['export_settings'] 1b7W360UV

1002 export_as = self.ctx.params['export_as'] 1b7W360UV

1003 configuration = self.get_config() 1b7W360UV

1004 try: 1b7W0UV

1005 # TODO(the-13th-letter): keep track of auto-close; try 

1006 # os.dup if feasible 

1007 outfile = cast( 1b7W0UV

1008 'TextIO', 

1009 ( 

1010 export_settings 

1011 if hasattr(export_settings, 'close') 

1012 else click.open_file(os.fspath(export_settings), 'wt') 

1013 ), 

1014 ) 

1015 # Don't specifically catch TypeError or ValueError here if 

1016 # the passed-in fileobj is not a writable text stream. This 

1017 # will never happen on the command-line (thanks to `click`), 

1018 # and for programmatic use, our caller may want accurate 

1019 # error information. 

1020 with outfile: 1b7WUV

1021 if export_as == 'sh': 1b7WUV

1022 this_ctx = self.ctx 1W

1023 prog_name_pieces = collections.deque([ 1W

1024 this_ctx.info_name or 'vault', 

1025 ]) 

1026 while ( 1W

1027 this_ctx.parent is not None 

1028 and this_ctx.parent.info_name is not None 

1029 ): 

1030 prog_name_pieces.appendleft(this_ctx.parent.info_name) 1W

1031 this_ctx = this_ctx.parent 1W

1032 cli_helpers.print_config_as_sh_script( 1W

1033 configuration, 

1034 outfile=outfile, 

1035 prog_name_list=prog_name_pieces, 

1036 ) 

1037 else: 

1038 json.dump( 1b7WUV

1039 configuration, 

1040 outfile, 

1041 ensure_ascii=False, 

1042 indent=2, 

1043 sort_keys=True, 

1044 ) 

1045 except OSError as exc: 10

1046 self.err( 10

1047 _msg.TranslatedString( 

1048 _msg.ErrMsgTemplate.CANNOT_EXPORT_VAULT_SETTINGS, 

1049 error=exc.strerror, 

1050 filename=exc.filename, 

1051 ).maybe_without_filename(), 

1052 ) 

1053 

1054 def run_subop_query_phrase_or_key_change( 

1055 self, 

1056 *, 

1057 empty_service_permitted: bool, 

1058 configuration: _types.VaultConfig | None = None, 

1059 ) -> collections.ChainMap[str, Any]: 

1060 """Query the master passphrase or master SSH key, if changed. 

1061 

1062 If the user indicates they want to change the master passphrase 

1063 or master SSH key for this call, or if they want to configure 

1064 a stored global or service-specific master passphrase or master 

1065 SSH key, then query the user. 

1066 

1067 This is not a complete command-line call operation in and of 

1068 itself. 

1069 

1070 Args: 

1071 empty_service_permitted: 

1072 True if an empty service name is permitted, False otherwise. 

1073 configuration: 

1074 The vault configuration, parsed from disk. If not 

1075 given, we read the configuration from disk ourselves. 

1076 

1077 The returned effective configuration already contains 

1078 a relevant slice of the vault configuration. However, 

1079 some callers need access to the full configuration *and* 

1080 need the slice within the effective configuration to 

1081 refer to the same object. 

1082 

1083 Returns: 

1084 The effective configuration for the (possibly empty) given 

1085 service, as a [chained map][collections.ChainMap]. Any 

1086 master passphrase or master SSH key overrides that may be in 

1087 effect are stored in the first map. 

1088 

1089 Raises: 

1090 click.UsageError: 

1091 The service name was empty, and an empty service name 

1092 was not permitted as per the method parameters. 

1093 

1094 Warning: 

1095 It is the caller's responsibility to vet any interactively 

1096 entered master passphrase for Unicode normalization issues. 

1097 

1098 """ 

1099 service = self.ctx.params['service'] 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1100 use_key = self.ctx.params['use_key'] 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1101 use_phrase = self.ctx.params['use_phrase'] 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1102 if configuration is None: 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1103 configuration = self.get_config() 1ABHLMIbXNcrwOGFC

1104 service_keys_on_commandline = { 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1105 'length', 

1106 'repeat', 

1107 'lower', 

1108 'upper', 

1109 'number', 

1110 'space', 

1111 'dash', 

1112 'symbol', 

1113 } 

1114 settings: collections.ChainMap[str, Any] = collections.ChainMap( 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1115 { 

1116 k: v 

1117 for k in service_keys_on_commandline 

1118 if (v := self.ctx.params.get(k)) is not None 

1119 }, 

1120 cast( 

1121 'dict[str, Any]', 

1122 configuration['services'].get(service, {}) if service else {}, 

1123 ), 

1124 cast('dict[str, Any]', configuration.get('global', {})), 

1125 ) 

1126 if not service and not empty_service_permitted: 1adABHLMIpbfntDPxuKQRSXNsmlcrJwOgheGFikjoC

1127 err_msg = _msg.TranslatedString( 1XO

1128 _msg.ErrMsgTemplate.SERVICE_REQUIRED, 

1129 service_metavar=_msg.TranslatedString( 

1130 _msg.Label.VAULT_METAVAR_SERVICE 

1131 ), 

1132 ) 

1133 raise click.UsageError(str(err_msg)) 1XO

1134 if use_key: 1adABHLMIpbfntDPxuKQRSNsmlcrJwgheGFikjoC

1135 settings.maps[0]['key'] = base64.standard_b64encode( 1LMtDPQRSJ

1136 cli_helpers.select_ssh_key( 

1137 ctx=self.ctx, 

1138 error_callback=self.err, 

1139 warning_callback=self.warning, 

1140 ) 

1141 ).decode('ASCII') 

1142 elif use_phrase: 1adABHIpbfntDxuKNsmlcrwgheGFikjoC

1143 maybe_phrase = cli_helpers.prompt_for_passphrase() 1ABpbtDsmlcrw

1144 if not maybe_phrase: 1ABpbtDsmlcrw

1145 self.err( 1D

1146 _msg.TranslatedString( 

1147 _msg.ErrMsgTemplate.USER_ABORTED_PASSPHRASE 

1148 ) 

1149 ) 

1150 else: 

1151 settings.maps[0]['phrase'] = maybe_phrase 1ABpbtsmlcrw

1152 return settings 1adABHLMIpbfntDxuKNsmlcrwgheGFikjoC

1153 

1154 def run_op_store_config_only(self) -> None: # noqa: C901,PLR0912,PLR0914,PLR0915 

1155 """Update the stored vault configuration. 

1156 

1157 Depending on the presence or the absence of a service name, 

1158 update either the service-specific settings, or the global 

1159 settings. (An empty service name is respected, i.e., updates 

1160 the former.) 

1161 

1162 Respect the `--unset=SETTING` option to unset the named 

1163 settings, and the `--notes` option to edit notes interactively 

1164 in a spawned text editor. Issue multiple warnings, if 

1165 appropriate, e.g. for Unicode normalization issues with stored 

1166 passphrases or conflicting stored passphrases and keys. Respect 

1167 the `--overwrite-config` and `--merge-config` options when 

1168 writing the imported configuration to disk, and the 

1169 `--modern-editor-interface` and 

1170 `--vault-legacy-editor-interface` options when editing notes. 

1171 

1172 Raises: 

1173 click.UsageError: 

1174 The user requested that the same setting be both unset 

1175 and set. 

1176 

1177 """ 

1178 service = self.ctx.params['service'] 1adpbfntDPxuKQRSsmlcrJgheikjo

1179 use_key = self.ctx.params['use_key'] 1adpbfntDPxuKQRSsmlcrJgheikjo

1180 use_phrase = self.ctx.params['use_phrase'] 1adpbfntDPxuKQRSsmlcrJgheikjo

1181 unset_settings = self.ctx.params['unset_settings'] 1adpbfntDPxuKQRSsmlcrJgheikjo

1182 overwrite_config = self.ctx.params['overwrite_config'] 1adpbfntDPxuKQRSsmlcrJgheikjo

1183 edit_notes = self.ctx.params['edit_notes'] 1adpbfntDPxuKQRSsmlcrJgheikjo

1184 modern_editor_interface = self.ctx.params['modern_editor_interface'] 1adpbfntDPxuKQRSsmlcrJgheikjo

1185 configuration = self.get_config() 1adpbfntDPxuKQRSsmlcrJgheikjo

1186 user_config = self.get_user_config() 1adpbfntDPxuKQRSsmlcrJgheikjo

1187 settings = self.run_subop_query_phrase_or_key_change( 1adpbfntDPxuKQRSsmlcrJgheikjo

1188 configuration=configuration, empty_service_permitted=True 

1189 ) 

1190 overrides = settings.maps[0] 1adpbfntDxuKsmlcrgheikjo

1191 view: collections.ChainMap[str, Any] 

1192 view = ( 1adpbfntDxuKsmlcrgheikjo

1193 collections.ChainMap(*settings.maps[:2]) 

1194 if service 

1195 else collections.ChainMap(settings.maps[0], settings.maps[2]) 

1196 ) 

1197 if use_key: 1adpbfntDxuKsmlcrgheikjo

1198 view['key'] = overrides['key'] 1t

1199 elif use_phrase: 1adpbfntDxuKsmlcrgheikjo

1200 view['phrase'] = overrides['phrase'] 1pbtsmlcr

1201 try: 1pbtsmlcr

1202 cli_helpers.check_for_misleading_passphrase( 1pbtsmlcr

1203 ('services', service) if service else ('global',), 

1204 overrides, 

1205 main_config=user_config, 

1206 ctx=self.ctx, 

1207 ) 

1208 except AssertionError as exc: 1r

1209 self.err( 1r

1210 _msg.TranslatedString( 

1211 _msg.ErrMsgTemplate.INVALID_USER_CONFIG, 

1212 error=exc, 

1213 filename=None, 

1214 ).maybe_without_filename(), 

1215 ) 

1216 if 'key' in settings: 1pbtsmlc

1217 if service: 1p

1218 w_msg = _msg.TranslatedString( 1p

1219 _msg.WarnMsgTemplate.SERVICE_PASSPHRASE_INEFFECTIVE, 

1220 service=json.dumps(service), 

1221 ) 

1222 else: 

1223 w_msg = _msg.TranslatedString( 1p

1224 _msg.WarnMsgTemplate.GLOBAL_PASSPHRASE_INEFFECTIVE 

1225 ) 

1226 self.warning(w_msg) 1p

1227 if not view.maps[0] and not unset_settings and not edit_notes: 1adpbfntDxuKsmlcgheikjo

1228 err_msg = _msg.TranslatedString( 1D

1229 _msg.ErrMsgTemplate.CANNOT_UPDATE_SETTINGS_NO_SETTINGS, 

1230 settings_type=_msg.TranslatedString( 

1231 _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_SERVICE 

1232 if service 

1233 else _msg.Label.CANNOT_UPDATE_SETTINGS_METAVAR_SETTINGS_TYPE_GLOBAL # noqa: E501 

1234 ), 

1235 ) 

1236 raise click.UsageError(str(err_msg)) 1D

1237 for setting in unset_settings: 1adpbfntxuKsmlcgheikjo

1238 if setting in view.maps[0]: 1adK

1239 err_msg = _msg.TranslatedString( 1K

1240 _msg.ErrMsgTemplate.SET_AND_UNSET_SAME_SETTING, 

1241 setting=setting, 

1242 ) 

1243 raise click.UsageError(str(err_msg)) 1K

1244 if not cli_helpers.is_completable_item(service): 1adpbfntxusmlcgheikjo

1245 self.warning( 1g

1246 _msg.TranslatedString( 

1247 _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE, 

1248 service=service, 

1249 ), 

1250 ) 

1251 subtree: dict[str, Any] = ( 1adpbfntxusmlcgheikjo

1252 configuration['services'].setdefault(service, {}) # type: ignore[assignment] 

1253 if service 

1254 else configuration.setdefault('global', {}) 

1255 ) 

1256 if overwrite_config: 1adpbfntxusmlcgheikjo

1257 subtree.clear() 1ad

1258 else: 

1259 for setting in unset_settings: 1adpbfntxusmlcgheikjo

1260 subtree.pop(setting, None) 1ad

1261 subtree.update(view) 1adpbfntxusmlcgheikjo

1262 assert _types.is_vault_config(configuration), ( 1adpbfntxusmlcgheikjo

1263 f'Invalid vault configuration: {configuration!r}' 

1264 ) 

1265 if edit_notes: 1adpbfntxusmlcgheikjo

1266 assert service is not None 1nikjo

1267 notes_instructions = _msg.TranslatedString( 1nikjo

1268 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_INSTRUCTION_TEXT 

1269 ) 

1270 notes_marker = _msg.TranslatedString( 1nikjo

1271 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_MARKER 

1272 ) 

1273 notes_legacy_instructions = _msg.TranslatedString( 1nikjo

1274 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_LEGACY_INSTRUCTION_TEXT 

1275 ) 

1276 old_notes_value = subtree.get('notes', '') 1nikjo

1277 if modern_editor_interface: 1nikjo

1278 text = '\n'.join([ 1nikjo

1279 str(notes_instructions), 

1280 str(notes_marker), 

1281 old_notes_value, 

1282 ]) 

1283 else: 

1284 text = old_notes_value or str(notes_legacy_instructions) 1ikj

1285 notes_value = click.edit(text=text, require_save=False) 1nikjo

1286 assert notes_value is not None 1nikjo

1287 if ( 1nikjo

1288 not modern_editor_interface 

1289 and notes_value.strip() != old_notes_value.strip() 

1290 ): 

1291 backup_file = cli_helpers.config_filename( 1ij

1292 subsystem='notes backup' 

1293 ) 

1294 backup_file.write_text(old_notes_value, encoding='UTF-8') 1ij

1295 self.warning( 1ij

1296 _msg.TranslatedString( 

1297 _msg.WarnMsgTemplate.LEGACY_EDITOR_INTERFACE_NOTES_BACKUP, 

1298 filename=str(backup_file), 

1299 ), 

1300 ) 

1301 subtree['notes'] = notes_value.strip() 1ij

1302 elif ( 1nikjo

1303 modern_editor_interface and notes_value.strip() != text.strip() 

1304 ): 

1305 notes_lines = collections.deque( 1nikjo

1306 notes_value.splitlines(keepends=True) 

1307 ) 

1308 while notes_lines: 1nikjo

1309 line = notes_lines.popleft() 1ikj

1310 if line.startswith(str(notes_marker)): 1ikj

1311 notes_value = ''.join(notes_lines) 1i

1312 break 1i

1313 else: 

1314 if not notes_value.strip(): 1nkjo

1315 self.err( 1nko

1316 _msg.TranslatedString( 

1317 _msg.ErrMsgTemplate.USER_ABORTED_EDIT 

1318 ) 

1319 ) 

1320 subtree['notes'] = notes_value.strip() 1ikj

1321 self.put_config(configuration) 1adpbftxusmlcgheikj

1322 

1323 def run_op_derive_passphrase(self) -> None: 

1324 """Derive a passphrase. 

1325 

1326 Derive a service passphrase using the effective settings from 

1327 both the command-line and the stored configuration. If any 

1328 service notes are stored for this service, print them as well. 

1329 

1330 (An empty service name is permitted, though discouraged for 

1331 compatibility reasons.) 

1332 

1333 Issue a warning (if appropriate) for Unicode normalization 

1334 issues with the interactive passphrase. Respect the 

1335 `--print-notes-before` and `--print-notes-after` options when 

1336 printing notes. 

1337 

1338 Raises: 

1339 click.UsageError: 

1340 No master passphrase or master SSH key was given on both 

1341 the command-line and in the vault configuration on disk. 

1342 """ 

1343 service = self.ctx.params['service'] 1ABHLMIbXNcr24wOGFC

1344 use_key = self.ctx.params['use_key'] 1ABHLMIbXNcr24wOGFC

1345 use_phrase = self.ctx.params['use_phrase'] 1ABHLMIbXNcr24wOGFC

1346 print_notes_before = self.ctx.params['print_notes_before'] 1ABHLMIbXNcr24wOGFC

1347 user_config = self.get_user_config() 1ABHLMIbXNcr24wOGFC

1348 settings = self.run_subop_query_phrase_or_key_change( 1ABHLMIbXNcrwOGFC

1349 empty_service_permitted=False 

1350 ) 

1351 if use_phrase: 1ABHLMIbNcrwGFC

1352 try: 1ABbcrw

1353 cli_helpers.check_for_misleading_passphrase( 1ABbcrw

1354 cli_helpers.ORIGIN.INTERACTIVE, 

1355 {'phrase': settings['phrase']}, 

1356 main_config=user_config, 

1357 ctx=self.ctx, 

1358 ) 

1359 except AssertionError as exc: 1r

1360 self.err( 1r

1361 _msg.TranslatedString( 

1362 _msg.ErrMsgTemplate.INVALID_USER_CONFIG, 

1363 error=exc, 

1364 filename=None, 

1365 ).maybe_without_filename(), 

1366 ) 

1367 phrase: str | bytes 

1368 overrides = cast('dict[str, int | str]', settings.maps[0]) 1ABHLMIbNcwGFC

1369 # If either --key or --phrase are given, use that setting. 

1370 # Otherwise, if both key and phrase are set in the config, 

1371 # use the key. Otherwise, if only one of key and phrase is 

1372 # set in the config, use that one. In all these above 

1373 # cases, set the phrase via vault.Vault.phrase_from_key if 

1374 # a key is given. Finally, if nothing is set, error out. 

1375 if use_key: 1ABHLMIbNcwGFC

1376 phrase = cli_helpers.key_to_phrase( 1LM

1377 cast('str', overrides['key']), 

1378 error_callback=self.err, 

1379 warning_callback=self.warning, 

1380 ) 

1381 elif use_phrase: 1ABHIbNcwGFC

1382 phrase = cast('str', overrides['phrase']) 1ABbcw

1383 elif settings.get('key'): 1HIbNGFC

1384 phrase = cli_helpers.key_to_phrase( 1HI

1385 cast('str', settings['key']), error_callback=self.err 

1386 ) 

1387 elif settings.get('phrase'): 1bNGFC

1388 phrase = cast('str', settings['phrase']) 1bGFC

1389 else: 

1390 err_msg = _msg.TranslatedString( 1N

1391 _msg.ErrMsgTemplate.NO_KEY_OR_PHRASE 

1392 ) 

1393 raise click.UsageError(str(err_msg)) 1N

1394 overrides.pop('key', '') 1ABHLMIbcwGFC

1395 overrides.pop('phrase', '') 1ABHLMIbcwGFC

1396 assert service is not None 1ABHLMIbcwGFC

1397 vault_service_keys = { 1ABHLMIbcwGFC

1398 'length', 

1399 'repeat', 

1400 'lower', 

1401 'upper', 

1402 'number', 

1403 'space', 

1404 'dash', 

1405 'symbol', 

1406 } 

1407 kwargs = { 1ABHLMIbcwGFC

1408 cast('str', k): cast('int', settings[k]) 

1409 for k in vault_service_keys 

1410 if k in settings 

1411 } 

1412 result = vault.Vault(phrase=phrase, **kwargs).generate(service) 1ABHLMIbcwGFC

1413 service_notes = cast('str', settings.get('notes', '')).strip() 1ABHLMIbcwGFC

1414 if print_notes_before and service_notes.strip(): 1ABHLMIbcwGFC

1415 click.echo(f'{service_notes}\n', err=True, color=self.ctx.color) 1F

1416 click.echo(result.decode('ASCII'), color=self.ctx.color) 1ABHLMIbcwGFC

1417 if not print_notes_before and service_notes.strip(): 1ABHLMIbcwGFC

1418 click.echo(f'\n{service_notes}\n', err=True, color=self.ctx.color) 1GFC

1419 

1420 

1421@derivepassphrase.command( 

1422 'vault', 

1423 context_settings={'help_option_names': ['-h', '--help']}, 

1424 cls=cli_machinery.CommandWithHelpGroups, 

1425 help=( 

1426 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_01), 

1427 _msg.TranslatedString( 

1428 _msg.Label.DERIVEPASSPHRASE_VAULT_02, 

1429 service_metavar=_msg.TranslatedString( 

1430 _msg.Label.VAULT_METAVAR_SERVICE 

1431 ), 

1432 ), 

1433 ), 

1434 epilog=( 

1435 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_01), 

1436 _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_VAULT_EPILOG_02), 

1437 ), 

1438) 

1439@click.option( 

1440 '-p', 

1441 '--phrase', 

1442 'use_phrase', 

1443 is_flag=True, 

1444 help=_msg.TranslatedString( 

1445 _msg.Label.DERIVEPASSPHRASE_VAULT_PHRASE_HELP_TEXT 

1446 ), 

1447 cls=cli_machinery.PassphraseGenerationOption, 

1448) 

1449@click.option( 

1450 '-k', 

1451 '--key', 

1452 'use_key', 

1453 is_flag=True, 

1454 help=_msg.TranslatedString( 

1455 _msg.Label.DERIVEPASSPHRASE_VAULT_KEY_HELP_TEXT 

1456 ), 

1457 cls=cli_machinery.PassphraseGenerationOption, 

1458) 

1459@click.option( 

1460 '-l', 

1461 '--length', 

1462 metavar=_msg.TranslatedString( 

1463 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1464 ), 

1465 callback=cli_machinery.validate_length, 

1466 help=_msg.TranslatedString( 

1467 _msg.Label.DERIVEPASSPHRASE_VAULT_LENGTH_HELP_TEXT, 

1468 metavar=_msg.TranslatedString( 

1469 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1470 ), 

1471 ), 

1472 cls=cli_machinery.PassphraseGenerationOption, 

1473) 

1474@click.option( 

1475 '-r', 

1476 '--repeat', 

1477 metavar=_msg.TranslatedString( 

1478 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1479 ), 

1480 callback=cli_machinery.validate_occurrence_constraint, 

1481 help=_msg.TranslatedString( 

1482 _msg.Label.DERIVEPASSPHRASE_VAULT_REPEAT_HELP_TEXT, 

1483 metavar=_msg.TranslatedString( 

1484 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1485 ), 

1486 ), 

1487 cls=cli_machinery.PassphraseGenerationOption, 

1488) 

1489@click.option( 

1490 '--lower', 

1491 metavar=_msg.TranslatedString( 

1492 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1493 ), 

1494 callback=cli_machinery.validate_occurrence_constraint, 

1495 help=_msg.TranslatedString( 

1496 _msg.Label.DERIVEPASSPHRASE_VAULT_LOWER_HELP_TEXT, 

1497 metavar=_msg.TranslatedString( 

1498 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1499 ), 

1500 ), 

1501 cls=cli_machinery.PassphraseGenerationOption, 

1502) 

1503@click.option( 

1504 '--upper', 

1505 metavar=_msg.TranslatedString( 

1506 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1507 ), 

1508 callback=cli_machinery.validate_occurrence_constraint, 

1509 help=_msg.TranslatedString( 

1510 _msg.Label.DERIVEPASSPHRASE_VAULT_UPPER_HELP_TEXT, 

1511 metavar=_msg.TranslatedString( 

1512 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1513 ), 

1514 ), 

1515 cls=cli_machinery.PassphraseGenerationOption, 

1516) 

1517@click.option( 

1518 '--number', 

1519 metavar=_msg.TranslatedString( 

1520 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1521 ), 

1522 callback=cli_machinery.validate_occurrence_constraint, 

1523 help=_msg.TranslatedString( 

1524 _msg.Label.DERIVEPASSPHRASE_VAULT_NUMBER_HELP_TEXT, 

1525 metavar=_msg.TranslatedString( 

1526 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1527 ), 

1528 ), 

1529 cls=cli_machinery.PassphraseGenerationOption, 

1530) 

1531@click.option( 

1532 '--space', 

1533 metavar=_msg.TranslatedString( 

1534 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1535 ), 

1536 callback=cli_machinery.validate_occurrence_constraint, 

1537 help=_msg.TranslatedString( 

1538 _msg.Label.DERIVEPASSPHRASE_VAULT_SPACE_HELP_TEXT, 

1539 metavar=_msg.TranslatedString( 

1540 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1541 ), 

1542 ), 

1543 cls=cli_machinery.PassphraseGenerationOption, 

1544) 

1545@click.option( 

1546 '--dash', 

1547 metavar=_msg.TranslatedString( 

1548 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1549 ), 

1550 callback=cli_machinery.validate_occurrence_constraint, 

1551 help=_msg.TranslatedString( 

1552 _msg.Label.DERIVEPASSPHRASE_VAULT_DASH_HELP_TEXT, 

1553 metavar=_msg.TranslatedString( 

1554 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1555 ), 

1556 ), 

1557 cls=cli_machinery.PassphraseGenerationOption, 

1558) 

1559@click.option( 

1560 '--symbol', 

1561 metavar=_msg.TranslatedString( 

1562 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1563 ), 

1564 callback=cli_machinery.validate_occurrence_constraint, 

1565 help=_msg.TranslatedString( 

1566 _msg.Label.DERIVEPASSPHRASE_VAULT_SYMBOL_HELP_TEXT, 

1567 metavar=_msg.TranslatedString( 

1568 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1569 ), 

1570 ), 

1571 cls=cli_machinery.PassphraseGenerationOption, 

1572) 

1573@click.option( 

1574 '-n', 

1575 '--notes', 

1576 'edit_notes', 

1577 is_flag=True, 

1578 help=_msg.TranslatedString( 

1579 _msg.Label.DERIVEPASSPHRASE_VAULT_NOTES_HELP_TEXT, 

1580 service_metavar=_msg.TranslatedString( 

1581 _msg.Label.VAULT_METAVAR_SERVICE 

1582 ), 

1583 ), 

1584 cls=cli_machinery.ConfigurationOption, 

1585) 

1586@click.option( 

1587 '-c', 

1588 '--config', 

1589 'store_config_only', 

1590 is_flag=True, 

1591 help=_msg.TranslatedString( 

1592 _msg.Label.DERIVEPASSPHRASE_VAULT_CONFIG_HELP_TEXT, 

1593 service_metavar=_msg.TranslatedString( 

1594 _msg.Label.VAULT_METAVAR_SERVICE 

1595 ), 

1596 ), 

1597 cls=cli_machinery.ConfigurationOption, 

1598) 

1599@click.option( 

1600 '-x', 

1601 '--delete', 

1602 'delete_service_settings', 

1603 is_flag=True, 

1604 help=_msg.TranslatedString( 

1605 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_HELP_TEXT, 

1606 service_metavar=_msg.TranslatedString( 

1607 _msg.Label.VAULT_METAVAR_SERVICE 

1608 ), 

1609 ), 

1610 cls=cli_machinery.ConfigurationOption, 

1611) 

1612@click.option( 

1613 '--delete-globals', 

1614 is_flag=True, 

1615 help=_msg.TranslatedString( 

1616 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_GLOBALS_HELP_TEXT, 

1617 ), 

1618 cls=cli_machinery.ConfigurationOption, 

1619) 

1620@click.option( 

1621 '-X', 

1622 '--clear', 

1623 'clear_all_settings', 

1624 is_flag=True, 

1625 help=_msg.TranslatedString( 

1626 _msg.Label.DERIVEPASSPHRASE_VAULT_DELETE_ALL_HELP_TEXT, 

1627 ), 

1628 cls=cli_machinery.ConfigurationOption, 

1629) 

1630@click.option( 

1631 '-e', 

1632 '--export', 

1633 'export_settings', 

1634 metavar=_msg.TranslatedString( 

1635 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1636 ), 

1637 help=_msg.TranslatedString( 

1638 _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_HELP_TEXT, 

1639 metavar=_msg.TranslatedString( 

1640 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1641 ), 

1642 ), 

1643 cls=cli_machinery.StorageManagementOption, 

1644 shell_complete=cli_helpers.shell_complete_path, 

1645) 

1646@click.option( 

1647 '-i', 

1648 '--import', 

1649 'import_settings', 

1650 metavar=_msg.TranslatedString( 

1651 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1652 ), 

1653 help=_msg.TranslatedString( 

1654 _msg.Label.DERIVEPASSPHRASE_VAULT_IMPORT_HELP_TEXT, 

1655 metavar=_msg.TranslatedString( 

1656 _msg.Label.PASSPHRASE_GENERATION_METAVAR_NUMBER 

1657 ), 

1658 ), 

1659 cls=cli_machinery.StorageManagementOption, 

1660 shell_complete=cli_helpers.shell_complete_path, 

1661) 

1662@click.option( 

1663 '--overwrite-existing/--merge-existing', 

1664 'overwrite_config', 

1665 default=False, 

1666 help=_msg.TranslatedString( 

1667 _msg.Label.DERIVEPASSPHRASE_VAULT_OVERWRITE_HELP_TEXT 

1668 ), 

1669 cls=cli_machinery.CompatibilityOption, 

1670) 

1671@click.option( 

1672 '--unset', 

1673 'unset_settings', 

1674 multiple=True, 

1675 type=click.Choice([ 

1676 'phrase', 

1677 'key', 

1678 'length', 

1679 'repeat', 

1680 'lower', 

1681 'upper', 

1682 'number', 

1683 'space', 

1684 'dash', 

1685 'symbol', 

1686 'notes', 

1687 ]), 

1688 help=_msg.TranslatedString( 

1689 _msg.Label.DERIVEPASSPHRASE_VAULT_UNSET_HELP_TEXT 

1690 ), 

1691 cls=cli_machinery.CompatibilityOption, 

1692) 

1693@click.option( 

1694 '--export-as', 

1695 type=click.Choice(['json', 'sh']), 

1696 default='json', 

1697 help=_msg.TranslatedString( 

1698 _msg.Label.DERIVEPASSPHRASE_VAULT_EXPORT_AS_HELP_TEXT 

1699 ), 

1700 cls=cli_machinery.CompatibilityOption, 

1701) 

1702@click.option( 

1703 '--modern-editor-interface/--vault-legacy-editor-interface', 

1704 'modern_editor_interface', 

1705 default=False, 

1706 help=_msg.TranslatedString( 

1707 _msg.Label.DERIVEPASSPHRASE_VAULT_EDITOR_INTERFACE_HELP_TEXT 

1708 ), 

1709 cls=cli_machinery.CompatibilityOption, 

1710) 

1711@click.option( 

1712 '--print-notes-before/--print-notes-after', 

1713 'print_notes_before', 

1714 default=False, 

1715 help=_msg.TranslatedString( 

1716 _msg.Label.DERIVEPASSPHRASE_VAULT_PRINT_NOTES_BEFORE_HELP_TEXT, 

1717 service_metavar=_msg.TranslatedString( 

1718 _msg.Label.VAULT_METAVAR_SERVICE 

1719 ), 

1720 ), 

1721 cls=cli_machinery.CompatibilityOption, 

1722) 

1723@cli_machinery.version_option(cli_machinery.vault_version_option_callback) 

1724@cli_machinery.color_forcing_pseudo_option 

1725@cli_machinery.standard_logging_options 

1726@click.argument( 

1727 'service', 

1728 metavar=_msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE), 

1729 required=False, 

1730 default=None, 

1731 shell_complete=cli_helpers.shell_complete_service, 

1732) 

1733@click.pass_context 

1734def derivepassphrase_vault( 

1735 ctx: click.Context, 

1736 /, 

1737 **_kwargs: Any, # noqa: ANN401 

1738) -> None: 

1739 """Derive a passphrase using the vault(1) derivation scheme. 

1740 

1741 This is a [`click`][CLICK]-powered command-line interface function, 

1742 and not intended for programmatic use. See the 

1743 derivepassphrase-vault(1) manpage for full documentation of the 

1744 interface. (See also [`click.testing.CliRunner`][] for controlled, 

1745 programmatic invocation.) 

1746 

1747 [CLICK]: https://pypi.org/package/click/ 

1748 

1749 Parameters: 

1750 ctx (click.Context): 

1751 The `click` context. 

1752 

1753 Other Parameters: 

1754 service (str | None): 

1755 A service name. Required, unless operating on global 

1756 settings or importing/exporting settings. 

1757 use_phrase (bool): 

1758 Command-line argument `-p`/`--phrase`. If given, query the 

1759 user for a passphrase instead of an SSH key. 

1760 use_key (bool): 

1761 Command-line argument `-k`/`--key`. If given, query the 

1762 user for an SSH key instead of a passphrase. 

1763 length (int | None): 

1764 Command-line argument `-l`/`--length`. Override the default 

1765 length of the generated passphrase. 

1766 repeat (int | None): 

1767 Command-line argument `-r`/`--repeat`. Override the default 

1768 repetition limit if positive, or disable the repetition 

1769 limit if 0. 

1770 lower (int | None): 

1771 Command-line argument `--lower`. Require a given amount of 

1772 ASCII lowercase characters if positive, else forbid ASCII 

1773 lowercase characters if 0. 

1774 upper (int | None): 

1775 Command-line argument `--upper`. Same as `lower`, but for 

1776 ASCII uppercase characters. 

1777 number (int | None): 

1778 Command-line argument `--number`. Same as `lower`, but for 

1779 ASCII digits. 

1780 space (int | None): 

1781 Command-line argument `--space`. Same as `lower`, but for 

1782 the space character. 

1783 dash (int | None): 

1784 Command-line argument `--dash`. Same as `lower`, but for 

1785 the hyphen-minus and underscore characters. 

1786 symbol (int | None): 

1787 Command-line argument `--symbol`. Same as `lower`, but for 

1788 all other ASCII printable characters except lowercase 

1789 characters, uppercase characters, digits, space and 

1790 backquote. 

1791 edit_notes (bool): 

1792 Command-line argument `-n`/`--notes`. If given, spawn an 

1793 editor to edit notes for `service`. 

1794 store_config_only (bool): 

1795 Command-line argument `-c`/`--config`. If given, saves the 

1796 other given settings (`--key`, ..., `--symbol`) to the 

1797 configuration file, either specifically for `service` or as 

1798 global settings. 

1799 delete_service_settings (bool): 

1800 Command-line argument `-x`/`--delete`. If given, removes 

1801 the settings for `service` from the configuration file. 

1802 delete_globals (bool): 

1803 Command-line argument `--delete-globals`. If given, removes 

1804 the global settings from the configuration file. 

1805 clear_all_settings (bool): 

1806 Command-line argument `-X`/`--clear`. If given, removes all 

1807 settings from the configuration file. 

1808 export_settings (TextIO | os.PathLike[str] | None): 

1809 Command-line argument `-e`/`--export`. If a file object, 

1810 then it must be open for writing and accept `str` inputs. 

1811 Otherwise, a filename to open for writing. Using `-` for 

1812 standard output is supported. 

1813 import_settings (TextIO | os.PathLike[str] | None): 

1814 Command-line argument `-i`/`--import`. If a file object, it 

1815 must be open for reading and yield `str` values. Otherwise, 

1816 a filename to open for reading. Using `-` for standard 

1817 input is supported. 

1818 overwrite_config (bool): 

1819 Command-line arguments `--overwrite-existing` (True) and 

1820 `--merge-existing` (False). Controls whether config saving 

1821 and config importing overwrite existing configurations, or 

1822 merge them section-wise instead. 

1823 unset_settings (Sequence[str]): 

1824 Command-line argument `--unset`. If given together with 

1825 `--config`, unsets the specified settings (in addition to 

1826 any other changes requested). 

1827 export_as (Literal['json', 'sh']): 

1828 Command-line argument `--export-as`. If given together with 

1829 `--export`, selects the format to export the current 

1830 configuration as: JSON ("json", default) or POSIX sh ("sh"). 

1831 modern_editor_interface (bool): 

1832 Command-line arguments `--modern-editor-interface` (True) 

1833 and `--vault-legacy-editor-interface` (False). Controls 

1834 whether editing notes uses a modern editor interface 

1835 (supporting comments and aborting) or a vault(1)-compatible 

1836 legacy editor interface (WYSIWYG notes contents). 

1837 print_notes_before (bool): 

1838 Command-line arguments `--print-notes-before` (True) and 

1839 `--print-notes-after` (False). Controls whether the service 

1840 notes (if any) are printed before the passphrase, or after. 

1841 

1842 """ 

1843 vault_context = _VaultContext(ctx) 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

1844 vault_context.validate_command_line() 1adqABHLMIpbf8zYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

1845 vault_context.dispatch_op() 1adqABHLMIpbfzYZ17W360ntDPxuKQRSXNsmlcTr24JwOUV5ghEeyGvFikjoC

1846 

1847 

1848if __name__ == '__main__': 

1849 derivepassphrase()