# 3.9. Extended Example: Bash-lite: A Simple Command-line Shell¶

This Extended Example creates a minimal shell similar to the bash shell used in Linux and macOS. When this program runs, it will read a line of text at a time from the user. This line will be used as a command line, running in a separate process. The user can enter quit or logout to exit.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 #include #include #include #include #include #include #include #include /* Set the maximum allowable length for a command line */ #define MAX_LINE_LENGTH 1024 /* Allow spaces, tabs, newline, and carriage return to match whitespace between tokens */ #define WHITESPACE " \t\n\r" void run_child_process (char *, char **, char *); char **tokenize_arguments (char *, char **); char **get_out_name (char *, char **, char **); int main (void) { char buffer[MAX_LINE_LENGTH + 1]; memset (buffer, 0, MAX_LINE_LENGTH + 1); /* Main loop: iterate until user enters "quit" or "logout" */ while (true) { /* Display a minimal prompt and read the command line */ printf ("\$ "); if (! fgets (buffer, MAX_LINE_LENGTH, stdin)) break; /* Get the command for this line without any arguments */ char *command = strtok (buffer, WHITESPACE); /* Check for reserved keywords to quit the shell */ if (! strncmp (command, "quit", 5) || ! strncmp (command, "logout", 7)) break; /* Get the array of arguments and determine the output file to use (if the line ends with "> output" redirection). */ char *output = NULL; char **arg_list = tokenize_arguments (buffer, &output); /* Security precaution: This is a simple shell that should not be used for running commands in root. */ if (! strncmp (command, "sudo", 5) || ! strncmp (command, "su", 3)) { printf ("*WARNING* This implementation does not " "support super-user commands\n"); continue; } /* If something went wrong, skip this line */ if (arg_list == NULL) { perror ("-bash-lite: syntax error\n"); continue; } /* Copy the command name as the first argument */ arg_list[0] = command; /* Create the child process and execute the command in it */ pid_t child_pid = fork (); assert (child_pid >= 0); if (child_pid == 0) run_child_process (command, arg_list, output); /* Parent waits for the child, then frees up allocated memory for the argument list and moves on to the next line */ wait (NULL); free (arg_list); memset (buffer, 0, MAX_LINE_LENGTH + 1); } return EXIT_SUCCESS; } /* Runs a command in an already created child process. The command string should already be copied as the first argument in the list. If the user typed an output redirection ("> out" or ">out"), then output_file is the name of the file to create. Otherwise, output_file is NULL. Should never return. */ void run_child_process (char *command, char **arg_list, char *output_file) { int out_fd = -1; /* If there is a specified output file, open it and redirect STDOUT to write to this file */ if (output_file != NULL) { out_fd = open (output_file, O_RDWR | O_CREAT); if (out_fd < 0) { fprintf (stderr, "-bash-lite: failed to open file %s\n", output_file); exit (EXIT_FAILURE); } /* Make the file readable and redirect STDOUT */ fchmod (out_fd, 0644); dup2 (out_fd, STDOUT_FILENO); } /* Use execvp, because we are not doing a PATH lookup and the arguments are in a dynamically allocated array */ execvp (command, arg_list); /* Should never reach here. Print an error message, free up resources, and exit. */ fprintf (stderr, "-bash-lite: %s: command not found\n", command); free (arg_list); if (out_fd >= 0) close (out_fd); exit (EXIT_FAILURE); } /* Given a command line (buffer), create the list of arguments to use for the child process. If the command line ends in an output redirection, update the output_file pointer to point to the name of the file to use. */ char ** tokenize_arguments (char *buffer, char **output_file) { assert (buffer != NULL); assert (output_file != NULL); char *token = NULL; /* Allocate an initial array for 10 arguments; this can grow later if needed */ size_t arg_list_capacity = 10; char **arguments = calloc (arg_list_capacity, sizeof (char *)); assert (arguments != NULL); /* Leave the first space blank for the command name */ size_t arg_list_length = 1; while ( (token = strtok (NULL, WHITESPACE)) != NULL) { /* If token starts with >, it is an output redirection. The rest of the line must be the file name. Need to pass both the rest of the token and the buffer, as there might not be a space before the file name. */ if (token[0] == '>') return get_out_name (&token[1], output_file, arguments); /* If current argument array is full, double its capacity */ if ((arg_list_length + 1) == arg_list_capacity) { arg_list_capacity *= 2; arguments = realloc (arguments, arg_list_capacity * sizeof (char *)); assert (arguments != NULL); } /* Add the token to the end of the argument list */ arguments[arg_list_length++] = token; } return arguments; } /* Determine the output file from either the token that begins with the '>' character (but that character was removed) or from the next token on the command line. Note that all tokens after the output file name will be ignored. */ char ** get_out_name (char *token, char **output_file, char **arguments) { /* If token is not an empty string, it contains the output file name */ if (strlen (token) != 0) { *output_file = token; return arguments; } /* Token is empty, so there was a space after the '>' symbol. There should be one token left that is the file name. */ token = strtok (NULL, WHITESPACE); if (token == NULL) { /* This is an error, no file name was passed */ free (arguments); return NULL; } /* The last token is the file name, so return it and the argument list */ *output_file = token; return arguments; }