OS X Terminal.app: how to start a new tab in the same directory as the current tab?

24

12

I frequently need to open a new tab in the same directory as my current tab to do something else while my current tab is occupied by a long running process. However, by default when you create a new tab, Terminal.app starts at ~/. Any idea how to make it auto jump?

Riobard

Posted 2009-10-26T23:28:39.277

Reputation: 464

Thanks guys for many quick responses! I'm fine with launching a new tab by invoking a script, but I was wondering if there is other way to do it since I'm not able to run a script if there is already a program running and occupying the current tab :| – Riobard – 2009-10-27T23:40:19.957

Answers

10

In OS X 10.7 (Lion), Terminal.app supports this natively: New Windows/Tabs open in: Same working directory

Alistair A. Israel

Posted 2009-10-26T23:28:39.277

Reputation: 201

It's too bad Apple doesn't do backports... would love to see this feature in Snow Leopard. – Wander Nauta – 2011-08-07T17:35:50.573

4I have it set but does not work for me. The preferences window says something about enabling escape sequences before this can work. – Ram on Rails – 2013-02-25T07:23:59.380

2

One must be very careful when passing strings through different environments.

I run 10.4, so my ‘tfork’ script always opens a new window instead. It should be easy to adapt it to use a tab:

#!/bin/sh

# source: http://www.pycs.net/bob/weblog/2004/02/23.html#P49
# Rewritten to use osascript args -> run handler args.
# Added ability to pass additional initial command and args to new shell.
#    Bug: Non ASCII characters are unreliable on Tiger.
#         Tiger's osascript seems to expect args to be encoded in
#         the system's primary encoding (e.g. MacRoman).
#         Once in AppleScript, they are handled OK. Terminal sends them
#         back to the shell as UTF-8.

test $# -eq 0 && set -- : # default command if none given
osascript - "$(pwd)" "$@" <<\EOF
on run args
  set dir to quoted form of (first item of args)
  set cmd_strs to {}
  repeat with cmd_str in rest of args
    set end of cmd_strs to quoted form of cmd_str
  end
  set text item delimiters to " "
  set cmd to cmd_strs as Unicode text
  tell app "Terminal" to do script "cd " & dir & " && " & cmd
end
EOF

Example: tfork git log -p ..FETCH_HEAD


Amendment: cwd of an already running process “occupying” a Terminal tab

The idea of “the current directory of the program occupying the current tab” is not as obvious as one might expect.

Each Terminal tab has a single tty device that is used by the processes it runs (initially, a shell; thereafter, whatever the shell starts).

Each (normal) Terminal tty has a single foreground process group that one might consider as “occupying” the tty.

Each process group can have multiple processes in it.

Each process can have its own current working directory (cwd) (some environments give each thread their own cwd or cwd-equivalent, but we will ignore that).

The preceding facts establish a kind of trail that from tty to cwd: tty -> foreground process group -> processes of the foreground process group -> cwds.

The first part (from tty to foreground processes) of the problem can be solved with the output from ps:

ps -o tty,pid,tpgid,pgid,state,command | awk 'BEGIN{t=ARGV[1];ARGC=1} $1==t && $3==$4 {print $2}' ttyp6

(where “ttyp6” is the name of the tty of interest)

The mapping from process (PID) to cwd can be made with lsof:

lsof -F 0n -a -p 2515,2516 -d cwd

(where “2515,2516” is a comma-separated list of the processes of interest)

But under Tiger, I see no direct way to get the tty device name of a particular Terminal window. There is a horribly ugly way of getting the tty name in Tiger. Maybe Leopard or Snow Leopard can do better.

I put it all together in an AppleScript like this:

