Running Commands in All tmux Sessions, Windows and Panes
We'll even support custom logic where you can control running anything depending on which process is running.
Prefer video? Here it is on YouTube.
You can do this for many things but a use case I had was I normally have a dozen or so tmux sessions, each with an assortment of windows and panes.
If I update my shell’s profile, rc or aliases it’s super tedious to manually
goto ~40 different panes and source in those files. Also if I use my
dotfiles’ set-theme
script to switch
themes, I had to close and open ~10 nvim instances to see the new theme.
Wouldn’t it be cool if you could send a specific shell command to all tmux shell sessions and even go as far as optionally killing any running process in that session such as nvim, htop or whatever else you have so that the shell command can run?
tmux has a built in feature setw synchronize-panes on
but it won’t help us
for the above use case. This only works for the current window and it doesn’t
handle the case of having some panes having a process running that’s not your
shell.
Using wishful thinking, what if our custom command looked like this:
tmux-shell-cmd "whoami"
tmux-shell-cmd --kill ". ${ZDOTDIR}/.zprofile && . ${ZDOTDIR}/.zshrc"
You could even go as far as setting up aliases like this:
# sz is short for "source zsh" and SZ is the same thing BUT LOUDER!
alias sz='. ${ZDOTDIR}/.zprofile && . ${ZDOTDIR}/.zshrc'
alias SZ='tmux-shell-cmd --kill "sz"'
The good news is, all of the above exists today in my dotfiles.
Here’s the tmux-shell-cmd
script which we’ll cover in this post. By cover,
I mean I added extra comments to the script here to explain what’s happening
and how you can extend it if you have slightly different use cases.
#!/usr/bin/env bash
# Run a shell command in all open tmux sessions, windows and panes.
set -o pipefail
set -o nounset
AUTO_KILL="${AUTO_KILL:-}"
KILL=
ARGS=()
# Handle parsing arguments sent to this script. Other than the --kill arg, it's
# expected the rest are related to the command you're running.
for arg in "${@}"; do
case "${arg}" in
--kill)
KILL=1
;;
*)
ARGS+=("${arg}")
;;
esac
done
COMMAND="${ARGS[*]}"
# This covers a few ways to use the script.
if [[ -z "${COMMAND:-}" ]]; then
cat <<EOF
Usage:
tmux-shell-cmd whoami
tmux-shell-cmd --kill "echo 'kill any process before running this command'"
AUTO_KILL=1 tmux-shell-cmd --kill <same as above except it won't prompt you>
EOF
exit 1
fi
# If we're killing processes let's certainly give ourselves a warning in case
# we need to save files or close something important. The kill will happen with
# a SIGTERM so it's graceful but still, this could have side effects.
#
# We support AUTO_KILL so you can call this script from another script and
# not be blocked by having to answer yes manually.
if [[ -n "${KILL}" && -z "${AUTO_KILL}" ]]; then
printf "All processes running in any tmux shell session (nvim, etc.) will be killed, save you work! Are you sure? (y/n) "
read -r yn
if [[ "${yn}" != "y" ]]; then
printf "\nAborting, your command was not run within any shell session.\n"
exit
fi
fi
# This is a neat feature provided by tmux which allows us to loop over all
# panes. The -a includes all sessions, you can use -s for only the current
# session if you wanted that behavior instead. -F is how the output will be
# formatted, these values are provided by tmux.
tmux list-panes -a -F "#{session_name}:#{window_index}.#{pane_index} #{pane_pid}" | while read -r pane pid; do
# The running process will always be your shell (zsh, bash, etc.), in this
# case if we have something like nvim or htop running there will be a child
# process of that pid.
child_pid=$(pgrep -P "${pid}")
was_killed=
# Here we only want to kill a child process like nvim, htop, etc..
if [[ -n "${KILL}" && -n "${child_pid}" ]]; then
# We're using pkill and kill so child processes get killed for processes
# that spawn a parent which cannot be killed with a normal kill. pkill is
# useful for processes like docker compose up.
kill "${child_pid}"
pkill -P "${child_pid}"
was_killed=1
fi
# NOTE: You can add optional custom logic here if you wanted, such as only
# doing something when a specific command is running. For example, you can
# do `ps -p "${child_pid}" -o command=` which gets the full command for the
# child pid (ie. nvim myfile). You can use `-o comm=` to get just the binary
# instead (ie. nvim).
# We have these extra conditions because we want to run the command when
# either of these conditions are true:
# - There's no children (ie. a regular shell session)
# - There's a child process and it was killed
#
# Then we send the command into each pane, the C-m at the end sends in Enter
# which invokes the command instead of just placing it into your shell.
[[ -z "${child_pid}" || -n "${was_killed}" ]] && tmux send-keys -t "${pane}" "${COMMAND}" "C-m"
done
I would call the above a phase 1 solution as it directly solves my immediate use cases. It may evolve over time. I suggest checking my dotfiles to see the latest version of it.
The video below goes over the script and using it in detail.
# Demo Video
Timestamps
- 0:25 – We won’t be using synchronize-panes
- 1:26 – Use case of sourcing your shell files and updating themes
- 3:17 – Seeing how the script works
- 6:51 – Taking a look at the script
- 8:46 – Going over the tmux list-panes command
- 10:33 – Looping over the output
- 11:18 – Differentiating the pid vs child pid and process names
- 16:23 – Breaking down the kill logic
- 17:45 – Using tmux send-keys
- 18:33 – An alias to source your shell’s files for easy access
- 20:28 – Tying it into the set-themes script
What use cases will you use this for? Let me know below.