From c48926a4a4a5451ebeea90e1da0421d871edddcb Mon Sep 17 00:00:00 2001 From: Farkhod Sadykov Date: Sun, 28 Jan 2024 05:03:44 +0100 Subject: [PATCH] Non-interactive mode for --shell and integration changes (#455) * Added --interaction that works with --shell option. * Changed shell integrations to use new --no-interaction option. * Moved shell integrations into dedicated file integration.py. * Changed --install-integration logic to install integrations without downloading sh scripts. * Removed validation for PROMPT argument, empty string by default. * Fixing an issue when sgpt is being called from non-interactive shell environments. * Fixed and optimised Dockerfile. * README.md improvements, and new feature examples. * New funny demo video. --- Dockerfile | 12 ++--------- README.md | 44 +++++++++++++++++++++++++++++++++++----- sgpt/__version__.py | 2 +- sgpt/app.py | 34 +++++++++++++++++++------------ sgpt/handlers/handler.py | 3 +-- sgpt/integration.py | 27 ++++++++++++++++++++++++ sgpt/utils.py | 23 ++++++++++++++------- tests/test_shell.py | 18 ++++++++++++++++ 8 files changed, 125 insertions(+), 38 deletions(-) create mode 100644 sgpt/integration.py diff --git a/Dockerfile b/Dockerfile index b7c6c959..9f87b7b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,10 @@ FROM python:3-slim -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 -ENV PIP_ROOT_USER_ACTION ignore WORKDIR /app COPY . /app -RUN pip install --no-cache --upgrade pip \ - && pip install --no-cache /app \ - && addgroup --system app && adduser --system --group --home /home/app app \ - && mkdir -p /tmp/shell_gpt \ - && chown -R app:app /tmp/shell_gpt - -USER app +RUN apt-get update && apt-get install -y gcc +RUN pip install --no-cache /app && mkdir -p /tmp/shell_gpt VOLUME /tmp/shell_gpt diff --git a/README.md b/README.md index b44b5a55..2217e38e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ShellGPT A command-line productivity tool powered by AI large language models (LLM). This command-line tool offers streamlined generation of **shell commands, code snippets, documentation**, eliminating the need for external resources (like Google search). Supports Linux, macOS, Windows and compatible with all major Shells like PowerShell, CMD, Bash, Zsh, etc. -https://github.com/TheR1D/shell_gpt/assets/16740832/721ddb19-97e7-428f-a0ee-107d027ddd59 +https://github.com/TheR1D/shell_gpt/assets/16740832/9197283c-db6a-4b46-bfea-3eb776dd9093 ## Installation ```shell @@ -35,6 +35,19 @@ Error Detected: Memory allocation failed at line 12. Possible Solution: Consider increasing memory allocation or optimizing application memory usage. ``` +You can also use all kind of redirection operators to pass input: +```shell +sgpt "summarise" < document.txt +# -> The document discusses the impact... +sgpt << EOF +What is the best way to lear Golang. +Provide simple hello world example. +EOF +# -> The best way to learn Golang... +sgpt <<< "What is the best way to learn shell redirects?" +# -> The best way to learn shell redirects is through... +``` + ### Shell commands Have you ever found yourself forgetting common shell commands, such as `find`, and needing to look up the syntax online? With `--shell` or shortcut `-s` option, you can quickly generate and execute the commands you need right in the terminal. @@ -65,14 +78,14 @@ sgpt -s "start nginx container, mount ./index.html" # -> [E]xecute, [D]escribe, [A]bort: e ``` -We can still use pipes to pass input to `sgpt` and get generate shell commands: +We can still use pipes to pass input to `sgpt` and generate shell commands: ```shell -cat data.json | sgpt -s "POST localhost with json" +sgpt -s "POST localhost with" < data.json # -> curl -X POST -H "Content-Type: application/json" -d '{"a": 1, "b": 2}' http://localhost # -> [E]xecute, [D]escribe, [A]bort: e ``` -Applying additional shell magic in our prompt, in this example passing file names to ffmpeg: +Applying additional shell magic in our prompt, in this example passing file names to `ffmpeg`: ```shell ls # -> 1.mp4 2.mp4 3.mp4 @@ -81,9 +94,14 @@ sgpt -s "ffmpeg combine $(ls -m) into one video file without audio." # -> [E]xecute, [D]escribe, [A]bort: e ``` +If you would like to pass generated shell command using pipe, you can use `--no-interaction` option. This will disable interactive mode and will print generated command to stdout. In this example we are using `pbcopy` to copy generated command to clipboard: +```shell +sgpt -s "find all json files in current folder" --no-interaction | pbcopy +``` + ### Shell integration -Shell integration enables the use of ShellGPT with hotkeys in your terminal, supported by both Bash and ZSH shells. This feature puts `sgpt` completions directly into terminal buffer (input line), allowing for immediate editing of suggested commands. +This is a **very handy feature**, which allows you to use `sgpt` shell completions directly in your terminal, without the need to type `sgpt` with prompt and arguments. Shell integration enables the use of ShellGPT with hotkeys in your terminal, supported by both Bash and ZSH shells. This feature puts `sgpt` completions directly into terminal buffer (input line), allowing for immediate editing of suggested commands. https://github.com/TheR1D/shell_gpt/assets/16740832/bead0dab-0dd9-436d-88b7-6abfb2c556c1 @@ -248,6 +266,21 @@ Entering REPL mode, press Ctrl+C to exit. It is a Python script that uses the random module to generate and print a random integer. ``` +You can also enter REPL mode with initial prompt by passing it as an argument or stdin or even both: +```shell +sgpt --repl temp < my_app.py +``` +```text +Entering REPL mode, press Ctrl+C to exit. +──────────────────────────────────── Input ──────────────────────────────────── +name = input("What is your name?") +print(f"Hello {name}") +─────────────────────────────────────────────────────────────────────────────── +>>> What is this code about? +The snippet of code you've provided is written in Python. It prompts the user... +>>> Follow up questions... +``` + ### Function calling [Function calls](https://platform.openai.com/docs/guides/function-calling) is a powerful feature OpenAI provides. It allows LLM to execute functions in your system, which can be used to accomplish a variety of tasks. To install [default functions](https://github.com/TheR1D/shell_gpt/tree/main/sgpt/default_functions/) run: ```shell @@ -392,6 +425,7 @@ Possible options for `CODE_THEME`: https://pygments.org/styles/ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Assistance Options ─────────────────────────────────────────────────────────────────────────────────────╮ │ --shell -s Generate and execute shell commands. │ +│ --interaction --no-interaction Interactive mode for --shell option. [default: interaction] │ │ --describe-shell -d Describe a shell command. │ │ --code -c Generate only code. │ │ --functions --no-functions Allow function calls. [default: functions] │ diff --git a/sgpt/__version__.py b/sgpt/__version__.py index 6849410a..c68196d1 100644 --- a/sgpt/__version__.py +++ b/sgpt/__version__.py @@ -1 +1 @@ -__version__ = "1.1.0" +__version__ = "1.2.0" diff --git a/sgpt/app.py b/sgpt/app.py index a9a2eaf1..da565843 100644 --- a/sgpt/app.py +++ b/sgpt/app.py @@ -1,10 +1,11 @@ -# To allow users to use arrow keys in the REPL. import os + +# To allow users to use arrow keys in the REPL. import readline # noqa: F401 import sys import typer -from click import BadArgumentUsage, MissingParameter +from click import BadArgumentUsage from click.types import Choice from sgpt.config import cfg @@ -24,7 +25,7 @@ def main( prompt: str = typer.Argument( - None, + "", show_default=False, help="The prompt to generate completions for.", ), @@ -51,6 +52,11 @@ def main( help="Generate and execute shell commands.", rich_help_panel="Assistance Options", ), + interaction: bool = typer.Option( + True, + help="Interactive mode for --shell option.", + rich_help_panel="Assistance Options", + ), describe_shell: bool = typer.Option( False, "--describe-shell", @@ -156,20 +162,22 @@ def main( # but rest of the stdin to be used as a inputs. For example: # echo "hello\n__sgpt__eof__\nThis is input" | sgpt --repl temp # In this case, "hello" will be used as a init prompt, and - # "This is input" will be used as a input to the REPL. + # "This is input" will be used as "interactive" input to the REPL. + # This is useful to test REPL with some initial context. for line in sys.stdin: if "__sgpt__eof__" in line: break stdin += line prompt = f"{stdin}\n\n{prompt}" if prompt else stdin - # Switch to stdin for interactive input. - if os.name == "posix": - sys.stdin = open("/dev/tty", "r") - elif os.name == "nt": - sys.stdin = open("CON", "r") - - if not prompt and not editor and not repl: - raise MissingParameter(param_hint="PROMPT", param_type="string") + try: + # Switch to stdin for interactive input. + if os.name == "posix": + sys.stdin = open("/dev/tty", "r") + elif os.name == "nt": + sys.stdin = open("CON", "r") + except OSError: + # Non-interactive shell. + pass if sum((shell, describe_shell, code)) > 1: raise BadArgumentUsage( @@ -225,7 +233,7 @@ def main( functions=function_schemas, ) - while shell: + while shell and interaction: option = typer.prompt( text="[E]xecute, [D]escribe, [A]bort", type=Choice(("e", "d", "a", "y"), case_sensitive=False), diff --git a/sgpt/handlers/handler.py b/sgpt/handlers/handler.py index 75773be6..39afc811 100644 --- a/sgpt/handlers/handler.py +++ b/sgpt/handlers/handler.py @@ -35,7 +35,6 @@ def _handle_with_markdown(self, prompt: str, **kwargs: Any) -> str: with Live( Markdown(markup="", code_theme=self.theme_name), console=Console(), - refresh_per_second=8, ) as live: if self.disable_stream: live.update( @@ -45,7 +44,7 @@ def _handle_with_markdown(self, prompt: str, **kwargs: Any) -> str: for word in self.get_completion(messages=messages, **kwargs): full_completion += word live.update( - Markdown(full_completion, code_theme=self.theme_name), + Markdown(markup=full_completion, code_theme=self.theme_name), refresh=not self.disable_stream, ) return full_completion diff --git a/sgpt/integration.py b/sgpt/integration.py new file mode 100644 index 00000000..fd19fc6d --- /dev/null +++ b/sgpt/integration.py @@ -0,0 +1,27 @@ +bash_integration = """ +# Shell-GPT integration BASH v0.2 +_sgpt_bash() { +if [[ -n "$READLINE_LINE" ]]; then + READLINE_LINE=$(sgpt --shell <<< "$READLINE_LINE" --no-interaction) + READLINE_POINT=${#READLINE_LINE} +fi +} +bind -x '"\\C-l": _sgpt_bash' +# Shell-GPT integration BASH v0.2 +""" + +zsh_integration = """ +# Shell-GPT integration ZSH v0.2 +_sgpt_zsh() { +if [[ -n "$BUFFER" ]]; then + _sgpt_prev_cmd=$BUFFER + BUFFER+="⌛" + zle -I && zle redisplay + BUFFER=$(sgpt --shell <<< "$_sgpt_prev_cmd" --no-interaction) + zle end-of-line +fi +} +zle -N _sgpt_zsh +bindkey ^l _sgpt_zsh +# Shell-GPT integration ZSH v0.2 +""" diff --git a/sgpt/utils.py b/sgpt/utils.py index 6dbb0b2d..f33430e7 100644 --- a/sgpt/utils.py +++ b/sgpt/utils.py @@ -5,9 +5,10 @@ from typing import Any, Callable import typer -from click import BadParameter +from click import BadParameter, UsageError from sgpt.__version__ import __version__ +from sgpt.integration import bash_integration, zsh_integration def get_edited_prompt() -> str: @@ -65,17 +66,25 @@ def wrapper(cls: Any, value: str) -> None: @option_callback def install_shell_integration(*_args: Any) -> None: """ - Installs shell integration. Currently only supports Linux. + Installs shell integration. Currently only supports ZSH and Bash. Allows user to get shell completions in terminal by using hotkey. - Allows user to edit shell command right away in terminal. + Replaces current "buffer" of the shell with the completion. """ # TODO: Add support for Windows. # TODO: Implement updates. - if platform.system() == "Windows": - typer.echo("Windows is not supported yet.") + shell = os.getenv("SHELL", "") + if shell == "/bin/zsh": + typer.echo("Installing ZSH integration...") + with open(os.path.expanduser("~/.zshrc"), "a", encoding="utf-8") as file: + file.write(zsh_integration) + elif shell == "/bin/bash": + typer.echo("Installing Bash integration...") + with open(os.path.expanduser("~/.bashrc"), "a", encoding="utf-8") as file: + file.write(bash_integration) else: - url = "https://raw.githubusercontent.com/TheR1D/shell_gpt/shell-integrations/install.sh" - os.system(f'sh -c "$(curl -fsSL {url})"') + raise UsageError("ShellGPT integrations only available for ZSH and Bash.") + + typer.echo("Done! Restart your shell to apply changes.") @option_callback diff --git a/tests/test_shell.py b/tests/test_shell.py index 43afde80..346d4b42 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -159,3 +159,21 @@ def test_shell_and_describe_shell(completion): completion.assert_not_called() assert result.exit_code == 2 assert "Error" in result.stdout + + +@patch("openai.resources.chat.Completions.create") +def test_shell_no_interaction(completion): + completion.return_value = comp_chunks("git commit -m test") + role = SystemRole.get(DefaultRoles.SHELL.value) + + args = { + "prompt": "make a commit using git", + "--shell": True, + "--no-interaction": True, + } + result = runner.invoke(app, cmd_args(**args)) + + completion.assert_called_once_with(**comp_args(role, args["prompt"])) + assert result.exit_code == 0 + assert "git commit" in result.stdout + assert "[E]xecute" not in result.stdout