261 lines
10 KiB
Bash
Executable File
261 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# wait-for-text.sh - Poll a tmux pane for text pattern and exit when found
|
|
#
|
|
# PURPOSE:
|
|
# Synchronize with interactive programs running in tmux by waiting for specific
|
|
# output patterns (e.g., shell prompts, program completion messages).
|
|
#
|
|
# HOW IT WORKS:
|
|
# 1. Captures pane output at regular intervals (default: 0.5s)
|
|
# 2. Searches captured text for the specified pattern using grep
|
|
# 3. Exits successfully (0) when pattern is found
|
|
# 4. Exits with error (1) if timeout is reached
|
|
#
|
|
# USE CASES:
|
|
# - Wait for Python REPL prompt (>>>) before sending commands
|
|
# - Wait for gdb prompt before issuing breakpoint commands
|
|
# - Wait for "compilation complete" before running tests
|
|
# - Synchronize with any interactive CLI tool in tmux
|
|
#
|
|
# EXAMPLE:
|
|
# # Wait for Python prompt on custom socket
|
|
# ./wait-for-text.sh -S /tmp/my.sock -t session:0.0 -p '^>>>' -T 10
|
|
#
|
|
# # Wait for exact string "Ready" (fixed string, not regex)
|
|
# ./wait-for-text.sh -t myapp:0.0 -p 'Ready' -F -T 30
|
|
#
|
|
# DEPENDENCIES:
|
|
# - bash (with [[, printf, sleep)
|
|
# - tmux (for capture-pane)
|
|
# - grep (for pattern matching)
|
|
# - date (for timeout calculation)
|
|
#
|
|
|
|
# Bash strict mode:
|
|
# -e: Exit immediately if any command fails
|
|
# -u: Treat unset variables as errors
|
|
# -o pipefail: Pipe fails if any command in pipeline fails
|
|
set -euo pipefail
|
|
|
|
# Get script directory to source registry library
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=lib/registry.sh
|
|
source "$SCRIPT_DIR/lib/registry.sh"
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: wait-for-text.sh -t target -p pattern [options]
|
|
OR: wait-for-text.sh -s session -p pattern [options]
|
|
OR: wait-for-text.sh -p pattern [options] # auto-detect single session
|
|
|
|
Poll a tmux pane for text and exit when found.
|
|
|
|
Target Selection (priority order):
|
|
-s, --session session name (looks up socket/target in registry)
|
|
-t, --target tmux target (session:window.pane), explicit
|
|
(no flags) auto-detect if only one session in registry
|
|
|
|
Options:
|
|
-p, --pattern regex pattern to look for, required
|
|
-S, --socket tmux socket path (for custom sockets via -S)
|
|
-F, --fixed treat pattern as a fixed string (grep -F)
|
|
-T, --timeout seconds to wait (integer, default: 15)
|
|
-i, --interval poll interval in seconds (default: 0.5)
|
|
-l, --lines number of history lines to inspect (integer, default: 1000)
|
|
-h, --help show this help
|
|
|
|
Examples:
|
|
# Using session name
|
|
wait-for-text.sh -s my-python -p '>>>' -T 10
|
|
|
|
# Auto-detect single session
|
|
wait-for-text.sh -p '>>>' -T 10
|
|
|
|
# Explicit socket/target (backward compatible)
|
|
wait-for-text.sh -S /tmp/my.sock -t session:0.0 -p '>>>'
|
|
USAGE
|
|
}
|
|
|
|
# ============================================================================
|
|
# Default Configuration
|
|
# ============================================================================
|
|
|
|
# Required parameters (must be provided by user)
|
|
target="" # tmux target pane (format: session:window.pane)
|
|
pattern="" # regex pattern or fixed string to search for
|
|
|
|
# Optional parameters
|
|
session_name="" # session name for registry lookup
|
|
socket="" # tmux socket path (empty = use default tmux socket)
|
|
grep_flag="-E" # grep mode: -E (extended regex, default) or -F (fixed string)
|
|
timeout=15 # seconds to wait before giving up
|
|
interval=0.5 # seconds between polling attempts
|
|
lines=1000 # number of pane history lines to capture and search
|
|
|
|
# ============================================================================
|
|
# Parse Command-Line Arguments
|
|
# ============================================================================
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-s|--session) session_name="${2-}"; shift 2 ;; # Set session name for registry lookup
|
|
-t|--target) target="${2-}"; shift 2 ;; # Set target pane
|
|
-p|--pattern) pattern="${2-}"; shift 2 ;; # Set search pattern
|
|
-S|--socket) socket="${2-}"; shift 2 ;; # Set custom socket path
|
|
-F|--fixed) grep_flag="-F"; shift ;; # Use fixed string matching
|
|
-T|--timeout) timeout="${2-}"; shift 2 ;; # Set timeout duration
|
|
-i|--interval) interval="${2-}"; shift 2 ;; # Set poll interval
|
|
-l|--lines) lines="${2-}"; shift 2 ;; # Set history depth
|
|
-h|--help) usage; exit 0 ;; # Show help and exit
|
|
*) echo "Unknown option: $1" >&2; usage; exit 1 ;; # Error on unknown option
|
|
esac
|
|
done
|
|
|
|
# ============================================================================
|
|
# Session Resolution
|
|
# ============================================================================
|
|
# Resolve session name to socket/target if provided
|
|
# Priority: 1) Explicit -S/-t, 2) Session name -s, 3) Auto-detect single session
|
|
|
|
if [[ -n "$socket" && -n "$target" ]]; then
|
|
# Priority 1: Explicit socket and target provided (backward compatible)
|
|
: # Use as-is, no resolution needed
|
|
elif [[ -n "$session_name" ]]; then
|
|
# Priority 2: Session name provided, look up in registry
|
|
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
|
|
echo "Error: Session '$session_name' not found in registry" >&2
|
|
echo "Use 'list-sessions.sh' to see available sessions" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Extract socket and target from session data
|
|
socket=$(echo "$session_data" | jq -r '.socket')
|
|
target=$(echo "$session_data" | jq -r '.target')
|
|
|
|
# Update activity timestamp
|
|
registry_update_activity "$session_name" 2>/dev/null || true
|
|
elif [[ -z "$socket" && -z "$target" ]]; then
|
|
# Priority 3: No explicit params, try auto-detect single session
|
|
session_count=$(registry_list_sessions 2>/dev/null | jq '.sessions | length' 2>/dev/null || echo "0")
|
|
|
|
if [[ "$session_count" == "1" ]]; then
|
|
# Single session exists, auto-use it
|
|
auto_session_name=$(registry_list_sessions | jq -r '.sessions | keys[0]')
|
|
session_data=$(registry_get_session "$auto_session_name")
|
|
socket=$(echo "$session_data" | jq -r '.socket')
|
|
target=$(echo "$session_data" | jq -r '.target')
|
|
|
|
# Update activity timestamp
|
|
registry_update_activity "$auto_session_name" 2>/dev/null || true
|
|
elif [[ "$session_count" == "0" ]]; then
|
|
echo "Error: No sessions found in registry" >&2
|
|
echo "Create a session with 'create-session.sh' or specify -t and -S explicitly" >&2
|
|
exit 1
|
|
else
|
|
echo "Error: Multiple sessions found ($session_count total)" >&2
|
|
echo "Please specify session name with -s or use -t/-S explicitly" >&2
|
|
echo "Use 'list-sessions.sh' to see available sessions" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# ============================================================================
|
|
# Validate Required Parameters and Dependencies
|
|
# ============================================================================
|
|
|
|
# Check that required parameters were provided (after resolution)
|
|
if [[ -z "$target" || -z "$pattern" ]]; then
|
|
echo "target and pattern are required" >&2
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
# Validate that timeout is a positive integer (regex: one or more digits)
|
|
if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
|
|
echo "timeout must be an integer number of seconds" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Validate that lines is a positive integer
|
|
if ! [[ "$lines" =~ ^[0-9]+$ ]]; then
|
|
echo "lines must be an integer" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Check that tmux is installed and available in PATH
|
|
if ! command -v tmux >/dev/null 2>&1; then
|
|
echo "tmux not found in PATH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ============================================================================
|
|
# Calculate Deadline for Timeout
|
|
# ============================================================================
|
|
|
|
# Get current time in epoch seconds (Unix timestamp)
|
|
start_epoch=$(date +%s)
|
|
# Calculate deadline: current time + timeout duration
|
|
deadline=$((start_epoch + timeout))
|
|
|
|
# ============================================================================
|
|
# Main Polling Loop
|
|
# ============================================================================
|
|
# Repeatedly capture pane output and search for pattern until found or timeout
|
|
|
|
while true; do
|
|
# --------------------------------------------------------------------------
|
|
# Step 1: Capture pane output from tmux
|
|
# --------------------------------------------------------------------------
|
|
# tmux capture-pane options:
|
|
# -p: Print to stdout (instead of saving to paste buffer)
|
|
# -J: Join wrapped lines (prevents false line breaks from terminal width)
|
|
# -t: Target pane to capture from
|
|
# -S: Start line (negative = relative to end, e.g., -1000 = last 1000 lines)
|
|
#
|
|
# ${socket:+-S "$socket"} syntax explanation:
|
|
# - If $socket is set: expands to -S "$socket"
|
|
# - If $socket is empty: expands to nothing
|
|
# This allows optional socket parameter without breaking the command
|
|
#
|
|
# Error handling:
|
|
# 2>/dev/null: Suppress error messages if pane doesn't exist
|
|
# || true: Don't fail script if capture fails (exit 0 instead)
|
|
#
|
|
pane_text="$(tmux ${socket:+-S "$socket"} capture-pane -p -J -t "$target" -S "-${lines}" 2>/dev/null || true)"
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Step 2: Search captured text for pattern
|
|
# --------------------------------------------------------------------------
|
|
# Use printf to safely output text (handles special characters correctly)
|
|
# Pipe to grep to search for pattern
|
|
# $grep_flag: Either -E (regex) or -F (fixed string), set by --fixed flag
|
|
# --: Marks end of options (allows patterns starting with -)
|
|
# >/dev/null: Discard grep output (we only care about exit code)
|
|
# Exit code 0 = pattern found, 1 = not found
|
|
#
|
|
if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then
|
|
# SUCCESS: Pattern found in pane output
|
|
exit 0
|
|
fi
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Step 3: Check if timeout has been reached
|
|
# --------------------------------------------------------------------------
|
|
now=$(date +%s)
|
|
if (( now >= deadline )); then
|
|
# TIMEOUT: Pattern not found within specified time
|
|
echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2
|
|
echo "Last ${lines} lines from $target:" >&2
|
|
printf '%s\n' "$pane_text" >&2 # Show what was captured (for debugging)
|
|
exit 1
|
|
fi
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Step 4: Wait before next poll attempt
|
|
# --------------------------------------------------------------------------
|
|
# Sleep for specified interval before checking again
|
|
# Default: 0.5 seconds (configurable via --interval)
|
|
sleep "$interval"
|
|
done
|