Shell program in C has odd fork behaviour - c

I am writing a C program to emulate a simple shell. This shell will basically evaluate commands like any other shell (ls, cat, etc.), as well as handle pipelining and redirection.
Currently, I am trying to start out by getting user input, tokenizing it, and executing the command provided (e.g. executing only "ls" and not "ls -l"). However, I am having a lot of difficulty with the forking. It seems that every time I fork, something goes wrong and hundreds of identical processes are created, leading to my computer freezing and me having to restart. The code appears to be correct, but I have no idea what is causing this behaviour. Below is the relevant portion of my code (main method and input tokenizer method).
int main() {
char inputLine[512]; //user input
char *args[10]; //arguments
char* pathVar = "/bin/";//path for argument
char programPath[512]; //pathVar + args[0]
int n; //count variable
//loop
while (1) {
//print prompt, get input
printf("input> ");
fgets(inputLine, 512, stdin);
n = tokenizer(inputLine, args);
//fork process
pid_t pid = fork();
if (pid != 0) { //if parent
wait(NULL);
} else { //if child
//format input for execution
strcpy(programPath, pathVar);
strcat(programPath, args[0]);
//execute user command
int returnVal = execv(programPath, args);
}
}
return 0;
}
int tokenizer(char *input, char *args[]) {
char *line; //current line
int i = 0; //count variable
line = input;
args[i] = strtok(line, " ");
do {
i++;
line = NULL;
args[i] = strtok(line, " ");
} while (args[i] != NULL);
return i;
}

Putting it all together:
You need to check fork and execv for failure.
You should exit after an execv failure (and perhaps after a fork failure).
And you need to add \n to the strtok delimiters (or remove the newline from the input line in some other way).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAXARGS 10
#define PATH "/bin/"
int main() {
char inputLine[BUFSIZ];
char *args[MAXARGS];
char programPath[BUFSIZ + sizeof(PATH) + 10];
while (1) {
printf(":-> ");
if (fgets(inputLine, BUFSIZ, stdin) == NULL) /* ctrl-D entered */
break;
tokenize(inputLine, args);
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid != 0) { /* parent */
wait(NULL);
} else { /* child */
strcpy(programPath, PATH);
strcat(programPath, args[0]);
execv(programPath, args); /* will not return unless it fails */
perror("execv");
exit(EXIT_FAILURE);
}
}
return 0;
}
int tokenize(char *input, char *args[]) {
int i = 0;
args[0] = strtok(input, " \n");
for (i = 0; args[i] && i < MAXARGS-1; ++i)
args[++i] = strtok(NULL, " \n");
return i;
}

You should check that execv doesn't fail and also be sure to exit() at the end of the child block.
//execute user command
int returnVal = execv(programPath, args);
// check return from execv
if (returnVal < 0) {
perror("execv");
exit(1);
}
Also, beware using functions like strcpy in this context since they may lead to buffer overflows. If an untrusted attacker type is talking to your shell this type of security issue could let them break out of the "sandbox".

Related

Issues with shell program in C

