Skip to content

Commit

Permalink
Add first working version
Browse files Browse the repository at this point in the history
  • Loading branch information
kkyr committed Nov 23, 2023
1 parent 847f6d3 commit e6e6d0a
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 0 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# shellcheck-gpt

Automatically correct shell script issues by feeding [shellcheck](https://www.shellcheck.net) analysis into an LLM.

## Getting started

### Prerequisites

- [ShellCheck](https://www.shellcheck.net) should be installed and in your $PATH.
- [Go](https://go.dev/doc/install)
- A valid OpenAI API key

### Basic installation

shellcheck-gpt can be installed using the `go install` tool. Distribution methods that do not depend on Go are coming soon!

```shell
$ go install github.com/kkyr/shellcheck-gpt
```

## Usage

Store your OpenAI API key into the environment:

```shell
export OPENAI_API_KEY=replace-with-your-key
```

Run shellcheck-gpt against a script:

```shell
$ shellcheck-gpt script.sh
```

This will:

1. Run shellcheck against `script.sh`
1. Feed the script and the output of shellcheck into an LLM and ask it to make the corrections
1. Write the LLM's output onto stdout

If you'd like to write the output back into the file, use the `-w` flag:

```shell
$ shellcheck-gpt -w script.sh
```

## Configuration

By default, shellcheck-gpt uses the gpt-3.5-turbo model. You can modify this to another model using the `-m` flag:

```shell
$ shellcheck-gpt -m gpt-4-turbo script.sh
```

See available options and models using the `--help` flag:

```shell
$ shellcheck-gpt --help
```

## Contributing

Contributions are welcome!
7 changes: 7 additions & 0 deletions example/script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh
## Example: a typical script with several problems
for f in *.m3u
do
grep -qi 'hq.*mp3' "$f" \
&& printf 'Playlist %s contains a HQ file in mp3 format\n' "$f"
done
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module github.com/kkyr/shellcheck-gpt

go 1.21

require (
github.com/briandowns/spinner v1.23.0
github.com/fatih/color v1.16.0
github.com/sashabaranov/go-openai v1.17.8
)

require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/term v0.1.0 // indirect
)
17 changes: 17 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/sashabaranov/go-openai v1.17.8 h1:snuE7l0XQ1KAmkY/cODAEgxu2fl+g/ybXK6cKQzli/E=
github.com/sashabaranov/go-openai v1.17.8/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
220 changes: 220 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package main

import (
"bytes"
"context"
"flag"
"fmt"
"log"
"os"
"os/exec"
"runtime"
"text/template"
"time"

"github.com/briandowns/spinner"
"github.com/fatih/color"
openai "github.com/sashabaranov/go-openai"
)

const (
systemPrompt = `
You are a highly skilled systems engineer with expertise in crafting flawless shell scripts.
Your primary responsibility involves two key tasks: Firstly, receiving a shell script that has
been evaluated by a static analysis tool; and secondly, analyzing the provided list of warnings
and issues identified by this tool. Utilizing these inputs, your objective is to develop an
enhanced version of the shell script that effectively addresses and resolves all identified
warnings and issues.
Your task is to revise a shell script strictly based on the warnings and errors highlighted by
the static analysis tool. You are required to make changes only to address these specific issues.
Any modifications that do not directly or indirectly relate to correcting these warnings and errors
should be avoided. Additionally, you must preserve the original comments in the script, altering
them only if they become irrelevant due to the changes you implement.
Your response should exclusively consist of the updated shell script text, presented without using
a code block format.
`

userPrompt = `
SHELL_SCRIPT:
{{.ScriptContents}}
STATIC_ANALYSIS_OUTPUT:
{{.StaticAnalysisOutput}}
Your task is to revise the provided shell script, focusing solely on rectifying the warnings and errors
identified in the static analysis output.
Ensure that the output of your task is solely the modified shell script text, presented without the use
of a code block format.
`

gpt35turbo = "gpt-3.5-turbo"
gpt4turbo = "gpt-4-turbo"
)

var (
userPromptTmpl = template.Must(template.New("prompt").Parse(userPrompt))
client = openai.NewClient(os.Getenv("OPENAI_API_KEY"))
)

var (
// Command line flags
writeFile bool
showVersion bool
model string

version = "dev"
)

func init() {
flag.BoolVar(&writeFile, "w", false, "write shell script to input file")
flag.BoolVar(&showVersion, "v", false, "print version number and exit")
flag.StringVar(&model, "m", gpt35turbo, fmt.Sprintf("specify the model to use (%s or %s)", gpt35turbo, gpt4turbo))

flag.Usage = usage
}

func usage() {
fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] FILE\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Execute shellcheck on the given script and pass the results to a large language model " +
"for making appropriate corrections.\n\n")
fmt.Fprintf(os.Stderr, "The default behavior displays the modified script on the console. Use the '-w' flag " +
"to save the changes directly to the specified file.\n\n")
fmt.Fprintf(os.Stderr, "The shellcheck binary must be present in your path.\n\n")
fmt.Fprintln(os.Stderr, "OPTIONS:")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "ENVIRONMENT:")
fmt.Fprintln(os.Stderr, " OPENAI_API_KEY OpenAI API key")
}

func printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(color.Output, format, a...)
}

func main() {
flag.Parse()

if showVersion {
fmt.Printf("%s %s (runtime: %s)\n", os.Args[0], version, runtime.Version())
os.Exit(0)
}

if model != gpt35turbo && model != gpt4turbo {
fmt.Fprintf(os.Stderr, "%s: model must be %s or %s\n", os.Args[0], gpt35turbo, gpt4turbo)
os.Exit(1)
}

args := flag.Args()
if len(args) != 1 {
flag.Usage()
os.Exit(1)
}
filePath := args[0]

run(filePath)
}

func run(filePath string) {
printf("%s\n", "Running shellcheck against script")
analysis, err := runShellcheck(filePath)
if err != nil {
log.Fatal(err)
}

if analysis == "" {
printf("%s\n", color.GreenString("shellcheck detected no issues."))
return
}

printf("%s\n", color.YellowString("shellcheck detected potential issues"))

script, err := os.ReadFile(filePath)
if err != nil {
log.Fatalf("unable to read file: %v", err)
}

result, err := callCompletionAPI(string(script), string(analysis))
if err != nil {
log.Fatalf("error calling completion API: %v", err)
}

if writeFile {
os.WriteFile(filePath, []byte(result), 0644)
printf("%s %s\n", color.GreenString("Updated script written to"), color.GreenString(filePath))
printf("%s\n", color.YellowString("Double check it before you commit!"))
} else {
printf("%s\n", result)
}
}

func runShellcheck(filePath string) (string, error) {
cmd := exec.Command("shellcheck", filePath)

output, err := cmd.CombinedOutput()
if err != nil {
exitCode := cmd.ProcessState.ExitCode()
// shellcheck returns exit code 1 when it finds issues
if exitCode == 1 {
return string(output), nil
} else {
return "", fmt.Errorf("shellcheck exited with code %d: %s", exitCode, err)
}
}

return "", nil
}

func callCompletionAPI(script, analysis string) (string, error) {
spin := spinner.New(spinner.CharSets[26], 250*time.Millisecond)
spin.Prefix = "Waiting for completion response"
spin.Start()
defer spin.Stop()

data := map[string]string{
"ScriptContents": string(script),
"StaticAnalysisOutput": string(analysis),
}

var buffer bytes.Buffer
err := userPromptTmpl.Execute(&buffer, data)
if err != nil {
log.Fatalf("unable to format prompt: %v", err)
}

resp, err := client.CreateChatCompletion(
context.Background(),
getCompletionRequest(buffer.String()),
)
if err != nil {
return "", err
}

if len(resp.Choices) > 0 {
return resp.Choices[0].Message.Content, nil
}

return "", fmt.Errorf("empty response")
}

func getCompletionRequest(prompt string) openai.ChatCompletionRequest {
m := openai.GPT3Dot5Turbo
if model == gpt4turbo {
m = openai.GPT4TurboPreview
}

return openai.ChatCompletionRequest{
Model: m,
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: systemPrompt,
},
{
Role: openai.ChatMessageRoleUser,
Content: prompt,
},
},
}
}

0 comments on commit e6e6d0a

Please sign in to comment.