3

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!!

Keith
  • 4,129
  • 3
  • 13
  • 25
  • 5
    You can use `cerr` instead of `cout`, but that's kind of a subversion of its intent. – Mark Ransom Aug 18 '17 at 23:09
  • Well, it's a quite widespread one - I'm quite sure that curl (as well as many other common tools) do that. – Matteo Italia Aug 18 '17 at 23:30
  • @MarkRansom – I tried std::cerr << "% complete" and that did not work. The output was still redirected to the users file. – Keith Aug 19 '17 at 02:04
  • I take that back... std::err << "% complete" worked. I missed a spot which caused me to believe it had NOT worked. Thanks... This is great and doesn't require the users to know about any special things to do. – Keith Aug 19 '17 at 02:18
  • Instead of messing with stdout and detecting terminals you may pipe your output through `tee` utility that does exactly what you need. – Paul Aug 21 '17 at 15:39

5 Answers5

2

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.

spectras
  • 13,105
  • 2
  • 31
  • 53
0

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.

user0042
  • 7,917
  • 3
  • 24
  • 39
  • 1
    Actually, usually you *can* know if you are writing on a terminal or not - on POSIX systems the `isatty` function exists just for this reason (typically utilities use it to disable ASCII escapes for formatting when the output is not going to a terminal). – Matteo Italia Aug 18 '17 at 23:32
  • @MatteoItalia Bit of offleg for standard c++ though. But thanks for adding that information. – user0042 Aug 18 '17 at 23:37
0

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;
}
h0n3ycl0ud
  • 65
  • 4
  • 1
    That plain doesn't work. The `FIONREAD` ioctl is used to know whether a file descriptor has data ready. Here, the stdin one. This is completely unrelated to whether stdout, stderr or both have been redirected. – spectras Aug 18 '17 at 23:46
0

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");
dbush
  • 205,898
  • 23
  • 218
  • 273
  • – I tried std::cerr << "% complete" and that did not work. The output was still redirected to the users file. – Keith Aug 19 '17 at 02:04
0

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.

Keith
  • 4,129
  • 3
  • 13
  • 25