Nautilus is a Unix shell written in C that allows users to interact with the operating system through commands. It includes several features commonly found in Unix shells, such as input/output redirection and multipiping. This project is part of our Operating Systems course in the 3rd year of Computer Science.
- Hector Miguel Rodriguez Sosa - Group C311\
- Sebastian Suarez Gomez - Group C311
- Input redirection from a file instead of the keyboard (
<)\ - Output redirection to a file instead of the console (
>)\ - Output redirection to a file but appending instead of overwriting
(
>>)\ - Multipiping, which allows chaining multiple commands by using the
output of one command as the input of the next (
|)\ cd: lets the user change the working directory\help: provides information about the shell's functionalities\exit: exits the shell\history: displays the last 10 commands used on screen\again: given an index 0--9, re-executes the command from the history at that index\Ctrl+C: kills the current process using signals
To use Nautilus, simply clone the repository, compile the main.c file,
and run the generated executable from the root folder. An example using
GCC is:
gcc -o main main.c && ./mainThe shell will display a prompt like:
nautilus $ You should then type your commands there. For more detailed information,
type help.
Command reading is handled in the method ntl_loop, which prints the
shell prompt as long as the entered command is empty or when a command
finishes. It then passes execution to the ntl_execute method
(L532), whose role is to handle some edge cases and, above all, to
split the input line into commands and separators. Then the
ntl_parsing method (L391) iterates over the commands and
separators and executes each command depending on which separators
surround it.
Any command can be executed using the execvp(...) function. This can
be seen in the ntl_launch function (L337), which forks the main
command (the shell) and passes it to execvp. If it returns an error,
the same error is propagated back.
The other arguments of ntl_launch are used for piping and input/output
redirection. There are 6 possible options:
0. The command is plain (normal input and output).\
- Output goes to a file (
>).\ - Input comes from a file (
<).\ - Output is appended to a file (
>>).\ - Both input and output use files (e.g.,
command1 < file1 > file2).\ - Same as 5 but with output append (
command1 < file1 >> file2).
Piping is explained in help multi-pipe.
Our shell allows sending a Ctrl+C while a command is running.
To implement this, we first created a signal handler (sig_handler)
that captures any incoming SIGINT. It then passes it to the parent
process (the shell), which warns the child by sending the same SIGINT.
If the child decides to ignore it for any reason, a variable is set
marking that a SIGINT has already been sent. If another SIGINT is sent,
then the process is forcefully killed with a SIGTERM.
The signal handler is assigned in the method at (L24), which also ensures the process is running in the foreground and sets the shell as the parent of all processes. The handler itself is set at (L32) and (L33), while the handler function is defined at (L56).
There is a small bug: when a process is killed by SIGINT, the shell sometimes displays the prompt twice. Another related issue is that when builtin commands are killed, multiple prompts may be displayed.
The builtins history and again were implemented. The history
builtin (L184) reads the contents of a pre-created file, which
stores the last 10 commands.
Commands are stored right after a line is read using the
append_to_history function (L215). Since only the last 10 commands
can be stored, this function maintains the file as a circular queue: if
there are already 10 commands when adding a new one, the oldest one is
removed and the new one is added.
The again builtin (L289) allows the user to type again {index},
which retrieves the command from history at that index and passes it to
ntl_execute(). That command is also saved in history again.
For piping, the pipe() function is not used. Instead, buffer files are
used to store the output of one command and provide it as input to the
next.
In practice, this works just like normal piping. In simple terms, when
executing command1 | command2, the shell actually interprets it as
command1 > buffer_file1 ; command2 < buffer_file1. The buffer file is
always deleted afterwards.
This approach makes the implementation easier, since it reuses the same
logic already implemented for < and >. It's similar to using
pipe(), but instead of a file descriptor, it uses an ephemeral real
file inside the folder.
Piping starts at (L458). When the next operator is detected to be a pipe, it enters "pipe mode," where three cases can occur:\
- The first command is executed with
ntl_launch **(L337)**using option 1 (write to buffer file).\ - The last command is executed with option 2 (read from buffer file).\
- Middle commands are trickier: as with
pipe(), reading and writing from the same file causes errors. So two buffer files are alternated. If one command reads from buffer1, the next command writes to buffer2. On the next iteration, they are swapped again.