Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Running Commands in All tmux Sessions, Windows and Panes

running-commands-in-all-tmux-sessions-windows-and-panes.jpg

We'll even support custom logic where you can control running anything depending on which process is running.

Quick Jump:

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 dotfilesset-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.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per year (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments