11.1 Building a Simple Shell

Team-FLY

A shell is a process that does command-line interpretation. In other words, a shell reads a command line from standard input and executes the command corresponding to the input line. In the simplest case, the shell reads a command and forks a child to execute the command. The parent then waits for the child to complete before reading in another command. A real shell handles process pipelines and redirection, as well as foreground process groups, background process groups and signals.

This section starts with the simplest of shells. Later sections add features piece by piece. The shells use the makeargv function of Program 2.2 on page 37 to parse the command-line arguments. Section 11.2 adds redirection, and Section 11.3 adds pipelines. Section 11.4 explains how a shell handles signals for a foreground process. The programs for each of these phases are given, along with a series of exercises that point out the important issues. Work through these exercises before going on to the main part of the project. The heart of this project is signal handling and job control. Section 11.5 introduces the machinery needed for job control. Section 11.6 describes how background processes are handled without job control, and Section 11.7 introduces job control at the user level. Finally, Section 11.8 specifies the implementation of a complete shell with job control.

Program 11.1 shows Version 1 of ush (ultrasimple shell). The shell process forks a child that builds an argv type array and calls execvp to execute commands entered from standard input.

Program 11.1 ush1.c

Version 1 of ush has no error checking or prompts .

 #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #define MAX_BUFFER 256 #define QUIT_STRING "q" int makeargv(const char *s, const char *delimiters, char ***argvp); int main (void) {     char **chargv;     char inbuf[MAX_BUFFER];     for( ; ; ) {         gets(inbuf);         if (strcmp(inbuf, QUIT_STRING) == 0)             return 0;         if ((fork() == 0) && (makeargv(inbuf, " ", &chargv) > 0))             execvp(chargv[0], chargv);         wait(NULL);     } } 
Exercise 11.1

Run Program 11.1 with a variety of commands such as ls , grep and sort . Does ush1 behave as expected?

Answer:

No. Program 11.1 does not display a prompt or expand filenames containing wildcards such as * and ? . The ush1 shell also does not handle quotation marks in the same way as standard shells do. A normal shell allows quotation marks to guarantee that a particular argument is passed to the exec in its entirety and is not interpreted by the shell as something else. You may also notice that certain commands such as cd do not behave in the expected way.

Exercise 11.2

What happens if Program 11.1 doesn't call wait ?

Answer:

If a user enters a command before the previous one completes, the commands execute concurrently.

Another problem is that Version 1 of ush does not trap errors on execvp . This omission has some interesting consequences if you enter an invalid command. When execvp succeeds, control never comes back from the child. However, when it fails, the child falls through and tries to get a command line too!

Exercise 11.3

Run Program 11.1 with several invalid commands. Execute ps and observe the number of shells that are running. Try to quit. What happens?

Answer:

Each time you enter an invalid command, ush1 creates a new process that behaves like an additional shell. You must enter q once for each process.

Exercise 11.4

Only the child parses the command line in Program 11.1. What happens if the parent parses the command line before forking? What are the memory allocation and deallocation issues involved in moving the makeargv call before fork in these programs?

Answer:

When the child exits, all memory allocated by the child is freed. If the parent calls makeargv before fork , the shell has to later free the memory allocated by makeargv .

Version 1 of ush is susceptible to buffer overflows because it uses gets rather than fgets . A long command can exceed the space allocated for input. Program 11.2 shows an improved version of ush that prompts for user input and handles an unsuccessful execvp call. The system-defined constant MAX_CANON replaces the user-defined MAX_BUFFER , and fgets replaces gets .

The shell in Program 11.2 does not exit if there is an error on fork . In general, the shell should be impervious to errors ”and bullet-proofing takes a lot of effort. The function executecmd replaces the makeargv and execvp calls. Control should never return from this function.

Program 11.2 ush2.c

