From c601514e3d9504c00a46a2dd9ab54264f36aa016 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 11 Jan 2026 19:45:54 -0800 Subject: [PATCH] Add tests and docs for '--' argument separator requirement - Add TestArgumentSeparator class with 8 tests verifying that script arguments must come after '--' separator - Update create and edit command docstrings with examples showing correct usage of '--' separator - Update argument help text to indicate "(must come after '--')" - Add "Passing Arguments to Your Script" section to README Co-Authored-By: Claude Opus 4.5 --- README.md | 23 ++++++++++-- mcquack.py | 35 ++++++++++++++++--- test_mcquack.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7f88616..6e90576 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ mcquack list # Create and load an executable as a LaunchAgent mcquack create /path/to/your/script -# Create with additional arguments for the script +# Create with additional arguments for the script (note the -- separator) mcquack create /path/to/your/script -- --arg1 value1 --arg2 # Kickstart (immediately run) the LaunchAgent @@ -37,7 +37,7 @@ mcquack launch /path/to/your/script # Show the current arguments configured in the plist mcquack show /path/to/your/script -# Edit the arguments in the plist +# Edit the arguments in the plist (note the -- separator) mcquack edit /path/to/your/script -- --new-arg1 --new-arg2 # Unload (stop) the LaunchAgent @@ -47,6 +47,25 @@ mcquack unload /path/to/your/script mcquack delete /path/to/your/script ``` +### Passing Arguments to Your Script + +When passing arguments to your script with `create` or `edit`, you **must** use `--` to +separate mcquack's options from arguments intended for your script: + +```bash +# Correct: passes --config and --debug to your script +mcquack create my_script.sh -- --config /path/to/config --debug + +# Wrong: --help is interpreted as mcquack's --help flag, shows help instead +mcquack create my_script.sh --help + +# Correct: passes --help as an argument to your script +mcquack create my_script.sh -- --help +``` + +The `--` separator ensures that flags like `--help`, `--verbose`, etc. are passed to your +script rather than being interpreted by mcquack itself. + ## How it works mcquack creates plist files in `~/Library/LaunchAgents/` with the naming convention: diff --git a/mcquack.py b/mcquack.py index b3fb493..76bf347 100755 --- a/mcquack.py +++ b/mcquack.py @@ -74,9 +74,23 @@ def list_agents() -> None: @app.command() def create( script: Annotated[Path, typer.Argument(help="Path to the executable script")], - script_args: Annotated[list[str] | None, typer.Argument(help="Arguments to pass to the script")] = None, + script_args: Annotated[ + list[str] | None, + typer.Argument(help="Arguments to pass to the script (must come after '--')"), + ] = None, ) -> None: - """Create and load a LaunchAgent for a script.""" + """Create and load a LaunchAgent for a script. + + To pass arguments to your script, use '--' to separate mcquack options from + script arguments. For example: + + mcquack create my_script.sh -- --config /path/to/config + + Without '--', options like --help apply to mcquack itself: + + mcquack create my_script.sh --help # Shows this help + mcquack create my_script.sh -- --help # Passes --help to the script + """ script_path = script.resolve() if not script_path.exists(): @@ -170,9 +184,22 @@ def show( @app.command() def edit( script: Annotated[Path, typer.Argument(help="Path to the executable script")], - script_args: Annotated[list[str] | None, typer.Argument(help="New arguments for the script")] = None, + script_args: Annotated[ + list[str] | None, + typer.Argument(help="New arguments for the script (must come after '--')"), + ] = None, ) -> None: - """Edit the arguments in a plist.""" + """Edit the arguments in a plist. + + To update script arguments, use '--' to separate mcquack options from + script arguments. For example: + + mcquack edit my_script.sh -- --verbose --debug + + This replaces all existing script arguments. To clear arguments, omit them: + + mcquack edit my_script.sh + """ script_path = script.resolve() plist_path = get_plist_path(script_path) diff --git a/test_mcquack.py b/test_mcquack.py index 8237bc4..011a954 100644 --- a/test_mcquack.py +++ b/test_mcquack.py @@ -214,6 +214,99 @@ class TestDelete: assert "does not exist" in result.output +class TestArgumentSeparator: + """Tests verifying that '--' separator is required for passing arguments to scripts. + + This ensures that: + - 'mcquack create script.sh --help' shows help for the create command + - 'mcquack create script.sh -- --help' creates a plist with --help as a script arg + """ + + def test_create_help_without_separator(self, mock_dirs, mock_script, mock_launchctl): + """Verify that --help without -- shows create command help, not passed to script.""" + result = runner.invoke(app, ["create", str(mock_script), "--help"]) + assert result.exit_code == 0 + # Should show help text for create command + assert "Create and load a LaunchAgent" in result.stdout or "Usage:" in result.stdout + # Should NOT have created a plist + plist_path = get_plist_path(mock_dirs) + assert not plist_path.exists() + + def test_create_help_with_separator(self, mock_dirs, mock_script, mock_launchctl): + """Verify that -- --help passes --help as an argument to the script.""" + result = runner.invoke(app, ["create", str(mock_script), "--", "--help"]) + assert result.exit_code == 0 + assert "Created:" in result.stdout + + # Verify --help is in the plist as a script argument + plist_path = get_plist_path(mock_dirs) + assert plist_path.exists() + program_args = read_plist_args(plist_path) + assert "--help" in program_args + assert program_args[1] == "--help" + + def test_create_verbose_without_separator(self, mock_dirs, mock_script, mock_launchctl): + """Verify that --verbose without -- is treated as unknown option, not script arg.""" + result = runner.invoke(app, ["create", str(mock_script), "--verbose"]) + # Typer should reject unknown options + assert result.exit_code != 0 + assert "No such option" in result.output or "no such option" in result.output.lower() + # Should NOT have created a plist + plist_path = get_plist_path(mock_dirs) + assert not plist_path.exists() + + def test_create_verbose_with_separator(self, mock_dirs, mock_script, mock_launchctl): + """Verify that -- --verbose passes --verbose as an argument to the script.""" + result = runner.invoke(app, ["create", str(mock_script), "--", "--verbose"]) + assert result.exit_code == 0 + + plist_path = get_plist_path(mock_dirs) + program_args = read_plist_args(plist_path) + assert "--verbose" in program_args + + def test_edit_help_without_separator(self, mock_dirs, mock_script, mock_launchctl): + """Verify that edit --help shows edit command help.""" + # First create an agent + runner.invoke(app, ["create", str(mock_script)]) + + result = runner.invoke(app, ["edit", str(mock_script), "--help"]) + assert result.exit_code == 0 + assert "Edit the arguments" in result.stdout or "Usage:" in result.stdout + + def test_edit_help_with_separator(self, mock_dirs, mock_script, mock_launchctl): + """Verify that edit -- --help passes --help as a new argument.""" + # First create an agent + runner.invoke(app, ["create", str(mock_script)]) + + result = runner.invoke(app, ["edit", str(mock_script), "--", "--help"]) + assert result.exit_code == 0 + + plist_path = get_plist_path(mock_dirs) + program_args = read_plist_args(plist_path) + assert "--help" in program_args + + def test_create_mixed_args_with_separator(self, mock_dirs, mock_script, mock_launchctl): + """Verify multiple dash-prefixed args after -- are all passed to script.""" + result = runner.invoke(app, ["create", str(mock_script), "--", "--config", "/path", "--debug", "-v"]) + assert result.exit_code == 0 + + plist_path = get_plist_path(mock_dirs) + program_args = read_plist_args(plist_path) + # All args after -- should be in the plist + assert program_args[1:] == ["--config", "/path", "--debug", "-v"] + + def test_create_double_dash_as_argument(self, mock_dirs, mock_script, mock_launchctl): + """Verify that -- can be passed as a script argument using -- --.""" + result = runner.invoke(app, ["create", str(mock_script), "--", "--", "arg_after_double_dash"]) + assert result.exit_code == 0 + + plist_path = get_plist_path(mock_dirs) + program_args = read_plist_args(plist_path) + # The first -- is consumed by typer, the second becomes an argument + assert "--" in program_args + assert "arg_after_double_dash" in program_args + + class TestPlistFormat: """Tests verifying plist file format independently of plistlib."""