I am creating a Linux type shell program for a school project. So far I have implemented the basic Linux external commands like "ls","ps", etc., using execvp and basic pipes. As part of the project, the user can either run the program in interactive mode or batch mode. In interactive mode the user just enters a command when prompted. For batch mode, the user specifies a file in the command line where there is a list of commands to execute.
The problem I am having is in batch mode. In batch mode, if an invalid command is listed (e.g. "kdfg", for which "kdfg: command not found" while be the output), everything afterward continues, but everything afterward is executed twice. so if I have a "kghd" on one line and the next line is "ls", then the "ls" command will be executed twice. I've literally been looking at my code for hours and have tried a bunch of stuff, but to no avail.
My code is displayed below:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include<sys/wait.h>
#include<unistd.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
char* InputString();
char** GetArgs(char* com);
void Free(char** args);
char** GetCommands(char** line);
void PipedCommands(char* line);
int batch = 0; //Acts as bool for if there is a batch file given at command line
FILE* bFile; //This is just to make a quick tweek to the file if necssary to prevent any undefined behavior and keep track of where we are in the fil.
int b_fd;
int stdin_cpy;
int main(int argc, char** argv)
{
pid_t pid;
int status;
int fd;
int exitCom = 0; //acts as bool to check if an exit command was given.
char* line;
if(argc > 1) //check if batch file was given.
{
/*
if(freopen(argv[1], "r", stdin) == NULL) //I added this in case the file isn't found
{
printf("\nCould not open \"%s\". Terminated\n\n", argv[1]);
exit(1);
}
*/
//The following is to append a newline at the end of the file (if there isn't one).
//For some reaosn, there seems to be some undefined behavior if the input file isn't
//stricitly ended with a newline.
bFile = fopen(argv[1], "r+"); //open for reading and updating
if(bFile == NULL)
{
printf("\nCould not open \"%s\". Terminated\n\n", argv[1]);
exit(1);
}
fseek(bFile, -1, SEEK_END); //go to last character of file
if(fgetc(bFile) != '\n') //If last character is not a newline, append a newline to the file.
{
fprintf(bFile, "\n");
}
fclose(bFile); //close the file.
bFile = fopen(argv[1], "r"); //open file to keep track of when it ends
b_fd = open(argv[1], O_RDONLY); //open file again (with file descriptor this time) to duplicate it to stdin
stdin_cpy = dup(fileno(stdin)); //keep track of stdin file.
dup2(b_fd, 0); //duplicate to stdin so program takes input from bFile
close(b_fd);
batch = 1;
}
//int i=0; //this was used for debugging purposes
while(1)
{
printf("\n");
char** coms = GetCommands(&line);
for(int i=0; coms[i] != NULL; ++i) //loop goes through each command returned from GetCommands(...)
{
//fork and wait.
pid = fork();
wait(&status);
if(pid == 0)
{
int pipedCommand = 0;
//printf("\ncoms[%d]: %s\n", i, coms[i]);
for(int j=0; j<strlen(coms[i]); ++j)
{
if(coms[i][j] == '|')
{
pipedCommand = 1;
break;
}
}
if(pipedCommand == 1)
{
PipedCommands(coms[i]);
exit(1);
}
char** args = GetArgs(coms[i]);
//printf("\nargs[0]: %s\n", args[0]);
if(strcmp(args[0],"exit") == 0)
{
exit(5); //if exit command was given, exit status will be 5 (I just used 5 becuse I felt like it).
}
//printf("\nNo exit\n");
printf("\n");
execvp(args[0],args);
printf("%s: command not found\n", args[0]);
exit(1); //Normal exit exits with 1 or 0.
}
//Parent continues after child exits
else if(pid > 0)
{
//check exit status of child
if(WEXITSTATUS(status) == 5)
exitCom = 1; //set bool exitCom to 1 (true), indicating that the exit command was given
}
}
if(pid > 0)
{
free(line);
free(coms);
//Now that all commands in the line were executed, check exitCom and if it is 1 (exit command was given), the shell can now exit.
if(exitCom == 1)
{
printf("\n");
exit(0);
}
}
/*
if(i >= 5)
{
printf("\nFORCED EXIT\n"); //this was used for debugging purposes
exit(1);
}
++i;
*/
}
return 0;
}
char* InputString()
{
int len = 20;
char* str = (char*)malloc(sizeof(char)*len);
char* buff;
unsigned int i=0;
if(str != NULL)
{
int c = EOF;
//printf("%c", fgetc(bFile));
while( ((c = getchar()) != '\n') && (c != EOF) )
{
/*
//printf("%c", fgetc(bFile));
//fgetc(bFile);
if(feof(bFile))
{
printf("\n\nEnd of the line\n\n");
}
*/
str[i++] = (char)c;
if(i == len)
{
len = len*2;
str = (char*)realloc(str,sizeof(char)*len);
}
}
str[i] = '\0';
buff = (char*)malloc(i);
}
if(batch == 1)
{
if(fgets(buff, i, bFile) == NULL) //Once the end of file has been reached
{
dup2(stdin_cpy, 0); //revert input back to original stdin file so user can now enter commands interactively (this happens if exit command was not given)
close(stdin_cpy); //close stdin_copy
fclose(bFile); //close bFile as we have reached the end of it
batch = 0;
}
}
printf("\n");
return str;
}
//User enters a line of commands (1 or more). Commands are separated with a ';' being the delimeter.
char** GetCommands(char** line)
{
char** coms = (char**)malloc(sizeof(char*));
char delim[] = ";";
if(batch == 0)
printf("prompt> ");
fflush(stdout);
*line = InputString();
if(batch == 1)
printf("%s\n", *line);
strcat(*line, ";");
int i=0;
coms[i] = strtok(*line, delim);
while(coms[i] != NULL)
{
++i;
coms = (char**)realloc(coms, sizeof(char*) * (i+1));
coms[i] = strtok(NULL, delim);
//printf("\ni: %d\n", i);
}
return coms;
}
//A command obtained from GetCommands(...) is separated into various arguments with a space, ' ', being the delimiter.
char** GetArgs(char* com)
{
char** args = (char**)malloc(sizeof(char*));
char delim[] = " ";
//printf("\nline: %s\n", line);
int i=0;
args[i] = strtok(com, delim);
while(args[i] != NULL)
{
++i;
args = (char**)realloc(args, sizeof(char*) * (i+1));
args[i] = strtok(NULL, delim);
}
return args;
}
void PipedCommands(char* line)
{
char** coms = (char**)malloc(sizeof(char*));
int numComs;
char delim[] = "|";
int i=0;
coms[i] = strtok(line, delim);
while(coms[i] != NULL)
{
++i;
coms = (char**)realloc(coms, sizeof(char*) * (i+1));
coms[i] = strtok(NULL, delim);
}
numComs = i;
int fd[2];
pid_t pid;
int status;
int prev_p = 0;
// printf("\nnumComs: %d\n", numComs);
for(int i=0; i<numComs; ++i)
{
//printf("\ni: %d\n", i);
pipe(fd);
pid = fork();
wait(&status);
if(pid == 0)
{
//printf("\nChild\n");
if(i < numComs-1)
{
//printf("\ni < numComs-1\n");
//printf("%s", coms[i]);
//printf("coms[%d]: %s", i, coms[i]);
//printf("\nBefore dup2\n");
char** args = GetArgs(coms[i]);
//printf("\nexecvp in if\n");
if(prev_p != 0)
{
dup2(prev_p, 0);
close(prev_p);
}
dup2(fd[1], 1);
close(fd[1]);
execvp(args[0],args);
printf("%s: command not found\n", args[0]);
exit(3);
}
else
{
//printf("\nelse\n");
//printf("coms[%d]: %s", i, coms[i]);
//printf("\nBefore dup2 in else\n");
if(prev_p != 0)
{
dup2(prev_p, 0);
close(prev_p);
}
//close(fd[0]);
close(fd[1]);
char** args = GetArgs(coms[i]);
printf("\n");
execvp(args[0],args);
printf("%s: command not found\n", args[0]);
exit(3);
}
}
close(prev_p);
close(fd[1]);
prev_p = fd[0];
if(WEXITSTATUS(status) == 3)
{
close(fd[0]);
close(prev_p);
close(fd[1]);
return;
}
}
close(fd[0]);
close(prev_p);
close(fd[1]);
}
You can probably ignore the PipedCommands(...) function as I do not think the problem lies there.
Below is a simple batch file:
kldfg
whoami
Below is the output using the above batch file
kldfg
kldfg: command not found
whoami
jco0100
whoami
jco0100
The whoami command should only execute once, but it appears to execute twice. After that, the program reverts to interactive mode as it should and everything runs fine from there. Does anyone have any idea why this is happening. This only happens when an unknown command is entered. If all commands in the batch file are valid, nothing is outputted twice. It's only for batch files that have an unknown command that all commands after the unknown one are outputted twice.
Here's another example:
Batch file:
date
kldfg
whoami; ls | wc -l
date | wc -c
Output:
date
Tue Apr 13 19:43:19 CDT 2021
kldfg
kldfg: command not found
whoami; ls | wc -l
jco0100
34
date | wc -c
29
whoami; ls | wc -l
jco0100
34
date | wc -c
29
I got it working by disconnecting stdin on the child process before running the command:
...
freopen("/dev/null", "r", stdin); // disconnect
execcvp(args[0], args);
...
From this link: If I fork() and then do an execv(), who owns the console?
I had a crack at debugging it. To get you started on that road:
Compile your C program with debugging symbols:
$ gcc --debug your-program.c
Now debug your C program:
$ gdb a.out
This start a gdb interactive shell.
In gdb itself:
(gdb) list
(gdb) set follow-fork-mode parent
(gdb) breakpoint 69
list your code
tell the debugger to follow the parent when fork()
set a breakpoint at line 69
Run the program:
(gdb) run batch.txt
It will pause at line 69. Execute next line:
(gdb) next
Print a variable:
(gdb) print *coms
Continue running:
(gdb) continue
I leave the rest for you to explore.
I'm still not sure what's wrong with it. Something strange happens in InputString() after your fork fails with an unknown command. InputString() begins returning duplicates from getchar().
I didn't even know you could do that with stdin. Maybe just read from the file in a normal fashion, rather than clobbering stdin, and see if the problem goes away.
Don't write linux code much, I am trying to do more of that.
I took the fork and wait commands out so I could build it in mingw64 (because they arent supported in windows builds) and can't seem to reproduce the issue.
So I think the issue is in the multi-threading setup you have going there.
The "pid" variable is shared between every fork. which means when the fork command is called "pid" is set to whatever the last fork in the loop returned.
It looks like you are using an if statement checking the "pid" variable to see if this thread can execute the command. But wouldn't the main thread keep running right through that?
I don't know what fork() returns but "pid" is uninitialized, don't know if that matters.
Maybe this helps?

