Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making Sublime Text 2 command on linux behave as it does on MacOS X

There are many questions asking about accessing the Sublime Text 2 editor from the command line. The responses, in summary, are to make a symlink, alias or simple shell script to run the appropriate sublime_text command. I can do that. What I want is to make the linux version behave like the MacOS version.

On MacOS, I have the following:

ln -s /Applications/Sublime\ Text\ 2.app/Contents/SharedSupport/bin/subl ~/bin/subl

Then in my .zshrc:

alias subl="$HOME/bin/subl -n"
export EDITOR="$HOME/bin/subl -n -w"

This does two things. It gives me a subl command that opens any files given on the command line in a new window. The subl command does not block the terminal. It also sets up my editor to open sublime text to edit the arguments, but this time it does block. In particular, $EDITOR blocks until its arguments are closed. It does not block on unrelated sublime text windows.

I can achieve a similar effect on linux with the following:

In ~/bin/subl:

#! /bin/zsh

$HOME/Sublime\ Text\ 2/sublime_text -n $@ &

and then in ~/bin/subl_wait: (think mate_wait for TextMate users)

#! /bin/zsh

exec $HOME/Sublime\ Text\ 2/sublime_text -n -w $@

I can then set EDITOR to subl_wait, and things almost work. subl opens files for editing and doesn't block. subl_wait opens files for editing and does block.

The problem is that subl_wait is waiting until all open files are closed, not just its arguments.

Is it possible to get this working perfectly?

like image 430
user2024209 Avatar asked Jan 30 '13 06:01

user2024209


2 Answers

Looks like I've found the issue. (Thanks to this post: http://www.sublimetext.com/forum/viewtopic.php?f=2&t=7003 )

Basic point: sublime behaves differently depending upon whether an instance is already running!

If an instance is already running then sublime on linux behaves similarly to MacOS. If no instance is running then the terminal blocks until you exit sublime.

With that in mind, we just need to modify the scripts to make sure sublime is running:

in ~/bin/subl_start:

#! /bin/zsh

if [ ! "$(pidof sublime_text)" ] ; then
  # start sublime text main instance
  # echo "Starting Sublime Text 2"
  $HOME/Sublime\ Text\ 2/sublime_text &
  sleep 1 # needed to avoid a race condition
fi

in ~/bin/subl:

#! /bin/zsh

. $HOME/bin/subl_start

exec $HOME/Sublime\ Text\ 2/sublime_text -n $@

in ~/bin/subl_wait:

#! /bin/zsh

. $HOME/bin/subl_start

exec $HOME/Sublime\ Text\ 2/sublime_text -n -w $@

Note that I've used the -n flags everywhere. This might not be your cup of tea. If you are using -n then you possibly also want to look at your close_windows_when_empty setting.

like image 181
user2024209 Avatar answered Sep 27 '22 16:09

user2024209


Inspired by the OP's answer, I've created a bash wrapper script for Sublime Text that incorporates all your findings and runs on both OSX and Linux.

Its purpose is threefold:

  • provide a unified subl CLI that works like ST's own subl on OSX: invoke ST without blocking, unless waiting is explicitly requested.
  • encapsulate a workaround for the waiting-related bug on Linux.
  • when saved or symlinked to as sublwait, provide a sublwait CLI that automatically applies the --wait and --new-window options so as to make it suitable for use with $EDITOR (note that some programs, e.g. npm, require the $EDITOR to contain the name of an executable only - executables + options are not supported); also makes sure that at least one file is specified.

The only open question is whether the OP's approach to avoiding the race condition - sleep 1 - is robust enough.

Update: Note that subl on OSX is by default NOT placed in the $PATH - you normally have to do that manually. If you haven't done so, the script will now locate subl inside ST's application bundle; (it tries app names in the following sequence: 'Sublime Text', 'Sublime Text 2', 'Sublime Text 3', first in /Applications, then in ~/Applications.)

Here's the output from running the script with -h:

Multi-platform (OSX, Linux) wrapper script for invocation of Sublime Text (ST)
from the command line.

Linux:
  Works around undesired blocking of the shell (unless requested)
  and a bug when waiting for specific files to be edited.
