I’m stuck on Stage #wh6.
I tried others code as well and anything under the moon go version 1.24.0
Here are my logs:
[tester::#WH6] Running tests for Stage #WH6 (Autocompletion - Multiple completions)
[tester::#WH6] [setup] export PATH=/tmp/baz:$PATH
[tester::#WH6] [setup] export PATH=/tmp/foo:$PATH
[tester::#WH6] [setup] export PATH=/tmp/qux:$PATH
[tester::#WH6] [setup] Available executables:
[tester::#WH6] [setup] - xyz_bar
[tester::#WH6] [setup] - xyz_quz
[tester::#WH6] [setup] - xyz_foo
[tester::#WH6] Running ./your_program.sh
[tester::#WH6] ✓ Received prompt ($ )
[tester::#WH6] Typed "xyz_"
[your-program] $ xyz_
[tester::#WH6] ✓ Prompt line matches "$ xyz_"
[tester::#WH6] Pressed "<TAB>" (expecting bell to ring)
[tester::#WH6] Pressed "<TAB>" (expecting autocomplete to "xyz_bar xyz_foo xyz_quz")
[tester::#WH6] ✓ Received bell
[your-program] xyz_bar xyz_foo xyz_quz
[tester::#WH6] ✓ Prompt line matches "xyz_bar xyz_foo xyz_quz"
[tester::#WH6] Didn't find expected line.
[tester::#WH6] Expected: "$ xyz_"
[tester::#WH6] Received: "" (no line received)
[tester::#WH6] Assertion failed.
[tester::#WH6] Test failed
Heres my code: GitHub - oreoluwa-bs/cc-shell-go
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/chzyer/readline"
)
var BUILTINS = []string{"echo", "exit", "type", "pwd", "cd"}
var CMDs = map[string]func(out, err io.Writer, input []string){
"echo": echo,
"exit": exit,
"type": stype,
"pwd": pwd,
"cd": cd,
}
var allExecutables []string // Cache for all unique executable names from PATH
var consecutiveTabs int
var completer = readline.NewPrefixCompleter(
readline.PcItem("echo"),
readline.PcItem("exit"),
readline.PcItem("type"),
readline.PcItem("pwd"),
readline.PcItem("cd"),
readline.PcItemDynamic(listExecutables),
)
func listFiles(path string) func(string) []string {
return func(line string) []string {
names := make([]string, 0)
files, _ := os.ReadDir(path)
for _, f := range files {
names = append(names, f.Name())
}
return names
}
}
func readLineListner(rl *readline.Instance) func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
return func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
ok = false
if key == readline.CharTab {
consecutiveTabs++
cands, _ := completer.Do(line, pos)
if len(cands) == 0 {
rl.Write([]byte("\x07")) // Manually print the bell sound
}
if consecutiveTabs == 1 {
rl.Write([]byte("\a")) // Ring the bell on first TAB
} else if consecutiveTabs == 2 {
ok = true
consecutiveTabs = 0
}
} else {
consecutiveTabs = 0
}
return line, pos, ok
}
}
func main() {
allExecutables = getAllExecutables()
rl, err := readline.NewEx(
&readline.Config{
Prompt: "$ ",
AutoComplete: completer,
InterruptPrompt: "^C",
EOFPrompt: "exit",
},
)
if err != nil {
panic(err)
}
defer rl.Close()
rl.CaptureExitSignal()
rl.Config.SetListener(readLineListner(rl))
for {
line, err := rl.Readline()
if err == readline.ErrInterrupt {
if len(line) == 0 {
break
} else {
continue
}
} else if err == io.EOF {
break
}
// fmt.Fprint(os.Stdout, "$ ")
// command, err := bufio.NewReader(os.Stdin).ReadString('\n')
// if err != nil {
// fmt.Fprintln(os.Stderr, "Error reading input:", err)
// break
// }
line = strings.TrimSpace(line)
if line == "" {
continue
}
if err = handleCommands(line); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
}
}
func handleCommands(command string) error {
spl := parseInput(command)
cmd := spl[0]
// fmt.Println(strings.Join(spl, ","))
writeOut := io.Writer(os.Stdout)
writeErr := io.Writer(os.Stderr)
redirections := getRedirections(spl)
argsEnd := len(spl)
for _, r := range redirections {
if r.Index < argsEnd {
argsEnd = r.Index
}
}
args := spl[1:argsEnd]
for _, r := range redirections {
targetFile := spl[r.Index+1]
args = spl[1:r.Index]
w, err := getRedirectOut(r.Operator, targetFile)
if err != nil {
return err
}
if file, ok := w.(*os.File); ok {
if r.Operator == ">" || r.Operator == ">>" || r.Operator == "1>" || r.Operator == "1>>" {
writeOut = w
} else if r.Operator == "2>" || r.Operator == "2>>" {
writeErr = w
}
defer file.Close()
}
}
fn, ok := CMDs[cmd]
if !ok {
path, err := getExecutablePath(cmd)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("%s: command not found", cmd)
}
return err
}
var procAttr os.ProcAttr
procAttr.Files = []*os.File{os.Stdin,
writeOut.(*os.File), writeErr.(*os.File)}
a := []string{cmd}
a = append(a, args...)
proc, err := os.StartProcess(path, a, &procAttr)
if err != nil {
return err
}
_, err = proc.Wait()
return err
}
fn(writeOut, writeErr, args)
return nil
}
func stype(w, werr io.Writer, args []string) {
for _, arg := range args {
found := false
for _, d := range BUILTINS {
if d == arg {
found = true
break
}
}
if !found {
path, err := getExecutablePath(arg)
if err != nil {
if os.IsNotExist(err) {
fmt.Fprintf(werr, "%s: not found\n", arg)
return
}
fmt.Fprintf(werr, "%s: not found\n", arg)
return
}
fmt.Fprintf(w, "%s is %s\n", arg, path)
return
}
fmt.Fprintf(w, "%s is a shell builtin\n", arg)
return
}
}
func echo(w, werr io.Writer, args []string) {
str := strings.Join(args, " ")
fmt.Fprint(w, str+"\n")
}
func exit(w, werr io.Writer, args []string) {
if len(args) == 0 {
os.Exit(0)
return
}
codestr := args[0]
code, err := strconv.Atoi(codestr)
if err != nil {
os.Exit(1)
}
if code == 0 {
os.Exit(code)
}
os.Exit(1)
}
func pwd(w, werr io.Writer, _ []string) {
dir, err := os.Getwd()
if err != nil {
fmt.Fprintf(werr, "pwd: %v\n", err)
return
}
fmt.Fprintf(w, dir+"\n")
}
func cd(w, werr io.Writer, args []string) {
pth := args[0]
if strings.HasPrefix(pth, "~") {
homedir, err := os.UserHomeDir()
if err != nil {
fmt.Fprintf(werr, err.Error()+"\n")
return
}
pth = strings.Replace(pth, "~", homedir, 1)
}
err := os.Chdir(pth)
if err != nil {
fmt.Fprintf(werr, "cd: %s: No such file or directory\n", pth)
return
}
}
func getExecutablePath(cmd string) (string, error) {
dirs := strings.Split(os.Getenv("PATH"), string(os.PathListSeparator))
for _, dir := range dirs {
p := filepath.Join(dir, cmd)
info, err := os.Stat(p)
if err != nil {
continue
}
mode := info.Mode()
// can execute
if mode&0111 != 0 {
// File exists and is executable
return p, nil
}
}
return "", os.ErrNotExist
}
func getAllExecutables() []string {
dirs := strings.Split(os.Getenv("PATH"), string(os.PathListSeparator))
seen := make(map[string]bool)
var names []string
for _, dir := range dirs {
files, err := os.ReadDir(dir)
if err != nil {
continue // Skip invalid directories
}
for _, f := range files {
info, err := f.Info()
if err != nil {
continue
}
mode := info.Mode()
name := info.Name()
if mode&0111 != 0 && !seen[name] {
seen[name] = true
names = append(names, name)
}
}
}
slices.Sort(names)
return names
}
func listExecutables(line string) []string {
var matches []string
for _, exe := range allExecutables {
if strings.HasPrefix(exe, line) {
matches = append(matches, exe)
}
}
return matches
}
func parseInput(input string) []string {
var result []string
var current string
inSingleQuotes, inDoubleQuotes, escaped := false, false, false
for i := 0; i < len(input); i++ {
var curChar = input[i]
if escaped && inDoubleQuotes {
if curChar == '$' || curChar == '"' || curChar == '\\' {
current += string(curChar)
} else {
current += "\\"
current += string(curChar)
}
escaped = false
} else if escaped {
escaped = false
current += string(curChar)
} else if curChar == '\'' && !inDoubleQuotes {
inSingleQuotes = !inSingleQuotes
} else if curChar == '"' && !inSingleQuotes {
inDoubleQuotes = !inDoubleQuotes
} else if curChar == '\\' && !inSingleQuotes {
escaped = true
} else if curChar == ' ' && !inSingleQuotes && !inDoubleQuotes {
if current != "" {
result = append(result, current)
current = ""
}
} else {
current += string(curChar)
}
}
if current != "" {
result = append(result, current)
}
return result
}
type Redirection struct {
Index int
Operator string
}
func getRedirections(toks []string) []Redirection {
redirectionToks := []string{">>", ">", "1>", "1>>", "2>", "2>>"}
var redirections []Redirection
for _, v := range redirectionToks {
if found := slices.Index(toks, v); found != -1 {
redirections = append(redirections, Redirection{
Index: found,
Operator: v,
})
}
}
return redirections
}
func getRedirectOut(tok string, targetFile string) (io.Writer, error) {
var output io.Writer
if tok == ">>" || tok == "1>>" || tok == "2>>" {
ro, err := os.OpenFile(targetFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
output = ro
} else if tok == ">" || tok == "1>" || tok == "2>" {
ro, err := os.Create(targetFile)
if err != nil {
return nil, err
}
output = ro
}
return output, nil
}