#!/usr/bin/env bash kTHIS_HOMEPAGE='https://github.com/mklement0/ttab' kTHIS_NAME=${BASH_SOURCE##*/} kTHIS_VERSION='v0.8.0' # NOTE: This assignment is automatically updated by `make version VER=` - DO keep the 'v' prefix. unset CDPATH # To prevent unexpected `cd` behavior. # --- Begin: STANDARD HELPER FUNCTIONS die() { echo "$kTHIS_NAME: ERROR: ${1:-"ABORTING due to unexpected error."}" 1>&2; exit ${2:-1}; } dieSyntax() { echo "$kTHIS_NAME: ARGUMENT ERROR: ${1:-"Invalid argument(s) specified."} Use -h for help." 1>&2; exit 2; } # SYNOPSIS # openUrl # DESCRIPTION # Opens the specified URL in the system's default browser. openUrl() { local url=$1 open "$url" || { echo "Cannot locate or failed to open default browser; please go to '$url' manually." >&2; return 1; } } # Prints the embedded Markdown-formatted man-page source to stdout. printManPageSource() { sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/ { s///; t\np;}' "$BASH_SOURCE" } # Opens the man page, if installed; otherwise, tries to display the embedded Markdown-formatted man-page source; if all else fails: tries to display the man page online. openManPage() { local pager embeddedText if ! man 1 "$kTHIS_NAME" 2>/dev/null; then # 2nd attempt: if present, display the embedded Markdown-formatted man-page source embeddedText=$(printManPageSource) if [[ -n $embeddedText ]]; then pager='more' command -v less &>/dev/null && pager='less' # see if the non-standard `less` is available, because it's preferable to the POSIX utility `more` printf '%s\n' "$embeddedText" | "$pager" else # 3rd attempt: open the the man page on the utility's website openUrl "${kTHIS_HOMEPAGE}/doc/${kTHIS_NAME}.md" fi fi } # Prints the contents of the synopsis chapter of the embedded Markdown-formatted man-page source for quick reference. printUsage() { local embeddedText # Extract usage information from the SYNOPSIS chapter of the embedded Markdown-formatted man-page source. embeddedText=$(sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/!d; /^## SYNOPSIS$/,/^#/{ s///; t\np; }' "$BASH_SOURCE") if [[ -n $embeddedText ]]; then # Print extracted synopsis chapter - remove backticks for uncluttered display. printf '%s\n\n' "$embeddedText" | tr -d '`' else # No SYNOPIS chapter found; fall back to displaying the man page. echo "WARNING: usage information not found; opening man page instead." >&2 openManPage fi } # Indicate by exit code whether the OS version is 10.13 (High Sierra) or above. isHighSierraOrAbove() { local -i major minor read major minor <<<$(sw_vers -productVersion | awk -F. '{ print $1; print $2 }') # High Sierra == macOS 10.13; Big Sur is 11.x (( (major == 10 && minor >= 13) || major >= 11 )) } # --- End: STANDARD HELPER FUNCTIONS # --- PROCESS STANDARD, OUTPUT-INFO-THEN-EXIT OPTIONS. case $1 in --version) # Output version number and exit, if requested. echo "$kTHIS_NAME $kTHIS_VERSION"$'\nFor license information and more, visit '"$kTHIS_HOMEPAGE"; exit 0 ;; -h|--help) # Print usage information and exit. printUsage; exit ;; --man) # Display the manual page and exit, falling back to printing the embedded man-page source. openManPage; exit ;; --man-source) # private option, used by `make update-doc` # Print raw, embedded Markdown-formatted man-page source and exit printManPageSource; exit ;; --home) # Open the home page and exit. openUrl "$kTHIS_HOMEPAGE"; exit ;; esac # --- MAIN BODY # Undocumented DEBUGGING SUPPORT: if the very first argument is --dry-run, we print out the synthesized AppleScript rather than executing it. printScript=0 dryRun=0 { [[ $1 == '--dry-run' ]] && { dryRun=1; shift; }; } || { [[ $1 == '--print-script' ]] && { printScript=1; shift; }; } # Parse options isMacOS=$([[ $(uname) == 'Darwin' ]] && echo 1 || echo 0) dirAbs='' doNotChangeDir=0 tabTitle='' settingsName='' inBackground=0 targetTermSpecified=0 inNewWin=0 cls=0 terminalApp='' delayCmdSubmissionBy='' && kRE_FRACTIONAL_SECONDS='^[0-9]+(\.[0-9]+)?$' && kDEFAULT_CMD_DELAY='0.1' while getopts ':wgGqs:t:d:a:l:' opt; do # $opt will receive the option *letters* one by one; a trailing : means that an arg. is required, reported in $OPTARG. [[ $opt == '?' ]] && dieSyntax "Unknown option: -$OPTARG" [[ $opt == ':' ]] && dieSyntax "Option -$OPTARG is missing its argument." case "$opt" in w) inNewWin=1 ;; s) settingsName=$OPTARG ;; t) tabTitle=$OPTARG ;; d) dir=$OPTARG [[ -z $dir ]] && doNotChangeDir=1 (( doNotChangeDir )) || { dirAbs=$(cd -- "$dir" 2>/dev/null && echo "$PWD") || die "No such directory: $dir"; } ;; a) terminalApp=$OPTARG targetTermSpecified=1 ;; g) if (( inBackground )); then dieSyntax "Please specify EITHER -g OR -G."; fi inBackground=1 ;; G) if (( inBackground )); then dieSyntax "Please specify EITHER -g OR -G."; fi inBackground=2 ;; q) cls=1 ;; l) # "l" as in de*l*ay or *lag* - not great, but better than -y (and -d is already taken) delayCmdSubmissionBy=$OPTARG [[ $delayCmdSubmissionBy =~ $kRE_FRACTIONAL_SECONDS ]] || dieSyntax "-l argument must be 0 or a positive number in (optionally fractional) seconds." ;; *) # An unrecognized switch. dieSyntax "DESIGN ERROR: unanticipated option: $opt" ;; esac done shift $((OPTIND - 1)) # Skip the already-processed arguments (options). # Allow presetting the -l option via an environment variable. if [[ -z $delayCmdSubmissionBy ]]; then delayCmdSubmissionBy="$TTAB_CMD_DELAY" [[ -z $delayCmdSubmissionBy || $delayCmdSubmissionBy =~ $kRE_FRACTIONAL_SECONDS ]] || { delayCmdSubmissionBy=''; echo "WARNING: Ignoring -l preset via env. var. TTAB_CMD_DELAY, because its value isn't a positive number of (optionally fractional) seconds." >&2; } # If not set by env. var., use the default delay. # We use a [[ -z $delayCmdSubmissionBy ]] && delayCmdSubmissionBy="$kDEFAULT_CMD_DELAY" fi # All remaining arguments, if any, make up the command to execute in the new tab/window. # Identify the terminal application that was explicitly specified. Terminal=0 iTerm=0 gnomeTerminal=0 if [[ -n $terminalApp ]]; then shopt -s nocasematch # we want to match the application name case-INSensitively. case $terminalApp in 'Terminal'|'Terminal.app') # Note: 'Apple_Terminal' is what $TERM_PROGRAM contains when running from Terminal.app # Use standard Terminal.app application. Terminal=1 ;; 'iTerm'|'iTerm.app'|'iTerm2'|'iTerm2.app') # Note: 'iTerm.app' is what $TERM_PROGRAM contains when running from iTerm.app iTerm=1 ;; 'gnome-terminal') gnomeTerminal=1 ;; *) # Fail, if an unknown terminal was explicitly specified. die "'$terminalApp' is not a supported terminal application" ;; esac shopt -u nocasematch fi # Set target-terminal-app-appropriate variables used later. while :; do if [[ $iTerm == 1 || $TERM_PROGRAM == 'iTerm.app' ]]; then iTerm=1 terminalApp='iTerm' # will be used with `activate application` # Note: iTerm2's AppleScript syntax changed fundamentally in v3 (for the better, but incompatibly so), # so we need to distinguish versions below. # $iTermOld reflects a pre-v3 version. [[ $(osascript -e 'version of application "iTerm"') =~ ^(1|2) ]] && iTermOld=1 || iTermOld=0 elif [[ $Terminal == 1 || $TERM_PROGRAM == 'Apple_Terminal' ]]; then Terminal=1 terminalApp='Terminal' # will be used with `activate application` elif [[ $gnomeTerminal == 1 || -n $GNOME_TERMINAL_SCREEN ]]; then gnomeTerminal=1 terminalApp='gnome-terminal' # binary file name else # The calling program is not a known terminal. # Determine a platform-appropriate default. (( isMacOS )) && Terminal=1 || gnomeTerminal=1 continue fi break done # Make sure that the targeted terminal app is actually present. (( (iTerm || Terminal) && ! isMacOS )) && die "Terminal.app / iTerm2.app can only be targeted on macOS." # Note: It's hypothetically possible to install gnome-terminal on macOS, via MacPorts. (( gnomeTerminal )) && { which gnome-terminal &>/dev/null || die "Cannot locate Gnome Terminal's binary, gnome-terminal."; } (( iTerm )) && { osascript -e "version of application \"$terminalApp\"" &>/dev/null || die "Cannot locate Gnome Terminal's binary, gnome-terminal."; } # (( Terminal )) # no need to check - Terminal.app comes with macOS. # To be safe, clear any pre-existing variables with names matching those we'll be using below. for n in ${!CMD_*}; do unset $n; done # Note for Terminal.app: # We use *menu-based GUI scripting* rather than *sending keystrokes via CMD_NEWTAB_1='tell application "System Events" to keystroke "t" using {command down}' # !! We try to be locale-independent, so we don't use menu-item command *names*; this requires us to make the following ASSUMPTIONS, which we expect to hold in the future: # - the new *window* commands are assumed to be in a *submenu* of the *1st command* ('New Window > ...') of the Shell menu. # - the new *tab* commands are assumed to be in a *submenu* of the *2nd command* ('New Tab > ...') of the Shell menu. # (a) compared to sending a keystroke, this is more robust and predictable; with sending a keystroke, the shell command to execute is *sometimes* sent to the *previous* tab, as the keystroke may not have been processed yet. # In the default case, the GUI scripting finds the command by its keyboard shortcut so as to work locale-independently; it does, however, make the following assumptions (other than the pretty safe assumption that the command is in the 3rd menu from the left, the "Shell" menu in English): # (b) it also allows us to create new tabs *with specific settings*, by finding the menu item by *name* (settings names are locale-*in*dependent, fortunately) # Using this is preferable to trying to apply settings *after* tab creation with `set current settings of selected tab ...`, as the latter only applies *visual* attributes, not *behavioral* (such as what to do when the shell exits) if (( gnomeTerminal )); then if [[ -n $settingsName ]]; then CMD_OPT_PROFILE="--profile='${settingsName}'" fi fi if (( inNewWin )); then # create the tab in a NEW WINDOW if (( gnomeTerminal )); then CMD_NEWTAB_1='--window' elif (( iTerm )); then if (( iTermOld )); then # OLD iTerm syntax (v2-) # Note: Passing an empty $settingsName is accepted as specifying the default profile. CMD_NEWTAB_1='tell application "iTerm" to launch (make new terminal) session "'"$settingsName"'"' if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within iTerm CMD_SAVE_ACTIVE_TAB='set prevTerm to current terminal' CMD_REACTIVATE_PREV_TAB='set current terminal to prevTerm' fi else # NEW iTerm syntax (introduced in v3) if [[ -n $settingsName ]]; then CMD_NEWTAB_1='create window with profile "'"$settingsName"'"' else CMD_NEWTAB_1='create window with default profile' fi if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within iTerm CMD_SAVE_ACTIVE_TAB='set prevWin to current window' # !! Sadly, neither `set current window to` nor `set frontmost of to true` work, so we must use GUI scripting. CMD_REACTIVATE_PREV_TAB='tell application "System Events" to perform action "AXRaise" of window (name of prevWin) of application process "iTerm2"' fi fi else # Terminal.app if [[ -n $settingsName ]]; then # !! If no such settings exist, the resulting error message can be confusing. CMD_NEWTAB_1='tell application "System Events" to tell menu 1 of menu item 1 of menu 1 of menu bar item 3 of menu bar 1 of application process "Terminal" to click menu item "'"$settingsName"'"' CMD_NEWTAB_2='set newTab to selected tab of front window' else CMD_NEWTAB_1='' CMD_NEWTAB_2='set newTab to do script' # note that running `do script` without arguments (i.e., without a suffix specifying the target window (e.g., 'in front window')) creates a *new* window. fi if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within Terminal CMD_SAVE_ACTIVE_TAB='set prevWin to front window' CMD_REACTIVATE_PREV_TAB='set frontmost of prevWin to true' # Note that while *Terminal.app*'s windows have a `frontmost` property that allows their activation within the app, most other (all other?) apps' windows do not. fi fi else # Create the new tab in the CURRENT WINDOW (unlike `do shell script` with no target for creating a new *window*, `do shell script in front window` simply executes in the *current* tab, and there's no direct way to target a new one). if (( gnomeTerminal )); then CMD_NEWTAB_1='--tab' elif (( iTerm )); then if (( iTermOld )); then # OLD iTerm syntax (v2-) # Note: Passing an empty $settingsName is accepted as specifying the default profile. CMD_NEWTAB_1='tell application "iTerm" to launch (current terminal) session "'"$settingsName"'"' if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within iTerm CMD_SAVE_ACTIVE_TAB='set prevSess to current session of current terminal' CMD_REACTIVATE_PREV_TAB='select prevSess' fi else # NEW iTerm syntax (introduced in v3) if [[ -n $settingsName ]]; then CMD_NEWTAB_1='tell current window to create tab with profile "'"$settingsName"'"' else CMD_NEWTAB_1='tell current window to create tab with default profile' fi if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within iTerm CMD_SAVE_ACTIVE_TAB='set prevTab to current tab of current window' CMD_REACTIVATE_PREV_TAB='select prevTab' fi fi else # Terminal.app if [[ -n $settingsName ]]; then CMD_NEWTAB_1='tell application "System Events" to tell menu 1 of menu item 2 of menu 1 of menu bar item 3 of menu bar 1 of application process "Terminal" to click menu item "'"$settingsName"'"' else CMD_NEWTAB_1='tell application "System Events" to tell menu 1 of menu item 2 of menu 1 of menu bar item 3 of menu bar 1 of application process "Terminal" to click (first menu item whose value of attribute "AXMenuItemCmdChar" is "T" and value of attribute "AXMenuItemCmdModifiers" is 0)' fi CMD_NEWTAB_2='set newTab to selected tab of front window' if (( inBackground == 2 )); then # For use with -G: commands for saving and restoring the previous state within Terminal if isHighSierraOrAbove; then # !! In 10.13 (High Sierra), Terminal.app switched to treating tabs like individual *windows*. CMD_SAVE_ACTIVE_TAB='set prevTab to front window' CMD_REACTIVATE_PREV_TAB='set frontmost of prevTab to true' else CMD_SAVE_ACTIVE_TAB='set prevTab to (selected tab of front window)' CMD_REACTIVATE_PREV_TAB='set selected of prevTab to true' fi fi fi fi # Terminal/iTerm only: # Define the command that *synchronously* actives iTerm / Terminal. # Note that this is neeeded both with and without -g / -G: # * With -g / -G, unfortunately, the terminal app must still be activated briefly # in order for GUI scripting to work correctly, with the previously active # application getting reactivated afterward. # * With foregound operations, we also activate explicitly, so as to support # invocation from helper apps such as Alfred where the terminal may be # created implicitly and not gain focus by default. # !! On 10.10+, activate is no longer *synchronous*, so we must wait until Terminal is truly activated (frontmost) # !! iTerm has a setting that hides the application from the dock and from being switched to with ⌘-tab # !! (Appearance > General > Exclude from Dock and ⌘-Tab Application Switcher). When active, AppleScript can no longer # !! detect when iTerm is frontmost, which prevents: # !! * synchronously waiting for iTerm to become active # !! * the -g / -G options from working (they rely on saving the currently frontmost application, and if it is iTerm, that won't work) # !! Thus, we simply forgo synchronous activation in this case, and disallow use of the -g / -G options. canDetectActivation=1 if (( iTerm )); then if [[ '1' == $(defaults read com.googlecode.iterm2 HideFromDockAndAppSwitcher 2>/dev/null) ]]; then canDetectActivation=0; fi if (( ! canDetectActivation && inBackground )); then die "While iTerm's 'Exclude from Dock and ⌘-Tab Application Switcher' feature is turned on, the options -g and -G are unsupported." fi fi CMD_ACTIVATE="if not frontmost then activate if $canDetectActivation = 1 then repeat until frontmost delay 0.1 end repeat end if end if" # Terminal/iTerm only: # For use with -g and -G: commands for saving and restoring the previous application if (( inBackground )); then CMD_SAVE_ACTIVE_APPNAME='set prevAppPath to path to frontmost application as text' CMD_REACTIVATE_PREV_APP='activate application prevAppPath' fi # Note: The desired behavior is to ALWAYS OPEN A TAB IN THE DIRECTORY THE CALLER # CONSIDERS CURRENT, whether the new tab is being opened in the current or # a new window (unless a target dir. is explicitly specified with -d ). # Terminal and iTerm have different default behaviors, so we need to account for # that: # * When opening a tab in a new *window*, both Terminal and iTerm default to the *home* dir. # * When opening a new tab in the *current window*, # * iTerm: defaults to the home dir. # * Terminal: the *caller's currrent dir., as known to Terminal* (see below) is used. # Also, to be safe, if a target terminal is explicitly specified, we also # default to issuing setting the current dir. explicitly, because it might be a different terminal than the current one. if (( iTerm || targetTermSpecified )); then # iTerm2 always defaults to the home dir., so we must always add an explicit `cd` command to ensure that the current dir. is used. if [[ -z $dirAbs ]]; then dirAbs=$PWD fi else if (( gnomeTerminal )); then : # Nothing to do, because gnome-terminal always defaults to the caller's current dir. else # Terminal.app # While Terminal.app does default to the caller's current dir. when creating a tab # in the *current* window, it doesn't necessarily know the *immediate caller's* true $PWD, # so we have to compensate for that: # Terminal.app only knows the working directory of the *top-level* shell running in each tab (as it defines an aux. function, # update_terminal_cwd(), that is called via $PROMPT_COMMAND every time the prompt is displayed). # Thus, when this script is invoked inside another script, it is the *top-level* shell's working directory that is invariably used by # Terminal, even if the invoking script has changed the working directory. Since this is counter-intuitive, we compensate here # by explicitly setting the working directory to the invoking script's with a prepended 'cd' command. # $SHLVL tells us the nesting level of the current shell: # 1 == top-level shell; since this script itself runs in a subshell (2, if invoked directly from the top-level shell), we can safely assume # that another *script* has invoked us, if $SHLVL >= 3. if [[ -z $dirAbs && ($SHLVL -ge 3 || $inNewWin -eq 1) ]]; then dirAbs=$PWD fi fi fi # Optional commands that are only used if the relevant options were specified. quotedShellCmds='' shellCmdTokens=( "$@" ) if (( ${#shellCmdTokens[@]} )); then # Shell command(s) specified. if (( ${#shellCmdTokens[@]} == 1 )); then # Could be a mere command name like 'ls' or a multi-command string such as 'git bash && git status' # If only a single string was specified as the command to execute in the new tab: # It could either be a *mere command name* OR a *quoted string containing MULTIPLE commands*. # We use `type` to determine if it is a mere command name / executable in the # current dir., otherwise we assume that the operand is a *multi-command string* # in which case *we must use `eval` to execute it*. # Note: *Blindly* prepending `eval` is not an option, because it would only work in MOST, but NOT ALL cases, # such as not with commands whose names happen to contain substrings # that look like variable references (however rare that may be). ([[ -n $dirAbs ]] && cd -- "$dirAbs" # Change to the target dir. first, to accommodate calls such as ./foo that assume to be running in the target dir. # Note: Since we're forced to use $SHELL -c in gnome-terminal anyway, we do NOT need `eval` there. type "${shellCmdTokens[0]}" &>/dev/null) || { (( ! gnomeTerminal )) && shellCmdTokens=( 'eval' "${shellCmdTokens[@]}" ); } fi # Quote (escape) the custom command. # !! The use of printf '%q' with its \-based escaping means that # !! shells with different escaping rules are NOT supported, notably PowerShell. if (( gnomeTerminal )); then # Form a single, quoted string that will be passed to $SHELL -c quotedShellCmds=$(printf '%q' "${shellCmdTokens[*]}") else # The tricky part is to quote the command tokens properly when passing them to AppleScript: # Quote all parameters (as needed) using printf '%q' - this will perform backslash-escaping. # This will allow us to not have to deal with double quotes inside the double-quoted string that will be passed to `do script`. quotedShellCmds=$(printf ' %q' "${shellCmdTokens[@]}") # Note: $quotedShellCmds now has a leading space, but that's benign (a *trailing* space, by contrast, would be a problem with iTerm's `write text ...` command) fi # !! [AUTO-DERIVING A TAB TITLE DISABLED - there's ultimately no heuristic that's guaranteed to result in a meaningful title. Let users specify a title explicitly, if needed. ] fi # if (( ${#shellCmdTokens[@]} ) # Add a commmand / option to change the working dir., if needed. if (( gnomeTerminal )); then # gnome-terminal has a dedicated option # gnome-terminal *always* uses the caller's working dir. [[ -n $dirAbs ]] && CMD_OPT_CWD="--working-directory=\"$dirAbs\"" else # Terminal/iTerm # Prepend the 'cd' command, if specified or needed - unless suppressed. if [[ $doNotChangeDir -eq 0 && -n $dirAbs ]]; then quotedCdCmd=$(printf 'cd %q' "$dirAbs") if [[ -n $quotedShellCmds ]]; then quotedShellCmds="$quotedCdCmd;$quotedShellCmds" else quotedShellCmds=$quotedCdCmd fi fi fi # Append the 'clear' command, if requested. if (( cls )); then if [[ -n $quotedShellCmds ]]; then if (( gnomeTerminal )); then # Note: Since we're forced to use $SHELL -c in gnome-terminal, $quotedShellCmds # an *unquoted* string with \-escaping quotedShellCmds="$quotedShellCmds\ \&\&\ clear" else quotedShellCmds="$quotedShellCmds && clear" fi else quotedShellCmds='clear' fi fi # Synthesize the full shell command. if [[ -n $quotedShellCmds ]]; then # Terminal/iTerm only: # Pass the commands as a single AppleScript string, of necessity double-quoted. # For the benefit of AppleScript # - embedded backslashes must be escaped by doubling them # - embedded double quotes must be backlash-escaped quotedShellCmdsForAppleScript=${quotedShellCmds//\\/\\\\} quotedShellCmdsForAppleScript=${quotedShellCmdsForAppleScript//\"/\\\"} if (( gnomeTerminal )); then CMD_CUSTOM="$quotedShellCmds" elif (( iTerm )); then if (( iTermOld )); then # OLD iTerm syntax (v2-) CMD_CUSTOM="write (current session of current terminal) text \"${quotedShellCmdsForAppleScript}\"" else # NEW iTerm syntax (introduced in v3) CMD_CUSTOM="tell current session of current window to write text \"${quotedShellCmdsForAppleScript}\"" fi else # Terminal.app CMD_CUSTOM="do script \"${quotedShellCmdsForAppleScript}\" in newTab" fi fi if [[ -n $tabTitle ]]; then # custom tab title specified # For the benefit of AppleScript # - embedded backslashes must be escaped by doubling them # - embedded double quotes must be backlash-escaped tabTitle=${tabTitle//\\/\\\\} tabTitle=${tabTitle//\"/\\\"} if (( gnomeTerminal )); then CMD_TITLE="--title=\"$tabTitle\"" elif (( iTerm )); then if (( iTermOld )); then # OLD iTerm syntax (v2-) CMD_TITLE="tell current session of current terminal to set name to \"$tabTitle\"" else # NEW iTerm syntax (introduced in v3) CMD_TITLE="tell current session of current window to set name to \"$tabTitle\"" fi else # Terminal.app CMD_TITLE="set custom title of newTab to \"$tabTitle\"" fi fi if (( gnomeTerminal )); then # The new tab should become active, unless -G is specified. (( inBackground == 2 )) || CMD_OPT_ACTIVATE='--active' # Append a custom command, if specified. # !! gnome-terminal does NOT support submitting a command to be run in *whatever shell is configured for the target profile*. # !! Instead, a shell executable must be explicitly specified, and - given that POSIX-compatible shells do not support executing # !! a command with -c *and staying open* (not exiting) afterwards, the best we can do is to append `; exec $SHELL` to the -c # !! call, which is only an *approximation* of staying in the same shell, however. if [[ -n $CMD_CUSTOM ]]; then if [[ (${#shellCmdTokens[@]} -gt 1 && ${shellCmdTokens[0]} == 'exec') || (${#shellCmdTokens[@]} -eq 1 && ${shellCmdTokens[0]} =~ ('&&'|'||'|';')\ *exit\ *\;?\ *$) ]]; then # !! If the command starts with `exec` or ends in `exit`, we infer that the intent # !! is NOT to keep the tab open; since $SHELL -c automatically exits by default, there's nothing else to do. CMD_OPT_CUSTOM="-- $SHELL -c $CMD_CUSTOM" else # !! Artifically keep the tab open by re-executing $SHELL in interactive mode. # !! The shell environment of the original shell cannot be fully preserved that way. # !! Note the use of `trap` with signals EXIT (normal exit) and INT (Ctrl-C) to ensure # !! that the new interactive shell is also launched when the original shell is terminated with Ctrl-C. # !! Verified to work in dash, bash, ksh, and zsh (in bash, EXIT alone would cover Ctrl-C, but not in zsh, for instance). CMD_OPT_CUSTOM="-- $SHELL -c trap\ \'exec\ $SHELL\'\ INT\ EXIT\;\ $CMD_CUSTOM" fi fi script="$terminalApp $CMD_NEWTAB_1 $CMD_OPT_ACTIVATE $CMD_OPT_CWD $CMD_OPT_PROFILE $CMD_TITLE $CMD_OPT_CUSTOM" else # Terminal/iTerm2 # Insert a delay before submitting a custom command and/or directory-changing command via AppleScript # Note: Fortunately, AppleScript's `delay` command supports *fractional* seconds. CMD_DELAY= [[ -n $CMD_CUSTOM && -n $delayCmdSubmissionBy ]] && CMD_DELAY="delay $delayCmdSubmissionBy" # Now, synthesize the entire AppleScript # !! At least on 10.10, the commands to save the active application and to reactivate it later must be OUTSIDE of the tell application "Terminal" block to work. read -d '' -r script <' # - All other headings should be level-2 headings in ALL-CAPS. # - TEXT # - Use NO indentation for regular chapter text; if you do, it will # be indented further than list items. # - Use 4-space indentation, as usual, for code blocks. # - Markup character-styling markup translates to ROFF rendering as follows: # `...` and **...** render as bolded (red) text # _..._ and *...* render as word-individually underlined text # - LISTS # - Indent list items by 2 spaces for better plain-text viewing, but note # that the ROFF generated by marked-man still renders them unindented. # - End every list item (bullet point) itself with 2 trailing spaces too so # that it renders on its own line. # - Avoid associating more than 1 paragraph with a list item, if possible, # because it requires the following trick, which hampers plain-text readability: # Use ' ' in lieu of an empty line. #### : <<'EOF_MAN_PAGE' # ttab(1) - open a new terminal tab or window ## SYNOPSIS Opens a new terminal tab or window, on macOS in either Terminal.app or iTerm2.app; on Linux in Gnome Terminal, if available. ttab [-w] [-s ] [-t ] [-q] [-g|-G] [-d <dir>] [<cmd> ...] -w Open new tab in new terminal window. -s <settings> Assign a settings set (profile). -t <title> Specify title for new tab. -q Clear the new tab's screen. -g Terminal/iTerm only: create in background (don't activate application). -G Terminal/iTerm: don't activate new tab and create in background. gnome-terminal: don't activate new tab, except with -w. -d <dir> Specify working directory; -d '' disables inheriting the current dir. in Terminal/iTerm. -l <secs> Terminal/iTerm only: delay startup command submission; may be preset via env. var. TTAB_CMD_DELAY -a Terminal | iTerm Open the new tab in the given terminal app on macOS. <cmd> ... Command to execute in the new tab. "<cmd> ...; ..." Multi-command command line (passed as single operand). Standard options: `--help`, `--man`, `--version`, `--home` ## DESCRIPTION `ttab` opens a new terminal tab with a variety of options, including executing a command in the new tab, assigning a title and working directory, and opening the tab in a new window. Supports Terminal.app and iTerm2.app on macOS, and - with limitations - gnome-terminal on Linux. Note: iTerm2 and gnome-terminal support is currently not covered by the automated tests run before every release. IMPORTANT: On macOS, **Terminal/iTerm2 must be allowed assistive access** in order for this utility to work, which requires one-time authorization with administrative privileges. If you get error messages instead of being prompted, authorize the application via System Preferences > Security & Privacy > Privacy > Accessibility. The new tab or window inherits the calling shell's working directory by default. On macOS, the new tab or window runs a login shell (i.e., loads the user's shell profile) and inherits the calling shell's working directory. When specifying a command to execute in the new tab, quoted parameters are handled properly and there's no need to quote the command as a whole, provided it is a *single* command. Prefix such a single command with `exec` to exit the shell after the command terminates. If the tab's settings are configured to close tabs on termination of the shell, the tab will close automatically. To specify *multiple* commands, pass them as a single command-line string. Use `exit` as the last command to automatically close the tab when the command terminates, assuming the tab's settings are configured to close the tab on termination of the shell. Precede `exit` with `read -rsn 1` to wait for a keystroke first. IMPORTANT: Specifying a command to execute in the new tab has limitations: * Specifying a startup command is only supported for POSIX-compatible shells, because "\" is used for behind-the-scenes escaping, and the presence of the `eval` / `exec` builtins is assumed on macOS / Linux. Notably, this precludes passing commands to PowerShell. * With gnome-terminal (Linux), $SHELL, the user's default shell is invariably used to execute the command and to run in the new tab, even if the target profile (settings) is configured to run a different shell. Also, a workaround is employed to keep the tab open: because the $SHELL must be invoked with -c in order to process the command, it exits on completion, so `exec $SHELL` is executed afterwards to keep the tab open (unless you pass a single `exec` command or multi-command string that ends with `exit`); this second $SHELL instance cannot guarantee that the original's shell environment is fully preserved. ## OPTIONS * `-w` creates the new tab in a new window rather than in the front window. * `-s <settings>` specifies the settings set (profile) to apply to the new tab, determining the appearance and behavior of the new tab. o Terminal: settings sets are defined in Preferences > Profiles; name matching is case-*in*sensitive, and specifying nonexistent settings causes an error. o iTerm2: profiles are defined in Preferences > Profiles; name matching is case-*sensitive*, and specifying a nonexistent profile causes an error. o gnome-terminal: profiles are defined in Edit > Preferences; name matching is case-*sensitive*, and specifying a nonexistent profile falls back to to the default profile. * `-t <title>` specifies a custom title to assign to the new tab. * `-d <dir>` explicitly specifies a working directory for the new tab; by default, the invoking shell's working directory is inherited; in Terminal/iTerm, you can use `-d ''` to disable this inheriting. * `-q` (*q*uiet) issues a `clear` command after opening the new tab. Note that output will temporarily be visible while the tab is being opened; also, clearing is not performed if any command passed reports an overall nonzero exit code, so as to allow failures to be examined. * `-g` Terminal/iTerm2 only: (back*g*round) causes Terminal/iTerm2 not to activate, if it isn't the frontmost application); within the application, however, the new tab will become the active tab; useful in scripts that launch other applications and don't want Terminal/iTerm2 to steal focus later. * `-G` Terminal/iTerm2: causes Terminal/iTerm2 not to activate *and* the active element within the application not to change; i.e., the active window and tab stay the same. If Terminal/iTerm2 happens to be frontmost, the new tab will effectively open in the background. gnome-terminal: causes the new tab not to activate except if it is created in a new window (-w). NOTE: Terminal/iTerm2: With `-g` or `-G`, the new tab will still activate *briefly, temporarily*, for technical reasons. The temporary activation lasts as least as long as the effective -l value (command-submission delay). * `-l <secs>` Terminal/iTerm2 only: delays submitting a startup command by the specified number of seconds (fractions supported); useful for shells with initialization files that take a long time to process. Note that setting a working dir. with -d alone requires a startup command, and that in iTerm2 setting a working dir. is always required. The default is 0.1 secs; you can preset a different value via environment variable TTAB_CMD_DELAY. Note the impact on -g / -G. * `-a Terminal` or `-a iTerm2` explicitly specifies which terminal application to use on macOS; by default, the terminal application from which this utility is run is implied, if supported, with Terminal / gnome-terminal used as the default on macOS / Linux. This option is useful for calling this utility from non-terminal applications such as Alfred (https://www.alfredapp.com/) on macOS. ## STANDARD OPTIONS All standard options provide information only. * `-h, --help` Prints the contents of the synopsis chapter to stdout for quick reference. * `--man` Displays this manual page, which is a helpful alternative to using `man`, if the manual page isn't installed. * `--version` Prints version information. * `--home` Opens this utility's home page in the system's default web browser. ## LICENSE For license information and more, visit this utility's home page by running `ttab --home`. ## EXAMPLES # Open new tab in current terminal window: ttab # Open new tab in new terminal window: ttab -w # Open new tab with title 'Green' using settings (profile) 'Grass': ttab -t Green -s Grass # Open new tab and execute a command in it: ttab ls -l "$HOME/Library/Application Support" # Open new tab with specified working dir. and execute a command in it: ttab -d "$HOME/Library/Application Support" ls -l # Execute a command and exit. # If configured via the default profile, also close the tab. ttab exec /path/to/someprogram arg1 arg2 # Pass a multi-command string as a single, quoted string, wait for a # keystroke, then exit. ttab 'ls "$HOME/Library/Application Support"; echo Press any key to exit; read -rsn 1; exit' # Create a new tab explicitly in iTerm2. ttab -a iTerm2 echo "Hi from iTerm2." EOF_MAN_PAGE exit $ec