[Shell] Help us improve #cz2 Handle invalid commands

Hey @zbeers, it looks like the issue was caused by an extra \n, since print already appends a newline by default:

After each command, the tester will check if <command_name>: command not found is printed, and whether a prompt is printed for the next command.

This is very confusing, the solution will result in command not found for anything input, so why not state that any command input will not be found, because we havent created any commands yet? I understand you are building up to it, but still the instructions are confusing.

Hi @SoundBoySelecta, thanks for sharing your feedback!

Could you confirm if you’re referring to #cz2 Handle invalid commands or #ff0 REPL?

Actually, we do mention “all commands [are] invalid” in both stages:

That said, I agree that the instructions should be clearer in setting the right expectations upfront.

Hi @andy1li ,
Surprisingly, I ran into some unexpected trouble with this part of the exercise. However, I finally figured out why!

When building a shell, there are multiple ways to capture input:

  1. os.Stdin
  2. /dev/tty

Most real-world shells (Bash, Zsh, Fish, etc.) always read from /dev/tty, not stdin, to ensure:

  • The shell remains interactive even if stdin is redirected (e.g., when running in a pipeline).
  1. The shell can handle password prompts, interactive input, and terminal controls properly.
  2. The shell still works when running scripts while keeping user interaction possible.

So, I updated my code to read from /dev/tty, thinking this was the correct thing to do. However, I think your tests send input via stdin. This caused my implementaiton to fail despite working fine when manually running the shell locally.

Would it be possible to adjust the tests to accommodate /dev/tty as well? That would be amazing!

I really appreciate the effort you guys have put into making such an awesome product!

Thanks again! :blush:
Durga

1 Like

Hey @dgiridharan,
Thanks for the kind words!

In our tester, we internally use a pty to handle the communication between the tester and the user’s shell.

You can refer to the code here for more details.

I tried hacking around to make reads using go-tty work, but came up empty handed.

However, I managed to create a handle to /dev/tty and successfully read from it. This approach seems to work.

// New initializes a new Shell instance and returns in.
func New() (*Shell, error) {
	var input io.Reader

	ttyFile, err := os.Open("/dev/tty")
	if err == nil {
		input = ttyFile
	} else {
		// Fall back to stdin
		input = os.Stdin
	}

	shell := Shell{
		running:   false,
		readIn:    input,
		writerOut: os.Stdout,
	}

	return &shell, nil
}
1 Like

Hi @ryan-gang,
Amazing! Thanks for the quick response and you’re absolutely right. I was able to get the tests passing using the following example:

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	out := os.Stdout
	in, err := os.Open("/dev/tty")
	if err != nil {
		_, _ = fmt.Fprintf(out, "Error: %s\n", err)
		os.Exit(1)
	}
	defer in.Close()

	reader := bufio.NewReader(in)

	fmt.Fprint(out, "$ ")

	input, err := reader.ReadString('\n')
	if err != nil {
		_, _ = fmt.Fprintf(out, "Error: %s\n", err)
		os.Exit(1)
	}

	fmt.Fprintf(out, "%s: command not found\n", input[0:len(input)-1])
}

I was actually trying to use the go-tty library instead. Here’s a quick example using it:

package main

import (
	"fmt"
	"os"

	"github.com/mattn/go-tty"
)

func main() {
	out := os.Stdout
	tty, err := tty.Open()
	if err != nil {
		_, _ = fmt.Fprintf(out, "%s\n", err)
		os.Exit(1)
	}
	defer tty.Close()

	fmt.Fprint(out, "$ ")

	input, err := tty.ReadString()
	if err != nil {
		_, _ = fmt.Fprintf(out, "%s\n", err)
		os.Exit(1)
	}

	fmt.Fprintf(out, "%s: command not found\n", input)
}

When running this in the command line, it correctly prints proper output:

However, when running the test, I get this:

Initiating test run...
⏳ Turbo test runners busy. You are in queue.
Upgrade to skip the wait: https://codecrafters.io/turbo
Running tests. Logs should appear shortly...
[compile] Moved ./.codecrafters/run.sh → ./your_program.sh
[compile] Compilation successful.
Debug = true
[tester::#OO8] Running tests for Stage #OO8 (Print a prompt)
[tester::#OO8] Running ./your_program.sh
[your-program] $ 
[tester::#OO8] ✓ Received prompt
[tester::#OO8] Test passed.
[tester::#CZ2] Running tests for Stage #CZ2 (Handle invalid commands)
[tester::#CZ2] Running ./your_program.sh
[your-program] $ invalid_pear_command
[tester::#CZ2] Output does not match expected value.
[tester::#CZ2] Expected: "invalid_pear_command: command not found"
[tester::#CZ2] Received: ""
[tester::#CZ2] Assertion failed.
[tester::#CZ2] Test failed
View our article on debugging test failures: https://codecrafters.io/debug

So it seems to be a library-specific issue. I was hoping to use go-tty since it would simplify things quite a bit! It’s really unclear why this would is happening as the library does exactly what I did in my previous working example (but just abstracts everything).

Sorry for the trouble, and thanks again for the quick reply! I really appreciate it. You guys have built an incredible product, and I’m having a blast coding while learning a new language. :blush:

Cheers!

2 Likes