strstr() and exit(0) trouble in C

so I'm working on a simulated linux shell using C (homework), and I have it exiting the program if the user just simply types in "quit".
However, if they throw "exit" somewhere inside a command... "cat file ; exit"
I need it to execute the command like normal, then quit.
I know that I still have to filter the string entered and strip it of the exit, but for now, I'm just simply trying to get it to recognize the substring of "exit" using strstr() and to exit the program.
It just keeps looping if input contains a string with substring "exit" currently.
Thank you.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define bSize 1000
void driveLoop();
char *userInput(void);
int main(int argc, char **argv){
driveLoop();
return 0;
}
void driveLoop(void){
char *comTokens[100];
char *tempTokens;
char *command;
char *cd;
char *cdDir;
char *cdTemp;
char cdBuf[bSize];
char checkExit[] = "exit";
for (;;){
printf("> ");
command = userInput();
if (!command)
break;
char *exitPtr = strstr(command, checkExit); // using strstr on the pointer containing the input
int i = 0;
tempTokens = strtok(command, " \t\n");
while (tempTokens && i < 99){
comTokens[i++] = tempTokens;
tempTokens = strtok(NULL, "\t\n");
}
if (strcmp(comTokens[0], "exit") == 0){ // this exit is working normally
exit(0);
}
if (strcmp(comTokens[0], "cd") == 0){
cd = getcwd(cdBuf, sizeof(cdBuf));
cdDir = strcat(cd, "/");
cdTemp = strcat(cdDir, comTokens[1]);
continue;
}
comTokens[i] = NULL;
pid_t cFork = fork();
if (cFork == (pid_t) - 1){
perror("fork");
}
else if (cFork == 0){
execvp(comTokens[0], comTokens);
perror("exec");
if (exitPtr != NULL){ // here's where I'm calling the exit
exit(0); // nothing happens though. It just keeps looping.
}
}
else {
int status;
waitpid(cFork, &status, 0);
}
}
}
char *userInput(void){
char *input = NULL;
size_t size = 0;
getline(&input, &size, stdin);
return input;
}
else if (cFork == 0){
execvp(comTokens[0], comTokens);
perror("exec");
if (exitPtr != NULL){ // here's where I'm calling the exit
exit(0); // nothing happens though. It just keeps looping.
}
}
execvp doesn't return if it succeeds, so your if will normally never be executed. (And if execvp does return, you probably want to exit the child unconditionally, preferably with _exit().)
What you really want is for the parent process to exit. So you probably wanted to put this code in the parent branch of the fork:
else {
int status;
waitpid(cFork, &status, 0);
// add it here
if (exitPtr != NULL){
exit(0);
}
}

