#GP4 failing, unsure why

I’m stuck on Stage #GP4

I’ve tried capturing OS-specific home env vars in addition to user.home (which I expect should be all that is needed) and the home directory seems correct but the tests are failing. I think either the test is bugged or I’m fundamentally misunderstanding the challenge. From what I see, it is switching to /root which is in fact the correct home directory when running as root user on linux, but it is expecting /tmp/mango/apple/banana.

Here are my logs:

Submitting changes (commit: 7d05aac)...

⚡ 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.

[tester::#GP4] Running tests for Stage #GP4 (Navigation - The cd builtin: Home directory)
[tester::#GP4] Running ./your_program.sh
[your-program] $ cd /tmp/pineapple/apple/blueberry
[your-program] $ pwd
[your-program] /tmp/pineapple/apple/blueberry
[tester::#GP4] Received current working directory response
[your-program] $ cd ~
[your-program] $ pwd
[your-program] /root
[tester::#GP4] ^ Line does not match expected value.
[tester::#GP4] Expected: "/tmp/mango/apple/banana"
[tester::#GP4] Received: "/root"
[your-program] $ 
[tester::#GP4] Test failed

And here’s a snippet of my code:

private final Map<String, Consumer<String[]>> BUILT_INS = new HashMap<>();
private Path currentPath = Path.of("").toAbsolutePath();

void main() {
  // register built-ins
  registerBuiltIns();

  // cli processing loop
  while (true) {
    var input = IO.readln("$ ");
    if (input == null || input.isBlank()) {
      continue;
    } else {
      input = input.trim();
    }

    var hasArgs = input.contains(" ");
    var command = hasArgs
      ? input.substring(0, input.indexOf(" "))
      : input;
    var args = hasArgs
      ? parseArgs(input.substring(command.length()).trim())
      : new String[0];

    if ("exit".equals(command)) {
      System.exit(0);
    } else if (BUILT_INS.containsKey(command)) {
      BUILT_INS.get(command).accept(args);
    } else {
      execute(command, args);
    }
  }
}

private void cd(String[] args) {
  if (args.length == 0) {
    System.out.println("usage: cd <path>");
    return;
  }

  Path path;

  if (args[0].equals("~")) {
    path = getHomePath();
  } else if (args[0].startsWith("~" + File.separator)) {
    path = getHomePath()
      .resolve(args[0].substring(1 + File.separator.length()))
      .normalize()
      .toAbsolutePath();
  } else {
    path = currentPath
      .resolve(args[0])
      .normalize()
      .toAbsolutePath();
  }

  if (Files.exists(path)) {
    currentPath = path;
  } else {
    System.out.printf("cd: %s: No such file or directory%n", args[0]);
  }
}

private void echo(String[] args) {
  System.out.println(String.join(" ", args));
}

private void execute(String command, String[] args) {
  var path = getExecutablePath(command);
  if (path == null) {
    System.out.printf("%s: command not found%n", command);
    return;
  }

  var parts = new String[args.length + 1];
  parts[0] = command;
  System.arraycopy(args, 0, parts, 1, args.length);

  var processBuilder = new ProcessBuilder(parts);
  processBuilder.directory(currentPath.toFile());
  processBuilder.redirectErrorStream(true);
  try (
    var process = processBuilder.start();
    var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))
  ) {
    String line;
    while ((line = reader.readLine()) != null) {
      System.out.println(line);
    }
    process.waitFor();
  } catch (IOException | InterruptedException e) {
    e.printStackTrace();
    System.out.printf("%s: command not found%n", command);
  }
}

private void pwd(String[] args) {
  System.out.println(currentPath);
}

private void type(String[] args) {
  if (args.length == 0) {
    System.out.println("usage: type <command>");
    return;
  }

  var command = args[0];

  if (BUILT_INS.containsKey(command)) {
    System.out.printf("%s is a shell builtin%n", command);
    return;
  }

  var path = getExecutablePath(command);
  if (path == null) {
    System.out.printf("%s: not found%n", command);
    return;
  }

  System.out.printf("%s is %s%n", command, path);
}

private Path getExecutablePath(String command) {
  var systemPath = System.getenv("PATH");
  if (systemPath == null) {
    return null;
  }

  for (var path : systemPath.split(File.pathSeparator)) {
    var executablePath = Path.of(path)
      .resolve(command)
      .toAbsolutePath();
    if (Files.exists(executablePath) && Files.isExecutable(executablePath)) {
      return executablePath;
    }
  }

  return null;
}

private Path getHomePath() {
  var home = System.getProperty("user.home");
  if (home == null) {
    // try linux specific
    home = System.getenv("HOME");
    if (home == null) {
      // try windows specific
      home = System.getenv("USERPROFILE");
      if (home == null) {
        return currentPath;
      }
    }
  }

  return Path.of(home)
    .normalize()
    .toAbsolutePath();
}

private String[] parseArgs(String input) {
  if (input.isEmpty()) {
    return new String[0];
  }

  var args = new ArrayList<String>();
  var s = new StringBuilder();
  var i = 0;

  // parse arguments allowing for quoted strings
  // TODO impl escaping
  while (i < input.length()) {
    var c = input.charAt(i);

    if (c == '\'') {
      // treat contents within single quotes as a single argument
      i++;
      while (i < input.length() && input.charAt(i) != '\'') {
        s.append(input.charAt(i));
        i++;
      }
      i++;
    } else if (c == '"') {
      // treat contents within double quotes as a single argument
      i++;
      while (i < input.length() && input.charAt(i) != '"') {
        s.append(input.charAt(i));
        i++;
      }
      i++;
    } else if (Character.isWhitespace(c)) {
      // treat whitespace between args as delimiter
      if (!s.isEmpty()) {
        args.add(s.toString());
        s.setLength(0);
      }
      i++;
    } else {
      s.append(c);
      i++;
    }
  }

  if (!s.isEmpty()) {
    args.add(s.toString());
  }

  return args.toArray(String[]::new);
}

private void registerBuiltIns() {
  BUILT_INS.put("cd", this::cd);
  BUILT_INS.put("echo", this::echo);
  BUILT_INS.put("pwd", this::pwd);
  BUILT_INS.put("type", this::type);
}

Hey @kylexd, the issue is that user.home returns the JVM’s notion of the user’s home directory, which is determined when the JVM starts.

A shell shouldn’t rely on JVM-specific details. It should use the HOME environment variable, which determines the home directory from the shell’s perspective.

Oh, okay. I spent way more time than I’d like to admit stuck on that one. Removing the initial user.home worked. Thanks for your help, I appreciate it!