Repository: gitguardian/ggshield
Issue: `scan` options should be allowed later in the command

**GitGuardian Shield Version**

- I can reproduce this bug in the latest version

**Command executed**

```
ggshield scan path -v foo
```

**Describe the bug**

When the command is executed, ggshield complains like this:

```
Usage: ggshield scan path [OPTIONS] PATHS...
Try 'ggshield scan path -h' for help.

Error: No such option: -v
```

It will only work if one does: ` ggshield scan -v path foo`.

**Expected behavior**

It should be possible to use any `ggshield scan` options (see `ggshield scan --help`) after a `scan` sub-command.

Changed files:
ggshield/cmd/secret/scan/dockerarchive.py, ggshield/cmd/common_options.py, ggshield/cmd/scan/__init__.py, ggshield/cmd/secret/scan/ci.py, ggshield/cmd/secret/scan/prereceive.py, ggshield/cmd/secret/scan/range.py, tests/unit/cmd/scan/test_ci.py, ggshield/cmd/secret/scan/prepush.py, ggshield/cmd/secret/scan/docker.py, tests/unit/cmd/scan/test_path.py, ggshield/cmd/secret/scan/repo.py, tests/functional/secret/test_scan_docker.py, ggshield/cmd/secret/scan/pypi.py, ggshield/cmd/secret/scan/archive.py, ggshield/cmd/secret/scan/__init__.py, ggshield/cmd/secret/scan/secret_scan_common_options.py, ggshield/cmd/iac/scan.py, ggshield/cmd/secret/scan/docset.py, ggshield/cmd/secret/scan/path.py

diff --git a/ggshield/cmd/common_options.py b/ggshield/cmd/common_options.py
index 28f3d89..91aa602 100644
--- a/ggshield/cmd/common_options.py
+++ b/ggshield/cmd/common_options.py
@@ -35,6 +35,21 @@ def get_config_from_context(ctx: click.Context) -> UserConfig:
     return cast(UserConfig, ctx.obj["config"].user_config)
 
 
