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