on run
    (* Find the tty. *)
    -- This is ugly. But is seems to work on Tiger. Maybe newer releases can do better.
    tell application "Terminal"
        set w to window 1
        tell w
            set origName to name
            set title displays device name to not title displays device name
            set newName to name
            set title displays device name to not title displays device name
        end tell
    end tell
    set tty to extractTTY(origName, newName)
    if tty is "" then
        display dialog "Could not find the tty for of the current Terminal window." buttons "Cancel" cancel button "Cancel" default button "Cancel"
    end if

    (* Find the PIDs of the processes in the foreground process group on that tty. *)
    set pids to paragraphs of (do shell script "
ps -o pid,tty,tpgid,pgid,state,command |
awk '
    BEGIN   {t=ARGV[1];ARGC=1}
    $2==t && $3==$4 {print $1}
' " & quoted form of tty)
    if pids is {} or pids is {""} then
        display dialog "Could not find the processes for " & tty & "." buttons "Cancel" cancel button "Cancel" default button "Cancel"
    end if

    (* Find the unique cwds of those processes. *)
    set text item delimiters to {","}
    set lsof to do shell script "lsof -F 0n -a -d cwd -p " & quoted form of (pids as Unicode text) without altering line endings
    set text item delimiters to {(ASCII character 0) & (ASCII character 10)}
    set cwds to {}
    repeat with lsofItem in text items of lsof
        if lsofItem starts with "n" then
            set cwd to text 2 through end of lsofItem
            if cwds does not contain cwd then ¬
                set end of cwds to cwd
        end if
    end repeat
    if cwds is {} then
        display dialog "No cwds found!?" buttons "Cancel" cancel button "Cancel" default button "Cancel"
    end if
    if length of cwds is greater than 1 then
        set cwds to choose from list cwds with title "Multiple Distinct CWDs" with prompt "Choose the directory to use:" without multiple selections allowed and empty selection allowed
        if cwds is false then error number -128 -- cancel
    end if

    (* Open a new Terminal. *)
    tell application "Terminal" to do script "cd " & quoted form of item 1 of cwds
end run

to extractTTY(a, b)
    set str to textLeftAfterRemovingMatchingHeadAndTail(a, b)
    set offs to offset of "tty" in str
    if offs > 0 then
        return text offs through (offs + 4) of str
    end if
    return ""
end extractTTY
to textLeftAfterRemovingMatchingHeadAndTail(big, little)
    set text item delimiters to space
    if class of big is not list then set big to text items of big
    if class of little is not list then set little to text items of little
    set {maxLen, minLen} to {length of big, length of little}
    if maxLen < minLen then ¬
        set {big, little, maxLen, minLen} to {little, big, minLen, maxLen}

    set start to missing value
    repeat with i from 1 to minLen
        if item i of big is not equal to item i of little then
            set start to i
            exit repeat
        end if
    end repeat
    if start is missing value then
        if maxLen is equal to minLen then
            return ""
        else
            return items (minLen + 1) through end of big as Unicode text
        end if
    end if

    set finish to missing value
    repeat with i from -1 to -minLen by -1
        if item i of big is not equal to item i of little then
            set finish to i
            exit repeat
        end if
    end repeat
    if finish is missing value then set finish to -(minLen + 1)

    return items start through finish of big as Unicode text
end textLeftAfterRemovingMatchingHeadAndTail

Save it with Script Editor (AppleScript Editor in Snow Leopard) and use a launcher (e.g. FastScripts) to assign it to a key (or just run it from the AppleScript menu (enabled via /Applications/AppleScript/AppleScript Utility.app)).

Chris Johnsen

Posted 2009-10-26T23:28:39.277

Reputation: 31 786

1

I've posted a script that uses Chris Johnsen's code above and another script to open the new tab in the current directory with the current settings, mostly because I colour-coordinate my terminals. Thanks Chris, for that script, I've been using this for a few months now and it's a great time saver.

(* This script opens a new Terminal.app tab in the directory of the current tab with the same settings. You’ll need to, if you haven’t already, enable access for assistive devices as described here: http://www.macosxautomation.com/applescript/uiscripting/index.html

It’s almost all the work of two scripts put together, thank you to them:

Chris Johnsen’s script opens a new tab in the current directory: OS X Terminal.app: how to start a new tab in the same directory as the current tab?

Jacob Rus’s “menu_click” lets me create the tab with the same settings, as Terminal’s API doesn’t: http://hints.macworld.com/article.php?story=20060921045743404

If you change the name of a Terminal profile, the AppleScript API returns the old name until you restart the application, so the script won’t work on renamed settings until then. Ugh. Also, the necessity of activating Terminal to execute the menu command brings all the terminal windows to the front.

*)

-- from http://hints.macworld.com/article.php?story=20060921045743404
-- `menu_click`, by Jacob Rus, September 2006
-- 
-- Accepts a list of form: `{"Finder", "View", "Arrange By", "Date"}`
-- Execute the specified menu item.  In this case, assuming the Finder 
-- is the active application, arranging the frontmost folder by date.

on menu_click(mList)
    local appName, topMenu, r

    -- Validate our input
    if mList's length < 3 then error "Menu list is not long enough"

    -- Set these variables for clarity and brevity later on
    set {appName, topMenu} to (items 1 through 2 of mList)
    set r to (items 3 through (mList's length) of mList)

    -- This overly-long line calls the menu_recurse function with
    -- two arguments: r, and a reference to the top-level menu
    tell application "System Events" to my menu_click_recurse(r, ((process appName)'s ¬
        (menu bar 1)'s (menu bar item topMenu)'s (menu topMenu)))
end menu_click

on menu_click_recurse(mList, parentObject)
    local f, r

    -- `f` = first item, `r` = rest of items
    set f to item 1 of mList
    if mList's length > 1 then set r to (items 2 through (mList's length) of mList)

    -- either actually click the menu item, or recurse again
    tell application "System Events"
        if mList's length is 1 then
            click parentObject's menu item f
        else
            my menu_click_recurse(r, (parentObject's (menu item f)'s (menu f)))
        end if
    end tell
end menu_click_recurse



-- with the noted slight modification, from https://superuser.com/questions/61149/os-x-terminal-app-how-to-start-a-new-tab-in-the-same-directory-as-the-current-ta/61264#61264

on run
    (* Find the tty. *)
    -- This is ugly. But is seems to work on Tiger. Maybe newer releases can do better.
    tell application "Terminal"
        set w to the front window
        tell w
            set origName to name
            set title displays device name to not title displays device name
            set newName to name
            set title displays device name to not title displays device name
        end tell
    end tell
    set tty to extractTTY(origName, newName)
    if tty is "" then
        display dialog "Could not find the tty for of the current Terminal window." buttons "Cancel" cancel button "Cancel" default button "Cancel"
    end if

    (* Find the PIDs of the processes in the foreground process group on that tty. *)
    set pids to paragraphs of (do shell script "
ps -o pid,tty,tpgid,pgid,state,command |
awk '
    BEGIN   {t=ARGV[1];ARGC=1}
    $2==t && $3==$4 {print $1}
' " & quoted form of tty)
    if pids is {} or pids is {""} then
        display dialog "Could not find the processes for " & tty & "." buttons "Cancel" cancel button "Cancel" default button "Cancel"
    end if

    (* Find the unique cwds of those processes. *)
    set text item delimiters to {","}
    set lsof to do shell script "lsof -F 0n -a -d cwd -p " & quoted form of (pids as Unicode text) without altering line endings
    set text item delimiters to {(ASCII character 0) & (ASCII character 10)}
    set cwds to {}
    repeat with lsofItem in text items of lsof
        if lsofItem starts with "n" then
            set cwd to text 2 through end of lsofItem
            if cwds does not contain cwd then ¬
                set end of cwds to cwd
        end if
    end repeat
    if cwds is {} then
        display dialog "No cwds found!?" buttons "Cancel" cancel button "Cancel" default button "Cancel"
    end if
    if length of cwds is greater than 1 then
        set cwds to choose from list cwds with title "Multiple Distinct CWDs" with prompt "Choose the directory to use:" without multiple selections allowed and empty selection allowed
        if cwds is false then error number -128 -- cancel
    end if

    (* Open a new Terminal. *)

    -- Here is where I substituted the menu_click call to use the current settings

    tell application "Terminal"
        activate
        tell window 1
            set settings to name of current settings in selected tab
        end tell
    end tell
    menu_click({"Terminal", "Shell", "New Tab", settings})

    tell application "Terminal" to do script "cd " & quoted form of item 1 of cwds in selected tab of window 1
end run

to extractTTY(a, b)
    set str to textLeftAfterRemovingMatchingHeadAndTail(a, b)
    set offs to offset of "tty" in str
    if offs > 0 then
        return text offs through (offs + 6) of str
    end if
    return ""
end extractTTY
to textLeftAfterRemovingMatchingHeadAndTail(big, little)
    set text item delimiters to space
    if class of big is not list then set big to text items of big
    if class of little is not list then set little to text items of little
    set {maxLen, minLen} to {length of big, length of little}
    if maxLen < minLen then ¬
        set {big, little, maxLen, minLen} to {little, big, minLen, maxLen}

    set start to missing value
    repeat with i from 1 to minLen
        if item i of big is not equal to item i of little then
            set start to i
            exit repeat
        end if
    end repeat
    if start is missing value then
        if maxLen is equal to minLen then
            return ""
        else
            return items (minLen + 1) through end of big as Unicode text
        end if
    end if

    set finish to missing value
    repeat with i from -1 to -minLen by -1
        if item i of big is not equal to item i of little then
            set finish to i
            exit repeat
        end if
    end repeat
    if finish is missing value then set finish to -(minLen + 1)

    return items start through finish of big as Unicode text
end textLeftAfterRemovingMatchingHeadAndTail

backspace

Posted 2009-10-26T23:28:39.277

Reputation: 111

1

As mentioned elsewhere, if you are using Oh My Zsh, then you should just add the terminalapp plugin. In your .zshrc file (assuming you're already using the git plugin:

plugins=(terminalapp git)

Joshua J. McKinnon

Posted 2009-10-26T23:28:39.277

Reputation: 111

0

me

Posted 2009-10-26T23:28:39.277

Reputation:

The script at that link looks like it will have problems dealing with directory and command/args that have embedded single-quotes in them. Probably not too likely, but it is a bug. – Chris Johnsen – 2009-10-27T05:09:48.173

0

I use this alias/shell script to do it.

# modified from http://www.nanoant.com/programming/opening-specified-path-in-terminals-new-tab
alias twd=new_terminal_working_directory
function new_terminal_working_directory() {
osascript <<END 
        tell application "Terminal"
            tell application "System Events" to tell process "Terminal" to keystroke "t" using command down
        do script "cd $(pwd)" in first window
    end tell
END
}

Ted Naleid

Posted 2009-10-26T23:28:39.277

Reputation: 1 059

1That look likes it will have problems if the cwd has certain characters in it (shell meta-characters and control tokens; e.g. a directory with a space in it). – Chris Johnsen – 2009-10-27T05:12:23.910