Test not registering prompt on build your own shell in golang

I’m stuck on Stage #QP2

As you can see by running the same code, the prompt correctly appears. But for some reason the test fails to recognize the prompt.

Are the tests open source? Can I view the code on how the test is parsing the line?

Here are my logs:

❯ codecrafters submit
Submitting changes (commit: 5f969a0)...

⚡ This is a turbo test run. https://codecrafters.io/turbo

Running tests on your code. Logs should appear shortly...

[compile] Moved ./.codecrafters/run.sh → ./your_program.sh
[compile] Compilation successful.

Debug = true

[tester::#QP2] Running tests for Stage #QP2 (Command Completion - Builtin completion)
[tester::#QP2] Running ./your_program.sh
[tester::#QP2] Expected prompt ("$ ") but received ""
[tester::#QP2] Test failed

View our article on debugging test failures: https://codecrafters.io/debug

And here’s a snippet of my code:

package main

import (
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"slices"
	"strings"

	"github.com/ergochat/readline"
)

type Output struct {
	Content    string
	IsStdError bool
}

func tokenize(command string) []string {
	var tokens []string
	var current strings.Builder
	inSingleQuote := false
	inDoubleQuote := false
	escape := false
	space := false

	for _, ch := range command {
		if escape {
			current.WriteRune(ch)
			escape = false
			continue
		}
		if ch == ' ' && space && !inSingleQuote && !inDoubleQuote {
			continue
		} else {
			space = false
		}
		switch {
		// handling for single and double quotes
		case ch == '\'' && !inSingleQuote && !inDoubleQuote:
			inSingleQuote = true
		case ch == '\'' && inSingleQuote:
			inSingleQuote = false
		case ch == '"' && !inSingleQuote && !inDoubleQuote:
			inDoubleQuote = true
		case ch == '"' && inDoubleQuote:
			inDoubleQuote = false
		case ch == ' ' && !inSingleQuote && !inDoubleQuote:
			tokens = append(tokens, current.String())
			current.Reset()
			space = true
		case ch == '\\' && !inSingleQuote:
			escape = true
		default:
			current.WriteRune(ch)
		}
	}

	if current.Len() > 0 {
		tokens = append(tokens, current.String())
	}

	return tokens
}

func processTokens(tokens []string) []Output {
	builtin := []string{"exit", "echo", "type", "pwd", "cd"}
	var outputs []Output
	switch tokens[0] {
	case "echo":
		outputs = []Output{
			{
				Content:    fmt.Sprint(strings.Join(tokens[1:], " ") + "\n"),
				IsStdError: false,
			},
		}
		return outputs
	case "cat":
		files := tokens[1:]
		for _, file := range files {
			contentBytes, err := os.ReadFile(file)
			if err != nil {
				outputs = append(outputs, Output{Content: fmt.Sprintf("%v: nonexistent: No such file or directory", tokens[0]) + "\n", IsStdError: true})
				continue
			}
			outputs = append(outputs, Output{Content: string(contentBytes), IsStdError: false})
		}
		return outputs
	case "pwd":
		wd, err := os.Getwd()

		if err != nil {
			outputs = append(outputs, Output{Content: string(err.Error()) + "\n", IsStdError: true})
			return outputs
		}
		outputs = append(outputs, Output{Content: wd + "\n", IsStdError: true})
		return outputs
	case "type":
		for i := 1; i < len(tokens); i++ {
			if slices.Contains(builtin, tokens[i]) {
				outputs = append(outputs, Output{Content: fmt.Sprintf("%v is a shell builtin", tokens[i]) + "\n", IsStdError: false})
			} else {
				path, err := exec.LookPath(tokens[i])
				if err != nil {
					outputs = append(outputs, Output{Content: fmt.Sprint(tokens[i] + ": not found" + "\n"), IsStdError: true})
				} else {
					outputs = append(outputs, Output{Content: fmt.Sprintf("%v is %v", tokens[i], path) + "\n", IsStdError: false})
				}
			}
		}
		return outputs
	case "cd":
		var err error
		if tokens[1] == "~" {
			homeDir, err := os.UserHomeDir()
			if err != nil {
				outputs = append(outputs, Output{Content: fmt.Sprintf("error geting home directory of user: %v", err) + "\n", IsStdError: true})
				return outputs
			}
			err = os.Chdir(homeDir)
		} else {
			err = os.Chdir(tokens[1])
		}
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				outputs = append(outputs, Output{Content: fmt.Sprintf("cd: %v: No such file or directory", tokens[1]) + "\n", IsStdError: true})
				return outputs
			} else {
				outputs = append(outputs, Output{Content: fmt.Sprintf("error changing directory: %v", err) + "\n", IsStdError: true})
				return outputs
			}
		}
	default:
		_, err := exec.LookPath(tokens[0])
		outputs := []Output{}
		if err != nil {
			outputs = append(outputs, Output{Content: fmt.Sprint(tokens[0]+": not found") + "\n", IsStdError: true})
			return outputs
		} else {
			cmd := exec.Command(tokens[0], tokens[1:]...)
			cmd.Stdin = os.Stdin
			output, err := cmd.Output()
			if err != nil {
				outputs = append(outputs, Output{Content: fmt.Sprintf("%v: nonexistent: No such file or directory", tokens[0]) + "\n", IsStdError: true})
				return outputs
			}
			outputs = append(outputs, Output{Content: strings.TrimSpace(string(output)) + "\n", IsStdError: false})
			return outputs
		}
	}
	return nil
}

// find the first occurence of the index and value of any element in r that is present in s
func containsAny(s, r []string) (int, string) {
	var element string
	var index int
	for _, el := range r {
		if slices.Contains(s, el) {
			element = el
		}
	}
	if len(element) > 0 {
		index = slices.Index(s, element)
		return index, element
	}

	return 0, ""
}

var completer = readline.NewPrefixCompleter(
	readline.PcItem("echo"),
	readline.PcItem("exit"),
)

func main() {
	l, err := readline.NewFromConfig(&readline.Config{
		Prompt:          "$ ",
		HistoryFile:     "/tmp/readline.tmp",
		InterruptPrompt: "^C",
		EOFPrompt:       "exit",
		AutoComplete:    completer,

		HistorySearchFold: true,
	})
	if err != nil {
		panic(err)
	}
	defer l.Close()
	l.CaptureExitSignal()
	log.SetOutput(l.Stderr())
	for {
		line, err := l.Readline()
		if err == readline.ErrInterrupt {
			if len(line) == 0 {
				break
			} else {
				continue
			}
		} else if err == io.EOF {
			break
		}
		tokens := tokenize(line)
		if tokens[0] == "exit" {
			if len(tokens) > 1 {
				fmt.Fprintln(os.Stderr, "exit: too many arguments")
				continue
			}
			return
		}
		redirects := []string{">", "1>", "2>", ">>", "1>>", "2>>"}
		index, redirect := containsAny(tokens, redirects)
		if len(redirect) > 0 {
			p := tokens[:index]
			output := processTokens(p)
			//only one file on right side of ">" operator
			if len(tokens[index+1:]) != 1 {
				fmt.Println("Please enter only one file name")
				continue
			}
			fileName := tokens[index+1:][0]
			stdOutput := strings.Builder{}
			stdErrOutput := strings.Builder{}
			for _, o := range output {
				if o.IsStdError {
					stdErrOutput.WriteString(o.Content)
					continue
				}
				stdOutput.WriteString(o.Content)
			}
			stdOutputB := []byte(stdOutput.String())
			stdErrOutputB := []byte(stdErrOutput.String())
			if redirect == ">" || redirect == "1>" {
				_ = os.WriteFile(fileName, stdOutputB, 0644)
			} else if redirect == "2>" {
				_ = os.WriteFile(fileName, stdErrOutputB, 0644)
			} else if redirect == ">>" || redirect == "1>>" {
				f, _ := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
				defer f.Close()
				f.WriteString(stdOutput.String())
			} else if redirect == "2>>" {
				f, _ := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
				defer f.Close()
				f.WriteString(stdErrOutput.String())
			}

			if redirect != "2>" && redirect != "2>>" {
				if len(stdErrOutput.String()) > 0 {
					fmt.Print(stdErrOutput.String())
				}
			} else {
				if len(stdOutput.String()) > 0 {
					fmt.Print(stdOutput.String())
				}
			}
		} else {
			output := processTokens(tokens)
			for _, o := range output {
				fmt.Print(o.Content)
			}
		}
	}

}

Hey @hamza-m-masood, some third-party readline libraries don’t print the prompt quickly enough before our tester checks the output.

In Go, we recommend using github.com/chzyer/readline, which is known to work well with the Shell challenge.

Let me know if you’re still running into issues after switching!

Yes, the tester code is open source. You can view the code for stage #QP2 here.

Thanks for linking another library. I will test this and report back.

I can confirm that the new library passes the tests. Thanks!