3

I was thinking about self-referential lists in Haskell - for example you might have the following script, fibonacci.hs:

#!/usr/bin/env runghc

fibs :: [Integer]
fibs = 1:1:(zipWith (+) fibs $ tail fibs)

main :: IO ()
main = do
    mapM_ print $ take 50 fibs

Because the filename extensions are so similar (just an xp away), I reasoned that something similar must be possible in a shell script, attaching a process' stdout to its own stdin. All I could come up with is fibonacci.sh:

#!/usr/bin/env sh

{ echo 1; echo 1; } > fibonaccis

tail -f fibonaccis |
    python3 -c 'if True: # dummy to fix indentation
        b = int(input())
        for _ in range(48):
            a, b = b, int(input())
            print(a + b)' >> fibonaccis

cat fibonaccis

However I find it a bit unsatisfactory that I have to write everything to a file. Obviously this is a toy example, with far more optimal known algorithms, but what if I wanted simply to generate the 1000th Fibonacci number? It would then be unnecessary to retain the lines of stdout that the Python script wrote once it has consumed them.

So is there any better way of using a file to do this, or ideally a way to do this without a file, just using redirection? Or is this just unequivocally a terrible idea? Could this perhaps be done with a named pipe?

Whatever I have tried to search just comes up with ways to attach a process' stdout to the stdin of another process, which I am already vaguely familiar with. Bi-directional pipes doesn't seem to be exactly what I want either.

I am aware of the fact that some care will need to be taken to make sure the process is not buffering its I/O too liberally.

1 Answers1

3

As mathew gunther points out in his answer, a named pipe ("fifo") can be used to get a script to read its own output.

#!/bin/bash

# Create pipe and arrange to remove it when done.
mkfifo mypipe
trap 'rm -f mypipe' EXIT

# Just to start off the sequence. Note that this
# needs to be a background process to avoid
# blocking, as there is nobody reading from the
# pipe yet.
echo '0 1' >mypipe &

# Numbers to produce. Note that the produced numbers
# will overflow at n=93.
n=48

# Read the two numbers from the pipe and put the
# last of these back together with the next number
# in the sequence:
while [ "$n" -gt 0 ] && read a b; do
        c=$(( a + b ))
        printf '%d %d\n' "$b" "$c" >&3

        # Also print the "b" number as output.
        printf '%d\n' "$b"

        n=$(( n - 1 ))
done <mypipe 3>mypipe

I'm using an extra file descriptor for writing to the pipe in the loop, just to be able to output the generated sequence of numbers to the standard output stream as normally (and to not overload the standard error stream of this purpose).

If you want to use your Python code with this approach, you would likely do something like

#!/bin/bash

mkfifo mypipe
trap 'rm -f mypipe' EXIT

printf '%d\n%d\n' 0 1 >mypipe &

python3 -c 'if True: # dummy to fix indentation
    b = int(input())
    for _ in range(48):
        a, b = b, int(input())
        print(a + b)' <mypipe >mypipe

You'll notice that you do not get any output from this script, which is because we are using the output of the Python code as its own input. To get some output in the terminal, we'd do something similar in my shell loop above (write to something other than standard output):

#!/bin/bash

mkfifo mypipe
trap 'rm -f mypipe' EXIT

printf '%d\n%d\n' 0 1 >mypipe &

python3 -c '
import sys
b = int(input())
for _ in range(48):
    a, b = b, int(input())
    print(a + b)
    print(b, file=sys.stderr)' <mypipe >mypipe

Here, I (ab-)use the standard error stream to provide the actual result to the user in the terminal. It would have been neater if I had done it just like I did the shell loop in my first variation (handle the output to the pipe over a new file descriptor), but my knowledge of Python is severely lacking.

Kusalananda
  • 333,661