Reading/Writing From File or Stdin/Stdout

In many CLI applications, it's common to be able to read from a file or stdin, and write to a file or stdout. This allows for the chaining of many CLI applications via pipes |.

StdioPath

Note

StdioPath requires Python 3.12+. For older Python versions, see Alternative Approach (Python < 3.12) below.

The recommended approach is to use StdioPath, a Path subclass that treats - as stdin (for reading) or stdout (for writing). This follows common Unix convention used by many command-line tools.

from cyclopts import App
from cyclopts.types import StdioPath

app = App()

@app.default
def scream(input_: StdioPath, output: StdioPath):
    """Uppercase all input data.

    Parameters
    ----------
    input_:
        Input file path, or "-" for stdin.
    output:
        Output file path, or "-" for stdout.
    """
    data = input_.read_text()
    output.write_text(data.upper())

if __name__ == "__main__":
    app()
$ echo "hello cyclopts users." > demo.txt

$ python scream.py demo.txt -
HELLO CYCLOPTS USERS.

$ python scream.py demo.txt output.txt && cat output.txt
HELLO CYCLOPTS USERS.

$ echo "foo" | python scream.py - -
FOO

StdioPath is pre-configured with allow_leading_hyphen=True, so - can be passed as an argument without being interpreted as an option.

Defaulting to Stdin/Stdout

To make stdin/stdout the default when no argument is provided, use StdioPath("-") as the default value:

from cyclopts import App
from cyclopts.types import StdioPath

app = App()

@app.default
def scream(input_: StdioPath = StdioPath("-"), output: StdioPath = StdioPath("-")):
    """Uppercase all input data.

    Parameters
    ----------
    input_:
        Input file path. Defaults to stdin if not provided.
    output:
        Output file path. Defaults to stdout if not provided.
    """
    data = input_.read_text()
    output.write_text(data.upper())

if __name__ == "__main__":
    app()
$ echo "hello cyclopts users." > demo.txt

$ python scream.py demo.txt
HELLO CYCLOPTS USERS.

$ python scream.py demo.txt output.txt && cat output.txt
HELLO CYCLOPTS USERS.

$ echo "foo" | python scream.py
FOO

Binary Data

StdioPath also supports binary reading and writing:

@app.default
def process_binary(input_: StdioPath = StdioPath("-"), output: StdioPath = StdioPath("-")):
    data = input_.read_bytes()
    output.write_bytes(data)

Or using the context manager interface:

@app.default
def process_binary(input_: StdioPath = StdioPath("-"), output: StdioPath = StdioPath("-")):
    with input_.open("rb") as f_in, output.open("wb") as f_out:
        f_out.write(f_in.read())

Alternative Approach (Python < 3.12)

For Python versions before 3.12, or when you prefer an Optional[Path] pattern where None indicates stdin/stdout, you can use helper functions:

import sys
from cyclopts import App
from pathlib import Path
from typing import Optional

def read_str(input_: Optional[Path]) -> str:
    return sys.stdin.read() if input_ is None else input_.read_text()

def write_str(output: Optional[Path], data: str):
    sys.stdout.write(data) if output is None else output.write_text(data)

def read_bytes(input_: Optional[Path]) -> bytes:
    return sys.stdin.buffer.read() if input_ is None else input_.read_bytes()

def write_bytes(output: Optional[Path], data: bytes):
    sys.stdout.buffer.write(data) if output is None else output.write_bytes(data)

app = App()

@app.default
def scream(input_: Optional[Path] = None, output_: Optional[Path] = None):
    """Uppercase all input data.

    Parameters
    ----------
    input_ : Optional[Path]
        If provided, read data from file. If not provided, read from stdin.
    output_ : Optional[Path]
        If provided, write data to file. If not provided, write to stdout.
    """
    data = read_str(input_)
    processed = data.upper()
    write_str(output_, processed)

if __name__ == "__main__":
    app()
$ echo "hello cyclopts users." > demo.txt
$ python scream.py demo.txt
HELLO CYCLOPTS USERS.
$ python scream.py demo.txt output.txt
$ cat output.txt
HELLO CYCLOPTS USERS.
$ echo "foo" | python scream.py
FOO