+def create_ctx_callback(name: str) -> ClickCallback:
+    """Helper function to define a Click option callback for simple cases where we only
+    have to set a value on Click context object if the option is defined.
+    """
+
+    def callback(
+        ctx: click.Context, param: click.Parameter, value: Optional[ArgT]
+    ) -> Optional[ArgT]:
+        if value is not None:
+            ctx.obj[name] = value
+        return value
+
+    return callback
+
+
 def create_config_callback(*option_names: str) -> ClickCallback:
     """Helper function to define a Click option callback for simple cases where we only
     have to set a configuration attribute if the option is defined.
diff --git a/ggshield/cmd/iac/scan.py b/ggshield/cmd/iac/scan.py
index e26ba3a..182af99 100644
--- a/ggshield/cmd/iac/scan.py
+++ b/ggshield/cmd/iac/scan.py
@@ -88,13 +88,19 @@ def scan_cmd(
     """
     Scan a directory for IaC vulnerabilities.
     """
-    update_context(
-        ctx, exit_zero, minimum_severity, ignore_policies, ignore_paths, json
-    )
+    update_context(ctx, exit_zero, minimum_severity, ignore_policies, ignore_paths)
     result = iac_scan(ctx, directory)
     scan = ScanCollection(id=str(directory), type="path_scan", iac_result=result)
 
-    output_handler: OutputHandler = ctx.obj["output_handler"]
+    output_handler_cls: Type[OutputHandler]
+    if json:
+        output_handler_cls = IaCJSONOutputHandler
+    else:
+        output_handler_cls = IaCTextOutputHandler
+    config: Config = ctx.obj["config"]
+    output_handler = output_handler_cls(
+        show_secrets=False, verbose=config.user_config.verbose
+    )
     return output_handler.process_scan(scan)
 
 
@@ -104,7 +110,6 @@ def update_context(
     minimum_severity: str,
     ignore_policies: Sequence[str],
     ignore_paths: Sequence[str],
-    json: bool,
 ) -> None:
     config: Config = ctx.obj["config"]
     ctx.obj["client"] = create_client_from_config(config)
@@ -125,14 +130,6 @@ def update_context(
     if minimum_severity is not None:
         config.user_config.iac.minimum_severity = minimum_severity
 
-    output_handler_cls: Type[OutputHandler] = IaCTextOutputHandler
-    if json:
-        output_handler_cls = IaCJSONOutputHandler
-
-    ctx.obj["output_handler"] = output_handler_cls(
-        show_secrets=False, verbose=config.user_config.verbose
-    )
-
 
 def iac_scan(ctx: click.Context, directory: Path) -> Optional[IaCScanResult]:
     files = get_iac_files_from_paths(
diff --git a/ggshield/cmd/scan/__init__.py b/ggshield/cmd/scan/__init__.py
index d56c2a2..5789a02 100644
--- a/ggshield/cmd/scan/__init__.py
+++ b/ggshield/cmd/scan/__init__.py
@@ -1,4 +1,4 @@
-from typing import Any, List, Optional
+from typing import Any, Optional
 
 import click
 
@@ -18,7 +18,6 @@ from ggshield.cmd.secret.scan import (
     scan_group_impl,
 )
 from ggshield.core.text_utils import display_warning
-from ggshield.core.utils import json_output_option_decorator
 
 
 @click.group(
@@ -37,27 +36,6 @@ from ggshield.core.utils import json_output_option_decorator
         "archive": archive_cmd,
     },
 )
-@json_output_option_decorator
-@click.option(
-    "--show-secrets",
-    is_flag=True,
-    default=None,
-    help="Show secrets in plaintext instead of hiding them.",
-)
-@click.option(
-    "--output",
-    "-o",
-    type=click.Path(exists=False, resolve_path=True),
-    default=None,
-    help="Route ggshield output to file.",
-)
-@click.option(
-    "--banlist-detector",
-    "-b",
-    default=None,
-    help="Exclude results from a detector.",
-    multiple=True,
-)
 # Deprecated options
 @click.option(
     "--all-policies",
@@ -75,10 +53,6 @@ from ggshield.core.utils import json_output_option_decorator
 @click.pass_context
 def deprecated_scan_group(
     ctx: click.Context,
-    show_secrets: bool,
-    json_output: bool,
-    output: Optional[str],
-    banlist_detector: Optional[List[str]] = None,
     all_policies: Optional[bool] = None,
     ignore_default_excludes: bool = False,
     **kwargs: Any,
@@ -89,10 +63,4 @@ def deprecated_scan_group(
     display_warning(
         "Warning: Using `ggshield scan (...)` is deprecated. Use `ggshield secret scan (...)` instead.",
     )
-    return scan_group_impl(
-        ctx,
-        show_secrets,
-        json_output,
-        output,
-        banlist_detector,
-    )
+    return scan_group_impl(ctx)
diff --git a/ggshield/cmd/secret/scan/__init__.py b/ggshield/cmd/secret/scan/__init__.py
index 67c5b23..c0f2b11 100644
--- a/ggshield/cmd/secret/scan/__init__.py
+++ b/ggshield/cmd/secret/scan/__init__.py
@@ -1,5 +1,5 @@
 import os
-from typing import Any, List, Optional, Type
+from typing import Any, Optional
 
 import click
 
@@ -21,8 +21,6 @@ from ggshield.cmd.secret.scan.secret_scan_common_options import (
 from ggshield.core.client import create_client_from_config
 from ggshield.core.config import Config
 from ggshield.core.text_utils import display_error
-from ggshield.core.utils import json_output_option_decorator
-from ggshield.output import JSONOutputHandler, OutputHandler, TextOutputHandler
 
 
 @click.group(
@@ -41,27 +39,6 @@ from ggshield.output import JSONOutputHandler, OutputHandler, TextOutputHandler
         "docset": docset_cmd,
     },
 )
-@json_output_option_decorator
-@click.option(
-    "--show-secrets",
-    is_flag=True,
-    default=None,
-    help="Show secrets in plaintext instead of hiding them.",
-)
-@click.option(
-    "--output",
-    "-o",
-    type=click.Path(exists=False, resolve_path=True),
-    default=None,
-    help="Route ggshield output to file.",
-)
-@click.option(
-    "--banlist-detector",
-    "-b",
-    default=None,
-    help="Exclude results from a detector.",
-    multiple=True,
-)
 # Deprecated options
 @click.option(
     "--all-policies",
@@ -79,31 +56,15 @@ from ggshield.output import JSONOutputHandler, OutputHandler, TextOutputHandler
 @click.pass_context
 def scan_group(
     ctx: click.Context,
-    show_secrets: bool,
-    json_output: bool,
-    output: Optional[str],
-    banlist_detector: Optional[List[str]] = None,
     all_policies: Optional[bool] = None,
     ignore_default_excludes: bool = False,
     **kwargs: Any,
 ) -> int:
     """Commands to scan various contents."""
-    return scan_group_impl(
-        ctx,
-        show_secrets,
-        json_output,
-        output,
-        banlist_detector,
-    )
+    return scan_group_impl(ctx)
 
 
-def scan_group_impl(
-    ctx: click.Context,
-    show_secrets: bool,
-    json_output: bool,
-    output: Optional[str],
-    banlist_detector: Optional[List[str]] = None,
-) -> int:
+def scan_group_impl(ctx: click.Context) -> int:
     """Implementation for scan_group(). Must be a separate function so that its code can
     be reused from the deprecated `cmd.scan` package."""
     ctx.obj["client"] = create_client_from_config(ctx.obj["config"])
@@ -111,24 +72,10 @@ def scan_group_impl(
 
     config: Config = ctx.obj["config"]
 
-    if show_secrets is not None:
-        config.secret.show_secrets = show_secrets
-
-    if banlist_detector:
-        config.secret.ignored_detectors.update(banlist_detector)
-
     max_commits = get_max_commits_for_hook()
     if max_commits:
         config.max_commits_for_hook = max_commits
 
-    output_handler_cls: Type[OutputHandler] = TextOutputHandler
-    if json_output:
-        output_handler_cls = JSONOutputHandler
-
-    ctx.obj["output_handler"] = output_handler_cls(
-        show_secrets=config.secret.show_secrets, verbose=config.verbose, output=output
-    )
-
     return return_code
 
 
diff --git a/ggshield/cmd/secret/scan/archive.py b/ggshield/cmd/secret/scan/archive.py
index 1d410f9..652a8bc 100644
--- a/ggshield/cmd/secret/scan/archive.py
+++ b/ggshield/cmd/secret/scan/archive.py
@@ -8,11 +8,11 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.config import Config
 from ggshield.core.file_utils import get_files_from_paths
 from ggshield.core.text_utils import create_progress_bar
-from ggshield.output import OutputHandler
 from ggshield.scan import Files, ScanCollection, ScanContext, ScanMode, SecretScanner
 
 
@@ -71,5 +71,5 @@ def archive_cmd(
 
         scan = ScanCollection(id=path, type="archive_scan", results=results)
 
-        output_handler: OutputHandler = ctx.obj["output_handler"]
+        output_handler = create_output_handler(ctx)
         return output_handler.process_scan(scan)
diff --git a/ggshield/cmd/secret/scan/ci.py b/ggshield/cmd/secret/scan/ci.py
index 1e6e8ed..f5d7ab1 100644
--- a/ggshield/cmd/secret/scan/ci.py
+++ b/ggshield/cmd/secret/scan/ci.py
@@ -6,6 +6,7 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.cache import ReadOnlyCache
 from ggshield.core.git_shell import check_git_dir, get_list_commit_SHA
@@ -319,7 +320,7 @@ def ci_cmd(ctx: click.Context, **kwargs: Any) -> int:
             client=ctx.obj["client"],
             cache=ReadOnlyCache(),
             commit_list=commit_list,
-            output_handler=ctx.obj["output_handler"],
+            output_handler=create_output_handler(ctx),
             exclusion_regexes=ctx.obj["exclusion_regexes"],
             matches_ignore=config.secret.ignored_matches,
             scan_context=scan_context,
diff --git a/ggshield/cmd/secret/scan/docker.py b/ggshield/cmd/secret/scan/docker.py
index c2a9902..97832c1 100644
--- a/ggshield/cmd/secret/scan/docker.py
+++ b/ggshield/cmd/secret/scan/docker.py
@@ -6,9 +6,9 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.utils import handle_exception
-from ggshield.output import OutputHandler
 from ggshield.scan import ScanContext, ScanMode
 from ggshield.scan.docker import docker_save_to_tmp, docker_scan_archive
 
@@ -40,7 +40,7 @@ def docker_name_cmd(
 
     with tempfile.TemporaryDirectory(suffix="ggshield") as temporary_dir:
         config = ctx.obj["config"]
-        output_handler: OutputHandler = ctx.obj["output_handler"]
+        output_handler = create_output_handler(ctx)
 
         try:
             archive = Path(temporary_dir) / "archive.tar"
diff --git a/ggshield/cmd/secret/scan/dockerarchive.py b/ggshield/cmd/secret/scan/dockerarchive.py
index 8b3e50b..27b3abf 100644
--- a/ggshield/cmd/secret/scan/dockerarchive.py
+++ b/ggshield/cmd/secret/scan/dockerarchive.py
@@ -5,9 +5,9 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.utils import handle_exception
-from ggshield.output import OutputHandler
 from ggshield.scan import ScanContext, ScanMode
 from ggshield.scan.docker import docker_scan_archive
 
@@ -29,7 +29,7 @@ def docker_archive_cmd(
     Hidden command `ggshield secret scan docker-archive`
     """
     config = ctx.obj["config"]