Implementing a command line interpreter in C, special case

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define BUFFER 64
char *read_command(void);
char **parse_line(char *line);
int execute(char **arguments);
int main(void)
{
char *command = NULL;
char **arguments;
int status;
do
{
printf("protoulis_7968> ");
command = read_command();
arguments = parse_line(command);
status = execute(arguments);
free(arguments);
free(command);
}while(status);
}
char *read_command(void)
{
char *command = NULL;
ssize_t buf = 0;
getline(&command, &buf, stdin);
return command;
}
char **parse_line(char *line)
{
int buffer = BUFFER;
int pos = 0;
char **tokens = malloc(buffer * sizeof(char*));
char *token;
if (!tokens)
{
printf("Error allocating memory with malloc\n");
exit(0);
}
token = strtok(line, " \t\r\n\a");
while(token != NULL)
{
tokens[pos] = token;
pos++;
if (pos >= buffer)
{
buffer += BUFFER;
tokens = realloc(tokens, buffer * sizeof(char*));
if (!tokens)
{
printf("Error reallocating memory!\n");
exit(0);
}
}
token = strtok(NULL, " \t\r\n\a");
}
tokens[pos] = NULL;
return tokens;
}
int execute(char **arguments)
{
// printf("%*c\n", arguments);
int pid, waitPid, status;
pid = fork();
if(pid == 0) //child process
{
if (execvp(arguments[0], arguments) == -1)
perror("Error with EXECVP\n");
}
else if (pid < 0)
perror("Error PID < 0\n");
else //parent process
{
do
{
waitPid = waitpid(pid, &status, WUNTRACED);
}while(!WIFEXITED(status) && !WIFSIGNALED(status));
}
return 1;
}
Well, I have written the above code simulating a command line interpreter in C. I would like to be able to execute multiple commands by entering them in one line. I mean I want to pass as input for example the line: ls -l ; touch hello.c ; pwd. Having passed this entire line I want to separate the commands by the semicolon and let the system execute each command in any order. I believe I have to use the strtok function but have done many attempts and managed nothing. Any help would be really appreciated!
strtok will not suffice in your case. The reason is that it will take you to the next subcommand but to be able to execute this subcommand, you have to have it as a single string.
Two ways to solve this issue:
Count how many ';' there is, replace them by '\0' to have several contiguous strings in memory, then execute them one by one.
Write a function that splits your command string into a 2d array of subcommands, then execute them one by one.
Here's a code that does that if you need some inspiration:
Using linked lists: https://github.com/yoones/chelpers/blob/master/src/split.c
2d array version: https://github.com/yoones/hsn/blob/master/src/tools/split.c

