The file already contains the precompiled files and you can launch the shell just by running ./cash
.
If however, you wish to recompile the shell, run the following commands
make
./rash
The prompt string is generated by the printf
statement method in the main while loop. The Prompt string consists of 3 parts.
- The name of the user. An instance of
struct passwd
is populated with the current user's details usinggetpwuid(getuid())
. The current user's name is inpasswd->pw_name
. - The hostname of the system. Copied into a buffer using the
gethostname()
method. - The current working directory of the shell. Copied into a buffer using the
getcwd()
method.
Important: If any of the above functions fail, the prompt string cannot be generated, and the shell exits abnormally due to potentially insufficient run-time permissions.
The following built in commands were required to be handled:
echo
: Since by the timeecho()
is called frombuiltin_commands.c
, the string has been stripped of extra white spaces, the function just prints the command, skipping the first 5 characters ('e', 'c', 'h', 'o', ' ').pwd
: Gets the present working directory of the shell usinggetcwd()
.cd
: Skipping the first 3 characters of the input, ('c', 'd', ' '), leaves us with the path tocd
to. If the path is absolute, or relative to the current working directory, a singlechdir()
suffices. However, if the path is relative to~
, we fistchdir()
to~
using the absolute path to~
which is stored as a global variable, then we skip the first two characters of the provided path ('~', '/') andchdir()
again to the rest of the path.
Implemented in ls()
in functions.c
. Here is how it works:
- Assign a pointer to each argument in the command.
- Iterate over the pointers and identify the flags. Set
l_flag
anda_flag
as required.
If the given command is not recognized as one of those mentioned above, or pinfo
(next specification) or some others (mentioned later), it is treated as a system command. If the last character of the command string is &
, the background_process()
method is called to launch it as a background process, otherwise the foreground_process()
method is called to launch it as a foreground process. Both background_process()
and foreground_process()
are defined in functions.c
.
In both these functions, a pointer is assigned to each space seperated argument. If one of the arguments provided has a path realtive to ~
, it is expanded into its equivalent absolute path.
We then use fork()
to create a child process and check if it was successful. In the child process, if it is in background_process()
, we change the child's process group using setgid(0, 0)
, so that it is sent to the background, and cannot read from the terminal. It can however still write to the terminal if needed.
We then use execvp()
to load a new executable image onto the child process. If it is successful, the process (foreground/background) starts execution running. If not, an error message is printed regarding the given command and the process is exited from.
On the other hand, in the parent process, if it is in foreground_process()
, we wait for the child to exit and then claim the resources using wait()
. In background_process()
however, we insert the process into the child pool (explained in specification 6) and return to the prompt.
Implemented in pinfo()
in functions.c
. It is first checked if there is a single argument or two arguments. If it is just one, then we need to get information of the current process, whose pid is obtained using the getpid()
method. Otherwise, the pid is extracted from the command provided. Then contents of /proc/<pid>/stat
are read into a buffer, and each space seperated value is assigned a pointer.
- The first pointer points to the pid, but this can also be read from the command given.
- The third pointer points to the process status.
- The twenty third pointer points to the memory.
We then read the link whose path is /proc/<pid>/exe
to get the path to the executable and print it. If however, the mentioned process is a zombie process, this link won't exist, so we print an error message in its place.
The way this shell has been implemented, sets a cap on the maximum number of child processes that it can have running simultaneously to 512. A custom structure, struct child
, defined in definitions.h
, stores a process pid and corresponding name. We initilize an array of 512 such stuctures and set all their pids to -1
using the init_child_list()
method in functions.c
, indicating that that particular process is empty. This array is referred to as the child pool.
In main()
, one of the first things we do is install a signal handler using the init_child_process_handler()
method defined in utilities.c
, that will run each time a SIGCHLD
signal is sent to the shell. This is done using an instance of struct sigaction
. Note that no extra functionality that is provided by sigaction
but not signal()
has been used. sigaction
has been chosen because it is the newer, and arguably better approach to define custom signal handlers.
When a new background process is created, it is first checked that current number of child processes is less than the maximum limit. If not, the child process is not created and an error is shown. Otherwise, we fill the pid and process name in the first empty location in the array.
When a background process is terminated, the handler is called. Within the handler, we loop over all the children who have exited since the shell recieved the SIGCHLD signal, using waitpid()
. By default waitpid()
is blocking, which is not desirable in this situation, so we make use of the WNOHANG
flag to make it non-blocking. For each pid that we get from waitpid()
, we look for it in the child pool, and get its name by matching its pid. We print the termination message with the name and pid of the process and "normally" or "abnormally" depending on the exit status of the process. We then replace the pid of that element in the array with -1
to indicate that that position in the pool is free to be occupied by another child process.
Finally, the prompt string is printed again as an aesthetic feature.
The execute_command()
method in functions.c
has been modified to parse the command and check if any kind of I/O redirection is required. If it is, then the required file is opened and the required streams are connected to them. Then the functions corresponding to the command is executed. After returning from the command specific function, at the end of the execute_command()
function, the files are closed and streams are set to default.
A new function has been added in the call stack, called handle_pipes()
, which is called by parse_input()
and inturn calls execute_command()
to execute it acfter handling piping logic.
Inside handle_pipes()
, we first calculate the number of pipes. If it is 0, we directly send the input string to execute_command()
. Instead, if there are some pipes, we split to commands. Say there are n
commands, then we create n
pipes, meaning 2n
file descriptors and populate them using the pipe()
function. We then create n
child processes of the shell. We link the STDOUT
of the ith
child to the ith
write end of the pipe and the STDIN
of the ith
child to the (i-1)th
read end of the pipe. So, the STDIN
of the first child is still default and the STDOUT
of the last child is also still default. This creates a pipe chain in which the ith
child reads the input from the output of the (i-1)th
child.
The way the first two specifications have been implemented, this specification has inherently been implemented.
-
jobs
: Implemented injobs()
infunctions.c
. Iterates over the children in child pool and fetches each one's status from the processes'proc/<pid>/stat
file. -
fg <job number>
: Implemented infg()
inuserdefined_commands.c
. Checks command usage, then verifies the given job number. Once it is confirmed that the job is a valid one, it gets its pid from the child pool, gets its process group, removes the child from the pool, tells the shell to ignore allSTDIN
andSTDOUT
related events and then gives terminal control to the process group belonging to said job. The job is also sent theSIGCONT
signal, incase it had stopped in the background. While the process runs in the foreground, the shell waits on it using thewait()
syscall. Once the foreground process terminates or has been stopped, the terminal control is returned to the shell and the default response toSTDIN
andSTDOUT
events is restored. -
bg <job number>
: Implemented inbg()
inuserdefined_commands.c
. Checks command usage, then verifies the given job number. Once it is confirmed that the job is a valid one, it gets its pid from the child pool. Then it sends theSIGCONT
signal to that process using thekill()
method, to tell it to change its status from stopped to running. -
sig <job_index> <signal>
: Implented insig()
inuserdefined_commands.c
. Checks command usage, then verifies the given job number. Once it is confirmed that the job is a valid one, it gets its pid from the child pool, and sends the signal specified by the user
For both the signals, a new action was defined using the sigaction
structure.
-
Ctrl-Z
: The hanlder for this is relatively straightforward. It makes a new line and prints the prompt string again. To make sure that when a Ctrl-Z is hit on a foreground process, it is added to the job pool, one bit of modification needed to be made inforeground_process()
andfg()
. Thewait()
was replaced withwaitpid()
and theWUNTRACED
flag was set. This way, we could inspect the status code to check if the process had been stopped or terminated using theWIFSTOPPED
macro. If the process has been stopped, it is added to the child pool, because it has been sent to the background. -
Ctrl-C
: The hanlder for this is essentially the same as that for the previous one. It prints a new line and prints the prompt string again.