-    output_handler: OutputHandler = ctx.obj["output_handler"]
+    output_handler = create_output_handler(ctx)
 
     scan_context = ScanContext(
         scan_mode=ScanMode.DOCKER,
diff --git a/ggshield/cmd/secret/scan/docset.py b/ggshield/cmd/secret/scan/docset.py
index 39ad371..6c2ffc8 100644
--- a/ggshield/cmd/secret/scan/docset.py
+++ b/ggshield/cmd/secret/scan/docset.py
@@ -6,11 +6,11 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.constants import MAX_WORKERS
 from ggshield.core.text_utils import create_progress_bar, display_info
 from ggshield.core.utils import handle_exception
-from ggshield.output import OutputHandler
 from ggshield.scan import File, ScanCollection, ScanContext, ScanMode, SecretScanner
 
 
@@ -59,7 +59,7 @@ def docset_cmd(
     scan docset JSONL files.
     """
     config = ctx.obj["config"]
-    output_handler: OutputHandler = ctx.obj["output_handler"]
+    output_handler = create_output_handler(ctx)
     try:
         with create_progress_bar(doc_type="files") as progress:
             task_scan = progress.add_task(
diff --git a/ggshield/cmd/secret/scan/path.py b/ggshield/cmd/secret/scan/path.py
index 428674f..f48b9ad 100644
--- a/ggshield/cmd/secret/scan/path.py
+++ b/ggshield/cmd/secret/scan/path.py
@@ -5,12 +5,12 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.constants import MAX_WORKERS
 from ggshield.core.file_utils import get_files_from_paths
 from ggshield.core.text_utils import create_progress_bar
 from ggshield.core.utils import handle_exception
-from ggshield.output import OutputHandler
 from ggshield.scan import ScanCollection, ScanContext, ScanMode, SecretScanner
 
 
@@ -33,7 +33,7 @@ def path_cmd(
     scan files and directories.
     """
     config = ctx.obj["config"]
-    output_handler: OutputHandler = ctx.obj["output_handler"]
+    output_handler = create_output_handler(ctx)
     try:
         files = get_files_from_paths(
             paths=paths,
diff --git a/ggshield/cmd/secret/scan/prepush.py b/ggshield/cmd/secret/scan/prepush.py
index b128dfa..beb84db 100644
--- a/ggshield/cmd/secret/scan/prepush.py
+++ b/ggshield/cmd/secret/scan/prepush.py
@@ -7,6 +7,7 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.git_shell import (
     check_git_dir,
@@ -119,7 +120,7 @@ def prepush_cmd(ctx: click.Context, prepush_args: List[str], **kwargs: Any) -> i
             client=ctx.obj["client"],
             cache=ctx.obj["cache"],
             commit_list=commit_list,
-            output_handler=ctx.obj["output_handler"],
+            output_handler=create_output_handler(ctx),
             exclusion_regexes=ctx.obj["exclusion_regexes"],
             matches_ignore=config.secret.ignored_matches,
             scan_context=scan_context,
diff --git a/ggshield/cmd/secret/scan/prereceive.py b/ggshield/cmd/secret/scan/prereceive.py
index 183b092..23f031b 100644
--- a/ggshield/cmd/secret/scan/prereceive.py
+++ b/ggshield/cmd/secret/scan/prereceive.py
@@ -9,6 +9,7 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.cache import ReadOnlyCache
 from ggshield.core.git_shell import get_list_commit_SHA, git
@@ -138,7 +139,7 @@ def prereceive_cmd(
     scan as a pre-receive git hook.
     """
     config = ctx.obj["config"]
-    output_handler = ctx.obj["output_handler"]
+    output_handler = create_output_handler(ctx)
 
     if os.getenv("GL_PROTOCOL") == "web":
         # We are inside GitLab web UI
diff --git a/ggshield/cmd/secret/scan/pypi.py b/ggshield/cmd/secret/scan/pypi.py
index afa604e..4b50aa4 100644
--- a/ggshield/cmd/secret/scan/pypi.py
+++ b/ggshield/cmd/secret/scan/pypi.py
@@ -11,12 +11,12 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.config import Config
 from ggshield.core.constants import MAX_WORKERS
 from ggshield.core.file_utils import get_files_from_paths
 from ggshield.core.text_utils import create_progress_bar
-from ggshield.output import OutputHandler
 from ggshield.scan import Files, ScanCollection, ScanContext, ScanMode, SecretScanner
 
 
@@ -95,7 +95,7 @@ def pypi_cmd(
     scan a pypi package <NAME>.
     """
     config: Config = ctx.obj["config"]
-    output_handler: OutputHandler = ctx.obj["output_handler"]
+    output_handler = create_output_handler(ctx)
 
     with tempfile.TemporaryDirectory(suffix="ggshield") as temp_dir:
         save_package_to_tmp(temp_dir=temp_dir, package_name=package_name)
diff --git a/ggshield/cmd/secret/scan/range.py b/ggshield/cmd/secret/scan/range.py
index 053d7e2..16fefc2 100644
--- a/ggshield/cmd/secret/scan/range.py
+++ b/ggshield/cmd/secret/scan/range.py
@@ -4,6 +4,7 @@ import click
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.git_shell import get_list_commit_SHA
 from ggshield.core.utils import handle_exception
@@ -43,7 +44,7 @@ def range_cmd(
             client=ctx.obj["client"],
             cache=ctx.obj["cache"],
             commit_list=commit_list,
-            output_handler=ctx.obj["output_handler"],
+            output_handler=create_output_handler(ctx),
             exclusion_regexes=ctx.obj["exclusion_regexes"],
             matches_ignore=config.secret.ignored_matches,
             scan_context=scan_context,
diff --git a/ggshield/cmd/secret/scan/repo.py b/ggshield/cmd/secret/scan/repo.py
index 2badb0a..4170a56 100644
--- a/ggshield/cmd/secret/scan/repo.py
+++ b/ggshield/cmd/secret/scan/repo.py
@@ -7,6 +7,7 @@ from pygitguardian import GGClient
 
 from ggshield.cmd.secret.scan.secret_scan_common_options import (
     add_secret_scan_common_options,
+    create_output_handler,
 )
 from ggshield.core.cache import Cache
 from ggshield.core.config import Config
@@ -46,7 +47,7 @@ def repo_cmd(
         return scan_repo_path(
             client=client,
             cache=cache,
-            output_handler=ctx.obj["output_handler"],
+            output_handler=create_output_handler(ctx),
             config=config,
             scan_context=scan_context,
             repo_path=repository,
@@ -58,7 +59,7 @@ def repo_cmd(
             return scan_repo_path(
                 client=client,
                 cache=cache,
-                output_handler=ctx.obj["output_handler"],
+                output_handler=create_output_handler(ctx),
                 config=config,
                 scan_context=scan_context,
                 repo_path=tmpdirname,
diff --git a/ggshield/cmd/secret/scan/secret_scan_common_options.py b/ggshield/cmd/secret/scan/secret_scan_common_options.py
index 836d3a7..22aa991 100644
--- a/ggshield/cmd/secret/scan/secret_scan_common_options.py
+++ b/ggshield/cmd/secret/scan/secret_scan_common_options.py
@@ -6,17 +6,48 @@ from ggshield.cmd.common_options import (
     AnyFunction,
     add_common_options,
     create_config_callback,
+    create_ctx_callback,
     get_config_from_context,
 )
 from ggshield.core.config.user_config import SecretConfig
 from ggshield.core.filter import init_exclusion_regexes
 from ggshield.core.utils import IGNORED_DEFAULT_WILDCARDS
+from ggshield.output import JSONOutputHandler, OutputHandler, TextOutputHandler
 
 
 def _get_secret_config(ctx: click.Context) -> SecretConfig:
     return get_config_from_context(ctx).secret
 
 
+_json_option = click.option(
+    "--json",
+    "json_output",
+    is_flag=True,
+    default=None,
+    help="JSON output results",
+    callback=create_ctx_callback("use_json"),
+)
+
+
+_output_option = click.option(
+    "--output",
+    "-o",
+    type=click.Path(exists=False, resolve_path=True),
+    default=None,
+    help="Route ggshield output to file.",
+    callback=create_ctx_callback("output"),
+)
+
+
+_show_secrets_option = click.option(
+    "--show-secrets",
+    is_flag=True,
+    default=None,
+    help="Show secrets in plaintext instead of hiding them.",
+    callback=create_config_callback("secret", "show_secrets"),
+)
+
+
 _exit_zero_option = click.option(
     "--exit-zero",
     is_flag=True,
@@ -59,12 +90,48 @@ _ignore_known_secrets_option = click.option(
 )
 
 
+def _banlist_detectors_callback(
+    ctx: click.Context, param: click.Parameter, value: Optional[List[str]]
+) -> Optional[List[str]]:
+    if value is not None:
+        config = _get_secret_config(ctx)
+        config.ignored_detectors.update(value)
+
+    return value
+
+
+_banlist_detectors_option = click.option(
+    "--banlist-detector",
+    "-b",
+    default=None,
+    help="Exclude results from a detector.",
+    multiple=True,
+    callback=_banlist_detectors_callback,
+)
+
+
 def add_secret_scan_common_options() -> Callable[[AnyFunction], AnyFunction]:
     def decorator(cmd: AnyFunction) -> AnyFunction:
         add_common_options()(cmd)
+        _json_option(cmd)
+        _output_option(cmd)
+        _show_secrets_option(cmd)
         _exit_zero_option(cmd)
         _exclude_option(cmd)
         _ignore_known_secrets_option(cmd)
+        _banlist_detectors_option(cmd)
         return cmd
 
     return decorator
+
+
+def create_output_handler(ctx: click.Context) -> OutputHandler:
+    """Read objects defined in ctx.obj and create the appropriate OutputHandler
+    instance"""
+    use_json = ctx.obj.get("use_json", False)
+    output_handler_cls = JSONOutputHandler if use_json else TextOutputHandler
+    config = ctx.obj["config"].user_config
+    output = ctx.obj.get("output")
+    return output_handler_cls(
+        show_secrets=config.secret.show_secrets, verbose=config.verbose, output=output
+    )
diff --git a/tests/functional/secret/test_scan_docker.py b/tests/functional/secret/test_scan_docker.py
index 3e98d67..2884bb6 100644
--- a/tests/functional/secret/test_scan_docker.py
+++ b/tests/functional/secret/test_scan_docker.py
@@ -46,7 +46,7 @@ def test_scan_docker() -> None:
 
 
 def test_scan_docker_json() -> None:
-    proc = run_ggshield_scan("--json", "docker", TEST_DOCKER_IMAGE)
+    proc = run_ggshield_scan("docker", TEST_DOCKER_IMAGE, "--json")
     assert_is_valid_json(proc.stdout)
 
 
diff --git a/tests/unit/cmd/scan/test_ci.py b/tests/unit/cmd/scan/test_ci.py
index 27bb71f..c5a15bb 100644
--- a/tests/unit/cmd/scan/test_ci.py
+++ b/tests/unit/cmd/scan/test_ci.py
@@ -81,8 +81,8 @@ def test_gitlab_ci_range(
             "-v",
             "secret",
             "scan",
-            *json_arg,
             "ci",
+            *json_arg,
         ],
     )
     assert_invoke_ok(result)
diff --git a/tests/unit/cmd/scan/test_path.py b/tests/unit/cmd/scan/test_path.py
index 72d9fd1..3e88145 100644
--- a/tests/unit/cmd/scan/test_path.py
+++ b/tests/unit/cmd/scan/test_path.py
@@ -131,8 +131,8 @@ class TestPathScan:
                     "secret",
                     "scan",
                     "-v",
-                    *json_arg,
                     "path",
+                    *json_arg,
                     "--exit-zero",
                     "file_secret",
                 ],