Expected: "$ xyz_" but received “”

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
}


Fixed it. Code is in the repo. thanks to GitHub - logan1o1/codecrafters-shell-go

Hey @oreoluwa-bs, would you mind sharing what was wrong? Would love to see if we can improve the tester / instructions.

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.