Skip to content

Instantly share code, notes, and snippets.

@rmpel
Last active April 20, 2025 13:58
Show Gist options
  • Save rmpel/f5c220b5e49baf0df4f5bbf5b7e76fdf to your computer and use it in GitHub Desktop.
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 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