From d7c0e43dc497c2549cc6795f36f8db1e71a661f2 Mon Sep 17 00:00:00 2001 From: Myles Scolnick Date: Thu, 19 Dec 2024 13:57:47 -0500 Subject: [PATCH] cli: better support for bash completion (#3230) better support for bash completion, with instructions how to install. image --- marimo/_cli/cli.py | 67 ++++++++++++++++++++++++----- marimo/_cli/convert/commands.py | 2 +- marimo/_cli/development/commands.py | 10 +++-- marimo/_cli/export/commands.py | 41 +++++++++++++----- tests/_cli/test_cli.py | 9 ++++ 5 files changed, 102 insertions(+), 27 deletions(-) diff --git a/marimo/_cli/cli.py b/marimo/_cli/cli.py index 5ae56b9130c..14737de0fe2 100644 --- a/marimo/_cli/cli.py +++ b/marimo/_cli/cli.py @@ -3,9 +3,9 @@ import json import os -import pathlib import sys import tempfile +from pathlib import Path from typing import Any, Optional import click @@ -286,7 +286,7 @@ def main( help=sandbox_message, ) @click.option("--profile-dir", default=None, type=str, hidden=True) -@click.argument("name", required=False) +@click.argument("name", required=False, type=click.Path()) @click.argument("args", nargs=-1, type=click.UNPROCESSED) def edit( port: Optional[int], @@ -575,7 +575,7 @@ def new( type=bool, help=sandbox_message, ) -@click.argument("name", required=True) +@click.argument("name", required=True, type=click.Path()) @click.argument("args", nargs=-1, type=click.UNPROCESSED) def run( port: Optional[int], @@ -643,15 +643,12 @@ def run( @main.command(help="Recover a marimo notebook from JSON.") -@click.argument("name", required=True) +@click.argument( + "name", + required=True, + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) def recover(name: str) -> None: - path = pathlib.Path(name) - if not os.path.exists(name): - raise click.UsageError("Invalid NAME - %s does not exist" % name) - - if not path.is_file(): - raise click.UsageError("Invalid NAME - %s is not a file" % name) - click.echo(codegen.recover(name)) @@ -754,6 +751,54 @@ def env() -> None: click.echo(json.dumps(get_system_info(), indent=2)) +@main.command( + help="Install shell completions for marimo. Supports bash, zsh, fish, and elvish." +) +def shell_completion() -> None: + shell = os.environ.get("SHELL", "") + if not shell: + click.echo( + "Could not determine shell. Please set $SHELL environment variable.", + err=True, + ) + return + + shell_name = Path(shell).name + + commands = { + "bash": ( + 'eval "$(_MARIMO_COMPLETE=bash_source marimo)"', + ".bashrc", + ), + "zsh": ( + 'eval "$(_MARIMO_COMPLETE=zsh_source marimo)"', + ".zshrc", + ), + "fish": ( + "_MARIMO_COMPLETE=fish_source marimo | source", + ".config/fish/completions/marimo.fish", + ), + } + + if shell_name not in commands: + supported = ", ".join(commands.keys()) + click.echo( + f"Unsupported shell: {shell_name}. Supported shells: {supported}", + err=True, + ) + return + + cmd, rc_file = commands[shell_name] + click.secho("Run this command to enable completions:", fg="green") + click.secho(f"\n echo '{cmd}' >> ~/{rc_file}\n", fg="yellow") + click.secho( + "\nThen restart your shell or run 'source ~/" + + rc_file + + "' to enable completions", + fg="green", + ) + + main.command()(convert) main.add_command(export) main.add_command(config) diff --git a/marimo/_cli/convert/commands.py b/marimo/_cli/convert/commands.py index 19c8bf7265b..d3bd11a0972 100644 --- a/marimo/_cli/convert/commands.py +++ b/marimo/_cli/convert/commands.py @@ -16,7 +16,7 @@ @click.option( "-o", "--output", - type=str, + type=click.Path(), default=None, help=( "Output file to save the converted notebook to. " diff --git a/marimo/_cli/development/commands.py b/marimo/_cli/development/commands.py index 10fbef596d4..839e1db0029 100644 --- a/marimo/_cli/development/commands.py +++ b/marimo/_cli/development/commands.py @@ -340,10 +340,12 @@ def killall() -> None: @click.command( help="Inline packages according to PEP 723", name="inline-packages" ) -@click.argument("name", required=True) -def inline_packages( - name: str, -) -> None: +@click.argument( + "name", + required=True, + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) +def inline_packages(name: str) -> None: """ Example usage: diff --git a/marimo/_cli/export/commands.py b/marimo/_cli/export/commands.py index 319653b24cc..4a0acc49971 100644 --- a/marimo/_cli/export/commands.py +++ b/marimo/_cli/export/commands.py @@ -7,7 +7,6 @@ import click -import marimo._cli.cli_validators as validators from marimo._cli.parse_args import parse_args from marimo._cli.print import echo, green from marimo._dependencies.dependencies import DependencyManager @@ -126,14 +125,18 @@ async def start() -> None: @click.option( "-o", "--output", - type=str, + type=click.Path(), default=None, help=( "Output file to save the HTML to. " "If not provided, the HTML will be printed to stdout." ), ) -@click.argument("name", required=True, callback=validators.is_file_path) +@click.argument( + "name", + required=True, + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) @click.argument("args", nargs=-1, type=click.UNPROCESSED) def html( name: str, @@ -183,14 +186,18 @@ def export_callback(file_path: MarimoPath) -> ExportResult: @click.option( "-o", "--output", - type=str, + type=click.Path(), default=None, help=( "Output file to save the script to. " "If not provided, the script will be printed to stdout." ), ) -@click.argument("name", required=True, callback=validators.is_file_path) +@click.argument( + "name", + required=True, + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) def script( name: str, output: str, @@ -229,14 +236,18 @@ def export_callback(file_path: MarimoPath) -> ExportResult: @click.option( "-o", "--output", - type=str, + type=click.Path(), default=None, help=( "Output file to save the markdown to. " "If not provided, markdown will be printed to stdout." ), ) -@click.argument("name", required=True, callback=validators.is_file_path) +@click.argument( + "name", + required=True, + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) def md( name: str, output: str, @@ -284,7 +295,7 @@ def export_callback(file_path: MarimoPath) -> ExportResult: @click.option( "-o", "--output", - type=str, + type=click.Path(), default=None, help=( "Output file to save the ipynb file to. " @@ -298,7 +309,11 @@ def export_callback(file_path: MarimoPath) -> ExportResult: type=bool, help="Run the notebook and include outputs in the exported ipynb file.", ) -@click.argument("name", required=True, callback=validators.is_file_path) +@click.argument( + "name", + required=True, + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) def ipynb( name: str, output: str, @@ -348,7 +363,7 @@ def export_callback(file_path: MarimoPath) -> ExportResult: @click.option( "-o", "--output", - type=str, + type=click.Path(), required=True, help="Output directory to save the HTML to.", ) @@ -366,7 +381,11 @@ def export_callback(file_path: MarimoPath) -> ExportResult: show_default=True, help="Whether to show code by default in the exported HTML file.", ) -@click.argument("name", required=True, callback=validators.is_file_path) +@click.argument( + "name", + required=True, + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) def html_wasm( name: str, output: str, diff --git a/tests/_cli/test_cli.py b/tests/_cli/test_cli.py index f22db96f736..d336b54295c 100644 --- a/tests/_cli/test_cli.py +++ b/tests/_cli/test_cli.py @@ -798,6 +798,15 @@ def test_cli_run_sandbox_prompt_yes() -> None: p.kill() +def test_shell_completion() -> None: + p = subprocess.run( + ["marimo", "shell-completion"], + capture_output=True, + ) + assert p.returncode == 0 + assert p.stdout is not None + + HAS_DOCKER = DependencyManager.which("docker")