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:
- Pipe operator (
|
) - Conditional execution operators (
&&
or||
) - Parallel operator (
&
) - Sequential operator (
;
)
I/O Redirection
The shell must support the following redirection options:
< filename
- redirectsfilename
to standard input> filename
- redirects standard output tofilename
2> filename
- redirects standard error tofilename
&> filename
- redirects standard output and standard error tofilename
>> filename
- redirects standard output tofilename
in append mode2>> filename
- redirects standard error tofilename
in append mode
Hint: Look into open, dup2 and close.
Support Code
The support code consists of three directories:
src/
is the skeleton mini-shell implementation. You will have to implement missing parts marked asTODO
items.util/
stores a parser to be used as support code for implementing the assignment. For more information, you can check theutil/parser/README.md
file. You can use this parser or write your own.tests/
are tests used to validate (and grade) the assignment.
Building mini-shell
To build mini-shell, run make
in the src/
directory:
student@so:~/.../assignment-mini-shell$ cd src/
student@so:~/.../assignment-mini-shell/src$ make
Testing and Grading
The testing is automated.
Tests are located in the tests/
directory.
student@so:~/.../assignment-mini-shell/tests$ ls -F
Makefile grade.sh* run_all.sh* _test/
To test and grade your assignment solution, enter the tests/
directory and run grade.sh
.
Note that this requires linters being available.
The easiest is to use a Docker-based setup with everything installed, as shown in the section "Running the Linters".
When using grade.sh
you will get grades for correctness (maximum 90
points) and for coding style (maximum 10
points).
A successful run will provide you an output ending with:
### GRADE
Checker: 90/ 90
Style: 10/ 10
Total: 100/100
### STYLE SUMMARY
Running the Checker
To run the checker and everything else required, use the make check
command in the tests/
directory:
student@so:~/.../assignment-mini-shell/tests$ make check
make[1]: Entering directory '...'
rm -f *~
[...]
16) Testing sleep command...................................failed [ 0/100]
17) Testing fscanf function.................................failed [ 0/100]
18) Testing unknown command.................................failed [ 0/100]
Total: 0/100
For starters, tests will fail.
Each test is worth a number of points.
The total number of points is 90
.
The maximum grade is obtained by dividing the number of points to 10
, for a maximum grade of 9.00
.
A successful test run will show the output:
student@so:~/.../assignment-mini-shell/tests$ make check
make[1]: Entering directory '...'
rm -f *~
[...]
01) Testing commands without arguments......................passed [03/100]
02) Testing commands with arguments.........................passed [02/100]
03) Testing simple redirect operators.......................passed [05/100]
04) Testing append redirect operators.......................passed [05/100]
05) Testing current directory...............................passed [05/100]
06) Testing conditional operators...........................passed [05/100]
07) Testing sequential commands.............................passed [03/100]
08) Testing environment variables...........................passed [05/100]
09) Testing single pipe.....................................passed [05/100]
10) Testing multiple pipes..................................passed [10/100]
11) Testing variables and redirect..........................passed [05/100]
12) Testing overwritten variables...........................passed [02/100]
13) Testing all operators...................................passed [02/100]
14) Testing parallel operator...............................passed [10/100]
15) Testing big file........................................passed [05/100]
16) Testing sleep command...................................passed [07/100]
17) Testing fscanf function.................................passed [07/100]
18) Testing unknown command.................................passed [04/100]
Total: 90/100
The actual tests are located in the inputs/
directory.
student@os:~/.../assignment-mini-shell/tests/$ ls -F _test/inputs
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
Running the Linters
To run the linters, use the make lint command in the tests/
directory:
student@so:~/.../assignment-mini-shell/tests/$ make lint
[...]
cd .. && checkpatch.pl -f checker/*.sh tests/*.sh
[...]
cd .. && cpplint --recursive src/ tests/ checker/
[...]
cd .. && shellcheck checker/*.sh tests/*.sh
Note that the linters have to be installed on your system: checkpatch.pl
, cpplint
, shellcheck
with certain configuration options.
Debugging
To inspect the differences between the output of the mini-shell and the reference binary set DO_CLEANUP=no
in tests/_test/run_test.sh
.
To see the results of the tests, you can check tests/_test/outputs/
directory.
Memory leaks
To inspect the unreleased resources (memory leaks, file descriptors) set USE_VALGRIND=yes
and DO_CLEANUP=no
in tests/_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 tests/_test/outputs/
directory.