-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
} | ||
} |