I was given an assignment to write a mini-shell:
To write your own shell, you will need to start with a C program that will prompt the user for input and accept in a number of arguments from a command line. The commands entered will be accepted into your shell and then processed to understand if it is a built in command or something that needs to be executed via
fork
/exec
(NOTE: No use ofsystem
function). Your shell should emulate the standard shells in how it deals with background commands (&
). The interaction with your shell should be just like the standard shells. What I mean by this is to have a good usage statement returned if the arguments passed to the shell are not correct, and when there is an error you should send back a useful error, and not exit the shell, just continue.Namespaces allow for virtualization and sharing of spaces between parent and child processes. This is a part of the Linux operating system since 2008 that allows for the creation of different models to create containers for software applications. The most popular version of Linux namespaces is Docker. Your task for this lab is to create the option inside your shell through “built in” commands to move your shell into a container. The options for different containers can be added together in a
clone
orclone2
call. Here are some of the options:
- CLONE_NEWIPC - New namespace for IPC
- CLONE_NEWUTS - Setup new hostname and domain
- CLONE_NEWUSER - User and group changes
- CLONE_NEWNET - New network namespace
- CLONE_NEWNS - New mount namespace
When using a
clone
function, you will have the ability to run another function. To test your clone call, you will need to be able to demonstrate the change, and the best way to do that is to spawn another shell to “look around” at what changed. The best way to do this is to spawn another shell using thesystem
function call.
Here is my code implementing the above. Basic testing has been performed and it seems to work as intended:
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#define _GNU_SOURCE
#include <linux/sched.h>
#ifndef streq
#define streq(x, y) (strcmp((x), (y)) == 0)
#endif
#define MAX_COMMAND_LENGTH 100
#define MAX_PARAMS 10
typedef struct input
{
char** params;
int numParams;
bool inBackground;
} Command;
// split input into array of parameters
Command* parseCmd(char* input)
{
// handle bad input
if (!input || strlen(input) == 0)
return NULL;
// allocate space on heap for command struct, and parameter list
Command *cmd = calloc(1, sizeof(Command));
cmd->params = calloc(MAX_PARAMS + 1, sizeof(char*));
// setup strtok_r
char *next = NULL;
char *part = strtok_r(input, " ", &next);
// do-while so we can process first `part`, then rest of `input`
do
{
// check for background, if not found default setting is false
if (streq(part, "&"))
{
cmd->inBackground = true;
// don't store in parameter list for commands
continue;
}
cmd->params[cmd->numParams] = part;
++cmd->numParams;
}
while ((part = strtok_r(NULL, " ", &next)));
return cmd;
}
void freeCmd(Command* cmd)
{
free(cmd->params);
cmd->params = NULL;
free(cmd);
cmd = NULL;
}
int childFunct(void)
{
system("sh");
return 0;
}
int executeClone(Command *cmd)
{
const int STACK_SIZE = 1024;
unsigned long flags = 0;
if (cmd->numParams <= 1)
{
fprintf(stderr, "clone command requires arguments\n");
return 0; // non-fatal error
}
if (streq(cmd->params[1], "ipc"))
flags |= CLONE_NEWIPC;
if (streq(cmd->params[1], "uts"))
flags |= CLONE_NEWUTS;
if (streq(cmd->params[1], "user"))
flags |= CLONE_NEWUSER;
if (streq(cmd->params[1], "net"))
flags |= CLONE_NEWNET;
if (streq(cmd->params[1], "ns"))
flags |= CLONE_NEWNS;
if (!flags)
{
fprintf(stderr, "clone parameter not recognized\n");
return 0; // non-fatal error
}
char *stack = malloc(STACK_SIZE);
pid_t pid = clone(childFunct, stack + STACK_SIZE, flags, NULL);
// error
if (pid == -1)
{
char* error = strerror(errno);
fprintf(stderr, "clone: %s\n", error);
}
return 0;
}
int executeCmd(Command *cmd)
{
pid_t pid = fork();
// error
if (pid == -1)
{
char* error = strerror(errno);
fprintf(stderr, "fork: %s\n", error);
return 1;
}
// child process
else if (pid == 0)
{
// will daemonize child if background
// if (cmd->inBackground)
// setpgid(pid, 0);
// execute command
execvp(cmd->params[0], cmd->params);
// error occurred
char* error = strerror(errno);
fprintf(stderr, "ERROR: %s: %s\n", cmd->params[0], error);
return 2;
}
// parent process
else
{
// wait for child process to finish
if (!cmd->inBackground)
{
int childStatus = 0;
waitpid(pid, &childStatus, 0);
}
}
return 0;
}
int main(void)
{
char input[MAX_COMMAND_LENGTH + 1] = "";
Command *cmd = NULL;
while(1)
{
// print command prompt
fputs("> ", stdout);
// fgets returns NULL on Ctrl+D, so exit the loop on NULL
if (!fgets(input, sizeof(input), stdin))
break;
// remove trailing newline character, if any
if (input[strlen(input) - 1] == '\n')
input[strlen(input) - 1] = '\0';
// split cmd into array of parameters
cmd = parseCmd(input);
// handle bad command, continue running
if (!cmd)
continue;
// exit?
if (streq(cmd->params[0], "exit") || streq(cmd->params[0], "quit"))
break;
// test for built-in command
if (streq(cmd->params[0], "clone"))
{
if (executeClone(cmd))
break;
}
else // executable
{
if (executeCmd(cmd))
break;
}
freeCmd(cmd);
}
}
Any suggestions for improvement?