«  3.8. Semaphores   ::   Contents   ::   4.1. Networked Concurrency  »

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 <assert.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

/* 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;
}
«  3.8. Semaphores   ::   Contents   ::   4.1. Networked Concurrency  »

Contact Us License