Kyle Colantonio
Kyle Colantonio

Reputation: 454

How to redirect printf() to file and then back to console

I am working on a project that requires me to have output from within a mini shell on my C program to output to a file. Using ./program > file.txt will NOT work.

I have a mini shell program that runs small commands, and I would like to make it so when someone has a > filename at the end of a command, it will redirect all text from printf() to a file and instead of the console, and then redirect it back into the console.

How can I accomplish this in C?

Upvotes: 2

Views: 25884

Answers (2)

Jonathan Leffler
Jonathan Leffler

Reputation: 755006

Normal shell with fork and exec

If you have a normal shell where you're using fork() and execvp() or one of its relatives, then you should fork the child process and in the child handle the I/O redirection (with open(), dup2() or dup(), and close()), leaving the parent's I/O unchanged.

Assuming you have parsed the command into:

char *argv[] = { "simple-command", "arg1", "arg2", 0 };

and the standard output file name into:

char *file1 = "filename";

then:

if ((pid = fork()) < 0)
    ...report error...
else if (pid == 0)
{
    int fd = open(file1, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0)
        ...report error and exit...
    dup2(fd, STDOUT_FILENO);
    close(fd);
    execvp(argv[0], argv);
    ...report error and exit...
}
else
{
    /* Do whatever the parent needs to do */
}

Note that the parent does not need to change its standard output at all.

You can also use the sequence:

close(STDOUT_FILENO);
int fd = open(file1, O_WRONLY | O_CREAT | O_TRUNC, 0644); // Or 0666
if (fd < 0)
    ...report error and exit...
if (fd != STDOUT_FILENO)
    ...standard input must have been closed too...

Redirecting built-in commands without fork and exec

Quite apart from redirecting in the wrong process, using freopen() or perhaps fdopen() and reopening /dev/stdout won't work. The file that /dev/stdout points to changes as the program is run and files are opened or closed as the main program changes what its file descriptor 1 (standard output) points at.

If you really need to redirect standard output of the main shell for a built-in, where the fork() and exec()-family functions are not used, then you need to do the equivalent of:

fflush(stdout);  // Safety precaution
int fd1 = open(file1, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd1 < 0)
    ...report error and do not continue...
int fd2 = dup(STDOUT_FILENO);
if (fd2 < 0)
    ...report error and do not continue...
if (dup2(fd2, STDOUT_FILENO) < 0)
    ...report error and do not continue...
close(fd1);  // check for error?
...do whatever operations need standard output redirected...
fflush(stdout);  // Safety precaution
if (dup2(fd1, STDOUT_FILENO) < 0)  // Bug fixed!
    ...report error and do not continue...
close(fd2);

Working code

I'm sorry, I'm not fully understanding this code (new to C). Can you give a full example?

OK. I've foregone the generality of a pointer to function for the built-in command which I first coded; I concluded it would probably confuse you more. I decline to do without a 'print an error' function (though I'd often beef it up to report on errno with strerror() as well as printing the basic error message).

I called the source file simpleshell.c and the program, therefore, simpleshell, but that's over-claiming what it does.

#include <stdarg.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

static int err_report(const char *fmt, ...);

static int redirect_echo_to_file(char **argv, char *file)
{
    /* Connect standard output to given file */
    fflush(stdout);
    int fd1 = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd1 < 0)
        return err_report("Failed to open %s for writing\n", file);
    int fd2 = dup(STDOUT_FILENO);
    if (fd2 < 0)
        return err_report("Failed to duplicate standard output\n");
    if (dup2(fd1, STDOUT_FILENO) < 0)
        return err_report("Failed to duplicate %s to standard output\n", file);
    close(fd1);

    /* Write to standard output */
    char *pad = "";
    while (*argv != 0)
    {
        printf("%s%s", pad, *argv++);
        pad = " ";
    }
    putchar('\n');

    /* Reconnect original standard output */
    fflush(stdout);
    if (dup2(fd2, STDOUT_FILENO) < 0)
        return err_report("Failed to reinstate standard output\n");
    close(fd2);
    return 0;
}

int main(void)
{
    char *file1 = "file.01";
    char *file2 = "file.02";
    char *arguments[] = { "Hello", "world!", "This", "is your", "echo", "speaking." };
    printf("This is the surrogate shell at work\n");
    printf("Echo the same message to two different files (%s and %s)\n", file1, file2);
    if (redirect_echo_to_file(arguments, file1) != 0 ||
        redirect_echo_to_file(arguments, file2) != 0)
        return -1;
    printf("Regular shell output on standard output.\n");
    printf("Please check %s and %s for a special message.\n", file1, file2);
    return 0;
}

static int err_report(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    return -1;
}

Sample output

$ ./simpleshell
This is the surrogate shell at work
Echo the same message to two different files (file.01 and file.02)
Regular shell output on standard output.
Please check file.01 and file.02 for a special message.
$ cat file.01
Hello world! This is your echo speaking.
$ cat file.02
Hello world! This is your echo speaking.
$ 

Upvotes: 6

Eric J.
Eric J.

Reputation: 150208

You can use freopen.

freopen("desiredFileName", "w", stdout);

Once you are done, you can undo the reassignment like this:

freopen("/dev/stdout", "w", stdout);

on Unix/Linux systems. I'm not sure whether there is a portable way to revert back.

UPDATE

If possible, you can use fprintf instead of printf, providing either stdout or a handle to the desired file, as appropriate.

Upvotes: 4

Related Questions