Rust-DIY Shell-Issues opening file while running `echo xx 2> yy` on `Stage #vz4`

I’m stuck on Stage #vz4.

Here are my logs:

[compile]    Compiling codecrafters-shell v0.1.0 (/app)
[compile]     Finished `release` profile [optimized] target(s) in 1.78s
[compile] Moved ./.codecrafters/run.sh → ./your_program.sh
[compile] Compilation successful.
Debug = true
[tester::#VZ4] Running tests for Stage #VZ4 (Redirection - Redirect stderr)
[tester::#VZ4] [setup] export PATH=/tmp/pear/pineapple/pear:$PATH
[tester::#VZ4] Running ./your_program.sh
[tester::#VZ4] [setup] echo -n "pear" > "/tmp/quz/pear"
[your-program] $ ls -1 nonexistent 2> /tmp/foo/baz.md
[your-program] $ cat /tmp/foo/baz.md
[your-program] ls: nonexistent: No such file or directory
[tester::#VZ4] ✓ Received redirected error message
[your-program] $ echo 'David file cannot be found' 2> /tmp/foo/foo.md
[tester::#VZ4] Failed to read file ("/tmp/foo/foo.md"): open /tmp/foo/foo.md: no such file or directory
[your-program] David file cannot be found
[your-program] $ 
[tester::#VZ4] Assertion failed.
[tester::#VZ4] Test failed

What makes me confused is the part below:

[your-program] $ echo 'David file cannot be found' 2> /tmp/foo/foo.md
[tester::#VZ4] Failed to read file ("/tmp/foo/foo.md"): open /tmp/foo/foo.md: no such file or directory
[your-program] David file cannot be found

Everything seems to be normal on the local machine (since I’ve already created /tmp/foo and /tmp/bar directories), Failed to read file never occurs. But that’s not happening on the online test platform. Has anyone encountered any similar issue?

And here’s a snippet of my code:

#[derive(Debug, Clone, Default)]
pub struct Executor<'a> {
  redirect_stdout: RedirectStdout,
  redirect_stderr: RedirectStderr,
  open_mode: OpenMode,
  tokens: Vec<&'a str>,
}

impl Executor<'_> {
  fn get_redirection_file(&self, path: &str) -> File {
    let path = Path::new(path);
    if let Some(parent) = path.parent() {
      let _ = fs::create_dir_all(parent);
    }

    fs::OpenOptions::new()
      .read(true)
      .write(true)
      .create(true)
      .append(self.open_mode() == OpenMode::Append)
      .open(path)
      .unwrap()
  }
}

impl Executor<'_> {
  pub fn stdout(&self) -> Stdio {
    match self.redirect_stdout() {
      RedirectStdout::No => Stdio::inherit(),
      RedirectStdout::Yes(path) => {
        let file = self.get_redirection_file(path);
        Stdio::from(file)
      }
    }
  }

  pub fn stderr(&self) -> Stdio {
    match self.redirect_stderr() {
      RedirectStderr::No => Stdio::inherit(),
      RedirectStderr::Yes(path) => {
        let file = self.get_redirection_file(path);
        Stdio::from(file)
      }
    }
  }
}

impl Executor<'_> {
  pub fn print(&self, content: &str) {
    match self.redirect_stdout() {
      RedirectStdout::No => print!("{}", content),
      RedirectStdout::Yes(path) => {
        let mut file = self.get_redirection_file(path);
        file.write_all(content.as_bytes()).unwrap();
      }
    }
  }

  #[inline]
  pub fn println(&self, content: &str) {
    self.print(&format!("{}\n", content));
  }

  pub fn eprint(&self, content: &str) {
    match self.redirect_stderr() {
      RedirectStderr::No => eprint!("{}", content),
      RedirectStderr::Yes(path) => {
        let mut file = self.get_redirection_file(path);
        file.write_all(content.as_bytes()).unwrap();
      }
    }
  }

  #[inline]
  pub fn eprintln(&self, content: &str) {
    self.eprint(&format!("{}\n", content));
  }
}

The parser part has been verified to be well-implemented, and here’s the unit test:

#[test]
fn test_redirect_stdout_1() {
  let input = "echo hello 1> world";
  let mut tokenizer = super::Tokenizer::new(input.chars());
  let tokens = tokenizer.tokenize();
  assert_eq!(tokens, ["echo", "hello"]);
  assert_eq!(
    tokenizer.redirect_stdout,
    super::RedirectStdout::Yes("world".to_string())
  );
  assert_eq!(tokenizer.redirect_stderr, super::RedirectStderr::No);
  assert_eq!(tokenizer.open_mode, super::OpenMode::Truncate)
}

#[test]
fn test_redirect_stdout_2() {
  let input = "echo hello > world";
  let mut tokenizer = super::Tokenizer::new(input.chars());
  let tokens = tokenizer.tokenize();
  assert_eq!(tokens, ["echo", "hello"]);
  assert_eq!(
    tokenizer.redirect_stdout,
    super::RedirectStdout::Yes("world".to_string())
  );
  assert_eq!(tokenizer.redirect_stderr, super::RedirectStderr::No);
  assert_eq!(tokenizer.open_mode, super::OpenMode::Truncate)
}

#[test]
fn test_redirect_stderr() {
  let input = "echo hello 2> world";
  let mut tokenizer = super::Tokenizer::new(input.chars());
  let tokens = tokenizer.tokenize();
  assert_eq!(tokens, ["echo", "hello"]);
  assert_eq!(tokenizer.redirect_stdout, super::RedirectStdout::No);
  assert_eq!(
    tokenizer.redirect_stderr,
    super::RedirectStderr::Yes("world".to_string())
  );
  assert_eq!(tokenizer.open_mode, super::OpenMode::Truncate)
}

#[test]
fn test_redirect_append_stdout_1() {
  let input = "echo hello 1>> world";
  let mut tokenizer = super::Tokenizer::new(input.chars());
  let tokens = tokenizer.tokenize();
  assert_eq!(tokens, ["echo", "hello"]);
  assert_eq!(
    tokenizer.redirect_stdout,
    super::RedirectStdout::Yes("world".to_string())
  );
  assert_eq!(tokenizer.redirect_stderr, super::RedirectStderr::No);
  assert_eq!(tokenizer.open_mode, super::OpenMode::Append)
}

#[test]
fn test_redirect_append_stdout_2() {
  let input = "echo hello >> world";
  let mut tokenizer = super::Tokenizer::new(input.chars());
  let tokens = tokenizer.tokenize();
  assert_eq!(tokens, ["echo", "hello"]);
  assert_eq!(
    tokenizer.redirect_stdout,
    super::RedirectStdout::Yes("world".to_string())
  );
  assert_eq!(tokenizer.redirect_stderr, super::RedirectStderr::No);
  assert_eq!(tokenizer.open_mode, super::OpenMode::Append)
}

I’ve got the solution. The key is, whether you call Executor::print or Executor::eprint, you should initialize both the stdout redirection file and the stderr redirection file.

The previous implementation only initialize stdout redir file while calling Executor::print, and stderr redir file while calling Executor::eprint.

So here’s a fix:

pub fn stdout(&self) -> Stdio {
  // stderr
  if let RedirectStderr::Yes(path) = self.redirect_stderr() {
    self.get_redirection_file(path);
  }
  // stdout
  match self.redirect_stdout() {
    RedirectStdout::No => Stdio::inherit(),
    RedirectStdout::Yes(path) => {
      let file = self.get_redirection_file(path);
      Stdio::from(file)
    }
  }
}

pub fn stderr(&self) -> Stdio {
  // stdout
  if let RedirectStdout::Yes(path) = self.redirect_stdout() {
    self.get_redirection_file(path);
  }
  // stderr
  match self.redirect_stderr() {
    RedirectStderr::No => Stdio::inherit(),
    RedirectStderr::Yes(path) => {
      let file = self.get_redirection_file(path);
      Stdio::from(file)
    }
  }
}

pub fn print(&self, content: &str) {
  // stderr
  if let RedirectStderr::Yes(path) = self.redirect_stderr() {
    self.get_redirection_file(path);
  }
  // stdout
  match self.redirect_stdout() {
    RedirectStdout::No => print!("{}", content),
    RedirectStdout::Yes(path) => {
      let mut file = self.get_redirection_file(path);
      file.write_all(content.as_bytes()).unwrap();
    }
  }
}

pub fn eprint(&self, content: &str) {
  // stdout
  if let RedirectStdout::Yes(path) = self.redirect_stdout() {
    self.get_redirection_file(path);
  }
  // stderr
  match self.redirect_stderr() {
    RedirectStderr::No => eprint!("{}", content),
    RedirectStderr::Yes(path) => {
      let mut file = self.get_redirection_file(path);
      file.write_all(content.as_bytes()).unwrap();
    }
  }
}
1 Like

Great job figuring it out! We’re also actively working on improving the tester’s error messages to make things clearer.