As beautiful as a shell π
- Introduction
- Our Implementation of Minishell
- Builtins
- Prompt
- Extras
- Installation
- References
- Summary
- License
- Authors
This project is all about recreating your very own (mini)shell, taking bash (Bourne Again SHell) as a reference.
As we just said, we are asked to implement our shell, but what is a shell to begin with? If we think of (for example) Linux as a nut or a seashell, the kernel/seed is the nut's core and must be surrounded by a cover or shell. Likewise, the shell we are implementing works as a command interpreter securely communicating with the OS kernel, and allows us to perform several tasks from a command line, namely execute commands, create or delete files or directories, or read and write content of files, among (many) other things
The general idea for this shell is to read a string of commands in a prompt using readline. Before anything, it is highly recommended to take a deep dive into the bash manual, as it goes over every detail we had to consider when doing this project. Minishell
involves heavy parsing of the string read by readline
, thus it is crucial to divide the code of the project into different parts: the lexer
, the expander
, the parser
, and lastly the executor
This first part covers the part of our code in charge of expanding environment variables with $
followed by characters, Here we also split the input string into small chunks or tokens to handle pipes, redirections, and expansions better.
After reading from the stdin
we use a function which separates the string taking spaces and quotes into account. For example:
string: echo "hello there" how are 'you 'doing? $USER |wc -l >outfile
output: {echo, "hello there", how, are, 'you 'doing?, $USER, |wc, -l, >outfile, NULL}
Then, we apply the expander functions on top of every substring of the original string, resulting in something similar to this:
output: {echo, "hello there", how, are, 'you 'doing?, pixel, |wc, -l, >outfile, NULL}
Note: if a variable is not found, the $var part of the string will be replaced by an empty string
Lastly, we have another split function called check_others
which separates with <
, |
, or >
, but only if those chars are outside of quotes:
output: {echo, "hello there", how, are, 'you 'doing?, pixel, |, wc, -l, >, outfile, NULL}
The parser is in charge of storing the tokenized string and saving it in a useful manner for the executor to use later. Our data structure is managed as follows:
typedef struct s_args
{
char *arg;
struct s_args *head;
struct s_args *next;
} t_args;
typedef struct s_exe
{
char *cmd;
char *option;
t_args *arg;
char **args;
char *stdin;
char *stdout;
char *stdin2;
char *stdout2;
t_env *envr;
struct s_exe *next;
struct s_exe *head;
} t_exe;
Here is a short summary of what every variable is used for
Parameter | Description |
---|---|
cmd |
Stores a command name (ls, pwd, ...), or a path to the executable file |
arg |
Linked list of all arguments (ls -l -a, pwd -L, ...) |
args |
Array of arguments (ls -l -a, pwd -L, ...), used for execve function |
stdin |
Input redirection (ls < file.txt, pwd < file.txt, ...) |
stdout |
Output redirection (ls > file.txt, pwd > file.txt, ...) |
stdin2 |
Input redirection (ls << file.txt, pwd << file.txt, ...) |
stdout2 |
Output redirection (ls >> file.txt, pwd >> file.txt, ...) |
envr |
Linked list of environment variables (PATH, HOME, ...) |
After running our lexer and expander, we have a two-dimensional array. Following the previous example, it was the following:
{echo, "hello there", how, are, 'you 'doing?, pixel, |, wc, -l, >, outfile, NULL}
Now, our parser starts building the linked list of args (t_args *arg
), which is filled in the following way:
- Iterate over the one-dimensional array
- Whenever a redirection is found, check the type of redirection and retrieve a file descriptor containing the info we need as the infile
- Check that the file descriptor that has been opened is valid (!= -1) and continue
- If a pipe is found, add a new node to the list of commands
- In all other cases add whatever words are found to the argument list (
argv
) we callfull_cmd
Here's how the variables will look like according to the example we used before:
cmds:
cmd 1:
infile: 0 (default)
outfile: 1 (redirected to pipe)
full_path: NULL (because echo is a builtin)
full_cmd: {echo, hello there, how, are, you doing?, pixel, NULL}
cmd 2:
infile: 0 (contains output of previous command)
outfile: 3 (fd corresponding to the open file 'outfile')
full_path: /bin/wc
full_cmd: {wc, -l, NULL}
With all our data properly on our structs, the executer
has all the necessary information to execute commands. For this part we use separate processess to execute non-builtins commands inside child processes that redirect stdin
and stdout
. If we are given a full path (e.g. /bin/ls
) then we do not need to look for the full path of the command and can execute directly with execve. If we are given a relative path then we use the PATH
environment variable to determine the full_path
of a command. After all commands have started running, we retrieve the exit status of the most recently executed command with the help of waitpid
Once all commands have finished running the allocated memory is freed and a new prompt appears to read the next command
Here is a handy mindmap of our code structure to help you understand everything we mentioned previously
We were asked to implement some basic builtins with the help of some functions, here is a brief overview of them:
Builtin | Description | Options | Parameters | Helpful Functions |
---|---|---|---|---|
echo |
Prints arguments separated with a space followed by a new line | -n |
βοΈ | write |
cd |
Changes current working directory, updating PWD and OLDPWD |
β | βοΈ | chdir |
pwd |
Prints current working directory | β | β | getcwd |
env |
Prints environment | β | β | write |
export |
Adds/replaces variable in environment | β | βοΈ | β |
unset |
Removes variable from environment | β | βοΈ | β |
As mentioned previously, we use readline
to read the string containing the shell commands. To make it more interactive, readline
receives a string to be used as a prompt. We have tweaked the looks of it to be nice to use. The prompt is structured as follows:
Bash@Allah $>
These are a few neat extras that were not explicitly mentioned on the subject of the project but we thought would make the whole experience nicer
When running new instances of minishell or minishell withouth environment (env -i ./minishell
), some environment variables need to be updated manualy, namely the shell level (SHLVL
) or the _
variable
Here's the env when minishell is launched without an environment:
- Prerequisites
Make sure you have these packages installed:
gcc make python-norminette readline (valgrind on Linux)
Note for MacOS: to install a recent version of readline, you need to use homebrew: brew install readline
- Cloning the Repositories
git clone https://github.com/haytham10/minishell.git
cd minishell
make
As per the norm, this project compiles an executable called minishell
, and it is compiled using the .c
files.
As we developed the project, we recorded some demos of how the project looked. Here is an overview of the most relevant "releases" we made:
v1.0
Basic stuff working, no pipes or exit status redirection
v2.0
Pipes working, plus some exit statuses
v3.0
Heavily cleaned code, misc fixes, ready for submission :)
This was our biggest project yet, and it sure was challenging. Co-developing can be tricky. We had fun in the process though :)
This project is licensed under the MIT License - see the LICENSE file for details.
Octobre 27th, 2022