Skip to content

Instantly share code, notes, and snippets.

@csalzano
Last active August 13, 2026 21:16
Show Gist options
  • Select an option

  • Save csalzano/636bfbc7c3f115737d43d126a60112eb to your computer and use it in GitHub Desktop.

Select an option

Save csalzano/636bfbc7c3f115737d43d126a60112eb to your computer and use it in GitHub Desktop.
Wrapper for "wp db search" with output that is easier to read
#!/bin/bash
# wp-db-search2.sh
#
# Enriches `wp db search` output with option names, post titles/status,
# permalinks, and highlighted match context.
#
# Author: Corey Salzano <https://breakfastco.xyz>
# License: MIT
#
# Copyright (c) 2026 Corey Salzano
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
#
# Usage
# -----
# Copy to a WordPress root directory where WP-CLI (`wp`) is available.
#
# ./wp-db-search2.sh <search-term>
# ./wp-db-search2.sh --with-revisions <search-term>
#
# Options:
# --with-revisions Include revision posts/meta (hidden by default)
#
# Examples:
# ./wp-db-search2.sh '[hover'
# ./wp-db-search2.sh --with-revisions 'One PDF. Every expedition.'
#
# Tips:
# - Quote search terms that contain spaces or shell special chars
# - Matches are grouped by options, post meta, and posts
# - Stats summary prints at the end (counts + revisions hidden)
# Color codes
RESET='\033[0m'
BOLD='\033[1m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
RED='\033[0;31m'
BLUE='\033[0;34m'
GRAY='\033[0;90m'
MAGENTA='\033[0;35m'
# Parse arguments
INCLUDE_REVISIONS=false
SEARCH_TERM=""
while [[ $# -gt 0 ]]; do
case $1 in
--with-revisions)
INCLUDE_REVISIONS=true
shift
;;
*)
if [ -z "$SEARCH_TERM" ]; then
SEARCH_TERM="$1"
fi
shift
;;
esac
done
if [ -z "$SEARCH_TERM" ]; then
echo "Usage: $0 [--with-revisions] <search-term>"
echo "Example: $0 '[hover'"
echo "Example: $0 --with-revisions 'One PDF. Every expedition.'"
exit 1
fi
# Highlight SEARCH_TERM in a wp db search snippet and print it.
# Uses the CLI snippet as-is (including accumulated continuation lines) —
# no extra DB round-trip for the value body.
print_highlighted_snippet() {
local snippet="$1"
local indent="${2:- }"
local preview highlighted
preview=$(printf '%s' "$snippet" | tr '\n\r' ' ' | sed 's/ */ /g;s/^ *//;s/ *$//')
highlighted=$(printf '%s' "$preview" | awk -v search="$SEARCH_TERM" -v yellow="${YELLOW}" -v gray="${GRAY}" '{
result = ""
pos = 1
len = length($0)
search_len = length(search)
while (pos <= len) {
idx = index(substr($0, pos), search)
if (idx > 0) {
result = result substr($0, pos, idx - 1) yellow search gray
pos = pos + idx + search_len - 1
} else {
result = result substr($0, pos)
break
}
}
print result
}')
if [ ${#preview} -gt 200 ]; then
echo -e "${indent}${GRAY}${highlighted:0:200}...${RESET}"
else
echo -e "${indent}${GRAY}${highlighted}${RESET}"
fi
}
# Pending match buffer: wp db search often splits a single match across lines
# (ID:first-fragment, then continuation lines). We accumulate until the next
# ID:/table header/EOF, then flush once.
PENDING_KIND=""
PENDING_SNIPPET=""
PENDING_SKIP=false
PENDING_OPTION_ID=""
PENDING_OPTION_NAME=""
PENDING_META_ID=""
PENDING_POST_ID=""
PENDING_META_KEY=""
PENDING_POST_TITLE=""
PENDING_POST_TYPE=""
PENDING_POST_STATUS=""
PENDING_STATUS_COLOR=""
PENDING_PERMALINK=""
PENDING_POSTS_COLUMN=""
clear_pending() {
PENDING_KIND=""
PENDING_SNIPPET=""
PENDING_SKIP=false
PENDING_OPTION_ID=""
PENDING_OPTION_NAME=""
PENDING_META_ID=""
PENDING_POST_ID=""
PENDING_META_KEY=""
PENDING_POST_TITLE=""
PENDING_POST_TYPE=""
PENDING_POST_STATUS=""
PENDING_STATUS_COLOR=""
PENDING_PERMALINK=""
PENDING_POSTS_COLUMN=""
}
ensure_postmeta_header() {
if [ "$PENDING_POSTMETA_HEADER" = true ]; then
if [ "$FIRST_RESULT" = false ]; then
echo ""
fi
echo -e "${BOLD}${MAGENTA}━━━ POST META ━━━${RESET}"
FIRST_RESULT=false
PENDING_POSTMETA_HEADER=false
POSTMETA_HEADER_OUTPUT=true
fi
}
ensure_posts_header() {
if [ "$PENDING_POSTS_HEADER" = true ]; then
if [ "$FIRST_RESULT" = false ]; then
echo ""
fi
echo -e "${BOLD}${BLUE}━━━ POSTS (${PENDING_POSTS_COLUMN}) ━━━${RESET}"
FIRST_RESULT=false
PENDING_POSTS_HEADER=false
fi
}
flush_pending_match() {
if [ -z "$PENDING_KIND" ]; then
return
fi
if [ "$PENDING_SKIP" = true ]; then
clear_pending
return
fi
case "$PENDING_KIND" in
options)
echo ""
echo -e " ${BOLD}${YELLOW}${RESET} ${CYAN}Option:${RESET} ${BOLD}${PENDING_OPTION_NAME}${RESET} ${GRAY}| ID: ${GREEN}${PENDING_OPTION_ID}${RESET}"
print_highlighted_snippet "$PENDING_SNIPPET" ""
;;
postmeta)
ensure_postmeta_header
echo ""
echo -e " ${BOLD}${BLUE}${RESET} ${BOLD}Post:${RESET} ${BOLD}${PENDING_POST_TITLE}${RESET}"
echo -e " ${GRAY}Post ID:${RESET} $PENDING_POST_ID ${GRAY}|${RESET} ${GRAY}Type: $PENDING_POST_TYPE${RESET} ${GRAY}|${RESET} ${GRAY}Status:${RESET} ${PENDING_STATUS_COLOR}$PENDING_POST_STATUS${RESET} ${GRAY}|${RESET} ${GRAY}Meta ID: $PENDING_META_ID${RESET}"
if [ -n "$PENDING_META_KEY" ]; then
echo -e " ${MAGENTA}Meta Key:${RESET} $PENDING_META_KEY"
fi
if [ -n "$PENDING_PERMALINK" ]; then
echo -e " ${CYAN}Permalink:${RESET} $PENDING_PERMALINK"
fi
print_highlighted_snippet "$PENDING_SNIPPET"
;;
posts)
ensure_posts_header
echo ""
echo -e " ${BOLD}${BLUE}${RESET} ${BOLD}Post:${RESET} ${BOLD}${PENDING_POST_TITLE}${RESET}"
echo -e " ${GRAY}Post ID:${RESET} $PENDING_POST_ID ${GRAY}|${RESET} ${GRAY}Type: $PENDING_POST_TYPE${RESET} ${GRAY}|${RESET} ${GRAY}Status:${RESET} ${PENDING_STATUS_COLOR}$PENDING_POST_STATUS${RESET}"
if [ -n "$PENDING_PERMALINK" ]; then
echo -e " ${CYAN}Permalink:${RESET} $PENDING_PERMALINK"
fi
print_highlighted_snippet "$PENDING_SNIPPET"
;;
esac
clear_pending
}
append_pending_snippet() {
local fragment="$1"
if [ -z "$PENDING_KIND" ] || [ "$PENDING_SKIP" = true ]; then
return
fi
if [ -n "$PENDING_SNIPPET" ]; then
PENDING_SNIPPET="${PENDING_SNIPPET}"$'\n'"${fragment}"
else
PENDING_SNIPPET="$fragment"
fi
}
status_color_for() {
case "$1" in
publish) printf '%s' "$GREEN" ;;
draft) printf '%s' "$YELLOW" ;;
*) printf '%s' "$RED" ;;
esac
}
TABLE_PREFIX=$(wp db prefix 2>/dev/null)
if [ -z "$TABLE_PREFIX" ]; then
echo "Error: could not determine table prefix (is WP-CLI available in this directory?)"
exit 1
fi
TEMP_FILE=$(mktemp)
# One search with --stats: match lines + Success summary (avoids a second full scan)
wp db search "$SEARCH_TERM" --stats 2>&1 | grep -v "No text columns for table '${TABLE_PREFIX}actionscheduler" > "$TEMP_FILE"
# Process lines with state tracking
CURRENT_TABLE=""
FIRST_RESULT=true
PENDING_POSTMETA_HEADER=false
POSTMETA_HEADER_OUTPUT=false
PENDING_POSTS_HEADER=false
CURRENT_POSTS_COLUMN=""
LAST_POSTS_COLUMN=""
# Stats counters
OPTIONS_COUNT=0
POSTMETA_COUNT=0
POSTS_COUNT=0
POSTS_DISPLAYED=0
POSTS_REVISIONS_HIDDEN=0
POSTMETA_DISPLAYED=0
POSTMETA_REVISIONS_HIDDEN=0
OTHER_COUNT=0
TOTAL_MATCHES_FROM_STATS=""
while IFS= read -r line || [ -n "$line" ]; do
# Stats trailer from --stats
# WP-CLI prints "1 match" or "N matches" — match the shared prefix
if [[ "$line" =~ ^Success:\ Found[[:space:]]+([0-9]+)[[:space:]]+match ]]; then
flush_pending_match
TOTAL_MATCHES_FROM_STATS="${BASH_REMATCH[1]}"
continue
fi
# Skip warnings (including actionscheduler warnings that we filtered but might still appear)
if [[ "$line" =~ ^Warning:.*actionscheduler ]]; then
continue
elif [[ "$line" =~ ^Warning: ]]; then
flush_pending_match
echo -e "${YELLOW}${RESET} $line"
continue
fi
# Table header lines end the previous match's continuation block
if [[ "$line" == "${TABLE_PREFIX}options:option_value" ]]; then
flush_pending_match
CURRENT_TABLE="options"
if [ "$FIRST_RESULT" = false ]; then
echo ""
fi
echo -e "${BOLD}${CYAN}━━━ OPTIONS ━━━${RESET}"
FIRST_RESULT=false
continue
elif [[ "$line" == "${TABLE_PREFIX}postmeta:meta_value" ]]; then
flush_pending_match
CURRENT_TABLE="postmeta"
if [ "$POSTMETA_HEADER_OUTPUT" = false ]; then
PENDING_POSTMETA_HEADER=true
fi
continue
elif [[ "$line" == "${TABLE_PREFIX}posts:"* ]]; then
flush_pending_match
CURRENT_TABLE="posts"
PENDING_POSTMETA_HEADER=false
COLUMN_NAME="${line#${TABLE_PREFIX}posts:}"
CURRENT_POSTS_COLUMN="$COLUMN_NAME"
if [ "$CURRENT_POSTS_COLUMN" != "$LAST_POSTS_COLUMN" ]; then
PENDING_POSTS_HEADER=true
LAST_POSTS_COLUMN="$CURRENT_POSTS_COLUMN"
fi
continue
elif [[ "$line" == "${TABLE_PREFIX}"* ]]; then
flush_pending_match
CURRENT_TABLE="other"
PENDING_POSTMETA_HEADER=false
echo -e "${GRAY}$line${RESET}"
continue
fi
# Data / continuation lines
if [ "$CURRENT_TABLE" = "options" ]; then
if [[ "$line" =~ ^([0-9]+): ]]; then
flush_pending_match
PENDING_OPTION_ID="${BASH_REMATCH[1]}"
OPTIONS_COUNT=$((OPTIONS_COUNT + 1))
PENDING_OPTION_NAME=$(wp db query "SELECT option_name FROM ${TABLE_PREFIX}options WHERE option_id = $PENDING_OPTION_ID" --skip-column-names 2>/dev/null | head -1 | tr -d '\n')
if [ -z "$PENDING_OPTION_NAME" ]; then
echo "$line"
clear_pending
continue
fi
PENDING_KIND="options"
PENDING_SNIPPET="${line#*:}"
PENDING_SKIP=false
else
append_pending_snippet "$line"
fi
elif [ "$CURRENT_TABLE" = "postmeta" ]; then
if [[ "$line" =~ ^([0-9]+): ]]; then
flush_pending_match
PENDING_META_ID="${BASH_REMATCH[1]}"
META_INFO=$(wp db query "SELECT post_id, meta_key FROM ${TABLE_PREFIX}postmeta WHERE meta_id = $PENDING_META_ID" --skip-column-names 2>/dev/null | head -1 | tr -d '\n')
PENDING_POST_ID=$(echo "$META_INFO" | cut -f1)
PENDING_META_KEY=$(echo "$META_INFO" | cut -f2)
if [ -z "$PENDING_POST_ID" ]; then
echo "$line"
clear_pending
continue
fi
POST_INFO=$(wp db query "SELECT post_status, post_title, post_type FROM ${TABLE_PREFIX}posts WHERE ID = $PENDING_POST_ID" --skip-column-names 2>/dev/null | head -1 | tr -d '\n')
PENDING_POST_STATUS=$(echo "$POST_INFO" | awk -F'\t' '{print $1}')
PENDING_POST_TITLE=$(echo "$POST_INFO" | awk -F'\t' '{print $2}' | cut -c1-60)
PENDING_POST_TYPE=$(echo "$POST_INFO" | awk -F'\t' '{print $3}')
if [ "$PENDING_POST_TYPE" = "revision" ] && [ "$INCLUDE_REVISIONS" = false ]; then
POSTMETA_REVISIONS_HIDDEN=$((POSTMETA_REVISIONS_HIDDEN + 1))
PENDING_KIND="postmeta"
PENDING_SNIPPET=""
PENDING_SKIP=true
continue
fi
POSTMETA_COUNT=$((POSTMETA_COUNT + 1))
POSTMETA_DISPLAYED=$((POSTMETA_DISPLAYED + 1))
PENDING_PERMALINK=$(wp post get "$PENDING_POST_ID" --field=url 2>/dev/null | tr -d '\n')
PENDING_STATUS_COLOR=$(status_color_for "$PENDING_POST_STATUS")
PENDING_KIND="postmeta"
PENDING_SNIPPET="${line#*:}"
PENDING_SKIP=false
else
append_pending_snippet "$line"
fi
elif [ "$CURRENT_TABLE" = "posts" ]; then
if [[ "$line" =~ ^([0-9]+): ]]; then
flush_pending_match
PENDING_POST_ID="${BASH_REMATCH[1]}"
POSTS_COUNT=$((POSTS_COUNT + 1))
POST_INFO=$(wp db query "SELECT post_status, post_title, post_type FROM ${TABLE_PREFIX}posts WHERE ID = $PENDING_POST_ID" --skip-column-names 2>/dev/null | head -1 | tr -d '\n')
PENDING_POST_STATUS=$(echo "$POST_INFO" | awk -F'\t' '{print $1}')
PENDING_POST_TITLE=$(echo "$POST_INFO" | awk -F'\t' '{print $2}' | cut -c1-60)
PENDING_POST_TYPE=$(echo "$POST_INFO" | awk -F'\t' '{print $3}')
if [ "$PENDING_POST_TYPE" = "revision" ] && [ "$INCLUDE_REVISIONS" = false ]; then
POSTS_REVISIONS_HIDDEN=$((POSTS_REVISIONS_HIDDEN + 1))
PENDING_KIND="posts"
PENDING_SNIPPET=""
PENDING_SKIP=true
PENDING_POSTS_COLUMN="$CURRENT_POSTS_COLUMN"
continue
fi
POSTS_DISPLAYED=$((POSTS_DISPLAYED + 1))
PENDING_PERMALINK=$(wp post get "$PENDING_POST_ID" --field=url 2>/dev/null | tr -d '\n')
PENDING_STATUS_COLOR=$(status_color_for "$PENDING_POST_STATUS")
PENDING_KIND="posts"
PENDING_SNIPPET="${line#*:}"
PENDING_POSTS_COLUMN="$CURRENT_POSTS_COLUMN"
PENDING_SKIP=false
else
append_pending_snippet "$line"
fi
elif [ "$CURRENT_TABLE" = "other" ]; then
if [[ "$line" =~ ^([0-9]+): ]]; then
OTHER_COUNT=$((OTHER_COUNT + 1))
fi
echo "$line"
else
echo "$line"
fi
done < "$TEMP_FILE"
flush_pending_match
if [ -z "$TOTAL_MATCHES_FROM_STATS" ]; then
TOTAL_MATCHES_FROM_STATS=$((OPTIONS_COUNT + POSTMETA_COUNT + POSTS_COUNT))
fi
TOTAL_DISPLAYED=$((OPTIONS_COUNT + POSTMETA_DISPLAYED + POSTS_DISPLAYED))
TOTAL_REVISIONS_HIDDEN=$((POSTMETA_REVISIONS_HIDDEN + POSTS_REVISIONS_HIDDEN))
echo ""
echo -e "${BOLD}━━━ Search Statistics ━━━${RESET}"
echo -e " ${CYAN}Options:${RESET} $OPTIONS_COUNT"
if [ $POSTMETA_REVISIONS_HIDDEN -gt 0 ]; then
echo -e " ${MAGENTA}Post Meta:${RESET} $POSTMETA_DISPLAYED displayed ($POSTMETA_COUNT total, $POSTMETA_REVISIONS_HIDDEN revisions hidden)"
else
echo -e " ${MAGENTA}Post Meta:${RESET} $POSTMETA_COUNT"
fi
if [ $POSTS_REVISIONS_HIDDEN -gt 0 ]; then
echo -e " ${BLUE}Posts:${RESET} $POSTS_DISPLAYED displayed ($POSTS_COUNT total, $POSTS_REVISIONS_HIDDEN revisions hidden)"
else
echo -e " ${BLUE}Posts:${RESET} $POSTS_COUNT"
fi
if [ $OTHER_COUNT -gt 0 ]; then
echo -e " ${GRAY}Other tables:${RESET} $OTHER_COUNT"
fi
echo -e " ${BOLD}Total Matches:${RESET} $TOTAL_MATCHES_FROM_STATS"
if [ $TOTAL_REVISIONS_HIDDEN -gt 0 ]; then
echo -e " ${GRAY}($TOTAL_DISPLAYED displayed, $TOTAL_REVISIONS_HIDDEN revisions hidden)${RESET}"
fi
echo ""
rm "$TEMP_FILE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment