junegunn / fzf

:cherry_blossom: A command-line fuzzy finder
https://junegunn.github.io/fzf/
MIT License
62.32k stars 2.35k forks source link

How fzf operate with stdin/stdout/stderr/tty/pipe? #3741

Closed anki-code closed 3 months ago

anki-code commented 3 months ago

Hello! First of all thank you for this awesome tool! I've opened this as an issue because it's mostly deep technical question.

I'm working on investigating of how xonsh shell works with interactive tools like fzf. And I found some comments that pointed out on using /dev/tty directly:

Note from [fzf and Popen: Calling fzf from nim doesn't show fzfs TUI](https://stackoverflow.com/questions/51932881/calling-fzf-from-nim-doesnt-show-fzfs-tui): > Normally programs read and write stdin/stdout to communicate with the user, but fzf uses a clever trick: > It opens `/dev/tty` which is always the terminal (and not stdin/stdout which might not be seen by the user, as is here the case). > This way fzf can be used in pipes e.g. `find . -name '*.mp3' | fzf | xargs vlc`. Another note about this behavior from [jonathanslenders message](https://github.com/prompt-toolkit/python-prompt-toolkit/issues/502#issuecomment-492395207): > Normally /dev/stdin is the input device and /dev/stdout is the output device. The file descriptors correspond to 0 and 1 respectively. However, when a program is attached to a TTY, /dev/stdin and /dev/stdout will refer to the same device - the slave side of the TTY - even though the file descriptors remain 0 and 1. This means, we can actually also open FD 1 for reading or FD 0 for writing.

After this I tried to create pipelines manually and found that capturing stderr follows to disappearing fzf UI with ability to interact with it (you can repeat this by running in bash ls / | fzf 2> /tmp/fzf_stderr):

```python # brew install fzf import subprocess as sp ls = sp.Popen(('ls','/'), stdout=sp.PIPE) fzf = sp.Popen(["fzf"], stdin=ls.stdout, stdout=sp.PIPE) # stderr=sp.PIPE - hide the fzf UI head = sp.Popen(["head"], stdin=fzf.stdout, stdout=sp.PIPE) stdout, stderr = head.communicate() print('stdout:', repr(stdout)) print('stderr:', repr(stderr)) ``` I found that if we set `stderr=sp.PIPE` in `sp.Popen(["fzf"]` we will have the case when there is no fzf interface on the screen but if you press Enter or use arrows you will get the right result.

This confused me as well.

Could you please technically describe how fzf operate with stdin/stdout/stderr/tty/pipe? It will be awesome if you also can help to understand this more deeply.

Many thanks!

junegunn commented 3 months ago