Repository: marimo-team/marimo

Issue:
Invalid cli arguments do not cause marimo to return a non-zero exit status
### Describe the bug

I noticed this during work on #3445 -- even though `marimo shell-completion` emitted an error message, the exit code was still zero (`0`).

For many commands, the generation of a non-zero exit code is handled by the `click` decorators on the command function. However, if the command function does additional error checking (as is the case with `shell_completion`), the function must take action to set the exit code.

I'm happy to submit a PR for this -- I'll also search for any other "additional validation" paths that could also generate a misleading exit code.

### Environment

<details>

```
{
  "marimo": "0.10.13",
  "OS": "Windows",
  "OS Version": "11",
  "Processor": "ARMv8 (64-bit) Family 8 Model 805 Revision D0E, Qualcomm Technologies Inc",
  "Python Version": "3.13.0",
  "Binaries": {
    "Browser": "128.0.6613.138",
    "Node": "--"
  },
  "Dependencies": {
    "click": "8.1.3",
    "docutils": "0.21.2",
    "itsdangerous": "2.2.0",
    "jedi": "0.19.2",
    "markdown": "3.7",
    "narwhals": "1.22.0",
    "packaging": "24.2",
    "psutil": "6.1.1",
    "pygments": "2.19.1",
    "pymdown-extensions": "10.14",
    "pyyaml": "6.0.2",
    "ruff": "0.9.1",
    "starlette": "0.45.2",
    "tomlkit": "0.13.2",
    "typing-extensions": "missing",
    "uvicorn": "0.34.0",
    "websockets": "14.1"
  },
  "Optional Dependencies": {}
}

```

</details>


### Code to reproduce

### Undesired behavior:
```bash
❯ SHELL=bogus marimo shell-completion ; echo "exit code: $?"
Unsupported shell: bogus. Supported shells: bash, zsh, fish
exit code: 0
```

### Desired behavior:
```bash
❯ marimo tutorial bogus; echo "exit code: $?"
Error: Invalid value for '{intro|dataflow|ui|markdown|plots|sql|layout|fileformat|markdown-format|for-jupyter-users}': 'bogus' is not one of 'intro', 'dataflow', 'ui', 'markdown', 'plots', 'sql', 'layout', 'fileformat', 'markdown-format', 'for-jupyter-users'.
...
exit code: 2
```

Hints from issue comments:
@mscolnick: thank you @hwine, this should definitely be a great fix for correctness!

Solution patch:
diff --git a/marimo/_cli/cli.py b/marimo/_cli/cli.py
index 3a77241c..f9b88321 100644
--- a/marimo/_cli/cli.py
+++ b/marimo/_cli/cli.py
@@ -789,20 +789,19 @@ def env() -> None:
 
 
 @main.command(
-    help="Install shell completions for marimo. Supports bash, zsh, fish, and elvish."
+    help="Install shell completions for marimo. Supports bash, zsh, and fish."
 )
 def shell_completion() -> None:
     shell = os.environ.get("SHELL", "")
     if not shell:
-        click.echo(
+        raise click.UsageError(
             "Could not determine shell. Please set $SHELL environment variable.",
-            err=True,
         )
-        return
 
     # in case we're on a windows system, use .stem to remove extension
     shell_name = Path(shell).stem
 
+    # N.B. change the help message above when changing supported shells
     commands = {
         "bash": (
             'eval "$(_MARIMO_COMPLETE=bash_source marimo)"',
@@ -820,9 +819,8 @@ def shell_completion() -> None:
 
     if shell_name not in commands:
         supported = ", ".join(commands.keys())
-        click.echo(
-            f"Unsupported shell: {shell_name}. Supported shells: {supported}",
-            err=True,
+        raise click.UsageError(
+            f"Unsupported shell: {shell_name} (from $SHELL). Supported shells: {supported}",
         )
         return
 


Test patch:
diff --git a/tests/_cli/test_cli.py b/tests/_cli/test_cli.py
index 620badb6..67d40ae7 100644
--- a/tests/_cli/test_cli.py
+++ b/tests/_cli/test_cli.py
@@ -818,8 +818,8 @@ def test_cli_run_sandbox_prompt_yes() -> None:
             ),
         ),
         # invalid shell values, rc of 0, data only on stderr
-        # (N.B. rc will become 2 when Issue #3476 is fixed)
-        ("bogus", 0, False, True),
+        ("bogus", 2, False, True),
+        ("", 2, False, True),  # usage error displayed
     ],
 )
 def test_shell_completion(