Both platforms:
  When invoked as `sublwait`, automatically applies the
    --wait --new-window
  options to make it suitable for use with $EDITOR.

Therefore, you can to the following:
- Name this script `subl` for a CLI that supports ALL options.
  (On OSX, this will simply defer to the `subl` CLI that came with ST.)
- Place the script in a directory in your $PATH.
- In the same directory, create a symlink to the `subl` script named
  `sublwait`:
    ln -s subl sublwait
  and, if desired, add
    export EDITOR=sublwait
  to your shell profile.

Note that if you only use OSX, you can make do with ST's own subl and just save this script directly as sublwait.

Script source:

#!/usr/bin/env bash

# Multi-platform (OSX, Linux) wrapper script for invocation of Sublime Text (ST)
# from the command line. Invoke with -h for details.

[[ $1 == '-h' || $1 == '--help' ]] && showHelpOnly=1 || showHelpOnly=0
[[ $(basename "$BASH_SOURCE") == 'sublwait' ]] && invokedAsSublWait=1 || invokedAsSublWait=0
[[ $(uname) == 'Darwin' ]] && isOsX=1 || isOsX=0

# Find the platform-appropriate ST executable.
if (( isOsX )); then # OSX: ST comes with a bona-fide CLI, `subl`.

  # First, try to find the `subl` CLI in the $PATH.
  # Note: This CLI is NOT there by default; it must be created by symlinking it from
  #       its location inside the ST app bundle.
  # Find the `subl` executable, ignoring this script, if named subl' as well, or a
  # script by that name in the same folder as this one (when invoked via symlink 'sublwait').
  stExe=$(which -a subl | fgrep -v -x "$(dirname "$BASH_SOURCE")/subl" | head -1)
  # If not already in the path, look for it inside the application bundle. Try several locations and versions.
  if [[ -z $stExe ]]; then
    for p in {,$HOME}"/Applications/Sublime Text"{,' 2',' 3'}".app/Contents/SharedSupport/bin/subl"; do
      [[ -f $p ]] && { stExe=$p; break; }
    done
  fi
  [[ -x $stExe ]] || { echo "ERROR: Sublime Text CLI 'subl' not found." 1>&2; exit 1; }

else # Linux: `sublime_text` is the only executable - the app itself.

  stExe='sublime_text'
  which "$stExe" >/dev/null || { echo "ERROR: Sublime Text executable '$stExe' not found." 1>&2; exit 1; }

fi

# Show command-line help, if requested.
# Add preamble before printing ST's own help.
# Note that we needn't worry about blocking the
# shell in this case - ST just outputs synchronously
# to stdout, then exits.
if (( showHelpOnly )); then

  bugDescr=$(
    cat <<EOF
works around a bug on Linux (as of v2.0.2), where Sublime Text,
if it is not already running, mistakenly blocks until it is exited altogether.
EOF
  )

  if (( invokedAsSublWait )); then

    # We provide variant-specific help here.
    cat <<EOF

Wrapper script for Sublime Text suitable for use with the \$EDITOR variable.

Opens the specified files for editing in a new window and blocks the 
invoking program (shell) until they are closed.
In other words: the --wait and --new-window options are automatically
applied.

Aside from encapsulating this functionality without the need for options
- helpful for tools that require \$EDITOR to be an executable name only -
$bugDescr

Usage: sublwait file ...

EOF
    # Note: Adding other options doesn't make sense in this scenario
    #       (as of v2.0.2), so we do NOT show ST's own help here.

  else

    cat <<EOF

Multi-platform (OSX, Linux) wrapper script for invocation of
Sublime Text (ST) from the command line.

Linux:
  Works around undesired blocking of the shell (unless requested)
  and a bug when waiting for specific files to be edited.
