#!/bin/bash # AI-Grep: Semantic search tool # Beautiful implementation using composable functions SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/ai-common" show_usage() { cat << 'EOF' Usage: ai-grep [OPTIONS] PATTERN [FILE...] Semantic search based on meaning rather than exact string matching. Options: -v Select non-matching lines (semantic inverse) -n Show line numbers -c Show only count of matching lines -h Show this help message EOF } build_grep_prompt() { local pattern="$1" local invert_match="$2" local show_numbers="$3" local count_only="$4" local match_instruction if [[ "$invert_match" == "true" ]]; then match_instruction="Find lines that do NOT semantically match this pattern. Return only those non-matching lines, one per line." else match_instruction="Find lines that semantically match this pattern. Return only those matching lines, one per line." fi local output_instruction if [[ "$count_only" == "true" ]]; then output_instruction="Output only a single number - the count of matching lines." elif [[ "$show_numbers" == "true" ]]; then output_instruction="Prefix each line with its line number followed by a colon (format: 'N:line')." else output_instruction="Output only the lines themselves, no prefixes or formatting." fi cat << EOF You are a semantic grep tool. Analyze the following text and find lines that semantically match the pattern '$pattern'. CRITICAL: Output ONLY the requested lines or count. Do not include explanations, introductory text, or analysis. $match_instruction $output_instruction EOF } process_grep_response() { local response="$1" local count_only="$2" if [[ "$count_only" == "true" ]]; then local count if ! count=$(process_count_response "$response"); then return $? fi echo "$count" if [[ "$count" -gt 0 ]]; then return 0 else return "$EXIT_NO_MATCH" fi else process_filtered_response "$response" fi } validate_and_setup() { local pattern="$1" shift local files=("$@") ensure_dependencies ensure_argument_provided "Pattern" "$pattern" show_usage [[ ${#files[@]} -gt 0 ]] && ensure_files_exist "${files[@]}" } main() { local invert_match="false" local show_numbers="false" local count_only="false" local pattern="" local files=() # Parse options while getopts "vnch" opt; do case $opt in v) invert_match="true" ;; n) show_numbers="true" ;; c) count_only="true" ;; h) handle_help_option show_usage ;; \?) handle_invalid_option "$OPTARG" ;; esac done shift $((OPTIND-1)) pattern="$1" shift files=("$@") # Early validation with immediate exit on failure validate_and_setup "$pattern" "${files[@]}" # Process input with early exit local input input=$(process_input_sources "${files[@]}") || exit "$EXIT_NO_MATCH" # Execute LLM request with early exit local prompt response prompt=$(build_grep_prompt "$pattern" "$invert_match" "$show_numbers" "$count_only") response=$(execute_llm_request "$prompt" "$input") || handle_llm_error $? # Process response with early exit local result result=$(process_grep_response "$response" "$count_only") || exit $? echo "$result" } main "$@"