Input Redirection in C - Using fgets to read from stdin not working

Viewed 527

I have an assignment where I need to create a shell in C. One of the specifications is to implement input redirection when the redirection symbol is entered ('<'). Then I am attempting to read the input from stdin using fgets. After the input is read, the saved_stdin is restored to the normal input stream.

However, on the first time the input is redirected, it works perfectly. Then, on the times after that, nothing is read from stdin. I have tried debugging several times but cannot find my error. Would really appreciate your help!

My code is below so that you can be able to reproduce the error, and help me correct my mistakes. Also, make sure to create a file named text.txt in the directory where you're running the program. Contents of text.txt:

hello, how are you?

Note that the code is extremely simplified and I am simply using hard-coded arguments. In my actual implementation, input is being given by the user (I am 100% sure that there is nothing wrong with receiving the input).

#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>

#define MAX_ARG_LENGTH 100

int redir_in(char **args, int in_pos);
void echo();
void reset();

// Global Variables
int saved_stdin;
bool changed_in;

int main(void)
{
    // sample arguments (args)
    char *args[MAX_ARG_LENGTH] = {"echo", "<", "text.txt"};
    // position of '<'
    int pos = 1;

    printf("output 1: ");
    if(redir_in(args, pos) == 0){
        echo();
        reset();
    }


    printf("\noutput 2: ");
    if(redir_in(args, pos) == 0){
        echo();
        reset();
    }

    printf("\nexiting...\n");

    return 0;
}

void echo()
{
    char line[MAX_ARG_LENGTH];

    while(fgets(line, MAX_ARG_LENGTH, stdin) != NULL)
        printf("%s", line);
}

// args is an array of inputted string arguments, in_pos is the index 
// of '<' in the array
int redir_in(char **args, int in_pos)
{
    char *file_name;
    file_name = args[in_pos + 1];

    int fd;
    fd = open(file_name, O_RDONLY, S_IRUSR);

    if (fd < 0)
        return -1;

    saved_stdin = dup(0);

    dup2(fd, STDIN_FILENO);
    close(fd);

    changed_in = true;

    return 0;
}

void reset()
{
    if(changed_in){
        dup2(saved_stdin, 0);
        close(saved_stdin);
        changed_in = false;
    }
}

Expected output:

output 1: hello, how are you?
output 2: hello, how are you?
exiting...

Output I'm getting:

output 1: hello, how are you?
output 2: 
exiting...
3 Answers

For the answer to your question, please read upto the end of this answer, as there are more things that I had to comment about your code.

First, you mix constant identifiers and the values themselves in the code, just to confound your readers?

    saved_stdin = dup(0);

    dup2(fd, STDIN_FILENO);

or you write:

    saved_stdin = dup(STDIN_FILENO);

    dup2(fd, STDIN_FILENO);

or

    saved_stdin = dup(0);

    dup2(fd, 0);

Please, be uniform and coherent.

Second, if you use S_IFRUSR, then you need to include <sys/stat.h>, which you don't.

Also, if you declare main() as int main(void), please do the same with void echo(void) and void reset(void) as void echo() and void reset() means that the compiler should allow you to pass echo() an undefined list of parameters (legacy syntax for variable argument passing), and it will not detect possible errors in case you change the interface.

The redirection code should be ok, except for minor things like not checking dup() and dup2() return values. In my system (FreeBSD 13.0) stdin renounces to give you more input once it has flagged EOF, (this happens also now to linux) so to be able to continue reading from it (remember that stdio is a library, not a set of system calls like open() or close()), you should call to clearerr(stdin); once you have detected EOF in stdin in function echo():

void echo(void)
{
    char line[MAX_ARG_LENGTH];

    while(fgets(line, MAX_ARG_LENGTH, stdin) != NULL)
        printf("%s", line);
        clearerr(stdin);   /* <--- this call allows you to read again after EOF */
}

This is the reason for stdin not to read anything else after redirecting back. I included a call to echo(); in between the two if statements, to check that the redirection worked back to the saved_stdin.

You are mixing two distinct APIs here -- the Unix API working on int file descriptors (open, dup, dup2, close, with 0 for standard input) and the C standard API working on FILE * handles (fgets, with stdin for standard input).

This is generally a bad idea. It is possible to make them work with each other, but it is fragile, and (needlessly) platform-dependent.

You can redirect stdin using the standard API as well, which I would recommend on general terms. Use fopen, freopen (for attaching a file to stdin), fgets, and fclose.

stdio and redirection do not mix. You dup your file descriptors but stdio looks at FILE* which has no idea about these manipulations. Once stdin is at feof, it is at feof.

Use read instead of fgets (don't forget to null-terminate the resulting buffer, remember that it is not a string). Alternatively, you can try to set stdin buffering mode to _IONBF and/or call clearerr each time you dup corresponding fd, but I have not tried that.

Related