Skip to main content

Minishell

Objectives

  • Learn how shells create new child processes and connect the I/O to the terminal.
  • Gain a better understanding of the fork() function wrapper.
  • Learn to correctly execute commands written by the user and treat errors.

Statement

Introduction

A shell is a command-line interpreter that provides a text-based user interface for operating systems. Bash is both an interactive command language and a scripting language. It is used to interact with the file system, applications, operating system and more.

For this assignment you will build a Bash-like shell with minimal functionalities like traversing the file system, running applications, redirecting their output or piping the output from one application into the input of another. The details of the functionalities that must be implemented will be further explained.

Shell Functionalities

Changing the Current Directory

The shell will support a built-in command for navigating the file system, called cd. To implement this feature you will need to store the current directory path because the user can provide either relative or absolute paths as arguments to the cd command.

The built-in pwd command will show the current directory path.

Check the following examples below to understand these functionalities.

> pwd
/home/student
> cd operating-systems/assignments/minishell
> pwd
/home/student/operating-systems/assignments/minishell
> cd inexitent
no such file or directory
> cd /usr/lib
> pwd
/usr/lib

NOTE: Using the cd command without any arguments or with more than one argument doesn't affect the current directory path. Make sure this edge case is handled in a way that prevents crashes.

Closing the Shell

Inputting either quit or exit should close the minishell.

Running an Application

Suppose you have an executable named sum in the current directory. It takes arbitrarily many numbers as arguments and prints their sum to stdout. The following example shows how the minishell implemented by you should behave.

> ./sum 2 4 1
7

If the executable is located at the /home/student/sum absolute path, the following example should also be valid.

> /home/student/sum 2 4 1
7

Each application will run in a separate child process of the minishell created using fork.

Environment Variables

Your shell will support using environment variables. The environment variables will be initially inherited from the bash process that started your minishell application.

If an undefined variable is used, its value is the empty string: "".

NOTE: The following examples contain comments which don't need to be supported by the minishell. They are present here only to give a better understanding of the minishell's functionalities.

> NAME="John Doe"                    # Will assign the value "John Doe" to the NAME variable
> AGE=27 # Will assign the value 27 to the AGE variable
> ./identify $NAME $LOCATION $AGE # Will translate to ./identify "John Doe" "" 27 because $LOCATION is not defined

A variable can be assigned to another variable.

> OLD_NAME=$NAME    # Will assign the value of the NAME variable to OLD_NAME

Operators

Sequential Operator

By using the ; operator, you can chain multiple commands that will run sequentially, one after another. In the command expr1; expr2 it is guaranteed that expr1 will finish before expr2 is be evaluated.

> echo "Hello"; echo "world!"; echo "Bye!"
Hello
world!
Bye!
Parallel Operator

By using the & operator you can chain multiple commands that will run in parallel. When running the command expr1 & expr2, both expressions are evaluated at the same time (by different processes). The order in which the two commands finish is not guaranteed.

> echo "Hello" & echo "world!" & echo "Bye!"  # The words may be printed in any order
world!
Bye!
Hello
Pipe Operator

With the | operator you can chain multiple commands so that the standard output of the first command is redirected to the standard input of the second command.

Hint: Look into anonymous pipes and file descriptor inheritance while using fork.

> echo "Bye"                      # command outputs "Bye"
Bye
> ./reverse_input
Hello # command reads input "Hello"
olleH # outputs the reversed string "olleH"
> echo "world" | ./reverse_input # the output generated by the echo command will be used as input for the reverse_input executable
dlrow
Chain Operators for Conditional Execution

The && operator allows chaining commands that are executed sequentially, from left to right. The chain of execution stops at the first command that exits with an error (return code not 0).

# throw_error always exits with a return code different than 0 and outputs to stderr "ERROR: I always fail"
> echo "H" && echo "e" && echo "l" && ./throw_error && echo "l" && echo "o"
H
e
l
ERROR: I always fail

The || operator allows chaining commands that are executed sequentially, from left to right. The chain of execution stops at the first command that exits successfully (return code is 0).

# throw_error always exits with a return code different than 0 and outputs to stderr "ERROR: I always fail"
> ./throw_error || ./throw_error || echo "Hello" || echo "world!" || echo "Bye!"
ERROR: I always fail
ERROR: I always fail
Hello
Operator Priority

The priority of the available operators is the following. The lower the number, the higher the priority:

  1. Pipe operator (|)
  2. Conditional execution operators (&& or ||)
  3. Parallel operator (&)
  4. Sequential operator (;)

I/O Redirection

The shell must support the following redirection options:

  • < filename - redirects filename to standard input
  • > filename - redirects standard output to filename
  • 2> filename - redirects standard error to filename
  • &> filename - redirects standard output and standard error to filename
  • >> filename - redirects standard output to filename in append mode
  • 2>> filename - redirects standard error to filename in append mode

Hint: Look into open, dup2 and close.

Testing

The testing is automated. Tests are located in the inputs/ directory.

student@os:~/.../assignments/minishell/checker/_test/inputs$ ls -F
test_01.txt test_03.txt test_05.txt test_07.txt test_09.txt test_11.txt test_13.txt test_15.txt test_17.txt
test_02.txt test_04.txt test_06.txt test_08.txt test_10.txt test_12.txt test_14.txt test_16.txt test_18.txt

To execute tests you need to run:

student@os:~/.../assignments/minishell/checker$ ./run_all.sh

Debug

To inspect the differences between the output of the mini-shell and the reference binary set DO_CLEANUP=no in _test/run_test.sh. To see the results of the tests, you can check _test/outputs/ directory.

Memory leaks

To inspect the unreleased resources (memory leaks, file descriptors) set USE_VALGRIND=yes and DO_CLEANUP=no in _test/run_test.sh. You can modify both the path to the Valgrind log file and the command parameters. To see the results of the tests, you can check _test/outputs/ directory.

Checkpatch

checkpatch.pl is a script used in the development of the Linux kernel. It is used to check patches that are submitted to the kernel mailing list for adherence to the coding style guidelines of the Linux kernel.

The script checks the code for common coding style issues, such as indentation, spacing, line length, function and variable naming conventions, and other formatting rules. It also checks for some common errors, such as uninitialized variables, memory leaks, and other potential bugs.

You can download the checkpatch.pl script from the official Linux kernel repository.

Running the following command will show you linting warnings and errors:

./checkpatch.pl --no-tree --terse -f /path/to/your/code.c