Skip to content

Commit

Permalink
Non-interactive mode for --shell and integration changes (#455)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
TheR1D authored Jan 28, 2024
1 parent 361d1ea commit c48926a
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 38 deletions.
12 changes: 2 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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

Expand Down
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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] │
Expand Down
2 changes: 1 addition & 1 deletion sgpt/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.1.0"
__version__ = "1.2.0"
34 changes: 21 additions & 13 deletions sgpt/app.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,7 +25,7 @@

def main(
prompt: str = typer.Argument(
None,
"",
show_default=False,
help="The prompt to generate completions for.",
),
Expand All @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 1 addition & 2 deletions sgpt/handlers/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions sgpt/integration.py
Original file line number Diff line number Diff line change
@@ -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
"""
23 changes: 16 additions & 7 deletions sgpt/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions tests/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit c48926a

Please sign in to comment.