Skip to content

zuno1218/own-shell

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Own Shell

C Unix POSIX System Programming

A simplified Unix shell implemented in C using POSIX system calls.
This shell supports signal handling, I/O redirection, and multi-stage pipelines, demonstrating how Unix shells manage processes and file descriptors.

Result


Overview

This project implements a minimal Unix shell capable of executing commands with features commonly found in real shells.

1. Signal Handling

The shell must handle SIGINT (Ctrl+C) without terminating itself while waiting for input.

Example:

^C  
Your shell cannot be terminated using an interrupt signal!

When a command is running, the shell should remain alive while the child process follows the default SIGINT behavior.

2. I/O Redirection

The shell must redirect standard input and output using file descriptors.

Examples:

ls > file.txt  
ls >> file.txt  
sort < file.txt  
sort < file.txt > sorted.txt

Supported redirections:

Operator Description
< Redirect input
> Redirect output (overwrite)
>> Redirect output (append)

3. Pipelines

The shell must connect multiple commands so that the output of one process becomes the input of the next.

Examples:

ls -l | head  
cat test.c | sort | uniq  
ls | wc -l > out.txt 

The project supports multi-stage pipelines and their combination with I/O redirection.


Implementation

The shell is organized around a simple execution flow:

User Input  
↓  
Syntax Validation  
↓  
Pipeline Splitting  
↓  
fork()  
↓  
I/O Redirection with dup2()  
↓  
Argument Parsing  
↓  
execvp()  
↓  
Parent Waits with waitpid()

Each stage of a pipeline is executed in a separate child process, while the parent shell waits for all child processes to terminate.

Command Evaluation

The main loop repeatedly reads a command line, validates it, and passes it to eval().

Inside eval():

  1. The shell temporarily ignores SIGINT
  2. The command line is split into pipeline stages
  3. Each pipeline stage is executed in a child process
  4. In each child:
    • redirection is applied
    • the command string is converted into argv
    • execvp() is called
  5. The parent waits for all child processes using waitpid()

This structure separates the shell process from the actual command execution process, which is how real Unix shells behave.

Pipeline Handling

Pipelines are handled in the pipelining() function.

For a command such as:

command1 | command2 | command3

The shell:

  • finds each | separator in the command line
  • splits the original command string into individual pipeline stages
  • creates a new pipe between adjacent commands
  • calls fork() for each stage

Each child process then connects its standard input or output using dup2():

  • the first command writes to a pipe
  • middle commands read from the previous pipe and write to the next pipe
  • the last command reads from the previous pipe

This produces a process structure like:

shell
 ├── child1 (command1)
 │      stdout → pipe1
 │
 ├── child2 (command2)
 │      stdin  ← pipe1
 │      stdout → pipe2
 │
 └── child3 (command3)
        stdin ← pipe2

The parent shell closes unused pipe file descriptors after each fork(), preventing descriptor leaks and ensuring proper pipeline behavior.

I/O Redirection

Redirection is handled separately for each pipeline stage in the redirection() function.

The shell scans the command string for:

  • < : input redirection
  • > : output redirection (overwrite)
  • >> : output redirection (append)

For each operator, the shell:

  1. identifies the redirection type
  2. parses the target file name
  3. opens the file with open()
  4. redirects STDIN_FILENO or STDOUT_FILENO using dup2()
  5. closes the original file descriptor

A notable implementation detail is that the shell removes the redirection portion from the command string after processing it.

This prevents file names and redirection symbols from remaining in the final argv passed to execvp().

Because redirection is applied after pipeline splitting, each command in a pipeline can independently have its own input/output redirection.

Argument Parsing

After redirection is processed, each command string is converted into an argument vector by cmd_to_argv().

This function:

  • skips leading spaces
  • splits the command by delimiters such as spaces or redirection symbols
  • stores each token into argv
  • terminates the array with NULL

The resulting argv is then passed directly to execvp().

Syntax Error Detection

Before execution, the shell performs basic syntax validation with check_syntax_error().

It rejects malformed commands such as:

| ls
ls |
sort <

In particular, the shell checks for:

  • a pipeline starting with |
  • a pipeline ending with |
  • missing file names after <, >, or >>
  • unexpected special tokens appearing where a file name or command should exist

By rejecting invalid input before fork() and execvp(), the shell avoids unnecessary process creation and produces clearer error messages.

Signal Handling

The shell installs a custom SIGINT handler so that pressing Ctrl+C does not terminate the shell itself while waiting for input.

Behavior is divided into two cases:

Situation Result
Shell waiting for input Print a warning message and return to the prompt
Child process running Child follows the default SIGINT behavior

This is implemented by:

  • installing a custom signal handler in the shell loop
  • using sigsetjmp() / siglongjmp() to return safely to the prompt
  • ignoring SIGINT in the parent during command evaluation
  • restoring the default SIGINT behavior in child processes before execvp()

As a result, Ctrl+C interrupts running commands, but does not terminate the shell itself.

Built-in Command

The shell also supports a built-in quit command.

Unlike external commands, quit is handled without calling execvp().
When the command is detected, the shell exits directly through built-in command handling.


Example Usage

[JS]# echo Hello World
Hello World

[JS]# sort < myshell.c > sorted.txt

[JS]# ls | wc -l
6

[JS]# cat myshell.c | sort | uniq > out.txt

Build & Run

Compile using the provided Makefile:

$ make

Run the shell:

./myshell

Known Limitations

The shell intentionally implements only a subset of full Unix shell functionality.

Limitations include:

  • Background execution (&) is not supported
  • Stderr redirection (2>) is not implemented
  • Some edge cases of shell syntax are not handled
  • Double Quotes (") are not supported

Despite these limitations, the shell correctly supports the core mechanisms required for command execution.

About

My own shell based on POSIX system calls

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages