Skip to content

Md-Talim/codecrafters-shell-python

Repository files navigation

progress-banner

Codecrafters Shell Challenge

This project is a custom shell implementation, built as part Codecrafter's "Build Your Own Shell" Challenge. It supports core shell functionalities, including interpreting commands, executing programs, handling redirections, and providing autocompletion.

Features

  • Built-in commands: Supports echo, exit, type, pwd, and cd.
  • External commands: Executes programs found in the system's PATH.
  • Output redirection: Supports >, >>, 1>, 1>>, 2>, and 2>> for redirecting standard output and error.
  • Autocompletion: Uses readline to provide tab-completion for built-in and external commands.

Project Structure

  • Command Handling: The shell differentiates between built-in commands and external programs.
  • Autocompletion: Uses Python's readline module to provide command suggestions.
  • Redirection Handling: Redirects output or error streams to files when specified.
  • PATH Resolution: Retrieves executable programs from the system's PATH for external command execution.

What I Learned

  • Command Execution: How to differentiate between built-in and external commands.
  • Process Management: Using subprocess.Popen to execute external commands.
  • Redirection Handling: Managing input and output redirection with file handling.
  • Autocompletion: Implementing readline for interactive command suggestions.
  • Environment Variables: Understanding how PATH resolution works.
  • Error Handling: Managing missing files, invalid commands, and permission issues.

Code Walkthrough

1. Loading Available Programs

This function iterates through directories in the system's PATH and stores executable files in PATH_PROGRAMS, allowing the shell to identify external commands.

PATH_PROGRAMS: dict[str, pathlib.Path] = {}

def load_path_programs():
    for dir in os.getenv("PATH").split(os.pathsep):
        try:
            for entry in pathlib.Path(dir).iterdir():
                if entry.is_file() and os.access(entry, os.X_OK):
                    PATH_PROGRAMS[entry.name] = entry
        except FileNotFoundError:
            continue

load_path_programs()
2. Autocompletion Setup
  • Tab completion is provided by readline.
  • complete() filters and suggests commands matching user input.
  • display_matches() formats and displays autocompletion results.
COMPLETIONS: Final[list[str]] = sorted([*SHELL_BUILTINS, *PATH_PROGRAMS.keys()])

def display_matches(substitution, matches, longest_match_length):
    print()
    if matches:
        print("  ".join(sorted(matches)))
    print("$ " + substitution, end="")

def complete(text: str, state: int) -> str | None:
    matches = sorted(set([s for s in COMPLETIONS if s.startswith(text)]))
    return matches[state] if state < len(matches) else None

readline.set_completion_display_matches_hook(display_matches)
readline.parse_and_bind("tab: complete")
readline.set_completer(complete)
3. Handling Commands
  • The shell reads input, splits it into arguments, and handles redirection if detected.
  • The handle_input() function processes commands accordingly.
def main():
    while True:
        sys.stdout.write("$ ")
        arguments = shlex.split(input())
        out, err = sys.stdout, sys.stderr
        close_out, close_err = False, False

        try:
            if ">" in arguments:
                file_index = arguments.index(">")
                out = open(arguments[file_index + 1], "w")
                close_out = True
                arguments = arguments[:file_index]
            elif "1>" in arguments:
                file_index = arguments.index("1>")
                out = open(arguments[file_index + 1], "w")
                close_out = True
                arguments = arguments[:file_index]
            elif "2>" in arguments:
                file_index = arguments.index("2>")
                err = open(arguments[file_index + 1], "w")
                close_err = True
                arguments = arguments[:file_index]
            elif ">>" in arguments:
                file_index = arguments.index(">>")
                out = open(arguments[file_index + 1], "a")
                close_out = True
                arguments = arguments[:file_index]
            handle_input(arguments, out, err)
        finally:
            if close_err:
                err.close()
            if close_out:
                out.close()
4. Executing Commands
  • Uses Python 3.10's structural pattern matching for command execution.
  • Redirects built-in commands (echo, type, pwd, cd, exit).
  • Calls external programs from PATH_PROGRAMS.
def handle_input(arguments: str, out: TextIO, err: TextIO):
    match arguments:
        case ["echo", *args]:
            out.write(" ".join(args) + "\n")
        case ["type", cmd]:
            validate_command_type(cmd, out, err)
        case ["pwd"]:
            out.write(f"{os.getcwd()}" + "\n")
        case ["cd", dir]:
            change_directory(dir, out, err)
        case ["exit", *args]:
            sys.exit(0)
        case [command, *args] if command in PATH_PROGRAMS:
            process = subprocess.Popen([command, *args], stdout=out, stderr=err)
            process.wait()
        case invalid:
            out.write(f"{" ".join(invalid)}: command not found" + "\n")
5. Implementing `type` Command

Checks if a command is a built-in or an external program and returns its type.

def validate_command_type(command: str, out: TextIO, err: TextIO):
    if command in SHELL_BUILTINS:
        out.write(f"{command} is a shell builtin" + "\n")
        return
    if command in PATH_PROGRAMS:
        out.write(f"{command} is {PATH_PROGRAMS[command]}" + "\n")
        return
    out.write(f"{command}: not found" + "\n")
6. Implementing cd Command
  • Expands ~ to the user's home directory.
  • Checks if the target directory exists before changing it.
def change_directory(path: str, out: TextIO, err: TextIO):
    if path.startswith("~"):
        home = os.getenv("HOME") or "/root"
        path = path.replace("~", home)
    resolved_path = pathlib.Path(path)
    if not resolved_path.exists():
        out.write(f"cd: {path}: No such file or directory" + "\n")
        return
    os.chdir(resolved_path)

Running the Shell

python app/main.py

Wait a moment for programs from the PATH to load. The shell will start and wait for user input.

Conclusion

This project provided a hands-on experience in building a simple shell, covering core functionalities like command execution, navigation, autocompletion, and redirection.

About

A a custom shell implementation, built as part Codecrafter's "Build Your Own Shell" Challenge.

Topics

Resources

Stars

Watchers

Forks

Contributors 2

  •  
  •