initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ask
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# ask
|
||||||
|
|
||||||
|
A CLI tool that answers command-line questions using an OpenAI-compatible LLM API.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Go** 1.26+ (to build)
|
||||||
|
- Runtime tools: `tldr`, `man` / `col -b`, `glow`, `less`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repo> && cd ask
|
||||||
|
go build -o ask .
|
||||||
|
```
|
||||||
|
|
||||||
|
Or place the binary somewhere on your `PATH`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
ask [options] <question>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Mode | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| | default | Generates a runnable shell command (uses TLDR + man page context) |
|
||||||
|
| `-q` | question | Answers general command-line questions (rendered with glow) |
|
||||||
|
| `-H` | history | Answers questions about your shell history (last 200 lines) |
|
||||||
|
| `-p` | paginate | Pipes output through `less -R` |
|
||||||
|
| `-h` | help | Prints help text |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ask how to find large files
|
||||||
|
ask -q how does rsync work
|
||||||
|
ask -H what command did I use for docker yesterday
|
||||||
|
ask -p how to compress a directory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `OPENAI_BASE_URL` | `http://10.0.2.145:8090/v1` | API endpoint |
|
||||||
|
| `OPENAI_API_KEY` | — | API key (optional) |
|
||||||
|
| `OPENAI_MODEL` | `gpt-4o-mini` | Model name |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test_ask.sh
|
||||||
|
```
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
.TH ASK 1 "July 2026" "ask" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
ask \- answer command-line questions using an LLM
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B ask
|
||||||
|
[OPTION...] \fIquestion\fR
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B ask
|
||||||
|
consults an OpenAI-compatible LLM backend to answer questions about
|
||||||
|
Unix/Linux command-line tools. It has three operating modes.
|
||||||
|
.PP
|
||||||
|
In the default (command) mode,
|
||||||
|
.B ask
|
||||||
|
identifies the relevant tool, fetches its TLDR page and man page for
|
||||||
|
context, and returns a single runnable command line with placeholders
|
||||||
|
for arguments you need to customize.
|
||||||
|
.PP
|
||||||
|
In question mode (\fB\-q\fR), it answers general command-line questions
|
||||||
|
and renders the response in markdown through
|
||||||
|
.BR glow (1).
|
||||||
|
.PP
|
||||||
|
In history mode (\fB\-H\fR), it reads the last 200 lines of your shell
|
||||||
|
history (\fI~/.zsh_history\fR, \fI~/.bash_history\fR, or
|
||||||
|
\fI~/.history\fR) and answers questions about commands you have run.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-h
|
||||||
|
Display help text and exit.
|
||||||
|
.TP
|
||||||
|
.B \-q
|
||||||
|
Question mode. Answer general command-line questions with markdown
|
||||||
|
output rendered through
|
||||||
|
.BR glow (1).
|
||||||
|
.TP
|
||||||
|
.B \-H
|
||||||
|
History mode. Answer questions about your shell history.
|
||||||
|
.TP
|
||||||
|
.B \-p
|
||||||
|
Paginate output through
|
||||||
|
.BR less (1)
|
||||||
|
with raw character support (\fB\-R\fR).
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B OPENAI_BASE_URL
|
||||||
|
API endpoint URL.
|
||||||
|
Default: \fBhttp://10.0.2.145:8090/v1\fR
|
||||||
|
.TP
|
||||||
|
.B OPENAI_API_KEY
|
||||||
|
API key to include as a Bearer token in requests.
|
||||||
|
Optional, depending on the server.
|
||||||
|
.TP
|
||||||
|
.B OPENAI_MODEL
|
||||||
|
Model identifier to use.
|
||||||
|
Default: \fBgpt-4o-mini\fR
|
||||||
|
.SH EXAMPLES
|
||||||
|
.TP
|
||||||
|
Generate a command to find large files:
|
||||||
|
.B ask how to find large files
|
||||||
|
.TP
|
||||||
|
Explain how rsync works (question mode):
|
||||||
|
.B ask \-q how does rsync work
|
||||||
|
.TP
|
||||||
|
Check recent Docker usage (history mode):
|
||||||
|
.B ask \-H what command did I use for docker yesterday
|
||||||
|
.TP
|
||||||
|
Paginate output with less:
|
||||||
|
.B ask \-p how to compress a directory
|
||||||
|
.SH EXIT STATUS
|
||||||
|
.TP
|
||||||
|
0
|
||||||
|
Successful execution.
|
||||||
|
.TP
|
||||||
|
1
|
||||||
|
No question provided, or an API error occurred.
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I ~/.zsh_history
|
||||||
|
.TP
|
||||||
|
.I ~/.bash_history
|
||||||
|
.TP
|
||||||
|
.I ~/.history
|
||||||
|
Shell history files consulted in \fB\-H\fR mode.
|
||||||
|
.SH DEPENDENCIES
|
||||||
|
The following tools are expected at runtime:
|
||||||
|
.BR tldr (1),
|
||||||
|
.BR man (1),
|
||||||
|
.BR col (1),
|
||||||
|
.BR glow (1),
|
||||||
|
.BR less (1).
|
||||||
|
.SH BUGS
|
||||||
|
Report issues at \fIhttps://github.com/anomalyco/opencode/issues\fR.
|
||||||
|
.SH AUTHORS
|
||||||
|
Ole
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultBaseURL = "http://10.0.2.145:8090/v1"
|
||||||
|
|
||||||
|
type chatMessage struct {
|
||||||
|
Role string `json:"role"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []chatMessage `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatResponse struct {
|
||||||
|
Choices []struct {
|
||||||
|
Message chatMessage `json:"message"`
|
||||||
|
} `json:"choices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
help := flag.Bool("h", false, "Show help")
|
||||||
|
history := flag.Bool("H", false, "Answer questions about shell history")
|
||||||
|
question := flag.Bool("q", false, "Answer general questions, render with glow")
|
||||||
|
paginate := flag.Bool("p", false, "Paginate output using less")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *help {
|
||||||
|
printHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: no question provided")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
query := strings.Join(args, " ")
|
||||||
|
|
||||||
|
if *history {
|
||||||
|
handleHistory(query, *paginate)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *question {
|
||||||
|
handleQuestion(query, *paginate)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCommand(query, *paginate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
fmt.Println(`Usage: ask [options] <question>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h Show this help
|
||||||
|
-H Answer questions about your shell history
|
||||||
|
-q Answer general command questions (output rendered with glow)
|
||||||
|
-p Paginate output using less
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ask how to find large files
|
||||||
|
ask -H what command did I use for docker yesterday
|
||||||
|
ask -q how does rsync work
|
||||||
|
ask -p how to compress a directory`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBaseURL() string {
|
||||||
|
if v := os.Getenv("OPENAI_BASE_URL"); v != "" {
|
||||||
|
return strings.TrimRight(v, "/")
|
||||||
|
}
|
||||||
|
return defaultBaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAPIKey() string {
|
||||||
|
return os.Getenv("OPENAI_API_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getModel() string {
|
||||||
|
if v := os.Getenv("OPENAI_MODEL"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "gpt-4o-mini"
|
||||||
|
}
|
||||||
|
|
||||||
|
func chat(messages []chatMessage) (string, error) {
|
||||||
|
reqBody := chatRequest{
|
||||||
|
Model: getModel(),
|
||||||
|
Messages: messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := getBaseURL() + "/chat/completions"
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if key := getAPIKey(); key != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+key)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("http request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return "", fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var cr chatResponse
|
||||||
|
if err := json.Unmarshal(respBody, &cr); err != nil {
|
||||||
|
return "", fmt.Errorf("parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cr.Choices) == 0 {
|
||||||
|
return "", fmt.Errorf("no choices in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cr.Choices[0].Message.Content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCmd(name string, arg ...string) (string, error) {
|
||||||
|
var out, stderr bytes.Buffer
|
||||||
|
cmd := exec.Command(name, arg...)
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return strings.TrimSpace(out.String()), fmt.Errorf("%s: %w\n%s", name, err, strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(out.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTLDR(tool string) string {
|
||||||
|
out, err := runCmd("tldr", tool)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func getManPage(tool string) string {
|
||||||
|
// man pages can be long; limit via col -b for cleaner text
|
||||||
|
cmd := exec.Command("sh", "-c", fmt.Sprintf("man %s 2>/dev/null | col -b", tool))
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
_ = cmd.Run()
|
||||||
|
result := strings.TrimSpace(out.String())
|
||||||
|
if result == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(result) > 6000 {
|
||||||
|
result = result[:6000] + "\n... (truncated)"
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func getShellHistory() string {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates := []string{
|
||||||
|
filepath.Join(home, ".zsh_history"),
|
||||||
|
filepath.Join(home, ".bash_history"),
|
||||||
|
filepath.Join(home, ".history"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range candidates {
|
||||||
|
data, err := os.ReadFile(p)
|
||||||
|
if err == nil && len(data) > 0 {
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
if len(lines) > 200 {
|
||||||
|
lines = lines[len(lines)-200:]
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeOutput(text string, paginate bool) {
|
||||||
|
if paginate {
|
||||||
|
cmd := exec.Command("less", "-R")
|
||||||
|
cmd.Stdin = strings.NewReader(text)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
_ = cmd.Run()
|
||||||
|
} else {
|
||||||
|
fmt.Print(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCommand(query string, paginate bool) {
|
||||||
|
messages := []chatMessage{
|
||||||
|
{Role: "system", Content: "You identify Unix/Linux command-line tools from user questions. Respond with ONLY the tool name, nothing else. Example: 'how to find large files' -> 'find'. 'how to compress a directory' -> 'tar'. If no single tool fits, respond with 'general'."},
|
||||||
|
{Role: "user", Content: query},
|
||||||
|
}
|
||||||
|
|
||||||
|
tool, err := chat(messages)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error identifying tool: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
tool = strings.TrimSpace(strings.ToLower(tool))
|
||||||
|
|
||||||
|
var contextParts []string
|
||||||
|
if tool != "general" && tool != "" {
|
||||||
|
if t := getTLDR(tool); t != "" {
|
||||||
|
contextParts = append(contextParts, "=== TLDR ===\n"+t)
|
||||||
|
}
|
||||||
|
if m := getManPage(tool); m != "" {
|
||||||
|
contextParts = append(contextParts, "=== MAN PAGE ===\n"+m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context := strings.Join(contextParts, "\n\n")
|
||||||
|
|
||||||
|
systemPrompt := "You are a command-line expert. Given a user's question, provide the exact command line they need.\n"
|
||||||
|
if context != "" {
|
||||||
|
systemPrompt += "Use the context below for reference.\n\n" + context
|
||||||
|
}
|
||||||
|
systemPrompt += "\n\nIMPORTANT: Provide ONLY the command line, nothing else. Use placeholders like /path/to/dir or filename for things the user needs to customize. The command should be directly runnable after replacing placeholders. Do NOT include any explanation, markdown formatting, or backticks."
|
||||||
|
|
||||||
|
cmdMessages := []chatMessage{
|
||||||
|
{Role: "system", Content: systemPrompt},
|
||||||
|
{Role: "user", Content: query},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := chat(cmdMessages)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(strings.TrimSpace(result)+"\n", paginate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleHistory(query string, paginate bool) {
|
||||||
|
history := getShellHistory()
|
||||||
|
if history == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: no shell history found")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []chatMessage{
|
||||||
|
{Role: "system", Content: fmt.Sprintf("You are analyzing a user's shell history. Answer their question about their command history based on the data below. Be specific and reference actual commands from their history.\n\nShell history (last 500 lines):\n%s", history)},
|
||||||
|
{Role: "user", Content: query},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := chat(messages)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(strings.TrimSpace(result)+"\n", paginate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleQuestion(query string, paginate bool) {
|
||||||
|
messages := []chatMessage{
|
||||||
|
{Role: "system", Content: "You are a command-line expert. Answer the user's question about commands and tools. Provide a terse, informative response in markdown format. Include specific command examples where relevant. Keep it concise but thorough."},
|
||||||
|
{Role: "user", Content: query},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := chat(messages)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if paginate {
|
||||||
|
cmd := exec.Command("less", "-R")
|
||||||
|
cmd.Stdin = strings.NewReader(result)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
_ = cmd.Run()
|
||||||
|
} else {
|
||||||
|
cmd := exec.Command("glow")
|
||||||
|
cmd.Stdin = strings.NewReader(result)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
_ = cmd.Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
ASK="${ASK:-$(dirname "$0")/ask}"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
RESULTS=()
|
||||||
|
OUTFILE=$(mktemp /tmp/ask_test_out.XXXXXX)
|
||||||
|
ERRFILE=$(mktemp /tmp/ask_test_err.XXXXXX)
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$OUTFILE" "$ERRFILE"
|
||||||
|
rm -f /tmp/ask_test_*.out /tmp/ask_test_*.err /tmp/ask_test_*.tmp /tmp/ask_test_*.tmp2
|
||||||
|
}
|
||||||
|
|
||||||
|
err() { echo " FAIL: $*" >&2; ((FAIL++)); }
|
||||||
|
ok() { echo " PASS"; ((PASS++)); }
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
local label=$1 expected_exit=$2
|
||||||
|
shift 2
|
||||||
|
> "$OUTFILE" > "$ERRFILE"
|
||||||
|
set +e
|
||||||
|
"$ASK" "$@" >"$OUTFILE" 2>"$ERRFILE"
|
||||||
|
local ec=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$ec" -ne "$expected_exit" ]]; then
|
||||||
|
err "$label: expected exit $expected_exit, got $ec"
|
||||||
|
echo " stderr: $(head -c 200 "$ERRFILE" 2>/dev/null)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ok
|
||||||
|
RESULTS+=("$label: PASS")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_stdout_not_empty() {
|
||||||
|
local label=$1
|
||||||
|
if [[ ! -s "$OUTFILE" ]]; then
|
||||||
|
err "$label: stdout is empty"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_stdout_contains() {
|
||||||
|
local label=$1 pattern=$2
|
||||||
|
if ! grep -q "$pattern" "$OUTFILE" 2>/dev/null; then
|
||||||
|
err "$label: stdout missing '$pattern'"
|
||||||
|
echo " stdout: $(head -c 200 "$OUTFILE")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_stderr_contains() {
|
||||||
|
local label=$1 pattern=$2
|
||||||
|
if ! grep -q "$pattern" "$ERRFILE" 2>/dev/null; then
|
||||||
|
err "$label: stderr missing '$pattern'"
|
||||||
|
echo " stderr: $(head -c 200 "$ERRFILE")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_exit_ok() {
|
||||||
|
local label=$1
|
||||||
|
if ! run_test "$label" 0 "${@:2}"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Test suite: ask CLI ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# --- test 1: help output ---
|
||||||
|
echo "1) -h flag prints help"
|
||||||
|
if run_test "1) -h" 0 -h; then
|
||||||
|
check_stdout_contains "1) -h" "Usage: ask"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 2: no arguments exits 1 ---
|
||||||
|
echo "2) no arguments exits with error"
|
||||||
|
if run_test "2) no args" 1; then
|
||||||
|
check_stderr_contains "2) no args" "no question"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 3: default mode — simple command query ---
|
||||||
|
echo "3) default mode: list files by size"
|
||||||
|
if run_test "3) default" 0 how to list files by size; then
|
||||||
|
check_stdout_not_empty "3) default"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 4: default mode — different tool ---
|
||||||
|
echo "4) default mode: kill process by name"
|
||||||
|
if run_test "4) default" 0 how to kill a process by name; then
|
||||||
|
check_stdout_not_empty "4) default"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 5: -q flag (general question) ---
|
||||||
|
echo "5) -q mode: explain grep"
|
||||||
|
if run_test "5) -q" 0 -q explain grep; then
|
||||||
|
check_stdout_not_empty "5) -q"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 6: -H flag (history query) ---
|
||||||
|
echo "6) -H mode: ask about docker usage"
|
||||||
|
if run_test "6) -H" 0 -H "did I use docker recently"; then
|
||||||
|
check_stdout_not_empty "6) -H"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 7: -p flag paginates via less ---
|
||||||
|
echo "7) -p flag paginates via less"
|
||||||
|
# non-interactive: timeout after 2s, less exits 0 on normal, 141 on SIGPIPE, 124 on timeout
|
||||||
|
set +e
|
||||||
|
timeout 2 "$ASK" -p how to find large files >/dev/null 2>&1
|
||||||
|
ec=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$ec" -eq 0 || "$ec" -eq 141 || "$ec" -eq 124 ]]; then
|
||||||
|
ok
|
||||||
|
RESULTS+=("7) -p paginate: PASS")
|
||||||
|
else
|
||||||
|
err "7) -p: unexpected exit $ec"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 8: -q with -p combined ---
|
||||||
|
echo "8) -q -p combined"
|
||||||
|
set +e
|
||||||
|
timeout 2 "$ASK" -q -p "what is awk" >/dev/null 2>&1
|
||||||
|
ec=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$ec" -eq 0 || "$ec" -eq 141 || "$ec" -eq 124 ]]; then
|
||||||
|
ok
|
||||||
|
RESULTS+=("8) -q -p: PASS")
|
||||||
|
else
|
||||||
|
err "8) -q -p: unexpected exit $ec"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 9: query about a less common tool ---
|
||||||
|
echo "9) default mode: monitor disk IO"
|
||||||
|
if run_test "9) default" 0 how to monitor disk IO; then
|
||||||
|
check_stdout_not_empty "9) default"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- test 10: -H with a different question ---
|
||||||
|
echo "10) -H mode: what commands today"
|
||||||
|
if run_test "10) -H" 0 -H "what commands did I run today"; then
|
||||||
|
check_stdout_not_empty "10) -H"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Results: $PASS pass, $FAIL fail ==="
|
||||||
|
exit $FAIL
|
||||||
Reference in New Issue
Block a user