Both platforms:
  When invoked as \`sublwait\`, automatically applies the
    --wait --new-window
  options to make it suitable for use with \$EDITOR.

Therefore, you can to the following:
- Name this script \`subl\` for a CLI that supports ALL options.
  (On OSX, this will simply defer to the \`subl\` CLI that came with ST.)
- Place the script in a directory in your \$PATH.
- In the same directory, create a symlink to the \`subl\` script named
  \`sublwait\`:
    ln -s subl sublwait
  and, if desired, add
    export EDITOR=sublwait
  to your shell profile.

Sublime Text's own help:
------------------------
EOF

    # Finally, print ST's own help and exit.
    exec "$stExe" "$@"

  fi
  exit 0
fi


# Invoked as `sublwait`? -> automatically apply --wait --new-window options.
if (( invokedAsSublWait )); then

  # Validate parameters.
  # - We expect NO options - to keep things simple and predictable, we do NOT allow
  #   specifying additional options (beyond the implied ones).
  # - We need at least 1 file argument.
  # - As a courtesy, we ensure that no *directories* are among the arguments - ST doesn't support
  #  that properly (always waits for ST exit altogether); beyond that, however, we leave input
  #  validation to ST.
  if [[ "$1" =~ ^-[[:alnum:]]+$ || "$1" =~ ^--[[:alnum:]]+[[:alnum:]-]+$ ]]; then # options specified?
    { echo "ERROR: Unexpected option specified: '$1'. Use -h for help." 1>&2; exit 1; }
  elif (( $# == 0 )); then # no file arguments?
    { echo "ERROR: Missing file argument. Use -h for help." 1>&2; exit 1; }
  else # any directories among the arguments?
    # Note: We do NOT check for file existence - files could be created on demand.
    #       (Things can still go wrong - e.g., /nosuchdir/mynewfile - and ST doesn't
    #       handle that gracefully, but we don't want to do too much here.)
    for f in "$@"; do
      [[ ! -d "$f" ]] || { echo "ERROR: Specifying directories is not supported: '$f'. Use -h for help." 1>&2; exit 1; }
    done
  fi

  # Prepend the implied options.
  set -- '--wait' '--new-window' "$@"

fi

# Finally, invoke ST:
if (( isOsX )); then # OSX

  # `subl` on OSX handles all cases correctly; simply pass parameters through.
  exec "$stExe" "$@"

else # LINUX: `sublime_text`, the app executable itself, does have a CLI, but it blocks the shell.

  # Determine if the wait option was specified.
  mustWait=0
  if (( invokedAsSublWait )); then
    mustWait=1
  else
    # Look for the wait option in the parameters to pass through.
    for p in "$@"; do
      [[ $p != -* ]] && break # past options
      [[ $p == '--wait' || $p =~ ^-[[:alnum:]]*w[[:alnum:]]*$ ]] && { mustWait=1; break; }
    done
  fi

  if (( mustWait )); then # Invoke in wait-for-specified-files-to-close mode.

    # Quirk on Linux:
    # If sublime_text isn't running yet, we must start it explicitly first.
    # Otherwise, --wait will wait for ST *as a whole* to be closed before returning, 
    # which is undesired.
    # Thanks, http://stackoverflow.com/questions/14598261/making-sublime-text-2-command-on-linux-behave-as-it-does-on-macos-x
    if ! pidof "$stExe" 1>/dev/null; then
        # Launch as BACKGROUND task to avoid blocking.
        # (Sadly, the `--background` option - designed not to activate the Sublime Text window 
        #  on launching - doesn't actually work on Linux (as of ST v2.0.2 on Ubuntu 12.04).)
        ("$stExe" --background &)
        # !! We MUST give ST some time to start up, otherwise the 2nd invocation below will be ignored.
        # ?? Does a fixed sleep time of 1 second work reliably?
        sleep 1
    fi

    # Invoke in blocking manner, as requested.
    exec "$stExe" "$@"

  else # Ensure invocation in NON-blocking manner.

    if ! pidof "$stExe" 1>/dev/null; then # ST isn't running.
      # If ST isn't running, invoking it *always* blocks.
      # Therefore, we launch it as a background taks.
      # Invocation via a subshell (parentheses) suppresses display of the 
      # background-task 'housekeeping' info.
      ("$stExe" "$@" &)
    else # ST is already running, we can safely invoke it directly without fear of blocking.
      exec "$stExe" "$@"
    fi

  fi

fi
like image 22
mklement0 Avatar answered Sep 27 '22 16:09

mklement0