1

I'm using emacs, and I wish I could run a command to select window via working directory of that window, if there is none of such windows - create one. I can create tmux windows via that command

tmux new-window -c some_directory

But I can not find how to select window by directory, is there such command? Or, maybe it should be done differently, in loop by running validation comparing pwd with directory?

0

I wish I could run a command to select window via working directory of that window, […]. I can create tmux windows via that command:

tmux new-window -c some_directory

The command creates a window with a single pane in it. In tmux there is pane_current_path format which prints the current working directory of a pane (if possible). Targeted at a window, the format will print the current working directory of the active pane in the window. This effectively ignores inactive panes. If you have one pane per window then every pane is active in its respective window. Otherwise there is at least one inactive pane.

Note: man 1 tmux in my Debian uses the term "inactive pane" generally for panes that are not active; this is the meaning I use here. But few times the manual uses "inactive pane" where it should be "dead pane"; this is not the meaning I use here.

I'm not sure if you want to ignore inactive panes. With multiple panes in a window it's very easy to forget to make one special pane active when you select another window. In my workflow I never care which pane is active when I leave a window. Therefore I assume you want to select a pane via working directory of that pane.


I have the following script under the name _tmux-find-cwd in a directory where my PATH points to.

#!/bin/sh

if [ "$#" -ne 1 ]; then
   printf '%s\n' "exactly 1 argument needed" >&2
   exit 1
fi

if [ "$CD_TO_PANE" ]; then
   cd "$(tmux display -p '#{pane_current_path}')"
fi

tpth="$(realpath -e "$1//")" || exit 1

tmux list-panes -s -F '#{window_id} #{pane_id} #{pane_current_path}' \
| while IFS=' ' read -r wndw pne pth; do
    [ "$pth" = "$tpth" ] && tmux select-pane -t "$pne" && tmux select-window -t "$wndw" && exit 0
done || tmux new-window -c "$tpth"

In a shell I use it like this:

_tmux-find-cwd some_directory

The script tries to find a pane with matching working directory. The first match is selected, no more panes are tested. If there is no match, a window will be created as you requested.

Normally the script doesn't change its working directory, so some_directory as a relative path will also work. You may get unexpected results when there are symlinks involved, see this.


Then there is this line in my ~/.tmux.conf:

bind -T root C-M-j command-prompt -p 'dir:' "run-shell 'CD_TO_PANE=1 2>&1 _tmux-find-cwd \"%%\"'"

This way when I press Ctrl+Alt+j tmux asks for a directory and runs the script for me. Non-empty CD_TO_PANE variable makes the script cd to the working directory of the active pane, so relative paths conveniently work with respect to my current pane.

Note if I type typed string then tmux will spawn a POSIX shell with this command string:

CD_TO_PANE=1 2>&1 _tmux-find-cwd "typed string"

Typed string is implicitly embraced by double-quotes, so I don't have to worry about dir with spaces etc. Tilde expansion doesn't work but variables from the environment of the spawned shell are expanded (e.g. $HOME works). Command substitution ($(…)) also works. It's not the only way to inject code. Consider this typed string:

/"; rm "foo

It will execute the following command line:

CD_TO_PANE=1 2>&1 _tmux-find-cwd "/"; rm "foo"

So mind what you type. This vulnerability is unavoidable since run-shell takes a single string and apparently uses it with sh -c. It would be better if tmux could create a proper array of arguments in the first place. In the context of this answer on U&L SE our %% is like {} embedded in the shell code. I think there's nothing we can do. Single-quotes cannot really help because one can always type a closing quote, inject code and reopen the quoting. It may be by accident. E.g. one forgets about implicit quotes, wants to quote a dangerous string, uses additional quotes and effectively generates unquoted dangerous string that gets parsed. Foresight leads to disaster.

Maybe it's better to choose this variant:

bind -T root C-M-j command-prompt -p '$ _tmux-find-cwd' "run-shell 'CD_TO_PANE=1 2>&1 _tmux-find-cwd %%'"

which leads to

CD_TO_PANE=1 2>&1 _tmux-find-cwd typed string

Then you need to explicitly quote when needed. Tilde expansion will work. Code injection is as trivial as typing ;. This seems even more dangerous at first glance but I deliberately changed the prompt so it will look like you're typing a command in an interactive sh. If you forget whether you should quote or not, the resemblance to a shell should be a clue.


Additional notes, quirks and flaws:

  • realpath is very useful here but it's not required by POSIX.
  • If my premise is wrong and you really want to test working directories of windows (not all panes), then you should go with

    tmux list-windows -F '#{window_id} #{pane_current_path}'
    

    and adjust the rest of the code accordingly.

  • My code retrieves window_id, pane_id and pane_current_path in one step. In general it may be better to get a list of pane_ids first, iterate over it and retrieve other information separately for each pane (possibly with multiple tmux display -p … per pane). Rationale:

    • pane_current_path may contain newline characters (newline is a valid character in file names). This would break my original code. You can deal with it if you retrieve one path at a time.
    • More complex logic may depend on formats that you cannot unambiguously separate (with spaces or whatever) in one line. Retrieving them one by one solves the issue.
  • Command substitution $(…) gets rid of all trailing newlines. My code uses it to parse paths. For a path ending with a newline this will fail. I think I am able to fix the code but it would be cumbersome. I don't use paths with newlines, I guess neither do you.
  • You may wonder why I used realpath -e "$1//"; why not realpath -e "$1"? The latter command won't fail if $1 expands to an existing file which is not a directory. The trailing slash tells realpath I expect a directory, it saves me [ -d … ] later.
  • Why two slashes then instead of one? To avoid bare // when $1 expands to sole / (just in case, see this answer).
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.