4

I've condensed my problem into the following code:

#include <stdio.h>

int main(){
    char buffer[256];

    printf("Enter input: ");
    scanf("%s", buffer);

    system("/bin/sh");
    return 0;
}

If I run this program with user input, I get:

user@ubuntu:~/testing/temp$ ./main
Enter input: Test
$ 

With the last line being the shell that the program started.

But if I run the program with input coming from the pipeline:

user@ubuntu:~/testing/temp$ echo 'test' | ./main
Enter input: user@ubuntu:~/testing/temp$

The program doesn't seem to open the shell.


After some tinkering, I realized that if I did this:

user@ubuntu:~/testing/temp$ (python -c "print 'test'" ; echo 'ls') | ./main
a.out  main  main.c
Enter input: user@ubuntu:~/testing/temp

I was able to run the ls command in the opened shell.

So, two questions:

  1. Why doesn't the shell open like it did in the first case?
  2. How can I deal with this? It's very inconvenient to have to decide what commands to run before running the program: I'd much rather have a shell where I can dynamically choose what commands to run.
14
  • @downvoter: care to explain?
    – Daniel
    May 12 '15 at 19:17
  • This question is better suited for stack overflow.
    – bahamat
    May 12 '15 at 19:42
  • @bahamat: Is it? As far as I can tell the question is about UNIX, not about programming.
    – lcd047
    May 12 '15 at 19:48
  • 1
    Also, just to be clear, I didn't down vote. I did vote to close and move to stackoverflow.com.
    – bahamat
    May 12 '15 at 19:58
  • 1
    @Daniel: R. Stevens - Advanced Programming in the UNIX® Environment. It's a really good book, you won't regret any minute you spend reading it.
    – lcd047
    May 12 '15 at 20:47
3
  1. Why doesn't the shell open like it did in the first case?

In the first case, stdin is a terminal and the shell is interactive. It waits for your commands etc.

In the second case stdin is a pipe, and the shell is non-interactive. Your program consumes the first line on stdin (namely the string test\n), then the shell tries to read stdin and sees EOF. It exits, because that's what programs that get EOF on input are supposed to do.

In the third case the shell again is non-interactive, for the same reason. Your scanf() consumes the first line on stdin (i.e. test\n), then the shell reads ls. The shell runs ls, tries to read more commands, sees EOF, and exits.

  1. How can I deal with this?

If by "dealing with it" you mean running an interactive shell when stdin is connected to a pipe, the solution is to use a pty(7).

1
  • Dealing with it means between the scanf and the system, closing STDIN and reopening it to the /dev/pty. :)
    – Otheus
    May 12 '15 at 22:29
0

Your C program will behave very similarly to (read x; /bin/sh).

If you just type this snipped on a command line, then its standard input is connected to your terminal's keyboard; a line will be read, and then sh will read more lines until an end-of-file condition occurs (typically, one can hit Ctrl-D to cause one).

In the example where you pipe into your program, the input size is bounded; it's what you put there and nothing more: the lines after the first one, if any, will be interpreted by sh, which will then exit.

You could kindof simulate the pipe-less case by using cat to forward your keyboard input to sh:

(echo test; cat) | (read x; /bin/sh)

or perhaps:

(echo test; cat) | ./main

That probably won't be as nice as running a shell directly, however; detecting that its input isn't a terminal, it will probably disable its line editing capabilities.

0

To run an interactive shell connected to a pipe, you just have to make it interactive.

input | /bin/sh -i

The shell doesn't need a terminal to be interactive - it needs interactive input. In the main, the shell's behavior when run interactively has very little to do with terminals at all - (at least according to spec) - and generally differs from a non-interactive shell's behavior where error handling is concerned more than anything else. Interactive shells tend not to exit on error conditions that would otherwise cause a non-interactive shell to quit. A shell should default to interactive-mode if input comes from a terminal, though.

Some shells may seem to require terminal input for interactive use, but this is an illusion. In fact, these shells typically work very like your example case under the hood - bash, for example, sets up readline to handle the terminal i/o nitty-gritty on its behalf, zsh invokes its ZLE line-editor, and dash (if not compiled with the build-time option SMALL) links in BSD's libedit library. These line-editors read and process the terminal input into something like a line-by-line shell-script, which the shell then executes as appropriate.

You're not having an issue with any of those editors, though. Judging by your prompt and your execve call, you're calling up a dash that is compiled with the SMALL build-time option (the Debian default) in which case it will just work - but the only line-editing functions you might get out of it will be those natively provided by the terminal's line-discipline (see stty).

Your problem is that the shell doesn't have any input - when it detects EOF it dies as has already been noted elsewhere. You might do...

input | /bin/sh -i -o ignoreeof

But you will probably not like the results. dash won't do the quit on 10th consecutive null-read thing that some shells do - it will just print...

Type 'exit' to exit the shell

...to stderr forever. A little better could be...

cat input - | /bin/sh -i

...to concactenate the shell commands in the input file with cat's - stdin to its stdout and then to execute the results in an interactive /bin/sh. This will work - though you may want to ensure you configure the stty line-discipline to something like a canonical-state and with an appropriate erase key so you can at least get a functioning backspace. This is probably already set correctly - but it's worth checking anyway.

Another way...

echo ": some command; exec <$(tty)" | /bin/sh -i

The above methods will work because your pipeline is the current foreground job on the controlling terminal - your pipeline currently owns the terminal input and cat is actually reading it. This is contrasted by python -c and echo which do not read it - they only pass-on output as generated by command-line arguments - and so when their output ends so also does your shell's input. This is true unless the shell is instructed to look for input elsewhere as I do with echo in the second example.

The terminal is just one possible source of input of many to a shell - and it can be handled in many different ways. Your terminal's session leader - it looks like bash - waits for your pipeline's terminal control to end so it can regain control and do the next thing. This information will be passed to it as an asynchronous signal - this is what terminals are for. Terminals multiplex a display/input pair across any number of reading/printing processes. They do this very well.

For example:

$ PS1='bgsh1: ' sh -i +m & PS1='bgsh2: ' sh -i +m &
$ bgsh1: bgsh2: 
[2] + Stopped (tty input)        sh -i +m
[1] - Stopped (tty input)        sh -i +m
$ i=0; while [ "$((i+=1))" -lt 5 ]; do fg "%$(((i%2)+1))"; done
sh -i +m

bgsh2: var=something
bgsh2: kill -TSTP $$
sh -i +m

bgsh1: var=else
bgsh1: kill -TSTP $$
sh -i +m
bgsh2: echo $var; kill -TSTP $$
something
sh -i +m
bgsh1: echo $var; kill -TSTP $$
else
[1] + Stopped                    sh -i +m
[2] - Stopped                    sh -i +m
$ 

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.