Files
gh-dashed-claude-marketplac…/tools/wait-for-text.sh
2025-11-29 18:17:58 +08:00

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