exit command does not work properly in my own shell

I wrote a shell for an assignment and it works correctly, but there is a small run time error which i can not figure out. When the user enter the command 'exit' through the shell it should come out of newly created shell. But the problem is I have to type the command 'exit' several times to quit the shell. If someone can help me it will be a great pleasure for me! Thanks all!
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
char* cmndtkn[256];
char buffer[256];
char* path=NULL;
char pwd[128];
int main(){
//setting path variable
char *env;
env=getenv("PATH");
putenv(env);
system("clear");
printf("\t MY OWN SHELL !!!!!!!!!!\n ");
printf("_______________________________________\n\n");
while(1){
fflush(stdin);
getcwd(pwd,128);
printf("[MOSH~%s]$",pwd);
fgets(buffer,sizeof(buffer),stdin);
buffer[sizeof(buffer)-1] = '\0';
//tokenize the input command line
char* tkn = strtok(buffer," \t\n");
int i=0;
int indictr=0;
// loop for every part of the command
while(tkn!=NULL)
{
if(strcoll(tkn,"exit")==0 ){
exit(0);
}
else if(strcoll(buffer,"cd")==0){
path = buffer;
chdir(path+=3);}
else if(strcoll(tkn,"|")==0){
indictr=i;}
cmndtkn[i++] = tkn;
tkn = strtok(NULL," \t\n");
}cmndtkn[i]='\0';
// execute when command has pipe. when | command is found indictr is greater than 0.
if(indictr>0){
char* leftcmnd[indictr+1];
char* rightcmnd[i-indictr];
int a,b;
for(b=0;b<indictr;b++)
leftcmnd[b]=cmndtkn[b];
leftcmnd[indictr]=NULL;
for(a=0;a<i-indictr-1;a++)
rightcmnd[a]=cmndtkn[a+indictr+1];
rightcmnd[i-indictr]=NULL;
if(!fork())
{
fflush(stdout);
int pfds[2];
pipe(pfds);
if(!fork()){
close(1);
dup(pfds[1]);
close(pfds[0]);
execvp(leftcmnd[0],leftcmnd);
}
else{
close(0);
dup(pfds[0]);
close(pfds[1]);
execvp(rightcmnd[0],rightcmnd);
}
}else wait(NULL);
//command not include pipe
}else{
if(!fork()){
fflush(stdout);
execvp(cmndtkn[0],cmndtkn);
}else wait(NULL);
}
}
}
Like the cd command, the exit command has to be interpreted by the shell as a built-in; it must exit the loop or call the exit() function directly. However, it also appears that should be happening. Note that using strcoll() is a little unusual; normally, strcmp() is sufficient.
You should report problems if execvp() returns — and you must make sure the sub-shell exits so that you don't have multiple shell processes reading the input simultaneously. I'm left wondering if this problem is occurring, and that's why you have to type exit multiple times.
You also need to check that fgets() did not report an error. It always null terminates its input; your code does not zap the newline (you'd need strlen(buffer)-1 instead of sizeof(buffer)-1).
The code that reads and sets PATH is wrong. getenv("PATH") returns a pointer to the first character after the PATH= part; you then use that to 'set' the environment. Fortunately for you, the average value for PATH does not contain anything that looks like VAR=value, so it is functionally a no-op (though the information is probably copied into the environment, where it makes a mess without causing any major harm).
Your code indentation scheme is rococo at best — mostly, it is just woefully inconsistent. Please be consistent! The spacing of the lines in the code was also extremely erratic. When you're adding code in SO, do not use tabs, do use 4 spaces per indent level, do highlight a block of code that is left justified and use the {} button above the edit box to indent it as code. This also means you don't need to add blank lines to the code.
You aren't closing enough file descriptors. When you use dup() (or dup2()) to duplicate a pipe to standard input or standard output, you have to close both of the file descriptors returned by pipe().
On Linux, using fflush(stdin) is undefined behaviour, AFAIK. It is defined on Windows, but not on POSIX systems.
You don't check that your chdir() system call works.
Trying your code, I did get one runaway prompt. Unfortunately, I couldn't remember or see what triggered the runaway. The code below is mostly sanitized and seems to behave. I've annotated some critical changes — and not others. One of the things you should be doing for your own benefit is including trace like the dump_cmd() function so you can see what your program is doing.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
char *cmndtkn[256];
char buffer[256];
char *path = NULL;
char pwd[128];
static void dump_cmd(char **argv);
int main(void)
{
/*
//setting path variable
char *env;
env=getenv("PATH");
putenv(env);
system("clear");
*/
printf("\t MY OWN SHELL !!!!!!!!!!\n ");
printf("_______________________________________\n\n");
while (1)
{
//fflush(stdin);
getcwd(pwd, 128);
printf("[MOSH~%s]$", pwd);
if (fgets(buffer, sizeof(buffer), stdin) == 0)
{
putchar('\n');
break;
}
//buffer[sizeof(buffer)-1] = '\0';
buffer[strlen(buffer)-1] = '\0';
//tokenize the input command line
char *tkn = strtok(buffer, " \t\n");
int i = 0;
int indictr = 0;
// loop for every part of the command
while (tkn != NULL)
{
if (strcoll(tkn, "exit") == 0)
{
printf("Got: exit\n");
fflush(stdout);
exit(0);
}
else if (strcoll(tkn, "cd") == 0) // Was buffer, not tkn
{
printf("Got: cd (%s)\n", buffer + 3);
fflush(stdout);
path = buffer;
chdir(path += 3);
}
else if (strcoll(tkn, "|") == 0)
{
indictr = i;
}
cmndtkn[i++] = tkn;
tkn = strtok(NULL, " \t\n");
}
cmndtkn[i] = 0;
// execute when command has pipe. when | command is found indictr is greater than 0.
if (indictr > 0)
{
char *leftcmnd[indictr+1];
char *rightcmnd[i-indictr];
int a, b;
for (b = 0; b < indictr; b++)
leftcmnd[b] = cmndtkn[b];
leftcmnd[indictr] = NULL;
for (a = 0; a < i-indictr-1; a++)
rightcmnd[a] = cmndtkn[a+indictr+1];
rightcmnd[i-indictr-1] = NULL; // Did not include -1
if (!fork())
{
fflush(stdout);
int pfds[2];
pipe(pfds);
if (!fork())
{
dump_cmd(leftcmnd);
close(1);
dup(pfds[1]);
close(pfds[0]);
close(pfds[1]);
execvp(leftcmnd[0], leftcmnd);
fprintf(stderr, "failed to execvp() %s\n", leftcmnd[0]);
exit(1);
}
else
{
dump_cmd(rightcmnd);
close(0);
dup(pfds[0]);
close(pfds[0]);
close(pfds[1]);
execvp(rightcmnd[0], rightcmnd);
fprintf(stderr, "failed to execvp() %s\n", rightcmnd[0]);
exit(1);
}
}
else
wait(NULL);
}
else
{
//command does not include pipe
if (!fork())
{
dump_cmd(cmndtkn);
fflush(stdout);
execvp(cmndtkn[0], cmndtkn);
fprintf(stderr, "failed to execvp() %s\n", cmndtkn[0]);
exit(1);
}
else
wait(NULL);
}
}
return 0;
}
static void dump_cmd(char **argv)
{
int i = 0;
fprintf(stderr, "%d: Command:\n", (int)getpid());
while (*argv != 0)
fprintf(stderr, "%d: %d: [[%s]]\n", (int)getpid(), i++, *argv++);
}
I'm not keen on the code, but it does seem mostly sane.

Stream redirection and pipes when making a Linux shell

I have an assignment to create a Linux shell in C. Currently, I am stuck on implementing redirections and pipes. The code that I have so far is below. The main() parses user's input. If the command is built in, then that command is executed. Otherwise, the tokenized input is passed to execute() (I know that I should probably pull the built-in commands into their own function).
What execute() does is loop through the array. If it encounters <, >, or | it should take appropriate action. The first thing I am trying to get to work correctly is piping. I am definitely doing something wrong, though, because I cannot get it to work for even one pipe. For example, a sample input/output:
/home/ad/Documents> ls -l | grep sh
|: sh: No such file or directory
|
My idea was to get each of the directions and piping work for just one case, and then by making the function recursive I could hopefully use multiple redirections/pipes in the same command line. For example, I could do program1 < input1.txt > output1.txt or ls -l | grep sh > output2.txt.
I was hoping that someone can point out my errors in trying to pipe and perhaps offer some pointers in how to approach the case where multiple redirections/pipes are inputted by the user.
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int MAX_PATH_LENGTH = 1024; //Maximum path length to display.
int BUF_LENGTH = 1024; // Length of buffer to store user input
char * delims = " \n"; // Delimiters for tokenizing user input.
const int PIPE_READ = 0;
const int PIPE_WRITE = 1;
void execute(char **argArray){
char **pA = argArray;
int i = 0;
while(*pA != NULL) {
if(strcmp(argArray[i],"<") == 0) {
printf("<\n");
}
else if(strcmp(argArray[i],">") == 0) {
printf(">\n");
}
else if(strcmp(argArray[i],"|") == 0) {
int fds[2];
pipe(fds);
pid_t pid;
if((pid = fork()) == 0) {
dup2(fds[PIPE_WRITE], 1);
close(fds[PIPE_READ]);
close(fds[PIPE_WRITE]);
char** argList;
memcpy(argList, argArray, i);
execvp(argArray[0], argArray);
}
if((pid = fork()) == 0) {
dup2(fds[PIPE_READ], 0);
close(fds[PIPE_READ]);
close(fds[PIPE_WRITE]);
execvp(argArray[i+1], pA);
}
close(fds[PIPE_READ]);
close(fds[PIPE_WRITE]);
wait(NULL);
wait(NULL);
printf("|\n");
}
else {
if(pid == 0){
execvp(argArray[0], argArray);
printf("Command not found.\n");
}
else
wait(NULL);*/
}
*pA++;
i++;
}
}
int main () {
char path[MAX_PATH_LENGTH];
char buf[BUF_LENGTH];
char* strArray[BUF_LENGTH];
/**
* "Welcome" message. When mash is executed, the current working directory
* is displayed followed by >. For example, if user is in /usr/lib/, then
* mash will display :
* /usr/lib/>
**/
getcwd(path, MAX_PATH_LENGTH);
printf("%s> ", path);
fflush(stdout);
/**
* Loop infinitely while waiting for input from user.
* Parse input and display "welcome" message again.
**/
while(1) {
fgets(buf, BUF_LENGTH, stdin);
char *tokenPtr = NULL;
int i = 0;
tokenPtr = strtok(buf, delims);
if(strcmp(tokenPtr, "exit") == 0){
exit(0);
}
else if(strcmp(tokenPtr, "cd") == 0){
tokenPtr = strtok(NULL, delims);
if(chdir(tokenPtr) != 0){
printf("Path not found.\n");
}
getcwd(path, MAX_PATH_LENGTH);
}
else if(strcmp(tokenPtr, "pwd") == 0){
printf("%s\n", path);
}
else {
while(tokenPtr != NULL) {
strArray[i++] = tokenPtr;
tokenPtr = strtok(NULL, delims);
}
execute(strArray);
}
bzero(strArray, sizeof(strArray)); // clears array
printf("%s> ", path);
fflush(stdout);
}
}
Part of the problem is in the pipe handling code - as you suspected.
else if (strcmp(argArray[i], "|") == 0) {
int fds[2];
pipe(fds);
pid_t pid;
if ((pid = fork()) == 0) {
dup2(fds[PIPE_WRITE], 1);
close(fds[PIPE_READ]);
close(fds[PIPE_WRITE]);
char** argList;
memcpy(argList, argArray, i);
execvp(argArray[0], argArray);
}
if ((pid = fork()) == 0) {
dup2(fds[PIPE_READ], 0);
close(fds[PIPE_READ]);
close(fds[PIPE_WRITE]);
execvp(argArray[i+1], pA);
}
close(fds[PIPE_READ]);
close(fds[PIPE_WRITE]);
wait(NULL);
wait(NULL);
printf("|\n");
}
The first execvp() was probably intended to use argList since you've just copied some material there. However, you've copied i bytes, not i character pointers, and you've not ensured that the pipe is zapped and replaced with a null pointer.
memcpy(argList, argArray, i * sizeof(char *));
argList[i] = 0;
execvp(argList[0], argList);
Note that this has not verified that there is no buffer overflow on argList; Note that there is no space allocated for argList; if you use it, you should allocate the memory before doing the memcpy().
Alternatively, and more simply, you can do without the copy. Since you're in a child process, you can simply zap replace argArray[i] with a null pointer without affecting either the parent or the other child process:
argArray[i] = 0;
execvp(argArray[0], argArray);
You might also note that the second invocation of execvp() uses a variable pA which cannot be seen; it is almost certainly incorrectly initialized. As a moderately good rule of thumb, you should write:
execvp(array[n], &array[n]);
The invocations above don't conform to this schema, but if you follow it, you won't go far wrong.
You should also have basic error reporting and a exit(1) (or possibly _exit(1) or _Exit(1)) after each execvp() so that the child does not continue if it fails to execute. There is no successful return from execvp(), but execvp() most certainly can return.
Finally for now, these calls to execvp() should presumably be where you make your recursive call. You need to deal with pipes before trying to deal with other I/O redirection. Note that in a standard shell, you can do:
> output < input command -opts arg1 arg2
This is aconventional usage, but is actually permitted.
One good thing - you have ensured that the original file descriptors from pipe() are closed in all three processes (parent and both children). This is a common mistake which you have avoided making; well done.

Resources