Version 2 of ush handles simple command lines .

 #include <limits.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define PROMPT_STRING "ush2>>" #define QUIT_STRING "q" void executecmd(char *incmd); int main (void) {     pid_t childpid;     char inbuf[MAX_CANON];     int len;     for( ; ; ) {         if (fputs(PROMPT_STRING, stdout) == EOF)             continue;         if (fgets(inbuf, MAX_CANON, stdin) == NULL)             continue;         len = strlen(inbuf);         if (inbuf[len - 1] == '\n')             inbuf[len - 1] = 0;         if (strcmp(inbuf, QUIT_STRING) == 0)             break;         if ((childpid = fork()) == -1)             perror("Failed to fork child");         else if (childpid == 0) {             executecmd(inbuf);             return 1;         } else             wait(NULL);     }     return 0; } 

Program 11.3 shows a simple version of executecmd for Program 11.2. We will augment this function as we improve the shell. The executecmdsimple.c version simply constructs an argument array and calls execvp .

Program 11.3 executecmdsimple.c

A simplified version of executecmd for Program 11.2 .

 #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BLANK_STRING " " int makeargv(const char *s, const char *delimiters, char ***argvp); void executecmd(char *incmd) {     char **chargv;     if (makeargv(incmd, BLANK_STRING, &chargv) <= 0) {         fprintf(stderr, "Failed to parse command line\n");         exit(1);     }     execvp(chargv[0], chargv);     perror("Failed to execute command");     exit(1); } 
Exercise 11.5

Why does Program 11.3 treat a makeargv return value of 0 as an error?

Answer:

The makeargv returns the number of items in the command argument array. Technically, an empty command is not an error, and a real shell would ignore it without printing a warning message. For more complicated command lines that include redirection and pipelines, an empty command portion is considered to be an error. You may want to consider adding additional checks and not count it as an error in some circumstances.

Exercise 11.6

Try the cd command as input to Program 11.2. What happens? Why? Hint: Read the man page on cd for an explanation.

Answer:

The cd command changes the user's environment, so it must be internal to the shell. External commands are executed by children of the shell process, and a process cannot change the environment of its parent. Most shells implement cd as an internal command or a built-in command.

Exercise 11.7

What happens when Program 11.2 encounters commands such as ls -l and q with leading and interspersed extra blanks?

Answer:

Program 11.2 correctly handles commands such as ls -l because makeargv handles leading and interspersed blanks. The q command does not work because this command is handled directly by ush2 , which has no provision for handling interspersed blanks.

Exercise 11.8

Execute the command stty -a under your regular shell and record the current settings of the terminal control characters . The following is a possible example of what might appear.

 intr = ^c; quit = ^; erase = ^?; kill = ^u; eof = ^d; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^q; stop = ^s; susp = ^z; dsusp = ^y; rprnt = ^r; flush = ^o; werase = ^w; lnext = ^v; 

Try each of the control characters under ush2 and under a regular shell and compare the results.

In Exercise 11.8 the erase and werase continue to work even though there is no explicit code to handle them in ush2 because ush2 does not receive characters directly from the keyboard. Instead, the terminal device driver processes input from the keyboard and passes the input through additional modules to the program. As described in Section 6.5.1, terminals can operate in either canonical (line-buffered) or noncanonical mode. Canonical mode is the default.

In canonical mode, the terminal device driver returns one line of input at a time. Thus, a program does not receive any input until the user enters a newline character, even if the program just reads in a single character. The terminal device driver also does some processing of the line while the line is being gathered. If the terminal line driver encounters the erase or werase characters, it adjusts the input buffer appropriately.

Noncanonical mode allows flexibility in the handling of I/O. For example, an editing application might display the message "entering cbreak mode" to report that it is entering noncanonical mode with echo disabled and one-character-at-a-time input. In noncanonical mode, input is made available to the program after a user-specified number of characters have been entered or after a specified time has elapsed. The canonical mode editing features are not available. Programs such as editors usually operate with the terminal in noncanonical mode, whereas user programs generally operate with the terminal in canonical mode.

Team-FLY


Unix Systems Programming
UNIX Systems Programming: Communication, Concurrency and Threads
ISBN: 0130424110
EAN: 2147483647
Year: 2003
Pages: 274

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net