How do you cd to the directory of another screen window

gnu-screenterminal

gnome-terminal has a great feature where opening a new tab or window starts the new shell with cwd = the cwd of the previously-focussed window.

I usually run GNU screen in one of my gnome-terminal tabs. Partly for the scrollback, partly for the compact and enumarated display of 5 to 10 windows, partly for the quick keystrokes to switch to a specific or the previous tab.

But I've been starting to wish I could cd in one window to the CWD of another window. The question is, how?

(BTW, is a stackexchange Q&self-A an appropriate way to share a neat alias, shell function, or hack like this that I came up with? I don't have a block, and I don't think twitter or facebook would be good options.)

Best Answer

This method is portable to any Unix system, but depends on the functionality of GNU bash's cd -P to make it not ugly.

Put this (or the other version below, that preserves logical directories) in your ~/.bashrc (or .zshrc or whatever), so it runs for every interactive shell (inside and outside of screen):

CDS_PREFIX="/var/run/screen/S-$USER"  # screen uses this already
#CDS_PREFIX="/dev/shm/screen-$USER" # if /var/run isn't on tmpfs
# $STY = a string unique to the screen session
if [[ $STY ]]; then
        CDS_DIR="$CDS_PREFIX/dirs.$STY"
        unset CDS_PREFIX

        [[ -d $CDS_DIR ]] || mkdir -m 700 -p "$CDS_DIR"

        # old cmd-every-prompt design: avoids breakage if you run interactive bash from bash, then exit
        # Also, use this on systems without a /proc/<pid>/cwd
        #PROMPT_COMMAND='[[ $WINDOW ]] && ln -sf "$PWD" "$CDS_DIR/$WINDOW"'

        ln -sTf "/proc/$$/cwd" "$CDS_DIR/$WINDOW"       # -T saves a stat call
        cds() { cd -P "$CDS_DIR/$@"; }
else
        # CDS_DIR=( "$CDS_PREFIX"/dirs.* )  # cds will use the first array element
        cds() { cd -P "$CDS_PREFIX/"dirs.*/"$@"; }  # even support shells started before screen.  cd with multiple args takes the first one without complaint.
fi

So you can open a new screen window and cds 5 will take you to the cwd of the shell in window 5.

This even works for shells started OUTSIDE of screen, and even before your screen session existed. (since in that case, the glob expansion happens at runtime of cds, rather than when it was defined like with the array-variable hack that I commented out, since it's worse in every way.)

Total overhead:

  • on every interactive shell startup:
    • 8 non-comment lines of code to parse.
    • a stat
    • a ln -sTf to a directory on tmpfs
  • ongoing memory overhead after shell starts:
    • 1 shell var and 1 tiny function
    • no environment vars
  • storage:
    • a directory of one symlink per screen window (not removed after windows close)
  • per command:
    • none

Without cd -P, the \w in your $PS1 would expand to /proc/3069/cwd, rather than the canonical (symlinks followed) path you get with -P.

The version that uses PROMPT_COMMAND could modify cds() to cd to take you to the logical directory. (Store CWD as text in a file, instead of in a symlink. pwd > "$CDS_DIR/$WINDOW" is just a shell builtin, so less overhead than fork/execing a binary. Also saves the trouble of working around missing GNU readlink on some non-Linux systems.) This would be useful if you often work in symlinks to directories, where pwd -P (and /bin/pwd) isn't the same as pwd.

You could override the cd, pushd, and popd builtins with functions that update the symlink, instead of doing it EVERY prompt. With that setup, interactive use of things like (cd foo; command there) will fool your setup, because the cd that runs in the subshell will update the symlink, but without modifying the pwd of the main shell process.

Ok, here's the "other version", that hooks cd, pushd, and popd, and will take you to the same logical directory as your shell in whatever screen window:

# Use this version on systems without `/proc/<pid>/cwd`, or for logical directories (non-dereferencing of symlinks).

CDS_PREFIX="/var/run/screen/S-$USER"
#CDS_PREFIX="/dev/shm/screen-$USER"
if [[ $STY ]]; then
    CDS_DIR="$CDS_PREFIX/dirs.$STY"
    unset CDS_PREFIX
    [[ -d $CDS_DIR ]] || mkdir -m 700 -p "$CDS_DIR"

    function cd    () { command cd    "$@"; pwd > "$CDS_DIR/$WINDOW"; }
    function pushd () { command pushd "$@"; pwd > "$CDS_DIR/$WINDOW"; }
    function popd  () { command popd  "$@"; pwd > "$CDS_DIR/$WINDOW"; }
    #PROMPT_COMMAND='[[ $WINDOW ]] && pwd > "$CDS_DIR/$WINDOW"'
    if [[ -e "$CDS_DIR/$WINDOW" && ! -f "$CDS_DIR/$WINDOW" ]]; then
        rm -f "$CDS_DIR/$WINDOW"  # could exist if switching from symlink-to-dir style
    fi
    cd .

#   cds() { local d="$(<"$CDS_DIR/$1")"; shift; cd "$d" "$@"; }
    cds() { cd "$(<"$CDS_DIR/$1")"; }
else
    # CDS_DIR=( "$CDS_PREFIX"/dirs.* )  # cds will use the first array element
    cds() { cd "$(<"$CDS_PREFIX/"dirs.*/"$1")"; }  # even support shells started before screen.
fi

Overhead: an open(2) and write(2) to tmpfs for every cd, pushd, and popd you type or paste in. Otherwise the same. Still not going to hurt, on any system powerful enough to run bash and screen in the first place. :)

Credit to https://superuser.com/a/54161/20798 for the idea of hooking cd. I like that a LOT better than PROMPT_COMMAND. Found that while looking to see if anyone else had invented this, and whether I should post here or on https://unix.stackexchange.com/.

I used some bash-isms (like [[ ]]) because I think zsh supports them too, and hopefully nobody is using POSIX sh as their interactive shell.

Related Question