4

My program is a common shell that I am trying to write in C++. In addition to taking commands from the command line it has to be able to read commands in a file - the file name being passed as an optional arg, not by redirection.

If the arg is present I open the passed filename otherwise I open "/dev/stdin". I'm not thrilled about opening the dev file and it isn't my main question but if someone has a better method I would love to hear it.

Eventually I have to read commands given to the shell but first I must present the prompt if it I am reading from stdin or skip the prompt if the input is coming from a file. My question is: is there a better way to determine in getCommand if the input stream is stdin than declaring a global or passing a bool or similar hacks?

It occurs to me that if I can somehow use std::cin rather than opening the /dev file I can pass the stream as an istream. Would that make it easier to distinguish between the two? E.G. if (source == cin) ?

Thanks for any and all suggestions.

bool getCommand(ifstream source, std::string command)
{
    if (source == stdin)
        //print prompt to stdout

    // do the rest of stuff

    return true;
}


int main(int argc, char *argv[])
{
    std::ifstream input;
    std::string command;

    if (argc == 2)
    {
        input.open(argv[1], std::ifstream::in);

        if (! input)
        {
            perror("input command file stream open");
            exit(EXIT_FAILURE);
        }
    }
    else
    {
        input.open("/dev/stdin", std::ifstream::in);

        if (! input)
        {
            perror("input stdin stream open");
            exit(EXIT_FAILURE);
        }
    }

    //.......

    if (getCommand(input, command))
        //.......
}
user2844690
  • 41
  • 1
  • 2
  • 1
    "I must present the prompt if it I am reading from stdin" -- this seems to be the wrong condition. You should present the prompt if you're reading from a terminal. It's just anti-social to write special code to *stop* someone piping or redirecting input into your program :-) – Steve Jessop Oct 04 '13 at 00:01
  • Yes, look for input being a terminal. You can use the source code to bash to find the functions to use. Or strace a bash shell to see what it calls to check file descriptor 0. – Zan Lynx Oct 04 '13 at 00:03
  • There are many other problems in your example code too. Passing std::ifstream by value, not passing std::string by reference when it's an out param. – goji Oct 04 '13 at 00:04
  • Also, you should be able to achieve what u want without doing an open on '/dev/stdin', without much difficulty. – goji Oct 04 '13 at 00:05
  • Yes, thanks to everyone for their input. There is no restriction on stdin versus tty and I appreciate the correction. I will incorporate the distinctions into the project and use the info in the answers to overcome my mistake. – user2844690 Oct 04 '13 at 00:50

2 Answers2

3

If you use std::istream& as the type of your variable then you can use std::cin instead of opening /dev/stdin:

std::ifstream fileinput;
if (argc == 2)
{
    fileinput.open(argv[1], std::ifstream::in);

    if (! fileinput)
    {
        perror("input command file stream open");
        exit(EXIT_FAILURE);
    }
}
std::istream &input = (argc == 2) ? fileinput : std::cin;

The remainder of your question should be about identifying a tty (Detect if stdin is a terminal or pipe?). But failing that, once you've made the above change you could compare with the address of std::cin:

if (&input == &std::cin) {
    std::cout << "prompt:";
}

Obviously that won't identify the case where the command line argument is /dev/stdin, so it's a bit of a bodge. But I'd probably argue that's actually an improvement given the funny requirement, because someone can at least write yourprogram /dev/stdin < input.txt, or sort input.txt | yourprogram /dev/stdin in order to work around your restriction and avoid the prompt :-)

Community
  • 1
  • 1
Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
2

First off, if you want to read either from std::cin or from a std::ifstream, I would implement the work in terms of an std::istream and certainly avoid opening /dev/stdin:

std::istream  in(std::cin.rdbuf());
std::ifstream fin;
if (argc == 2) {
    fin.open(av[1]);
    // check that the file is actually open or produce an error
    in.rdbuf(fin.rdbuf());
}
else {
    // determine if you need to create a prompt
}

if (getCommand(in, command)) {
    ...
}

Now, to determine if you actually need to write a prompt it isn't sufficient to determine if you are reading from standard input but you also need to determine if the standard input is connected to something with a screen and a keyboard. There is no portable way to do so, however. On UNIXes there are functions like isatty() which can be used to determine if file descriptor 0 is connected to a tty. I would use this information to set up an iword() which can then be used to check if the stream need a prompt:

static int const needs_prompt = std::ios_base::xalloc();
...
in.iword(needs_prompt) = isatty(0);
...
if (in.iword(needs_prompt)) {
    std::cout << "pompt>";
}

If you want to get fancy you can create a custom std::streambuf which is hooked up with an std::ostream which is tie()ed to the input stream stream and writes a prompt upon sync(): Every time a read operation on an input stream is done the tie()ed std::ostream is flushed. However, this requires that you only read once for each prompt being produced, e.g. using std::getline(in, line). Obviously, if you don't need a prompt, you don't tie() an std::ostream. Below is a simple program demonstrating the auto-prompt approach (without doing any of the other business):

#include <iostream>
#include <streambuf>

struct prompt
    : std::streambuf
{
    std::string   d_prompt;
    std::ostream& d_out;
    prompt(std::string const& p, std::ostream& out)
        : d_prompt(p)
        , d_out(out)
    {
    }
    int sync() {
        this->d_out << this->d_prompt << std::flush;
        return 0;
    }
};

int main()
{
    prompt       p("prompt>", std::cout);
    std::ostream helper(&p);
    std::cin.tie(&helper);

    for (std::string line; std::getline(std::cin, line); ) {
        std::cout << "read '" << line << "'\n";
    }
}
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380