Lab-2. Simple command shell


The shell is a program that offers the user a text interface to run other programs. Commands are given at the command prompt by typing text after the prefix (prompt) that the shell prints.

The input format is <program_name> [argument1 ...].

For example, the command to copy one file to a new one is:

~/example1/$ cp example.c example-copy.c

Here the prefix is ~/example1/$ which usually states the active directory in which the shell is focused, the command is cp and the arguments example.c and example-copy.c.

The possibilities of shells are diverse. Within this exercise, it is necessary to achieve the following functionalities:

  1. run shell commands (just the ones listed): cd, exit, ps, kill and history
  2. starting other programs

Built-in commands

The cd command changes the active directory. Initial directory is the one in which the shell is started. Change is simplest to do with chdir(). It is not necessary to remember where we are in the shell because the current directory is easily retrieved with getcwd().

The exit command ends the shell. Before exiting, it is necessary to stop all running programs, e.g. by sending a signal SIGKILL (9).

The ps command should print all running processes that have not yet been completed. For example, printout can be:

PID   name
72146 prog
72138 something
72119 test
The kill command should allow sending a signal to one of the running processes (which can be seen with ps). For example, the kill72138 2 command should send a signal with the number 2 (which is SIGINT) to the process with PID 72138. Be sure to check that the specified process is initiated by this shell - do not allow sending signals to other processes.

The history command should print all previously issued commands in shell, including history number. That number can be later be used to rerun the same command in the future with format formatu !<redni-broj>. An example using this command follows.

student:~$ pwd
/home/student
student:~$ date
Fri Feb 14 13:09:24 CET 2025
student:~$ echo "Hello World!"
Hello World!
student:~$ history
    1  pwd
    2  date
    3  echo "Hello World!"
    4  history
student:~$ !3
echo "Hello World!"
Hello World!
student:~$

Starting a program

Programs should be able to run in the foreground and in the background. If there is a character & at the end of the command (the last "argument") then the program starts in the background. Otherwise, the program runs in the foreground and the shell must wait for its completion.

For example, with

$ ./prog 1 2 3

the program is launched in the foreground, and with

$ ./something a b c &

in the background.

It is necessary to keep records of all started processes, in order to manage them (scroll through them with ps and send them signals).

The shell can be briefly described by pseudocode:

repeat {
        read a new user request - a line of text from the standard input
        parse request (command and arguments)
 
        if the command is one of the built-in commands then
               perform this built-in command
        otherwise
               // it is assumed that it is a program name
               create a new process and in it:
                       Separate the process into a separate group*
                       run the program with one of the exec functions
 
               (continuation of the parent process, the shell)
               add process to the list of running processes
               if the startup is carried out in the foreground then
                       wait for the completion of the process
}
while command is not exit
 
// the command is exit.
send sigkill to all remaining processes
finish work

*Separating the new process into a new process group is necessary for signal management and input control (described below). Namely, if a signal is sent to the shell (e.g. with Ctrl+C keys), then in addition to the shell, all other processes created by the shell will also receive this signal (SIGINT). By separating the new process into a separate group, this will not happen. One of the functions for creating a new group is setpgid().

However, once the process is separated into a new group, it will not receive the signals that the user sends to it via the keyboard (e.g., Ctrl+C). Therefore, when the shell launches the program in the foreground, the shell should detect the signal addressed to it (it is enough to monitor just SIGINT) and forward it to the program in foreground (process).

Of the OS interface needed to realize the shell, the most important are certainly fork, exec*, kill and waitpid. Examples of their use are shown in the example with these instructions. More detailed information about a particular interface can be obtained with the man name-interface command. If the result is not what is desired (e.g. man kill describes the kill command, not the function) then add the section number before the interface name (e.g. man 2 kill). If this number does not give results, try the following (3, e.g. man 3 printf).

Running interactive programs

All running programs (processes) can print messages on the screen, i.e. in the terminal. However, when a user enters something to whom it goes, to what process? At the beginning, it goes to the shell because we give it orders. What if it starts a new process?

To be able to more precisely set who can receive the terminal input, it is necessary to use different groups of processes, already mentioned before. A specific group is given the right to use the terminal input. Since there is only one process in every group, then it will be able to use the terminal.

Initially, the input is assigned to the group in which the shell is also located. When the shell creates a new process, then the new process should ask for a separation into a new group (setpgid()). If the terminal input is to be given to this process, i.e. that new group, the shell can then make that with:

tcsetpgrp(STDIN_FILENO, getpgid(pid_of_new_process));

or by the process itself, immediately after fork but before exec*.

After the new foreground process is completed, control should be returned to the shell. The shell can take the input back to itself with:

tcsetpgrp(STDIN_FILENO, getpgid(0));

Since the shell is not in possession of the input at this time, the system reacts to this by sending a signal SIGTTOU to the shell, a signal that, according to the assumed behavior, temporarily stops the process. Since we do not want this, we can simply ignore this signal (thus set the shell behavior for that signal).

The new foreground process can also change some terminal settings, but we don't want them after the process is over. Therefore, functions can be used:

tcgetattr(STDIN_FILENO, &shell_term_settings);

to save the current state (before new process creation), and

tcsetattr(STDIN_FILENO, 0, &shell_term_settings);

to restore the state.

All of the above operations are shown in the code example (source code).

More details on some interfaces could be found on last year assignment manual. But dont solve assignement from it (they are more complex), solve those given above in this document.