50.005 Computer System Engineering
Information Systems Technology and Design
Singapore University of Technology and Design
Natalie Agus (Summer 2024)
Shell Builtin Commands
Your first task is to implement more shell builtin commands. Right now, only exit
is considered a builtin command:
// If the command is "exit", break out of the loop to terminate the shell
if (strcmp(cmd[0], "exit") == 0)
break;
All other commands should trigger a fork
and execvp
of a system program in [PROJECT_DIR]/bin
whose name matches the first word of the command.
Expand the Builtin Commands
The shell must be expanded support the following 7 builtin commands: cd, help, exit, usage, env, setenv, unsetenv
.
List the builtin commands
We suggest that you declare them as an array of char*
in shell.h
:
const char *builtin_commands[] = {
"cd", // Changes the current directory of the shell to the specified path. If no path is given, it defaults to the user's home directory.
"help", // List all builtin commands in the shell
"exit", // Exits the shell
"usage", // Provides a brief usage guide for the shell and its built-in command
"env", // Lists all the environment variables currently set in the shell
"setenv", // Sets or modifies an environment variable for this shell session
"unsetenv" // Removes an environment variable from the shell
};
An array of character pointers
const char *builtin_commands[]
declares an array of character pointers where each pointer is pointing to a constant character string.
Builtin command handler
It is recommended that you refactor the shell code such that each builtin command will call a specific handler function, instead of writing a gigantic if-else
string matching clause in the main
. You can do this elegantly in c by first declaring each handler in the header file:
/*
Handler of each shell builtin function
*/
int shell_cd(char **args);
int shell_help(char **args);
int shell_exit(char **args);
int shell_usage(char **args);
int list_env(char **args);
int set_env_var(char **args);
int unset_env_var(char **args);
Ensure they have the same signature.
Then declare an array of functions as follows.
/*** This is array of functions, with argument char ***/
int (*builtin_command_func[])(char **) = {
&shell_cd, // builtin_command_func[0]: cd
&shell_help, // builtin_command_func[1]: help
&shell_exit, // builtin_command_func[2]: exit
&shell_usage, // builtin_command_func[3]: usage
&list_env, // builtin_command_func[4]: env
&set_env_var, // builtin_command_func[5]: setenv
&unset_env_var // builtin_command_func[6]: unsetenv
};
An array of function pointers
This declaration creates an array of function pointers named builtin_command_func. Each element in the array is a pointer to a function that takes a single argument (char **) and returns an int. The functions pointed to by this array are intended to implement the functionality for each of the built-in commands supported by your shell.
Using the function pointers
The idea is that each builtin command has an index. When we process command from the user, we shall match whether any of the commands given matches any element in builtin_commands
. If so, we will call the corresponding builtin command handler, here’s a rough idea:
// Helper function to figure out how many builtin commands are supported by the shell
int num_builtin_functions()
{
return sizeof(builtin_commands) / sizeof(char *);
};
// Loop through our command list and check if the commands exist in the builtin command list
for (int command_index = 0; command_index < num_builtin_functions(); command_index++)
{
if (strcmp(args[0], builtin_commands[command_index]) == 0) // Assume args[0] contains the first word of the command
{
// We will create new process to run the function with the specific command except for builtin commands.
// These have to be done by the shell process.
return (*builtin_command_func[command_index])(args);
}
}
About
strcmp
In C, the
strcmp
function compares two strings and returns an integer value based on the outcome of the comparison. If the strings are equal, strcmp returns0
.
The expression (*builtin_command_func[command_index])(args)
is a way to call a function through a pointer obtained from an array of function pointers, using a specific index (command_index
) to select which function to call, and passing it an array of strings (args
) as arguments.
Here’s a breakdown of the expression.
Array of function pointers
builtin_command_func
is an array of pointers to functions. Each element of this array is a pointer to a function that matches a specific prototype: the functions take a single parameter of type char **
and return an int
.
Selecting a function pointer
builtin_command_func[command_index]
accesses the element at position command_index
within the array builtin_command_func
. This element is a pointer to a function. The value of command_index
determines which function pointer is selected, based on the command the user has input.
Dereferencing the function pointer
*builtin_command_func[command_index]
dereferences the function pointer obtained in the previous step.
Actually, in C, when you have a function pointer, you don’t actually need to explicitly dereference it with *
to call the function it points to. Both (*func)(args)
and func(args)
are valid and equivalent when func
is a function pointer. The dereferencing here is more about clarity and emphasis, showing that we are indeed dealing with a pointer.
Calling the handler function
(*builtin_command_func[command_index])(args)
calls the function pointed to by the selected element in the builtin_command_func
array, passing args
as its argument. args
is expected to be an array of strings (char pointers), which aligns with the expected parameter type of the functions being pointed to.
The whole expression invokes the function just like a normal function call, but the function being called is selected at runtime based on the value of command_index
.
Summary
In summary, this mechanism allows for a flexible and dynamic dispatch of function calls based on user input or other runtime conditions.
It’s a common pattern in C for implementing simple polymorphism or callback functions, enabling the selection and invocation of different functions without using if-else or switch-case statements directly on the
commandIndex
. This approach is especially useful in command line interpreters or shells, where the set of commands and their corresponding functions can be neatly organized in arrays.
Sample Output and Implementation Notes
You may decorate your shell prompt in any way you like.
cd
Change the current working directory of the shell. You can print out your current working directory as part of the shell’s prompt, or you can use the ld
command to list the current directory.
All commands must be able to be executed even after you cd
. Failure to do this results in -1% penalty.
help
Print out all builtin commands in the shell.
usage
Print a brief description on how to use each builtin command. Also prints a useful message if the command given is not part of CSEShell’s builtin command.
env
Print all environment variables of this shell. This should inherit your system’s environment variables too.
In C programming, environment variables can be accessed using the external variable environ
. You may refer to this code snippet to get started:
// This program will list all the environment variables available to it at runtime
#include <stdio.h>
// Declaration of the external variable 'environ'
// environ is actually defined in the C standard library
extern char **environ;
int main() {
char **env = environ; // Pointer to the array of environment strings
while (*env) { // Loop until NULL pointer is encountered
printf("%s\n", *env); // Print the current environment variable
env++; // Move to the next environment variable
}
return 0;
}
environ
The
environ
variable in C is a pointer to an array of strings that represent the environment variables for the current process
setenv
The command setenv KEY=VALUE
simply adds to the list of this process’ environment variables. It should not print anything:
However, you can check that it’s indeed registered to this process’ (the shell) environment variables by typing env
. Your newly set env variable should be visible at the bottom of the list:
It’s important not to attempt to remove an environment variable by directly modifying the environ
global variable or the strings to which it points. The correct way to modify the environment is through the use of functions like setenv()
, unsetenv()
, and putenv()
.
unsetenv
This command unset KEY
should delete any environment variable whose KEY
matches any existing environment variables. You don’t have to do anything if you attempt to unset an environment variable that doesn’t exist.
If you attempt to unset an environment variable that does not exist using unsetenv()
, the function is considered to succeed, and it returns 0
. The absence of the specified environment variable means there’s nothing to remove, which aligns with the desired outcome of unsetenv()
: ensuring that the environment variable is not present in the environment of the current process.
exit
As it originally was, exit
should gracefully quit the shell.