2

I am trying to compile a C program using Python and want to give input using "<" operator but it's not working as expected. If I compile the C program and run it by giving input though a file it works; for example

./a.out <inp.txt works

But similarly if I try to do this using a Python script, it did not quite work out as expected. For example:

import subprocess
subprocess.call(["gcc","a.c","-o","x"])
subprocess.call(["./x"])

and

import subprocess
subprocess.call(["gcc","a.c","-o","x"])
subprocess.call(["./x","<inp.txt"])

Both script ask for input though terminal. But I think in the second script it should read from file. why both the programs are working the same?

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Vipul
  • 566
  • 5
  • 29

3 Answers3

5

To complement @Jonathan Leffler's and @alastair's helpful answers:

Assuming you control the string you're passing to the shell for execution, I see nothing wrong with using the shell for convenience. [1]

subprocess.call() has an optional Boolean shell parameter, which causes the command to be passed to the shell, enabling I/O redirection, referencing environment variables, ...:

subprocess.call("./x <inp.txt", shell = True)

Note how the entire command line is passed as a single string rather than an array of arguments.


[1] Avoid use of the shell in the following cases:

  • If your Python code must run on platforms other than Unix-like ones, such as Windows.
  • If performance is paramount.
  • If you find yourself "outsourcing" tasks better handled on the Python side.

If you're concerned about lack of predictability of the shell environment (as @alastair is):

  • subprocess.call with shell = True always creates non-interactive non-login instances of /bin/sh - note that it is NOT the user's default shell that is used.

  • sh does NOT read initialization files for non-interactive non-login shells (neither system-wide nor user-specific ones).

    • Note that even on platforms where sh is bash in disguise, bash will act this way when invoked as sh.
  • Every shell instance created with subprocess.call with shell = True is its own world, and its environment is neither influenced by previous shell instances nor does it influence later ones.

  • However, the shell instances created do inherit the environment of the python process itself:

    • If you started your Python program from an interactive shell, then that shell's environment is inherited. Note that this only pertains to the current working directory and environment variables, and NOT to aliases, shell functions, and shell variables.

    • Generally, that's a feature, given that Python (CPython) itself is designed to be controllable via environment variables (for 2.x, see https://docs.python.org/2/using/cmdline.html#environment-variables; for 3.x, see https://docs.python.org/3/using/cmdline.html#environment-variables).

    • If needed, you can supply your own environment to the shell via the env parameter; note, however, that you'll have to supply the entire environment in that event, potentially including variables such as USER and HOME, if needed; simple example, defining $PATH explicitly:

      subprocess.call('echo $PATH', shell = True, \
                      env = { 'PATH': '/sbin:/bin:/usr/bin' })
      
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Is there “nothing wrong with using the shell for convenience”? You'd think so, but what happens if e.g. the user sets IFS? Or, since the calls above to gcc don't specify a full path, PATH? It really is better not to use the shell to run things if you can possibly avoid it. – al45tair Jul 15 '14 at 08:26
  • @alastair: `$IFS` or `$PATH` could only be changed (temporarily) from the very same shell command you're executing; see my update on how the shell instances are created and let me know if you still have concerns about a modified environment (I may well be missing something). That said, a compelling reason to avoid the shell is if you also need to support Windows (unless you deliberately use only commands simple enough to work on both platform types). – mklement0 Jul 15 '14 at 14:04
  • According to the `subprocess` docs, unless an environment is explicitly specified, it’s inherited from the Python process. If someone can manipulate the environment of *that* process, it could still cause unwanted behaviour. – al45tair Jul 15 '14 at 14:18
  • @alastair: That is worth noting, but that's more of a _feature_ than a pitfall - see my update. – mklement0 Jul 15 '14 at 21:01
  • 1
    Sure. I still think using the shell from code is generally to be discouraged, especially where there's a simple non-shell-based alternative available. That said, I've up voted your answer because it's now quite comprehensive IMO :-) – al45tair Jul 16 '14 at 08:42
  • @alastair: Thank you. It's our exchange here that encouraged me to look into this more - and I do see your point. Hopefully there is now enough information here for everyone to make an informed decisions about whether using the shell is right for their needs. – mklement0 Jul 16 '14 at 13:02
4

The shell does I/O redirection for a process. Based on what you're saying, the subprocess module does not do I/O redirection like that. To demonstrate, run:

subprocess.call(["sh","-c", "./x <inp.txt"])

That runs the shell and should redirect the I/O. With your code, your program ./x is being given an argument <inp.txt which it is ignoring.

NB: the alternative call to subprocess.call is purely for diagnostic purposes, not a recommended solution. The recommended solution involves reading the (Python 2) subprocess module documentation (or the Python 3 documentation for it) to find out how to do the redirection using the module.

import subprocess
i_file = open("inp.txt")
subprocess.call("./x", stdin=i_file)
i_file.close()

If your script is about to exit so you don't have to worry about wasted file descriptors, you can compress that to:

import subprocess
subprocess.call("./x", stdin=open("inp.txt"))
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
2

By default, the subprocess module does not pass the arguments to the shell. Why? Because running commands via the shell is dangerous; unless they're correctly quoted and escaped (which is complicated), it is often possible to convince programs that do this kind of thing to run unwanted and unexpected shell commands.

Using the shell for this would be wrong anyway. If you want to take input from a particular file, you can use subprocess.Popen, setting the stdin argument to a file descriptor for the file inp.txt (you can get the file descriptor by calling fileno() a Python file object).

al45tair
  • 4,405
  • 23
  • 30