Search code examples
pythonstdoutpipeline

sys.stdout.write() printing at wrong time with pipelines in Python


I'm having a problem in a homework assignment related to printing using the sys.stdout.write() function. What I have to do is to implement a simple shell which supports stdin/stdout redirections and pipelines. In the code below, from my program "ch.py", the problem is that the "%% " asking for an input seems to be printed at the wrong time when the expression entered contains a pipeline.

For example, if I enter "find -name ch.py | xargs grep import", what I want to get is:

%% find -name ch.py | xargs grep import
import os
import sys
import shlex
%% 

But what I actually get is:

%% find -name ch.py | xargs grep import
%% import os
import sys
import shlex

Since the problem is not present when the input contains no pipeline as in "ls" or "cat ch.py", I suppose that my main process doesn't wait for all child processes to complete before continuing. I tried calling "os.waitpid()" with other combinaisons of arguments or using "os.wait()" instead but nothing seems to fix the problem.

Could someone help me understand what I'm wrong about?

import os
import sys
import shlex


def main():
    while True:
        sys.stdout.write("%% ")
        sys.stdout.flush()

        user_input = sys.stdin.readline()

        if user_input:
            parsed_input, to_write = parse(user_input)
        else:  # user pressed Ctrl+D, exit program
            sys.stdout.write("Bye!\n")
            sys.exit(0)

        pid = os.fork()
        if pid == 0:  # EVALUATION PROCESS
            if to_write:              # if stdout redirection
                os.dup2(to_write, 1)  # write in file instead of stdout
                os.close(to_write)

            evaluate(parsed_input, len(parsed_input)-1)
        elif pid > 0:  # MAIN PROCESS
            os.waitpid(pid, 0)  # wait for evaluation to complete


def evaluate(parsed_input, pipe_count):
    if pipe_count == 0:
        # execute leftmost command of the expression
        os.execvp(parsed_input[0][0], parsed_input[0])
    elif pipe_count > 0:
        r, w = os.pipe()

        pid = os.fork()
        if pid == 0:  # CHILD
            os.close(w)
            os.dup2(r, 0)  # read from pipeline instead of stdin
            os.close(r)
            # execute right side of the pipeline
            os.execvp(parsed_input[-1][0], parsed_input[-1])
        elif pid > 0:  # PARENT
            os.close(r)
            os.dup2(w, 1)  # write in pipeline instead of stdout
            os.close(w)
            # evaluate left side of the pipeline
            evaluate(parsed_input[:-1], pipe_count-1)

Solution

  • Here the fixed code, thank you again Sami Laine.

    def evaluate(parsed_input, pipe_count):
        if pipe_count == 0:
            # execute leftmost command of the expression
            os.execvp(parsed_input[0][0], parsed_input[0])
        elif pipe_count > 0:
            r, w = os.pipe()
    
            pid = os.fork()
            if pid == 0:  # CHILD
                os.close(r)
                os.dup2(w, 1)  # write in pipeline instead of stdout
                os.close(w)
                # evaluate left side of the pipeline
                evaluate(parsed_input[:-1], pipe_count-1)
            elif pid > 0:  # PARENT
                os.close(w)
                os.dup2(r, 0)  # read from pipeline instead of stdin
                os.close(r)
                os.waitpid(pid, 0)  # wait for children to complete
                # execute right side of the pipeline
                os.execvp(parsed_input[-1][0], parsed_input[-1])