Implementing Readline Features
Pythonimport readline import atexit import os histfile = os.path.join(os.path.expanduser("~"), ".myapp_history") try: readline.read_history_file(histfile) readline.set_history_length(1000) except FileNotFoundError: pass atexit.register(readline.write_history_file, histfile) def completer(text, state): options = [cmd for cmd in COMMANDS if cmd.startswith(text)] if state < len(options): return options[state] return None readline.set_completer(completer) readline.parse_and_bind("tab: complete") while True: line = input("prompt> ")
Note: readline is Unix-only (GNU readline or libedit). On Windows, use pyreadline3 as a drop-in substitute, guarded by a try/except import.
Progress:
- Step 1: Import readline with platform guard
- Step 2: Set up history file (load on start, save on exit)
- Step 3: Configure history length limit
- Step 4: Implement completer function (stateful, index-based)
- Step 5: Bind completer with
parse_and_bind - Step 6: Set completer delimiters if needed (e.g., for path/attribute completion)
- Step 7: Test on target platforms (libedit vs GNU readline have different bind syntax)
Step 1: Platform-safe import
Pythontry: import readline except ImportError: readline = None # Windows fallback or degrade gracefully
Step 2-3: History management
Pythonimport readline, atexit, os histfile = os.path.expanduser("~/.app_history") try: readline.read_history_file(histfile) except FileNotFoundError: open(histfile, 'wb').close() readline.set_history_length(1000) atexit.register(readline.write_history_file, histfile)
Use readline.append_history_file(n, filename) for multi-session append instead of full overwrite when concurrent sessions are possible.
Step 4-5: Completion
The completer is called repeatedly with increasing state until it returns None:
Pythondef make_completer(options): def completer(text, state): matches = [o for o in options if o.startswith(text)] try: return matches[state] except IndexError: return None return completer readline.set_completer(make_completer(["start", "stop", "status"])) readline.parse_and_bind("tab: complete")
For attribute/path-like completion, adjust delimiters:
Pythonreadline.set_completer_delims(' \t\n`~!@#$%^&*()-=+[{]}\\|;:\'",<>/?')
Step 6: Detect libedit vs GNU readline (macOS defaults to libedit)
Pythonif readline.__doc__ and 'libedit' in readline.__doc__: readline.parse_and_bind("bind ^I rl_complete") else: readline.parse_and_bind("tab: complete")
Example 1: Filesystem path completion
Input: User types ls /usr/lo<TAB>
Output: Completer inspects text = "/usr/lo", globs matching paths, returns /usr/local/ on first Tab press.
Pythonimport glob def path_completer(text, state): matches = glob.glob(text + '*') matches = [m + '/' if os.path.isdir(m) else m for m in matches] return matches[state] if state < len(matches) else None readline.set_completer(path_completer) readline.set_completer_delims(' \t\n') readline.parse_and_bind("tab: complete")
Example 2: Persistent cross-session history
Input: App run 1 enters deploy prod, exits; app run 2 starts.
Output: Pressing Up-arrow in run 2 immediately recalls deploy prod because read_history_file loaded it at startup.
Example 3: Custom key binding
Input: Bind Ctrl-L to clear screen instead of default behavior. Output:
Pythonreadline.parse_and_bind(r'"\C-l": clear-screen')
- Always wrap
read_history_filein try/exceptFileNotFoundError— file won't exist on first run. - Cap history with
set_history_length()to avoid unbounded growth. - Register
write_history_fileviaatexit, not manually at every exit point. - Use
readline.set_pre_input_hook()sparingly — it's for pre-filling input, not general logic. - When completions are expensive (e.g., DB lookups), cache results per
textprefix within a single completion cycle sincestate=0,1,2...calls happen in quick succession. - Strip readline's own history entries for sensitive input (passwords) — use
input()'s underlyinggetpassinstead, since readline will persist everything to history. - Check
readline.__doc__for'libedit'to branch bind syntax on macOS.
- Assuming readline exists on Windows — it doesn't ship in stdlib there; always guard the import.
- Forgetting the completer must return
Noneto terminate, not raise or return empty string — infinite state loops otherwise. - Not resetting completer_delims when completing things like file paths — default delims split on
/, breaking full-path suggestions. - Writing history file on every input line — expensive; let
atexithandle it once, or useappend_history_fileperiodically instead. - Logging passwords into history — never route sensitive prompts through a readline-enabled
input()without disabling history for that call. - Ignoring libedit differences on macOS —
parse_and_bind("tab: complete")silently fails to bind Tab under libedit; needsbind ^I rl_complete.