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.
- Built-in commands: Supports
echo
,exit
,type
,pwd
, andcd
. - External commands: Executes programs found in the system's
PATH
. - Output redirection: Supports
>
,>>
,1>
,1>>
,2>
, and2>>
for redirecting standard output and error. - Autocompletion: Uses
readline
to provide tab-completion for built-in and external commands.
- 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.
- 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.
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)
python app/main.py
Wait a moment for programs from the PATH
to load. The shell will start and wait for user input.
This project provided a hands-on experience in building a simple shell, covering core functionalities like command execution, navigation, autocompletion, and redirection.