Keith
Keith

Reputation: 4881

Always output to screen and allow redirection

I'm writing a small CLI application and I want to allow the user to redirect to a file while standard cout statements go to the output.txt file I want progress to always to go the screen.

./myApp > output.txt
10% complete
...
90% complete
Completed

Is this possible? How can I do it?

Thanks in advance!!

Upvotes: 3

Views: 886

Answers (5)

Keith
Keith

Reputation: 4881

I found the official std:: method of handling this. There is another type... std::clog. This is specifically for information and always appears on the command line even though the user redirects the output of the program myProgram > out.txt.

Thanks this was great to see all the methods that this can be done.

Upvotes: 0

h0n3ycl0ud
h0n3ycl0ud

Reputation: 65

I figured out how to do it, even if the user redirects stderr. The following code gets the name of the current terminal and checks to see if our output is being redirected. It also has a my_write() function that allows you to write to both the terminal and the redirect file, if they've redirected stdout. You can use the my_write() function with the writetoterm variable where-ever you want to write something that you want to always be written to the terminal. The extern "C" has to be there, otherwise (on Debian 9 with GCC 6.3, anyway) the ttyname() function will just return NULL all the time.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <error.h>
#include <errno.h>
#include <sstream>

using std::string;
using std::fstream;
using std::cout;
using std::endl;
using std::cerr;
using std::stringstream;

void my_write(bool writetoterm, int termfd, string data)
{
    if(writetoterm)
    {
        int result = write(termfd, data.c_str(), data.length());
        if(result < data.length()){
            cerr << "Error writing data to tty" << endl;
        }
    }
    cout << data;
}

extern "C" {
char* GetTTY(int fd){
    //printf("%s", ttyname(fd));
    return ttyname(fd);
}
}

int main(int argc, char** argv){
    getenv("TTY");
    bool writetoterm = false;
    struct stat sb = {};

    if(!GetTTY(STDOUT_FILENO)){
        //not a TTY
        writetoterm = true;
    }
    int ttyfd = open(GetTTY(2), O_WRONLY);
    if(ttyfd < 0){
        //error in opening
        cout << strerror(errno) << endl;
    }
    string data = "Hello, world!\n";
    my_write(true, ttyfd, data);

    int num_for_cout = 42;
    stringstream ss;
    ss << "If you need to use cout to send something that's not a string" << endl;
    ss << "Do this: " << num_for_cout << endl;
    my_write(writetoterm, ttyfd, ss.str().c_str());

    return 0;
}

Upvotes: 0

spectras
spectras

Reputation: 13542

This will work even if both stdin and stdout have been redirected:

spectras@etherbee:~$ ./term
hello terminal!
spectras@etherbee:~$ ./term >/dev/null 2>&1
hello terminal!

The idea is to open the controlling terminal of the process directly, bypassing any redirection, like this:

#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int fd = open("/dev/tty", O_WRONLY);
    if (fd < 0 && errno != ENODEV) {
        /* something went wrong */
        return 1;
    }
    int hasTTY = (fd >= 0);

    if (hasTTY) {
        write(fd, "hello terminal!\n", 16);
    }
    return 0;
}

From man 4 tty:

The file /dev/tty is a character file with major number 5 and minor number 0, usually of mode 0666 and owner.group root.tty. It is a synonym for the controlling terminal of a process, if any.

If you're using C++, you might want to wrap the file descriptor into a custom streambuf, so you can use regular stream API on it. Alternately, some implementations of the C++ library offer extensions for that purpose. See here.
Or, if you don't care about getting the error code reliably, you could just std::ofstream terminal("/dev/tty").

Also as a design consideration if you do this, offering a quiet option to let the user turn off the writing to the terminal is a good idea.

Upvotes: 2

dbush
dbush

Reputation: 223719

You can write your progress indicators to the stderr stream. They will appear on the console if the user redirects stdout to a file.

For example:

fprintf(stderr, "10%% complete\n");

Upvotes: 0

user0042
user0042

Reputation: 8018

Your process cannot know if the shell redirects the standard console output (std::cout) or not.

So you'll need another handle that lets you output to the terminal independently of that redirection.

As @Mark mentioned in their comment you could (ab-)use1 std::cerr to do that, along with some ASCII trickery to overwrite the current output line at the terminal (look at backspace characters: '\b').


1)Not to mention the mess printed at the terminal if the output isn't actually redirected.

Upvotes: 0

Related Questions