lalo
lalo

Reputation: 929

Sublimetext integration with Windows Subsystem for Linux (WSL, bash)

Is there a way of executing bash commands from sublimetext?

In many projects it's just better developing with linux tools via the Windows Subsystem for Linux (WSL, bash) as most tools are built for linux.

In windows, you can run bash commands in a windows-console like this:

bash -c "echo \"my bash command here\""

Let's say I want to run a very specific build script like this:

bash -c "prettier mypath/ && eslint mypath/ --fix"

or like this:

bash -c "my_very_specific_build_script.sh"

or even better, having a hook executing a linter via a hook with: https://packagecontrol.io/packages/Hooks

bash -c "my_linting_script.sh"

and then call the script on every "save" like this:

"on_post_save_user":
    [
        {
            "command": "bash_execute",
            "args": {
              "command": "my_linting_script.sh",
            }
        }
    ],

This would be a game changer for all of us developers without a mac

MY ADVANCE SO FAR

In Keybindings this works (creates a log when ctrl+alt+b pressed)

{
  "keys": ["ctrl+alt+b"], 
  "command": "exec", 
  "args": {
    "cmd": "bash -c \"cd ~ && echo ok >> log.txt\"",
  } 
}

And this works in preferences triggered on every "save" using the "hooks" plugin:

"on_post_save_user": [
    {
        "command": "exec", 
        "args": {
          "cmd": "bash -c \"cd ~ && echo ok >> log.txt\"",
        },
            "scope": "window"

    }
],

Is this the best way?

An attempt at creating a plugin

I made a simple plugin that successfully runs "top" with bash in windows with WSL.

https://github.com/eduardoarandah/sublime-bash-execute

Just copy BashCommand.py in Packages/User and run in console:

view.run_command('bash_exec')

I've never made a sublime plugin, any help would be credited, appreciated and so on.

Upvotes: 4

Views: 4534

Answers (1)

OdatNurd
OdatNurd

Reputation: 22791

The internal exec command can run any arbitrary command that you choose to throw at it, so it's possible to use it directly to do this using that command if so desired.

Something to note is that the exec command can be given either cmd as a ["list", "of", "strings"] or shell_cmd as "a single string". When using cmd, the first item in the list is invoked directly and everything else is passed as arguments, while in the second case the string is passed directly to the shell as is.

Generally for items such as you're mentioning here, you want to use shell_cmd; since it's passed directly to the shell to execute, it can contain things such as redirection and chained commands.

As noted in your examples above, you can use shell_cmd and prefix any command that you want to execute with bash -c, wrapping the rest of the command in double quotes, and the exec command will run bash and pass it the command that you provided.

It's also possible to create a simple command in Sublime that behaves like exec does, while at the same time automatically injecting the appropriate item in there to get bash to execute the command for you.

Such a plugin might look like the following:

import sublime
import sublime_plugin

from Default.exec import ExecCommand


class BashExecCommand(ExecCommand):
    """
    A drop in replacement for the internal exec command that will run the
    given command directly in bash. Use of "shell_cmd" is recommended, but
    some care is taken to convert "cmd" to the appropriate "shell_cmd"
    equivalent.

    For use in key bindings, replace `exec` with `bash_exec` and you're good
    to go. For use in a build system, include the following lines to your
    build to tell Sublime to use this command to execute the build as well as
    how to kill it:

        "target": "bash_exec",
        "cancel": {"kill": True}
    """
    def run(self, **kwargs):
        # If we're being told to kill a running build, short circuit everything
        # and just do it directly. Not really needed, but it's a nice reminder
        # that custom builds need to handle this sort of thing in general.
        if kwargs.get("kill", False):
            return super().run(kill=True)

        # If there is a "cmd" argument, grab it out and convert it into a
        # string. Items in cmd that contain spaces are double quoted so that
        # they're considered a single argument, and all items are joined
        # together.
        cmd = kwargs.pop("cmd", None)
        if cmd is not None:
            cmd = " ".join(["\"%s\"" % s if " " in s else s for s in cmd])

        # If there is a "shell_cmd" argument, then modify it to include the
        # correct prefix to run the command via bash as well as to quote all
        # double quote characters so the shell sees them.
        #
        # This will fall back to the string we just gathered for "cmd" if no
        # shell_cmd is given.
        shell_cmd = kwargs.pop("shell_cmd", cmd)
        if shell_cmd is not None:
            kwargs["shell_cmd"] = "bash -c \"{0}\"".format(
                shell_cmd.replace("\"", "\\\""))

        # Defer to the built in exec command to run the command
        super().run(**kwargs)

If you redact away the comments that explain what this is doing, the change is ~20 lines or so. What this is doing is leveraging the internal exec command to do all of the heavy lifting for us. Thus we can just modify the arguments to the command automatically and let Sublime take care of the details.

As seen in the code, if a cmd argument is provided, it's converted into a string by joining all of the arguments together with space characters. Along the way any argument that contains a space is automatically double quoted so that it conveys properly to the shell.

If shell_cmd is provided, it's similarly extracted and converted into a string with a bash -c prefix; along the way any double quotes that appear in the command are quoted so that they convey properly to the shell.

Additionally, the converted value of cmd is used as the default if no shell_cmd is provided; thus if you specify shell_cmd that's always what is executed, and if you use cmd it's converted into the appropriate shell_cmd argument directly.

In use the command that this implements is bash_exec, and should be a drop in replacement for uses of exec in places where you're calling it directly, such as in key bindings, menu entries, command palette entries, via other commands, etc.

In addition, you can add the following two lines directly to a sublime-build file to use this command to execute builds undertaken that way; the first line tells Sublime to use this command instead of exec to execute the build, while the second tells it what arguments to pass to the command to get it to cancel a running build.

"target": "bash_exec",
"cancel": {"kill": True},

Caveats

This is really only of use on Windows; on Linux and MacOS Sublime already uses bash to execute shell_cmd without your having to do anything special. Using sublime.platform(), the command could determine if it's being executed on Windows and only rewrite commands on that OS and otherwise leave things alone, if desired.

Note also that I don't personally use WSL on my Windows machines, so this is untested in that environment (but it performs as expected on my Linux box); as such there may still be some tweaks to be made here, such as if bash isn't directly on the path or you want it to be configurable.

The last thing to mention is that the code to convert shell_cmd and cmd into a bash command line may require some more intelligence or subtle tweaks depending on what you're trying to pull off. If things don't seem to do what you expect, the Sublime console displays what command exec is running, which may shed some light on things.

Upvotes: 1

Related Questions