Add shell completion support to derivepassphrase
Marco Ricci

Marco Ricci commited on 2025-01-01 00:21:55
Zeige 1 geänderte Dateien mit 51 Einfügungen und 0 Löschungen.


`click` already includes shell completion out of the box, so what
remained to be done was to tell `click` what kind of values the
arguments expect:

  * The `path` argument for `derivepassphrase export vault` will be
    completed with a filename or the string `VAULT_PATH`.  Because this
    is not (as a whole) a supported standard category of completion
    items, we must generate the completion items ourselves in this case,
    including the partial filtering of results.

  * The `path` argument to the `--import` and `--export` options of
    `derivepassphrase vault` will be completed with a filename.  `click`
    handles this for us.

  * The `service` argument to `devirepassphrase vault` will be completed
    with a service name, which we must manually extract and filter.

The shell completion has so far only been tested interactively; in
particular, it is currently excluded from coverage testing.
... ...
@@ -1343,6 +1343,23 @@ def _load_data(
1343 1343
         assert_never(fmt)
1344 1344
 
1345 1345
 
1346
+def _shell_complete_vault_path(  # pragma: no cover
1347
+    ctx: click.Context,
1348
+    param: click.Parameter,
1349
+    incomplete: str,
1350
+) -> list[str | click.shell_completion.CompletionItem]:
1351
+    del ctx, param
1352
+    if incomplete and 'VAULT_PATH'.startswith(incomplete):
1353
+        ret: set[str | click.shell_completion.CompletionItem] = {'VAULT_PATH'}
1354
+        for f in os.listdir():
1355
+            if f.startswith(incomplete):
1356
+                ret.add(f + os.path.sep if os.path.isdir(f) else f)
1357
+        return sorted(ret)
1358
+    return [
1359
+        click.shell_completion.CompletionItem('', type='file'),
1360
+    ]
1361
+
1362
+
1346 1363
 @derivepassphrase_export.command(
1347 1364
     'vault',
1348 1365
     context_settings={'help_option_names': ['-h', '--help']},
... ...
@@ -1401,6 +1418,7 @@ def _load_data(
1401 1418
     'path',
1402 1419
     metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_METAVAR_PATH),
1403 1420
     required=True,
1421
+    shell_complete=_shell_complete_vault_path,
1404 1422
 )
1405 1423
 @click.pass_context
1406 1424
 def derivepassphrase_export_vault(
... ...
@@ -2179,6 +2197,36 @@ def _validate_length(
2179 2197
     return int_value
2180 2198
 
2181 2199
 
2200
+def _shell_complete_path(  # pragma: no cover
2201
+    ctx: click.Context,
2202
+    parameter: click.Parameter,
2203
+    incomplete: str,
2204
+) -> list[str | click.shell_completion.CompletionItem]:
2205
+    del ctx, parameter, incomplete
2206
+    return [click.shell_completion.CompletionItem('', type='file')]
2207
+
2208
+
2209
+def _shell_complete_service(  # pragma: no cover
2210
+    ctx: click.Context,
2211
+    parameter: click.Parameter,
2212
+    incomplete: str,
2213
+) -> list[str | click.shell_completion.CompletionItem]:
2214
+    del ctx, parameter
2215
+    try:
2216
+        config = _load_config()
2217
+        return [sv for sv in config['services'] if sv.startswith(incomplete)]
2218
+    except FileNotFoundError:
2219
+        try:
2220
+            config, _exc = _migrate_and_load_old_config()
2221
+            return [
2222
+                sv for sv in config['services'] if sv.startswith(incomplete)
2223
+            ]
2224
+        except FileNotFoundError:
2225
+            return []
2226
+    except Exception:  # noqa: BLE001
2227
+        return []
2228
+
2229
+
2182 2230
 DEFAULT_NOTES_TEMPLATE = """\
2183 2231
 # Enter notes below the line with the cut mark (ASCII scissors and
2184 2232
 # dashes).  Lines above the cut mark (such as this one) will be ignored.
... ...
@@ -2416,6 +2464,7 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
2416 2464
         ),
2417 2465
     ),
2418 2466
     cls=StorageManagementOption,
2467
+    shell_complete=_shell_complete_path,
2419 2468
 )
2420 2469
 @click.option(
2421 2470
     '-i',
... ...
@@ -2431,6 +2480,7 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
2431 2480
         ),
2432 2481
     ),
2433 2482
     cls=StorageManagementOption,
2483
+    shell_complete=_shell_complete_path,
2434 2484
 )
2435 2485
 @click.option(
2436 2486
     '--overwrite-existing/--merge-existing',
... ...
@@ -2478,6 +2528,7 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
2478 2528
     metavar=_msg.TranslatedString(_msg.Label.VAULT_METAVAR_SERVICE),
2479 2529
     required=False,
2480 2530
     default=None,
2531
+    shell_complete=_shell_complete_service,
2481 2532
 )
2482 2533
 @click.pass_context
2483 2534
 def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
2484 2535