Last active
April 20, 2025 13:58
-
-
Save rmpel/f5c220b5e49baf0df4f5bbf5b7e76fdf to your computer and use it in GitHub Desktop.
zsh automatic LocalWP context switcher when navigating folders of projects on MacOS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# this is a zsh script to automatically switch to the localwp shell when you enter a | |
# directory that contains a LocalWP website installation | |
# it is designed to trigger on any directory deeper or exactly the app/ folder of a localwp site. | |
# this code is FAR FROM perfect, but it works! Feel free to suggest/improve/etc | |
# | |
# this file is to be sourced in your .zshrc . | |
# | |
# Suggested install method: | |
# cd ~ ; mkdir -p .zsh ; git clone https://gist.github.com/f5c220b5e49baf0df4f5bbf5b7e76fdf.git .zsh/zsh-auto-localwp | |
# echo "source ~/.zsh/zsh-auto-localwp/zsh-auto-localwp.sh" >> ~/.zshrc | |
# | |
# Some software is required apart from tools you should already have on your mac; | |
# Prereq; LocalWP (required, of course ;) ) | |
# https://localwp.com/ | |
# Prereq; jq (required) | |
# Linux: sudo apt install jq | |
# MacOS: brew install jq | |
# If you do not already have brew, please go to https://brew.sh/ for installation instructions. | |
# Prereq; local-cli (optional) | |
# npm install -g @getflywheel/local-cli | |
# | |
# Method of operation: | |
# 1. When you enter a directory that matches a pattern ....../a-name/app/..... it will assume the sitename is a-name, and the site path is ....../a-name | |
# 2. It will look up the site id in the localwp sites.json file. If this path is listed, it will use the site id to set the environment. | |
# 3. If local-cli is available, it will check if the site is running, and start it if it is not. | |
# 4. When the environment is set, this will not be done again until you leave the site directory. | |
# 5. When you leave the site directory, the environment will be unset. You can jump between sites without any issues and each terminal will have their own environment. | |
# | |
# Notes; | |
# Script is far from DRY. Optimisation is needed. | |
# Script is far from perfect. It works, but it can and should be improved. | |
# Only developed and tested for MacOS using the Apple Terminal emulator. | |
# And for linux, MX Linux, xfce4 Terminal. | |
# And it goes without saying; only for zsh. | |
# | |
# This script is provided as-is, without any warranty or guarantee. Use at your own risk. | |
# Feel free to use, modify, share, etc. If you have improvements, please share them back. | |
# | |
# License; GPL v3, see https://www.gnu.org/licenses/gpl-3.0.html share-alike, attribution required. | |
### | |
# Global configuration; | |
# Define in your environment before loading this add-on. | |
# export AUTO_START_LOCALWP_APP=yes | |
AUTO_START_LOCALWP_APP=${AUTO_START_LOCALWP_APP:-no} | |
LOCAL_RUNTIME=~/.config/Local | |
LOCAL_RUNTIME_MACOS=~/Library/Application" "Support/Local | |
[ -d $LOCAL_RUNTIME ] && [ ! -d "$LOCAL_RUNTIME_MACOS" ] && echo Non-MacOS path detected. Local-CLI expects it. Creating symlinks. Please hold... && mkdir -p ~/Library/Application\ Support && ln -s ~/.config/Local ~/Library/Application\ Support/ && echo Done. | |
LOCAL_RUNTIME="$LOCAL_RUNTIME_MACOS" | |
[ ! -d "$LOCAL_RUNTIME" ] && echo LocalWP Runtime Dir not found. Inspect code, fix, try again. && return; | |
LOCAL_RUNTIME_DIRECTORY="$LOCAL_RUNTIME" | |
NOHUP=$(which setsid nohup | head -n 1) | |
### | |
# Get architecture | |
# returns linux, arm64 for macos on arm, x86_64 for macos on intel. Windows WSL support not yet here. Please contribute. | |
# @return string | |
function localwp_get_arch { | |
ARCHI=darwin | |
[ "" != "$(which arch)" ] && [ "arm64" = "$(arch)" ] && ARCHI=darwin-arm64 | |
[ -d /opt/Local/resources ] && ARCHI=linux | |
echo "$ARCHI" | |
} | |
### | |
# Get path to resources | |
# returns the path to the LocalWP resources directory | |
# @return string | |
function localwp_get_resources { | |
[ "" != "$LOCAL_APP_RESOURCES" ] && echo "$LOCAL_APP_RESOURCES" && return 0 | |
[ "linux" = "$(localwp_get_arch)" ] && LOCAL_APP_RESOURCES=/opt/Local/resources && echo "$LOCAL_APP_RESOURCES" && return 0 | |
LCLAPP="$(ls -1d {~,}/Applications/Local.app 2>/dev/null | grep Applications | sort | head -n 1)" | |
[ "" = "$LOCAL_APP_RESOURCES" ] && LOCAL_APP_RESOURCES="${LCLAPP}"/Contents/Resources | |
echo "$LOCAL_APP_RESOURCES" | |
} | |
### | |
# Start LocalWP. | |
# @param $1 Path to the detected LocalWP app. If missing, we just Hail Mary on Local.app | |
function localwp_launch_localwp { | |
local-cli list-sites >/dev/null 2>&1 && return 0 | |
echo -n "Starting Local.app ... Please wait ..." >&2 | |
# if linux, get local_app, start in background | |
AUTO_START_LOCALWP_APP=no | |
LCLAPP=$(localwp_find_local_app) | |
AUTO_START_LOCALWP_APP=yes | |
if [ "linux" = "$(localwp_get_arch)" ]; then | |
$NOHUP "$LCLAPP" >/dev/null 2>&1 & | |
else | |
open -a "$LCLAPP" | |
fi | |
# start a retry counter at 15; we wait at most 15 seconds for the app to start. | |
local TIMEOUT=15 | |
local RETRY=$TIMEOUT | |
# loop until we have a connection to the local-cli | |
while [ $RETRY -gt 0 ]; do | |
local-cli list-sites >/dev/null 2>&1 && break | |
sleep 1 | |
RETRY=$((RETRY-1)) | |
# echo a dot, no new line | |
echo -n "." >&2 | |
done | |
echo "" >&2 | |
local-cli list-sites >/dev/null 2>&1 && \ | |
echo "Local.app started in $((TIMEOUT-RETRY)) seconds." >&2 || \ | |
echo "Local.app did not start in $TIMEOUT seconds." >&2 | |
} | |
### | |
# Find the LocalWP app binary (Local.app) | |
# | |
function localwp_find_local_app { | |
[ -f /opt/Local/local ] && LCLAPP=/opt/Local/local || LCLAPP="$(ls -1d {~,}/Applications/Local.app 2>/dev/null | grep Applications | sort | head -n 1)" | |
echo "$LCLAPP" | |
} | |
### | |
# Find the MySQL binary path for a specific version, it will auto-detect the architecture and try the best match. | |
# @param $1 MySQL version | |
# | |
function localwp_find_mysql_binary_path { | |
ARCHI=$(localwp_get_arch) | |
LCLAPP="$(localwp_find_local_app)" | |
LOCAL_APP_RESOURCES=$(localwp_get_resources) | |
setopt nullglob | |
for i in \ | |
${LOCAL_RUNTIME_DIRECTORY}/lightning-services/mysql-${1}*/bin/${ARCHI}/bin \ | |
${LOCAL_APP_RESOURCES}/extraResources/lightning-services/mysql-${1}*/bin/${ARCHI}/bin \ | |
${LOCAL_RUNTIME_DIRECTORY}/lightning-services/mysql-${1}*/bin/darwin/bin \ | |
${LOCAL_APP_RESOURCES}/extraResources/lightning-services/mysql-${1}*/bin/darwin/bin \ | |
; do | |
if [ -d "$i" ]; then | |
export _LOCALWP_INTERNAL_CALL=1 | |
pushd $i >/dev/null 2>&1 | |
MYSQL_PATH=$(pwd -P) | |
popd >/dev/null 2>&1 | |
unset _LOCALWP_INTERNAL_CALL | |
break; | |
else | |
MYSQL_PATH="" | |
fi | |
done | |
unsetopt nullglob | |
echo "$MYSQL_PATH" | |
} | |
### | |
# Find the PHP binary path for a specific version, it will auto-detect the architecture and try the best match. | |
# @param $1 PHP version | |
# | |
function localwp_find_php_binary_path { | |
ARCHI=$(localwp_get_arch) | |
LCLAPP=$(localwp_find_local_app) | |
LOCAL_APP_RESOURCES=$(localwp_get_resources) | |
setopt nullglob | |
for i in \ | |
${LOCAL_RUNTIME_DIRECTORY}/lightning-services/php-${1}*/bin/${ARCHI}/bin \ | |
${LOCAL_APP_RESOURCES}/extraResources/lightning-services/php-${1}*/bin/${ARCHI}/bin \ | |
${LOCAL_RUNTIME_DIRECTORY}/lightning-services/php-${1}*/bin/darwin/bin \ | |
${LOCAL_APP_RESOURCES}/extraResources/lightning-services/php-${1}*/bin/darwin/bin \ | |
; do | |
if [ -d "$i" ]; then | |
export _LOCALWP_INTERNAL_CALL=1 | |
pushd "$i" >/dev/null 2>&1 | |
PHP_PATH=$(pwd -P) | |
popd >/dev/null 2>&1 | |
unset _LOCALWP_INTERNAL_CALL | |
break; | |
else | |
PHP_PATH="" | |
fi | |
done | |
unsetopt nullglob | |
echo "$PHP_PATH" | |
} | |
### | |
# Find the Imagick module path for a specific PHP version, it will auto-detect the architecture and try the best match. | |
# @param $1 PHP version | |
# | |
function localwp_find_php_imagick_path { | |
ARCHI=$(localwp_get_arch) | |
LCLAPP=$(localwp_find_local_app) | |
LOCAL_APP_RESOURCES=$(localwp_get_resources) | |
setopt nullglob | |
for i in \ | |
"${LOCAL_RUNTIME_DIRECTORY}/lightning-services/php-${1}+0/bin/${ARCHI}/ImageMagick/modules-Q16/coders" \ | |
"${LOCAL_APP_RESOURCES}/extraResources/lightning-services/php-${1}*/bin/${ARCHI}/ImageMagick/modules-Q16/coders \ | |
"${LOCAL_RUNTIME_DIRECTORY}/lightning-services/php-${1}+0/bin/darwin/ImageMagick/modules-Q16/coders" \ | |
"${LOCAL_APP_RESOURCES}/extraResources/lightning-services/php-${1}*/bin/darwin/ImageMagick/modules-Q16/coders \ | |
; do | |
if [ -d "$i" ]; then | |
export _LOCALWP_INTERNAL_CALL=1 | |
pushd $i >/dev/null 2>&1 | |
MGK_PATH=$(pwd -P) | |
popd >/dev/null 2>&1 | |
unset _LOCALWP_INTERNAL_CALL | |
break; | |
else | |
MGK_PATH="" | |
fi | |
done | |
unsetopt nullglob | |
echo "$MGK_PATH" | |
} | |
### | |
# Set the environment for a LocalWP site | |
# @param $1 Site name | |
# @param $2 Site ID | |
# @param $3 MySQL version | |
# @param $4 PHP version | |
# | |
function localwp_set_env { | |
LCLAPP="$(localwp_find_local_app)" | |
if [ "yes" = $AUTO_START_LOCALWP_APP ]; then | |
localwp_launch_localwp "$LCLAPP" | |
fi | |
echo -e "\033[33mSwitching to Local Shell for ${1}\033[0m" >&2 | |
if [ "" != "$(which local-cli)" ]; then | |
SITE_STATE=$(local-cli list-sites | grep -- "$2" | awk '{print $6}') | |
[ "halted" = "$SITE_STATE" ] && echo -e "\033[33m${1} not running, starting ...\033[0m" >&2 && local-cli start-site "$2" | |
fi | |
LOCAL_RUN=$LOCAL_RUNTIME_DIRECTORY/run | |
LOCAL_APP_RESOURCES=$(localwp_get_resources) | |
export WP_ENV=local-by-flywheel | |
export MYSQL_HOME="${LOCAL_RUN}/${2}/conf/mysql" | |
export PHPRC="${LOCAL_RUN}/${2}/conf/php" | |
export WP_CLI_CONFIG_PATH="$LOCAL_APP_RESOURCES/extraResources/bin/wp-cli/config.yaml" | |
export WP_CLI_DISABLE_AUTO_CHECK_UPDATE=1 | |
PATH="$(localwp_find_mysql_binary_path ${3}):$PATH" | |
PATH="$(localwp_find_php_binary_path ${4}):$PATH" | |
PATH="${LOCAL_APP_RESOURCES}/extraResources/bin/wp-cli/posix:$PATH" | |
PATH="${LOCAL_APP_RESOURCES}/extraResources/bin/composer/posix:$PATH" | |
export PATH | |
MGK=$(localwp_find_php_imagick_path ${4}) | |
[ "" != "${MGK}" ] && export MAGICK_CODER_MODULE_PATH="${MGK}" | |
export LOCALWP_SITE="$1" | |
export LOCALWP_SITE_ID="$2" | |
[ "" = "$LD_LIBRARY_PATH" ] && LD_LIBRARY_PATH="$(localwp_find_php_binary_path ${4})"/../shared-libs | |
export LD_LIBRARY_PATH | |
} | |
### | |
# Unset the environment for a LocalWP site | |
# | |
function localwp_unset_env { | |
LOCAL_RUN=$LOCAL_RUNTIME_DIRECTORY/run | |
LCLAPP="$(localwp_find_local_app)" | |
LOCAL_APP_RESOURCES=$(localwp_get_resources) | |
unset LD_LIBRARY_PATH | |
unset WP_ENV | |
unset MYSQL_HOME | |
unset PHPRC | |
unset WP_CLI_CONFIG_PATH | |
unset WP_CLI_DISABLE_AUTO_CHECK_UPDATE | |
unset MAGICK_CODER_MODULE_PATH | |
# remove paths from PATH | |
# remove paths matching ${LCLAPP}/Contents/Resources/extraResources/bin/ | |
# remove paths matching ${LOCAL_RUNTIME_DIRECTORY}/lightning-services | |
PATH=$(echo $PATH | tr ':' '\n' | grep -v "${LOCAL_APP_RESOURCES}/extraResources/" | grep -v "${LOCAL_RUNTIME_DIRECTORY}/lightning-services" | tr '\n' ':') | |
# trim trailing/leading colons and remove multiple colons in a row | |
PATH=$(echo $PATH | sed 's/:*$//;s/^:*//;s/:+/:/g') | |
export PATH | |
export LOCALWP_SITE= | |
export LOCALWP_SITE_ID= | |
} | |
### | |
# MAIN Automatically switch to the LocalWP shell when entering a LocalWP site directory | |
# | |
function localwp_auto_switch { | |
if [ "" != "$_LOCALWP_INTERNAL_CALL" ]; then | |
return; | |
fi | |
[ "" = "$(which jq)" ] && return; | |
SITES_JSON_PATH="$LOCAL_RUNTIME_DIRECTORY"/sites.json | |
THEPWD="$(pwd)" | |
if [[ "$THEPWD" =~ /.+/([^/]+)/app($|/.*) ]]; then | |
# split path at /app/ and get the first part, then the basename | |
SITE_PATH=$(awk -F/app/ '{print $1}' <<< "$THEPWD/") | |
SITE_ID=$(jq -r ".[] | select(.path == \"$SITE_PATH\") | .id" "$SITES_JSON_PATH") | |
# does not seem to be a localwp site, but maybe the path is different | |
if [ "" = "$SITE_ID" ]; then | |
SITE_REAL_PATH=$(echo "$SITE_PATH" | realpath) | |
SITE_ID=$(jq -r ".[] | select(.path == \"$SITE_REAL_PATH\") | .id" "$SITES_JSON_PATH") | |
SITE_PATH="$SITE_REAL_PATH" | |
fi | |
# still no site id, we are done. | |
[ "" = "$SITE_ID" ] && return; | |
# PHP Config already pointing to this site, we assume all is o.k. | |
if [[ "$PHPRC" =~ "/$SITE_ID/" ]]; then | |
return | |
fi | |
# get the site name | |
SITE_NAME=$(basename "$SITE_PATH") | |
echo -n -e "\033]0;Local Shell for ${SITE_NAME}\007" | |
MYSQL_VERSION=$(jq -r ".[] | select(.path == \"$SITE_PATH\") | .services.mysql.version" "$SITES_JSON_PATH") | |
PHP_VERSION=$(jq -r ".[] | select(.path == \"$SITE_PATH\") | .services.php.version" "$SITES_JSON_PATH") | |
# clear the environment | |
localwp_unset_env | |
# set the environment | |
localwp_set_env $SITE_NAME $SITE_ID $MYSQL_VERSION $PHP_VERSION | |
elif [[ "$PHPRC" =~ "/$SITE_ID/" ]]; then | |
# clear the environment | |
localwp_unset_env | |
# notify the user | |
echo -e "\033[33mSwitching off Local Shell\033[0m" >&2 | |
# clear the title | |
echo -n -e "\033]0;\007" | |
fi | |
} | |
function localwp_siteid_from_name { | |
SITES_JSON_PATH="$LOCAL_RUNTIME_DIRECTORY"/sites.json | |
SITE_ID=$(jq -r ".[] | select(.name == \"$1\") | .id" "$SITES_JSON_PATH") | |
if [ "" = "$SITE_ID" ]; then | |
echo "Site not found: $1" | |
return 1 | |
fi | |
echo "$SITE_ID" | |
} | |
function localwp_site_info_query { | |
# $1 is the query, $2 is the site id | |
SITES_JSON_PATH="$LOCAL_RUNTIME_DIRECTORY"/sites.json | |
jq .${2}${1} "$SITES_JSON_PATH" | xargs echo | |
} | |
function localwp_print_info { | |
local SITE_PATH="$(localwp_site_info_query .path "${1}")" | |
echo "SiteID: " ${1} | |
ITEM="${SITE_PATH}"/app | |
echo "Site root from current path: " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM=$(ls -d1 "${SITE_PATH}"/app/public "${SITE_PATH}"/app/public_html "${SITE_PATH}"/app/web 2>/dev/null | grep app | head -n1) | |
echo "Web root for this site (by disk): " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM="${SITE_PATH}"/app/.idea | |
echo ".IDEA folder location (by disk): " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM="${LOCAL_RUNTIME_DIRECTORY}"/run/"${1}"/conf/apache/site.conf | |
local APACHECONF="$ITEM" | |
echo "Apache config path: " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM="${SITE_PATH}"/conf/apache/site.conf.hbs | |
echo "Apache config template path: " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM=$(grep DocumentRoot "$APACHECONF" | head -n1 | awk -F'"' '{print $2}' ) | |
echo "Web root for this ste (Apache): " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM="${LOCAL_RUNTIME_DIRECTORY}"/run/"${1}"/conf/php/php.ini | |
local PHPCONF="$ITEM" | |
echo "PHP config path: " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM="${SITE_PATH}"/conf/php/php.ini.hbs | |
echo "PHP config template path: " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM=$(grep openssl.cafile "$PHPCONF" | head -n1 | awk -F'"' '{print $2}' ) | |
echo "WordPress SSL CA Cert path (PHP): " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
local NAME="$(localwp_site_info_query .name "${1}")" | |
ITEM="${LOCAL_RUNTIME_DIRECTORY}/run/router/nginx/conf/route.${NAME}.test.conf" | |
echo "Router Config: " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
ITEM="${LOCAL_RUNTIME_DIRECTORY}/run/router/nginx/certs/${NAME}.test.crt" | |
echo "SSL Certificate: " $( [ -e "$ITEM" ] && echo √ || echo x ) $ITEM | |
echo "TCP/IP ports:" | |
echo " Apache: " $(localwp_site_info_query ".services.apache.ports.HTTP[0]" "${1}") | |
echo " MySQL: " $(localwp_site_info_query ".services.mysql.ports.MYSQL[0]" "${1}") | |
echo " PHP CGI: " $(localwp_site_info_query ".services.php.ports.cgi[0]" "${1}") | |
echo " MailPit UI: " $(localwp_site_info_query ".services.mailpit.ports.WEB[0]" "${1}") | |
echo " MailPit SMTP: " $(localwp_site_info_query ".services.mailpit.ports.SMTP[0]" "${1}") | |
echo "URLs:" | |
echo " Website: " http://$(localwp_site_info_query ".domain" "${1}")/ | |
echo " Mailpit: " http://$(localwp_site_info_query ".domain" "${1}"):$(localwp_site_info_query ".services.mailpit.ports.WEB[0]" "${1}")/ | |
} | |
function localwp { | |
if [ "" = "$1" ]; then | |
echo "Usage: localwp <command>" | |
echo "Commands:" | |
echo " start <site> Start a LocalWP site by name" | |
echo " stop <site> Stop a LocalWP site by name" | |
echo " restart <site> Restart a LocalWP site by name" | |
echo " status <site> Get the status of a LocalWP site by name" | |
echo " list List all LocalWP sites" | |
return 1 | |
fi | |
if [ "yes" = $AUTO_START_LOCALWP_APP ]; then | |
localwp_launch_localwp "$LCLAPP" | |
fi | |
local CURSITE="$LOCALWP_SITE_ID" | |
[ "$2" != "" ] && CURSITE=$(localwp_siteid_from_name "$2") | |
if [ "start" = "$1" ]; then | |
local-cli start-site "$CURSITE" | |
elif [ "stop" = "$1" ]; then | |
local-cli stop-site "$CURSITE" | |
elif [ "restart" = "$1" ]; then | |
local-cli stop-site "$CURSITE" | |
local-cli start-site "$CURSITE" | |
elif [ "status" = "$1" ]; then | |
local-cli list-sites | grep -- "$CURSITE" | |
elif [ "info" = "$1" ]; then | |
localwp_print_info "$CURSITE" | |
elif [ "list" = "$1" ]; then | |
local-cli list-sites | |
else | |
echo "Unknown command: $1" | |
return 1 | |
fi | |
} | |
# autoload the hook | |
autoload -U add-zsh-hook | |
add-zsh-hook chpwd localwp_auto_switch |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment