If this documentation is helpful for you, feel free to buy me a coffee ;)
Legend
🔝 Link to top of the page / table of content
🔗 Link to external website
📖 Link to another section
💻 Link to related.c file
📄 Link to related.h file
💡 Hint
✏️ Example (foldable section)
✅ Yes / Success
❌ No / Fail
🔝 Table of Contents
- Introduction
- Installation
- Usage [Info Mode]
- Definitions
- Syntax [Quotes, Seperators]
- Structs [t_mbox]
- Environment Variables
- Builtins [42, cd, echo, env, exit, export, help, history, infomode, pwd, unset]
- Exit Status
- Signals
- How Frankenshell Operates
- Known Bugs
- Acknowledgments
🔝 Introduction
As ChatGPT said in 2023:
Minishell is a 🔗42 school project that aims to create a simplified Unix shell using 🔗bash as a reference. The idea behind Minishell is to develop a basic command-line interface (CLI) that can execute simple commands and handle input/output redirection. It's meant to serve as a learning exercise for students to gain a fundamental understanding of how shells work by implementing core features like parsing user input, managing processes, handling signals, creating builtins and executing system commands.
🔝 Installation
$ git clone https://github.com/ahokcool/frankenshell.git # Clone
$ cd frankenshell # Change directory
$ make # Compile
$ ./frankenshell [OPTIONS] # Run (Option: --info)
$ echo "Let's goooooo!" # Use
Let's goooooo!
$ exit # Exit
💡 make stats provides you with some information about the files and the number of functions.
🔝 Usage
frankenshell is designed to mimic the behavior of the traditional 🔗bash shell. So feel free to use it like bash.
To activate the info mode you can:
If the info mode is activated, frankenshell will print the following information during runtime:
- Input String States (📖String Management)*
- Token types and values (📖tokenizing)
- A visual representation of the ast (📖parsing)
* Also as 📖readable strings
✏️ Example echo "Hello" $USER "" '!' | wc -l
$ ./frankenshell -i
--------------------------------------------------------------------------------
| INFO MODE ACTIVATED! |
--------------------------------------------------------------------------------
frankenshell--> echo "Hello" $USER "" '!' | wc -l
--------------------------------------------------------------------------------
| INPUT STATES |
--------------------------------------------------------------------------------
original: (echo "Hello" $USER "" '!' | wc -l )
trimmed: (echo "Hello" $USER "" '!' | wc -l)
empty quotes: shifted: (echo "Hello" $USER �� '!' | wc -l)
readable: (echo "Hello" $USER E_ '!' | wc -l)
shifted: shifted: (echo��Hello��$USER�����!����wc����-l)
readable: (echo_DHelloD_$USER_E__S!S_P_wc____-l)
expanded: shifted: (echo��Hello��astein�����!����wc����-l)
readable: (echo_DHelloD_astein_E__S!S_P_wc____-l)
==================================================================================
--------------------------------------------------------------------------------
| TOKENIZER |
--------------------------------------------------------------------------------
type:(0) token:(echo)
type:(0) token:(Hello)
type:(0) token:(astein)
type:(0) token:()
type:(0) token:(!)
type:(1) token:(|)
type:(0) token:(wc)
type:(0) token:(-l)
==================================================================================
--------------------------------------------------------------------------------
| PARSER |
--------------------------------------------------------------------------------
[arg] (-l)
[cmd] (wc)
[|] (|)
[arg] (!)
[arg] ()
[arg] (astein)
[arg] (Hello)
[cmd] (echo)
==================================================================================
--------------------------------------------------------------------------------
| EXECUTOR |
| (cmd count: 2) |
--------------------------------------------------------------------------------
1
--------------------------------------------------------------------------------
| EXECUTOR |
| LAST EXIT STATUS: 0 |
--------------------------------------------------------------------------------
==================================================================================
frankenshell-->
✏️ Example ls -l -a
$ ./frankenshell -i
--------------------------------------------------------------------------------
| INFO MODE ACTIVATED! |
--------------------------------------------------------------------------------
frankenshell--> ls -l -a
--------------------------------------------------------------------------------
| INPUT STATES |
--------------------------------------------------------------------------------
original: (ls -l -a)
trimmed: (ls -l -a)
empty quotes: shifted: (ls -l -a)
readable: (ls -l -a)
shifted: shifted: (ls�-l�-a)
readable: (ls_-l_-a)
expanded: shifted: (ls�-l�-a)
readable: (ls_-l_-a)
==================================================================================
--------------------------------------------------------------------------------
| TOKENIZER |
--------------------------------------------------------------------------------
type:(0) token:(ls)
type:(0) token:(-l)
type:(0) token:(-a)
==================================================================================
--------------------------------------------------------------------------------
| PARSER |
--------------------------------------------------------------------------------
[arg] (-a)
[arg] (-l)
[cmd] (ls)
==================================================================================
--------------------------------------------------------------------------------
| EXECUTOR |
| (cmd count: 1) |
--------------------------------------------------------------------------------
total 276
drwxrwxr-x 10 astein astein 4096 Jan 10 11:10 .
drwxrwxr-x 3 astein astein 4096 Dez 15 14:03 ..
-rwxrwxr-x 1 astein astein 6323 Dez 15 20:15 art.sh
-rwxrwxr-x 1 astein astein 1498 Dez 15 20:03 count_stats.sh
drwxrwxr-x 2 astein astein 4096 Jan 9 14:42 docs
-rw-rw-r-- 1 astein astein 1 Jan 10 11:35 file
-rwxrwxr-x 1 astein astein 208296 Jan 10 09:01 frankenshell
drwxrwxr-x 8 astein astein 4096 Jan 9 20:50 .git
-rw-rw-r-- 1 astein astein 215 Dez 15 20:03 .gitignore
drwxrwxr-x 3 astein astein 4096 Jan 10 11:55 images
drwxrwxr-x 2 astein astein 4096 Jan 7 11:45 includes
drwxrwxr-x 4 astein astein 4096 Jan 10 09:01 libft
-rw-rw-r-- 1 astein astein 3429 Jan 7 14:20 Makefile
drwxrwxr-x 6 astein astein 4096 Jan 10 09:01 obj
-rw-rw-r-- 1 astein astein 1638 Jan 6 17:33 README1.md
-rw-rw-r-- 1 astein astein 2587 Jan 9 16:30 README.md
drwxrwxr-x 6 astein astein 4096 Jan 7 14:17 src
drwxrwxr-x 2 astein astein 4096 Dez 15 20:22 .vscode
--------------------------------------------------------------------------------
| EXECUTOR |
| LAST EXIT STATUS: 0 |
--------------------------------------------------------------------------------
==================================================================================
frankenshell-->
✏️ Example echo the home dir of $USER is storred in '$HOME'
$ ./frankenshell -i
--------------------------------------------------------------------------------
| INFO MODE ACTIVATED! |
--------------------------------------------------------------------------------
frankenshell--> echo the home dir of $USER is storred in '$HOME'
--------------------------------------------------------------------------------
| INPUT STATES |
--------------------------------------------------------------------------------
original: (echo the home dir of $USER is storred in '$HOME')
trimmed: (echo the home dir of $USER is storred in '$HOME')
empty quotes: shifted: (echo the home dir of $USER is storred in '$HOME')
readable: (echo the home dir of $USER is storred in '$HOME')
shifted: shifted: (echo�the�home�dir�of�$USER�is�storred�in��$HOME�)
readable: (echo_the_home_dir_of_$USER_is_storred_in_S$HOMES)
expanded: shifted: (echo�the�home�dir�of�astein�is�storred�in��$HOME�)
readable: (echo_the_home_dir_of_astein_is_storred_in_S$HOMES)
==================================================================================
--------------------------------------------------------------------------------
| TOKENIZER |
--------------------------------------------------------------------------------
type:(0) token:(echo)
type:(0) token:(the)
type:(0) token:(home)
type:(0) token:(dir)
type:(0) token:(of)
type:(0) token:(astein)
type:(0) token:(is)
type:(0) token:(storred)
type:(0) token:(in)
type:(0) token:($HOME)
==================================================================================
--------------------------------------------------------------------------------
| PARSER |
--------------------------------------------------------------------------------
[arg] ($HOME)
[arg] (in)
[arg] (storred)
[arg] (is)
[arg] (astein)
[arg] (of)
[arg] (dir)
[arg] (home)
[arg] (the)
[cmd] (echo)
==================================================================================
--------------------------------------------------------------------------------
| EXECUTOR |
| (cmd count: 1) |
--------------------------------------------------------------------------------
the home dir of astein is storred in $HOME
--------------------------------------------------------------------------------
| EXECUTOR |
| LAST EXIT STATUS: 0 |
--------------------------------------------------------------------------------
==================================================================================
frankenshell-->
✏️ Example << $DONT_EXPAND cat | wc -l
$ ./frankenshell -i
--------------------------------------------------------------------------------
| INFO MODE ACTIVATED! |
--------------------------------------------------------------------------------
frankenshell--> << $DONT_EXPAND cat | wc -l
--------------------------------------------------------------------------------
| INPUT STATES |
--------------------------------------------------------------------------------
original: (<< $DONT_EXPAND cat | wc -l)
trimmed: (<< $DONT_EXPAND cat | wc -l)
empty quotes: shifted: (<< $DONT_EXPAND cat | wc -l)
readable: (<< $DONT_EXPAND cat | wc -l)
shifted: shifted: (���$DONT_EXPAND�cat���wc�-l)
readable: (II_$DONT_EXPAND_cat_P_wc_-l)
expanded: shifted: (��$DONT_EXPAND�cat���wc�-l)
readable: (II$DONT_EXPAND_cat_P_wc_-l)
==================================================================================
--------------------------------------------------------------------------------
| TOKENIZER |
--------------------------------------------------------------------------------
type:(2) token:(<)
type:(2) token:(<)
type:(0) token:($DONT_EXPAND)
type:(0) token:(cat)
type:(1) token:(|)
type:(0) token:(wc)
type:(0) token:(-l)
==================================================================================
--------------------------------------------------------------------------------
| PARSER |
--------------------------------------------------------------------------------
[arg] (-l)
[cmd] (wc)
[|] (|)
[cmd] (cat)
[<<] ($DONT_EXPAND)
==================================================================================
--------------------------------------------------------------------------------
| EXECUTOR |
| (cmd count: 2) |
--------------------------------------------------------------------------------
frankendoc> Hello
frankendoc> World
frankendoc> $DONT_EXPAND
2
--------------------------------------------------------------------------------
| EXECUTOR |
| LAST EXIT STATUS: 0 |
--------------------------------------------------------------------------------
==================================================================================
frankenshell-->
✏️ Example echo hi"" "" "" ""there
$ ./frankenshell -i
--------------------------------------------------------------------------------
| INFO MODE ACTIVATED! |
--------------------------------------------------------------------------------
frankenshell--> echo hi"" "" "" ""there
--------------------------------------------------------------------------------
| INPUT STATES |
--------------------------------------------------------------------------------
original: (echo hi"" "" "" ""there
)
trimmed: (echo hi"" "" "" ""there)
empty quotes: shifted: (echo hi"" �� �� ""there)
readable: (echo hi"" E_ E_ ""there)
shifted: shifted: (echo�hi�����������there)
readable: (echo_hiDD_E__E__DDthere)
expanded: shifted: (echo�hi�����������there)
readable: (echo_hiDD_E__E__DDthere)
==================================================================================
--------------------------------------------------------------------------------
| TOKENIZER |
--------------------------------------------------------------------------------
type:(0) token:(echo)
type:(0) token:(hi)
type:(0) token:()
type:(0) token:()
type:(0) token:(there)
==================================================================================
--------------------------------------------------------------------------------
| PARSER |
--------------------------------------------------------------------------------
[arg] (there)
[arg] ()
[arg] ()
[arg] (hi)
[cmd] (echo)
==================================================================================
--------------------------------------------------------------------------------
| EXECUTOR |
| (cmd count: 1) |
--------------------------------------------------------------------------------
hi there
--------------------------------------------------------------------------------
| EXECUTOR |
| LAST EXIT STATUS: 0 |
--------------------------------------------------------------------------------
==================================================================================
frankenshell-->
🔝 Definitions
These definitions are used throughout this manual as follows.
builtin (📖builtins)
A command that is implemented internally by the shell itself, rather than by an executable program somewhere in the file system.
exit status (📖exit status)
The value returned by a command to its caller. The value is restricted to eight bits, so the maximum value is 255.
signal (📖signals)
A mechanism by which a process may be notified by the kernel of an event occurring in the system.
token (📖tokenizing)
A sequence of characters considered a single unit by frankenshell.
cycle (📖processing a cycle)
The input is processed after the user hits the return key, which involves a series of steps: cleaning the input string, expanding variables, tokenizing, constructing an ast, setting up pipes, forking processes, and updating the exit status.
ast / tree (📖parsing)
The parser in Frankenshell constructs an abstract syntax tree (AST).
🔝 Syntax
The syntax of frankenshell is designed to mimic the syntax of the traditional 🔗bash shell.
Note that redirections and arguments can be in any order. The only rule is that the filename (or limiter for heredoc) must appear immediately after the redirection.
✏️ Examples
# The following examples are all behaving the same way,
# which is to write "Hello World" into the file "file".
$ echo Hello World > file
$ echo Hello > file World
$ echo > file Hello World
- Single Quotes:
Enclosing text in single quotes'prevents the shell from interpreting any metacharacters within the quoted sequence. - Double Quotes:
Using double quotes"also prevents interpretation of metacharacters, except for the dollar sign$, which is used for 📖variable expansion.
If you use single quotes inside double quotes, the single quotes will be interpreted as normal characters and vice versa.
💡 The outer quotes are always the contextual quotes.
💡 A contextual quote (single or double) must always be closed with the matching quote.
2.
✏️ Examples
$ echo "this single quote: ' is inside contextual quotes and therefore not contextual!"
this single quote: ' is inside contextual quotes and therefore not contextual!
$ echo 'this double quote: " is inside contextual quotes and therefore not contextual!'
this double quote: " is inside contextual quotes and therefore not contextual!
$ echo "this is a pipe symbol | inside contextual quotes"
this is a pipe symbol | inside contextual quotes
$ echo "this single quote ' doesn't close the double quote
frankenshell: syntax error: unclosed quotes
The following characters are used as seperators for the input string:
| Symbol | Description |
|---|---|
' " |
contextual quotes |
| |
pipe |
< |
redirection in |
<< |
frankendoc aka heredoc |
> |
redirection out |
>> |
redirection out append |
;, &, &&,||, (, ), {, }, *, [, ], ~, !, =, +, -, /, %, ^, @, #, :, ,, .
All structs are defined int the header file 📄structs.h.
The struct mbox is the main structure of the program: it is being passed as an argument to most of the functions and contains all the info needed for the program to work.
The file 💻manage_mbox.c contains the functions for initializing and destroying the mbox instance.
typedef struct s_mbox
{
char *inp_orig; // ( echo $USER "" > file)
char *inp_trim; // (echo $USER "" > file)
char *inp_eq; // (echo $USER E_ > file)
char *inp_shift; // (echo $USER E_ O file)
char *inp_expand; // (echo astein E_ O file)
int consecutive_lt; // << lol << lol
bool syntax_err_encountered; // track the first error
t_env *env; // env vars as linked list
t_list *history_lst; // history as linked list
t_token *tokens; // tokens as linked list
t_token *tmp_token; // temp token for building ast
t_ast *ast; // root of the ast
t_ast *tmp_node; // temp node for building ast
t_exec exec; // execution data
int count_cycles; // cycles for heredoc error msg
t_bool info_mode; // info mode on/off
} t_mbox;
This is general information about environment variables in frankenshell.
Related Sections:
📖variable expansion
📖heredoc
📖env
📖export
📖unset
📖exit status
On 📖programm start a linked list (t_env) will be created from the enviromental variables (char **env) and stored in the 📖t_mbox struct. Variables represent a simple key-value pair. Key and value are strings. The key is always unique and the value can be empty. On 📖termination, the linked list is freed.
With the following 📖builtin commands variables can be...
- created using 📖export
- shown using 📖env
- sorted and shown using 📖export without arguments
- changed using 📖export
- deleted using 📖unset
A key has to match the following 🔗regex:
^[a-zA-Z_]+[a-zA-Z0-9_]*$
Explanation:
^ # start of string
[a-zA-Z_] # first char must be a letter or '_'
+ # one or more chars
[a-zA-Z0-9_]* # zero or more letters, numbers or '_'
$ # end of string
The following functions are implemented in the 💻env_vars file:
MANAGEMENT
'initialize_vars' # creates the ll on startup
'free_vars' # frees the ll
'free_var' # free the given node
'var_add_back' # adds the node to the end of the ll
READ FUNCTIONS
'is_var' # checks if the argument 'key' is present in list
'get_var_node' # returns a pointer to the node with the given key
'get_var_value' # returns a pointer to the value of the given key
'get_vars_count' # returns the amount of nodes in the ll
'get_env_as_matrix' # creates a matrix of strings from the ll
WRITE FUNCTIONS
'set_var_value' # updates/creates node with the given key and value
'set_var_value_int' # updates/creates node with the given key and int value
'increment_shlvl' # increases the value of the 'SHLVL' variable
'unset_var' # removes the node with the given key from the ll
💡 The linked list will be used for the execve function call. Refer to section (📖run command).
💡 The linked list will be used for storing the 📖exit status.
🔝 Builtins
Each builtin command in frankenshell is detailed below with specific information and examples:
| Command | File(s) | Description |
|---|---|---|
📖42 |
💻42.c |
42 it is ;) |
📖cd |
💻cd.c |
Changes the current directory. |
📖echo |
💻echo.c |
Displays a line of text. |
📖env |
💻env.c |
Displays the environment variables. |
📖exit |
💻exit.c |
Exits frankenshell. |
📖export |
💻export.c💻 export_utils.c |
Sets or exports environment variables. |
📖help |
💻help.c |
Displays this documentation page on github |
📖history |
💻history.c |
Displays the command history. |
📖infomode |
💻infomode.c |
Toggles the info mode. |
📖pwd |
💻pwd.c |
Prints the working directory. |
📖unset |
💻unset.c |
Unsets environment variables. |
The builtin 42 displays a 42 logo to STDOUT (or its redirection).
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 to n (all args will be ignored) |
| Exit Status | 0 |
| Affected Variables | [NONE] |
| File | 💻42.c |
Examples
| CMD | STDOUT | Explanation |
|---|---|---|
42 |
[the 42 logo] | |
42 foo bar |
[the 42 logo] | all args will be ignored |
The builtin cd runs a few checks to ensure the provided path is valid. Once it's all good, it uses the external function chdir to change the current working directory (wd) to this new path. At the same time, it updates the PWD variable to the new directory and OLDPWD to the previous one (if they exist).
chdir fails, an error message is printed and the exit status is set to 1.
💡 If PWD and/or OLDPWD are absent, the function operates normally and skips setting these variables.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 - 1 |
| Argument Format | absolute or relative path |
| Exit Status | 0 1 |
| Affected Variables | HOME OLDPWD PWD |
| File | 💻cd.c |
Examples
| CMD | STDERR | Exit Status | Explanation | Affected Variables1 |
|---|---|---|---|---|
cd |
0 |
if HOME is set |
HOME OLDPWD PWD |
|
cd |
cd: HOME not set |
1 |
if HOME is not set |
|
cd "" |
0 |
empty argument; wd doesn't update |
||
cd valid_path |
0 |
wd updates to ./valid_path |
OLDPWD PWD |
|
cd . |
0 |
wd doesn't update; OLDPWD updated! |
OLDPWD |
|
cd .. |
0 |
wd updates to parent folder |
OLDPWD PWD |
|
cd foo bar |
cd: too many arguments |
1 |
||
cd noExist |
frankenshell: cd: noExist: No such file or directory |
1 |
||
cd noPermDir |
frankenshell: cd: noPermDir: Permission denied |
1 |
||
cd file |
frankenshell: cd: file: Not a directory |
1 |
1 Bold variables will be updated.
The builtin echo outputs the strings it is given as arguments, seperated by one space each, to STDOUT (or its redirection). The flag -n can be used to prevent the trailing newline that is normally printed.
Attributes
| Attribute | Details |
|---|---|
| Flags | -n |
| Number of Arguments | 0 - n |
| Argument Format | all ASCII chars allowed |
| Exit Status | 0 |
| Affected Variables | [NONE] |
| File | 💻echo.c |
Examples
| CMD | STDOUT | LINEBREAK |
|---|---|---|
echo foo |
foo |
✅ |
echo "" '' foo |
foo |
✅ |
echo --n foo |
--n foo |
✅ |
echo -n foo |
foo |
❌ |
echo -n -nn -nnn foo |
foo |
❌ |
echo -n -nbar foo |
-nbar foo |
❌ |
echo -n foo -n |
foo -n |
❌ |
The builtin env outputs all variable key-value pairs of the linked list like key=value\n
💡 Refer to the section 📖environment variables for more information about environment variables.
env and export.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 |
| Exit Status | 0 127 |
| Affected Variables | [all] |
| File | 💻env.c |
Examples
| CMD | STDOUT | STDERR | Exit Status |
|---|---|---|---|
env |
... USER=astein HOME=/home/astein LANGUAGE=en ... |
0 |
|
env foo |
env: ‘foo’: No such file or directory |
127 |
|
env foo bar |
env: ‘foo’: No such file or directory |
127 |
The builtin exit terminates the calling process, outputs exit to STDERR and if provided with a numeric argument, it sets the exit status to that argument's value. If the argument exceeds 255, it will be subjected to modulo 256.
💡 Refer to the section 📖exit status for more information about exit status.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 - 1 |
| Argument Format | numeric [max long long] |
| Exit Status | 0 - 255 |
| Affected Variables | [NONE] |
| File | 💻exit.c |
Examples
| CMD | STDERR | Exit Status | Terminates |
|---|---|---|---|
exit |
exit |
[doesn't update] |
✅ |
exit 42 |
exit |
42 |
✅ |
exit -42 |
exit |
214 |
✅ |
exit 424242 |
exit |
50 |
✅ |
exit foo |
exit frankenshell: exit: foo: numeric argument required |
2 |
✅ |
exit foo 42 |
exit frankenshell: exit: foo: numeric argument required |
2 |
✅ |
exit foo bar |
exit frankenshell: exit: foo: numeric argument required |
2 |
✅ |
exit 42 42 |
exit frankenshell: exit: too many arguments |
11 |
❌ |
exit 42 foo |
exit frankenshell: exit: too many arguments |
11 |
❌ |
1 The exit status will only be updated to 1 if the last exit status was 0.
The builtin export updates (or creates) the enviromental variables inputed as key value pairs like key1=value1 key2=value2. If no argument is given, it will instead output all variables in alphabetical order.
💡 Refer to the section 📖environment variables for more information about environment variables.
env and export.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 - n |
| Argument Format | key=value |
| Exit Status | 0 1 |
| Affected Variables | [key] |
| Files | 💻export.c 💻 export_utils.c |
Examples
| CMD | Equal Sign | Valid Key1 | STDOUT | STDERR | Exit Status | Affected Variables2 |
|---|---|---|---|---|---|---|
export |
... declare -x LANG="en_US.UTF-8" declare -x LANGUAGE="en" ... 3 |
0 |
||||
export @ |
❌ | ❌ | frankenshell: export: `@': not a valid identifier |
1 |
||
export @=foo |
✅ | ❌ | frankenshell: export: `@=foo': not a valid identifier |
1 |
||
export foo |
❌ | ✅ | 0 |
|||
export foo=bar |
✅ | ✅ | 0 |
foo |
||
export foo=bar school=42 |
✅ | ✅ | 0 |
foo school |
1 Refer to the section 📖Environment Variables for more details about the key syntax.
2 Bold variables will be updated.
3 In alphabetical order with prefix declare -x .
The builtin help displays the documentation page on github.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 to n (all args will be ignored) |
| Exit Status | N/A |
| File | 💻help.c |
Examples
| CMD | Explanation |
|---|---|
help |
opens the documentation |
help 42 |
opens the documentation all args will be ignored |
The builtin history outputs all previous user input in a numbered list to STDOUT (or its redirection). To do so, it uses a linked list stored in the 📖t_mbox struct. 📖Each cycle, the input string is added to the list. On 📖termination, the linked list is freed.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 to n (all args will be ignored) |
| Exit Status | 0 |
| File | 💻history.c |
Examples
| CMD | STDOUT |
|---|---|
history |
1 echo hi 2 ls ... n history |
history foo bar |
1 echo hi 2 ls ... n history |
The builtin infomode toggles the info mode feature.
💡 Refer to the section 📖Info mode for more details about the info mode.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 to n (all args will be ignored) |
| Exit Status | 0 |
| File | 💻infomode.c |
Examples
| CMD | STDERR |
|---|---|
infomode |
INFO MODE ACTIVATED! |
infomode |
INFO MODE DEACTIVATED! |
The builtin pwd outputs the current wd using the external function getcwd. Like in bash all arguments will be ignored.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 to n (all args will be ignored) |
| Exit Status | 0 |
| Affected Variables | [NONE] |
| File | 💻pwd.c |
Examples
| CMD | STDOUT | Explanation |
|---|---|---|
pwd |
/mnt/daten/GIT/frankenshell |
|
pwd foo bar |
/mnt/daten/GIT/frankenshell |
all args will be ignored |
The builtin unset deletes the corresponding variables.
💡 Refer to the section 📖environment variables for more information about environment variables.
Attributes
| Attribute | Details |
|---|---|
| Flags | N/A |
| Number of Arguments | 0 to n |
| Argument Format | key |
| Exit Status | 0 |
| Affected Variables | [key] |
| File | 💻unset.c |
Examples
| CMD | Explanation | Exit Status | Affected Variables |
|---|---|---|---|
unset |
nothing happens | 0 |
|
unset foo bar |
the variables foo and bar will be deleted |
0 |
foo bar |
🔝 Exit Status
The current exit status of minishell is stored as an node with the key ? in the linked list of 📖environment variables.
The exit status will be updated in the following cases:
- on 📖startup to
0 - unclosed 📖quotes to
2 - syntax errors
- a command is not found
- a file is not found
- a permission is denied
- a child process terminates
- a sigle 📖builtin finished execution
- a 📖signal is received
Storing the exit status in the enviroment variables linked list simplifies the variable expansion of $?. $? always expands to the current exit status.
💡 Anyhow, if the linked list is used for execve, env or export the ? node will be ignored.
Each time a fork is happening all existing processes will be set to a specific signal status via the function conf_sig_handler in 💻signals.c. This signal status will
be used to determine which signal handling should be used in the regarding process. On 📖programm start the signal status will be set to SIG_STATE_MAIN.
The following Signals are treated in frankenshell:
| Shortcut | Signal Name | Description |
|---|---|---|
CTRL + C |
SIGINT | Interrupt signal |
CTRL + \ |
SIGQUIT | Quit signal |
CTRL + D |
EOF | An EOF is send to the input fd |
The following chart shows how the signal status is set for each process (explained on an example):
The following table shows all possible signal states defined in 📄config.h
| Signal State | CTRL + C |
CTRL + \ |
CTRL + D |
|---|---|---|---|
SIG_STATE_MAIN |
new promt exit status 130 |
ignored | exit frankenshell |
SIG_STATE_PARENT |
\n |
Quit (core dumped)\n |
default |
SIG_STATE_IGNORE |
ignored | ignored | default |
SIG_STATE_CHILD |
default | default | default |
SIG_STATE_CHILD_BUILTIN |
default | defatult | default |
SIG_STATE_HD_CHILD |
exit heredoc | ignored | exit heredoc with warning |
🔝 How Frankenshell Operates
The main tasks of frankenshell can be grouped into those steps:
- 📖Initialization
- 📖Processing Cycles (loop until
exitis called) - 📖Termination
💡 Below you can find a detailed description of each step.
On startup the program will initialize the 📖t_mbox struct. This struct contains all the information needed for the program to work. It is passed as an argument to most of the functions.
The following steps are executed during initialization:
- creating a linked list for the 📖environment variables
- creating an array for the 📖builtin commands to connect the command name with the function pointer
- seting the 📖signal status to
SIG_STATE_MAIN - setting the 📖exit status to
0 - setting the 📖info mode to
false(ortrueif the argument-i,--infois provided) - initializing all other filedescriptors and variables to
-1,0orNULL - displaying the promt
frankenshell--> - starting to read the input
All the following steps are executed for each cycle:
💡 Below you can find a detailed description of each step.
| Step | Example* |
|---|---|
| Finished Reading | echo "Hello" $USER "" '!' | wc -l |
| 📖Trim Input | echo "Hello" $USER "" '!' | wc -l |
| 📖Mark Empty Quotes | echo "Hello" $USER E_ '!' | wc -l |
| 📖Shift Separators | echo_DHelloD_$USER_E__S!S_P_wc____-l |
| 📖Variable Expansion | echo_DHelloD_astein_E__S!S_P_wc____-l |
*The 📖readable input string characters are displayed!
Special Characers will be 📖shifted to negative ASCII values. Therefore they are nor longer printable. To still be able to display the input string, the following characters will be used to represent the special characters:
| Character | Description |
|---|---|
_ |
No Space |
E |
Empty Token |
S |
Contextual Single Quotes |
D |
Contextual Double Quotes |
P |
Pipe |
I |
Input Redirection |
O |
Output Redirection |
💡 Activate the 📖info mode to see all input string states during runtime
First step is to trim the input. This means that all leading and trailing whitespaces are removed.
💡 Activate the 📖info mode to see all input string states during runtime
If somwehre in frankenshell we loop through an input string, sometimes we need to know if we are inside or outside of contextual quotes. The quote state function updates the quote_state variable to the current state of the quotes. While traversing the input string, the quote state will be updated for each character. If the current character is inside a quote, the quote state will be set to the matching quote. If the current character is outside of a quote, the quote state will be set to OUT_Q.
Logic:
if quote_state is OUT_Q (the cur_char is outside of contextual quotes)
if cur_char is a quote
update quote_state to the cur_char
else
quote_state stays OUT_Q
else (the cur_char is inside of contextual quotes)
if matching quote is found
update quote_state to OUT_Q
else
quote_state stays the same
✏️ Example echo "Hello" $USER "" '!' | wc -l
Trimmed String: echo "Hello 'astein", 'how are you?'
Quote State: OOOOODDDDDDDDDDDDDDOOOSSSSSSSSSSSSSO //O = OUT_Q, D = IN_DOUBLE_QUOTES, S = IN_SINGLE_QUOTES
Marked String: echo DHello 'asteinD, Show are you?S
Bash generates an empty token for a pair of empty contextual quotes if the two conditions are met:
- the empty quotes are not inside another pair of quotes
(refer toOUT_Qas the 📖quote state) - the empty quotes are surrounded by at least one whitespace character (on the beginning AND on the end of the string)
Empty quotes will be marked as EMPTY_TOKEN and NO_SPACE characters.
# define NO_SPACE -125
# define EMPTY_TOKEN -126
✏️ Examples
| inp_trim | inp_eq* | empty quotes |
|---|---|---|
echo "" |
echo E_ |
✅ |
echo '' |
echo E_ |
✅ |
echo """" |
echo E___ |
✅ |
echo "" "" "'hi" |
echo E_ E_ "'hi" |
✅ |
echo hi"" |
echo hi"" |
❌ |
echo hi'' |
echo hi'' |
❌ |
echo ""hi |
echo ""hi |
❌ |
echo ''hi |
echo ''hi |
❌ |
echo '""' |
echo '""' |
❌ |
echo "''" |
echo "''" |
❌ |
💡 Refer to the section 📖Shift Separators for a better understanding of the marked input string.
💡 Refer to the section 📖Readble Input String for a better understanding of the marked input string.
💡 Activate the 📖info mode to see all input string states during runtime
To prepare the input string for the tokenizer all seperating characters need to be found. To mark them frankenshell shifts their ASCII value by -126. This makes an easy check for all of them possible (ASCII < 0) without loosing their original value. A seperating char needs to be outside of any quotes to be shifted.
Those are the characters which we consider as seperating characters:
- whitespace (
,\n,\t,\v,\a,\b,\f,\r) - pipe (
|) - redirections (
<,>)
✏️ Examples
| inp_trim | inp_eq* | inp_shift* |
|---|---|---|
echo Hi > file |
** | echo_Hi_O_file |
echo Hi >> file |
** | echo_Hi_OO_file |
< file cat |
** | I_file_cat |
<< file cat |
** | II_file_cat |
echo Hi | wc |
** | echo_Hi_P_wc |
echo """" > file |
echo E___ > file |
echo_E____O_file |
echo "" "" "'hi" | wc |
echo E_ E_ "'hi" | wc |
echo_E__E__D'hiD_P_wc |
* 📖Readable Input String
** No empty quotes, so the same then inp_trim.
The 📖environment variable expansion works similar like in 🔗bash.
- Variable Expansion happens if the variable is not inside single 📖quotes.
- Whitespaces inside the variable value will be marked as
NO_SPACEcharacters (📖). Therefore the 📖tokenizer can make multiple tokens out of it. - If the variable is part of the 📖heredoc limiter it won't be expanded! (e.g.
<< $USER cat)
💡 Activate the 📖info mode to see the expanded input string states during runtime.
✏️ Examples
| Command | Valid Key | Expand | Output |
|---|---|---|---|
echo $USER |
✅ | ✅ | astein |
echo "$USER" |
✅ | ✅ | astein |
echo '$USER' |
✅ | ❌ | $USER |
<< $USER cat |
N/A | ❌ | Won't expand, so the EOF of the 📖heredoc will be $USER |
✏️ Examples (Special Cases)
| Special Case | Valid Key | Expand | Explanation | Output (example) |
|---|---|---|---|---|
echo $@ hi |
❌ | ❌ | first char of false key gets swallowed | hi |
echo $@@ hi |
❌ | ❌ | first char of false key gets swallowed | @ hi |
echo $1HOME |
❌ | ❌ | first char of false key gets swallowed | HOME |
$ |
❌ | ❌ | no key | $: command not found |
echo $ hi |
❌ | ❌ | no key | $ hi |
$? |
❎ | ✅ | 📖exit status of the last command | 42: command not found |
echo $? |
❎ | ✅ | 📖exit status of the last command | 42 |
echo $?? |
❎ | ✅ | 📖exit status of the last command | 42? |
echo $"USER" |
❌ | ❌ | the " block the key; contextual quotes get removed |
USER |
echo "foo $'BAR'" |
❌ | ❌ | the ' block the key; contextual quotes get removed |
foo $'BAR' |
echo 'foo $"BAR"' |
N/A | ❌ | inside contextual quotes ' -> no expansion |
foo $"BAR" |
echo 'foo $BAR' |
N/A | ❌ | inside contextual quotes ' -> no expansion |
foo $BAR |
echo foo$USER$HOME |
✅ ✅ | ✅ ✅ | the second $ is not an allowed char of a key therfore it terminates the first key. |
fooastein/home/astein |
echo foo $NOTEXIST bar |
✅ | ✅ | the key doesn't exist; expands to NULL |
a b |
If there are whitespaces in the expanded Variable they will be marked as NO_SPACE characters. Therefore the tokenizer will make multiple tokens out of it. Quote Protection of whitespaces is not supported inside a variable. (like in 🔗bash).
✏️ Examples
frankenshell--> export foo="Hello World"
frankenshell--> echo $foo
Hello World
frankenshell--> export foo="Hello' 'World" //tries a quote protect of whitespaces
Hello' 'World //doesn't work
The variable expansion for a heredoc limiter is a special case. Variable Expension is not allowed inside a heredoc limiter! Anyhow there some strange rules for the determine the limiter:
✏️ Examples
| Case | Limiter (send to tokenizer) |
Explanation | Limiter 1 (to exit hd) |
Var. expansion 2 (inside heredoc) |
|---|---|---|---|---|
<< $USER cat |
$USER |
$USER |
✅ | |
<< "FOO BAR" cat |
"FOO BAR" |
FOO BAR |
❌ | |
<< "$FOO $BAR" cat |
"$FOO $BAR" |
$FOO $BAR |
❌ | |
<< $'FOO' cat |
'FOO' |
$ is followed by contextual '; the $ will be removed |
FOO |
❌ |
<< $"FOO" cat |
"FOO" |
$ is followed by contextual "; the $ will be removed |
FOO |
❌ |
<< $"FOO"$"BAR" cat |
"FOO""BAR" |
twice: $ is followed by contextual quotes; the $ will be removed |
FOOBAR |
❌ |
1 The contextual quotes will be removed by the 📖heredoc function.
2 Different topic: Refer to the section 📖heredoc for the variable expansion inside a heredoc.
The 📖expanded input string will be tokenized. This means that the input string will be split into tokens. Each token will be marked with a type. The following token types exist:
enum e_token_type
{
WORD_TOKEN,
PIPE_TOKEN,
RED_IN_TOKEN,
RED_OUT_TOKEN,
};
To generate the tokens first the input string will be splited into an array. Therefore the NO_SPACE character is used as an seperator. Then a loop will go through the array and generate the tokens. The following table shows the token generation logic:
| Logic | Example |
|---|---|
| check for forbidden spaces between redir symbols | < < file cat |
if seperating characters in array entry (|, <, >, ', ") |
ls|wc |
| if quote state == outside quotes | ls|wc |
| create token with value: everything until next seperator found | token: ls |
| else | "ls|wc" |
| create token with value: everything until matching quote found | token: ls|wc |
| else | ls |
| create token with value: full array entry | token: ls |
✏️ Example echo "Hello" $USER "" '!' | wc -l
| Type | Value |
|---|---|
| WORD_TOKEN | echo |
| WORD_TOKEN | Hello |
| WORD_TOKEN | astein |
| WORD_TOKEN | |
| WORD_TOKEN | ! |
| PIPE_TOKEN | | |
| WORD_TOKEN | wc |
| WORD_TOKEN | -l |
💡 Activate the 📖info mode to see the token list during runtime
After 📖tokienizing the input string, the tokens will be parsed into an AST. If the AST couldn't be build the input contains an syntax error (e.g. ls | | wc). If a vaild AST could be built, it will be used for the 📖execution.
Each node of the ast tree is an instance of the t_ast struct. It therefore has a type, a content and a two pointers to it's left and right child node.
This table shows all possible node types and their possible node conections:
| Node Type | Left Child | Right Child |
|---|---|---|
PIPE |
CMD |
PIPE CMD |
CMD |
REDIR NULL |
ARG NULL |
REDIR |
REDIR NULL |
NULL |
ARG |
NULL |
ARG NULL |
To build a vaild AST we use the following logic (BNF Notation):
<job> : <command> '|' <job>
| <command>
;
<command> : <token list>
;
<token list> : [name] <token list>
| [arg] <token list>
| <redir> <token list>
;
<redir> : <redir in>
| <redir out>
;
<redir in> : '<<' [file]
| '<' [file]
;
<redir out> : '>>' [file]
| '>' [file]
;
The following ast is visualiszed in a tree-like structure (left to right):

💡 Activate the 📖info mode to see the ast tree during runtime
Possible syntax errors could be:
frankenshell: syntax error: unclosed quotes
frankenshell: syntax error near unexpected token `<'
frankenshell: syntax error near unexpected token `>'
frankenshell: syntax error near unexpected token `|'
frankenshell: syntax error near unexpected token `newline'
frankenshell: syntax error near unexpected token `foo'
💡 The boolean syntax_err_encountered in the 📖t_mbox struct makes sure, that even if there are multiple errors, only the first error will be printed (like bash does).
The execution of the ast is handled by the file 💻execute_ast.c.
The execution may contain the following steps. Checkout those sections for more details about the execution steps:
Note
- Before the execution starts a vaild ast must be created. (📖Parsing)
- If the ast couldn't be created the execution will be skipped.
- The executor traverses the ast tree always from left to right.
- This ensures that pipes and redirections will always be setup before any command is executed.
- All commands are executed in a child process.
- Exception: Single Builtin cmds
- Pipes (
|) allow the output of one command to be used as input for another, enabling command chaining.- Note: Redirections to pipes might be overwritten by 📖redirections to files / 📖heredoc.
- All childs will be spawn right after each other (so before the previous child terminates).
- Exception Heredoc: Before spawning a child process the parent process checks if the child includes a heredoc. In this case it waits until the child is finished. (like bash)
- The next child process waits (because of the open pipe fd) until the previous child process is finished.
- The parent waits for all childs to finish before it continues with the next cycle. Each time a child process is finished, the parent process updates the 📖exit status of the terminated child process.
The 🔗BPMN diagram below shows the main execution logic:
Here the command will be setup. This means that a pipe* and a fork command will be executed.
The child process will then 📖setup the redirections and 📖run the command.
* Exception: Single or Last Commands don't need a pipe!
The 🔗BPMN diagram below shows the execution logic for seting up the commands:
Redirections allow a flexible manipulation of command input and output, similar to standard bash functionality. The table below describes the redirection features available in frankenshell:
| Feature | Mode | Description | Example |
|---|---|---|---|
< |
Input Redirection | Redirects input from a file to a command. | wc < file.txt |
<< |
Heredoc | Allows inputting multiple lines until a termination string is reached. | << this_exits_the_hd wc |
> |
Output Redirection | Redirects the output of a command to a file, overwriting it if it already exists. | echo "replace by foo" > file.txt |
>> |
Append Output Redirection | Appends command output to a file, creating it if it doesn't exist. | echo "append foo" >> file.txt |
Since there can be multiple redirections we need to keep track if one of them fails. Since bash only troughs one error.
Example:
$ < file_not_exists < file_not_exists_2 cat
bash: file_not_exists: No such file or directory
The 🔗BPMN diagram below shows the execution logic for seting up the redirections:
The heredoc redirection allows inputing multiple lines until a termination string (aka limiter) is read, an EOF Signal (CTRL + D)1, or an SIGINT Signal (CTRL + C)2 is received. The heredoc runs always in a child process and is connected to the command with a pipe. The heredoc uses readline to read the user input. Variable Expansion inside the heredoc is supported!3,4
1 frankenshell: warning: frankendoc at line 1 delimited by end-of-file (wanted `foo')
2 Checked via the global variable: g_signal_status == SIGNAL_EXIT_HD
3 Like in bash: The expansion is not supposed to work if the limiter contains contextual quotes! Therefore the 📖extract limiter part of the 📖variable expansion obtains '," and $ characters.
4 The heredoc can't be terminated if the expanded input matches the limiter! (first check matching limiter, then expand variable)
The 🔗BPMN diagram below shows the heredoc execution logic:
Since until now everything is already setup, the command can be executed.
📖Builtin commands just need to call the function pointer.
External / System commands need to be executed via execve. Therefore the command path is searched in the 📖environment variables. If the command is not found, the 📖exit status will be set to 127 and an error message will be printed. To call execve the command arguments and the enviomental variables need to be converted into an array of strings.
The 🔗BPMN diagram below shows the execution logic for running an command:
After (successful or not) 📖executing the ast, all cycle realted variables will be freed. This includes:
- input strings
- tokens
- ast
Also all open filedescriptors will be closed.
All cycle realted variables will be freed on the end of each 📖each cycle. Therfore on termination of frankenshell only those variables (linked lists) need to be freed:
🔝 Known Bugs
export vs env
If the built-in export is called with a valid key but no equal sign (like export foo) foo should be listed in the output of the command export, but not in the output of the command env function call. In frankenshell, foo is not added to the linked list of variables and therefore is not printed in either case. A simple boolean in the s_env structure could solve this problem.
single cmds with redirs
Since frankenshell doesn't fork for a single builtin cmd like echo foo > out it changes the fds of the main process. This might affect the next cycle since the fds are not reseted.
🔝 Acknowledgments
Thx to those guys and gals for hints, tipps and feedback!
@Gabriel
@Martim
@Margarida
@Manuel
If this documentation is helpful for you, feel free to buy me a coffee ;)


