so

A more literate version of “t”

Volume 3, Issue 42; 29 Nov 2019

My second attempt to use org-mode and tangle: much improved!

The previous version of this post demonstrated that I’d failed to understand the significance of noweb support in org-mode. Having read that section of the manual, and learning that the “-i” option will get tangle to respect the indentation of my blocks, I’ve been able to improve things greatly. (Many thanks to the helpful respondents.)

What follows is a light rewrite of the original post.

I deal with servers all over the world: at home, at work, at my ISP, at AWS, etc. Several times a day, I want to open up a shell window on one of those remote systems.

Suppose I open up a root shell on my local machine, a remote shell at work, and a remote shell on AWS. Now I want to delete some files, so I type rm -rf …. The jokes almost write themselves, don’t they?

I have a couple of large monitors and a high-resolution color display. Rather than using ssh in my ordinary shell window, I can configure different terminal windows, with different color schemes: matrix green on black, that’s my root window; MarkLogic red background, that’s my work window; an Amazon orange background, that’s my AWS window; etc.

When Docker enters the picture, it gets a little more complicated. What you want to do in the Docker case isn’t ssh but docker exec. Except, to do that you have to know the ID of the container and those change everytime you start a new one.

I quickly got tired of that, so I wrote dps, a shell script to simplify my interations with docker. And then I realized that I could integrate that with my “start a terminal” script and t was born.

  • t → open a new terminal window on my local system.
  • t presentation → open a new terminal window on my local system with large fonts and high contrast.
  • t marklogic → open a terminal at work.
  • t aws → open a terminal at AWS.
  • t pihole → open a terminal on the RaspberryPi running Pi-hole.
  • t openwrt → open a terminal on my router.
  • t builder → open a terminal on the Docker containter “builder”.

What’s going on here is that you (optionally) pass t the name of a session that you’d like to start. The script will look up how to start that session and then starts it. For good measure you can specify a command to run instead of just logging in.

  • t -c "/share/ml-compile b9_0" builder → build the b9_0 branch of MarkLogic server on the “builder” container

The t script was initially written in Perl and ran on both Linux and the Mac. I recently convert it to Python in order to take advantage of the Python APIs for ITerm2, my prefered MacOS terminal.

The iTerm2 API

I’m not doing anything very interested or sophisticated with the iTerm2 API; I copied one of the examples and tinkered a bit.

Call the API · In the main body of my script, I initialize the workspace and then call run to execute the specified command with the named profile.

# Launch the app with iTerm
# pylint: disable=no-member
AppKit.NSWorkspace.sharedWorkspace().launchApplication_("iTerm2")
run(cmd, name)

I don’t really understand how AppKit works. It appears to be true, as pylint warns me, that there is no member named NSWorkspace, but it does, in fact, work, so I tell pylint to leave me alone.

Define the async callback · The run function defines the asynchronous function that’s part of the iTerm2 API. The nested definition has a closure over the outer function parameters so we don’t need the globals anymore! (Thanks Dave!)

The runiniterm function just implements part of the API; creating a new window in which my command runs.

def run(zshcommand, profile):
    """Execute the specified command in an iTerm2 window with
    the specified profile."""

    async def runiterm(connection):
        """Call the iTerm2 APIs."""
        app = await iterm2.async_get_app(connection)

        # Foreground the app
        await app.async_activate()

        if profile is not None:
            all_profiles = await iterm2.PartialProfile.async_query(connection)
            for prof in all_profiles:
                if prof.name == profile:
                    await prof.async_make_default()

        if zshcommand is None:
            zsh = "/usr/local/bin/zsh --login -i"
        else:
            zsh = '/usr/local/bin/zsh --login -i -c "%s"' % zshcommand
        await iterm2.Window.async_create(connection, command=zsh)

    iterm2.run_until_complete(runiterm, True)

Import the relevant modules · In order to use these APIs, you must have imported them. I’ve done that at the top of my script. The iterm2 and AppKit modules provide the ability to talk to iTerm2.

import iterm2
import AppKit

Defining sessions

There are (currently) four places where we can look to find the mapping from a session name to the underlying commands.

Configured sessions · To start with, there’s a configuration file ~/etc/t-profiles.json for just this purpose:

{
  "default": {},
  "root": {
    "profile": "Root",
    "command": "sudo su -"
  },
  "openwrt": {
    "profile": "Root",
    "command": "ssh openwrt"
  },
  "aws": {
    "profile": "AWS",
    "command": "ssh aws"
  },
  "dunsinane": {
    "profile": "Dunsinane",
    "command": "ssh dunsinane"
  },
  "presentation": {
    "profile": "Presentation"
  }
}

Each key in the top-level map is a session name. The profile identifes the iTerm2 profile to use and command specifies what should be run in that session.

For example, the Root profile is Matrix green-on-black, so running t root runs sudo su - in a new green-on-black terminal. The Presentation profile has a large font and high-contrast. It doesn’t specify a command so it’ll just be a new zsh shell.

The check_profiles function looks in the configuration file.

def check_profiles(term, command):
    """Search for the specified term in the global profiles."""
    name = None
    cmd = None

    try:
        with open("%s/etc/t-profiles.json" % HOME) as profile:
            profiles = json.load(profile)
    except FileNotFoundError:
        profiles = []
    except JSONDecodeError:
        profiles = []

    if term in profiles:
        if command is None and "command" in profiles[term]:
            cmd = profiles[term]["command"]
        if "profile" in profiles[term]:
            name = profiles[term]["profile"]

    return (name, cmd)

Docker sessions · Another place we could look is among the running Docker containers. The check_docker function looks there.

def check_docker(term, command):
    """Search for the specified term in the running Docker containers.
    This function relies on 'dps' a wrapper script that simplifies
    interaction with 'docker ps' and friends"""
    name = None
    cmd = None

    # Maybe a running container?
    proc = subprocess.run(args=["dps", "id", term], check=False, capture_output=True)
    if proc.returncode != 0:
        print("Ambigous container id: %s" % term, file=sys.stderr)
        sys.exit(1)
    cid = proc.stdout.decode("utf-8").strip()
    if cid != "":
        name = "Docker"
        if command is None:
            cmd = "dps login %s" % cid
        else:
            cmd = "dps exec %s %s" % (cid, command)

    return (name, cmd)

My dps script (not shown, but which I might feel inspired to write in Python and turn into a module this script can use) will search for containers by container name and image name. If we find a container name that matches, then we construct an appropriate command.

SSH sessions · Another place to look is in my ~/.ssh/config file. You can use the configuration to simplify SSH. For example, consider the following fragment of my SSH config (fuzzed for privacy):

Host aws
  Hostname ec2-XX-XX-XX.us-XXX.compute.amazonaws.com
  User ec2-user
  IdentityFile ~/.ssh/AWS-EC2.pem

If I run ssh aws, this configuration will connect to the correct host, as the correct user, with the correct key. I no longer have to type out the long form.

The check_ssh function looks for a matching SSH configuration.

def check_ssh(term, command):
    """Search for the specified term in my ssh configuration."""
    name = None
    cmd = None

    # Maybe ssh?
    hostre = re.compile(r"^Host\s+%s" % term)
    with open("%s/.ssh/config" % HOME, "r") as config:
        for line in config:
            if hostre.match(line):
                name = "SSH"
                cmd = "ssh %s" % term
                if command is not None:
                    cmd = cmd + " " + command

    return (name, cmd)

Host sessions · Last, but not least, we can look in /etc/hosts. This is possibly unnecessary or redundant. I could just treat any unrecognized session as the name of a host. But at the moment, I don’t, so I look up things like my Raspberry Pis and my NAS in /etc/hosts.

That’s what check_hosts does.

def check_hosts(term, command):
    """Search for the specified term in /etc/hosts."""
    name = None
    cmd = None

    hostre = re.compile(r"^\d+\.\d+\.\d+\.\d+\s+%s" % term)
    with open("/etc/hosts", "r") as hosts:
        for line in hosts:
            if hostre.match(line):
                name = "SSH"
                cmd = "ssh %s" % term
                if command is not None:
                    cmd = cmd + " " + command

    return (name, cmd)

Odds and ends

There’s a utility function to get command line arguments. Mostly because Pylint barked at me for having a main function that was too long.

def parse_command_line():
    """Get the terminal selection and optional command from the command line."""
    parser = argparse.ArgumentParser()
    parser.add_argument("--command", "-c", action="store", help="Command to run")

Note that we specify default as the default profile. That’ll come up later.

parser.add_argument(
    "profile", action="store", nargs="?", default="default", help="Profile name"
)
args = vars(parser.parse_args())
term = args["profile"]
command = args["command"]
return (term, command)

We always start new terminals in the home directory (for consistency). Near the top of the script, we store that in the HOME (for convenience).

HOME = os.environ["HOME"]

Right before we call the iTerm2 API, we change to that directory.

os.chdir(HOME)

Main

Finally, we get to the main function.

def main():
    """Run command in an iTerm window with profile"""

The initial session name and (optional) command come from the command line. Recall that the session will always have a value because it defaults to default.

(session, command) = parse_command_line()

Then we look for a matching configured, Docker, SSH or host session. First one wins.

(name, cmd) = check_profiles(session, command)
if name is None:
    (name, cmd) = check_docker(session, command)
if name is None:
    (name, cmd) = check_ssh(session, command)
if name is None:
    (name, cmd) = check_hosts(session, command)

If we didn’t find the session, that’s ok if it’s the special session default. Otherwise, raise an error.

if name is None:
    if session == "default":
        pass
    else:
        print("Error: no such profile: %s" % session, file=sys.stderr)
        sys.exit(1)

If we got this far, we’ve figured out what to do so we’ll call the iTerm2 API as described above and start the shell.

Boilerplate

This is a completely standard Python3 program; it begins with an appropriate “shebang”, a docstring, and some standard imports.

#!/usr/bin/env python3

"""Creates a new iTerm window to execute a command. Interrogates
Docker and SSH configurations to determine a reasonable default command.
Profiles can be configured for different environments.
"""

import re
import os
import sys
import json
import argparse
import subprocess
from json import JSONDecodeError

It also imports the iTerm2 API modules.

On the other hand, if you import this script as a module, then don’t try to run it. Not that it’s designed to be used as a module, mind you.

if __name__ == "__main__":
    main()

That’s the script.

This isn’t a very sophisticated literate program, but it does demonstrate how blocks can be reordered to aid exposition. (In this particular case, I think I’ve done it a little bit carelessly in places.)

Load this .org file up in Emacs:

#+TITLE: A more literate version of “t”
#+URI: /2019/11/29/t2
#+SUBJECT: Python
#+SUBJECT: OrgMode
#+REPLACES: /2019/11/29/t
#+OPTIONS: html-postamble:nil

My second attempt to use org-mode and tangle: much improved!

The previous version of this post demonstrated that I’d failed to
understand the significance of [[wiki:Noweb][noweb]] support in [[wiki:Org-mode][org-mode]]. Having read
that section of the manual, and learning that the “-i” option will get
tangle to respect the indentation of my blocks, I’ve been able to
improve things greatly. (Many thanks to the [[https://lists.gnu.org/archive/html/emacs-orgmode/2019-11/threads.html#00284][helpful respondents]].)

What follows is a light rewrite of the [[/2019/11/29/t][original post]].

I deal with servers all over the world: at home, at work, at my ISP,
at AWS, etc. Several times a day, I want to open up a shell window on
one of those remote systems.

Suppose I open up a root shell on my local machine, a remote shell at
work, and a remote shell on AWS. Now I want to delete some files, so I
type ~rm -rf …~. The jokes almost write themselves, don’t they?

I have a couple of large monitors and a high-resolution color display.
Rather than using ~ssh~ in my ordinary shell window, I can configure
different terminal windows, with different color schemes: matrix green
on black, that’s my root window; MarkLogic red background, that’s my
work window; an Amazon orange background, that’s my AWS window; etc.

When Docker enters the picture, it gets a little more complicated.
What you want to do in the Docker case isn’t ~ssh~ but ~docker exec~.
Except, to do that you have to know the ID of the container and those
change everytime you start a new one.

I quickly got tired of that, so I wrote ~dps~, a shell script to
simplify my interations with ~docker~. And then I realized that I
could integrate that with my “start a terminal” script and ~t~ was
born.

+ ~t~ → open a new terminal window on my local system.
+ ~t presentation~ → open a new terminal window on my local system
  with large fonts and high contrast.
+ ~t marklogic~ → open a terminal at work.
+ ~t aws~ → open a terminal at AWS.
+ ~t pihole~ → open a terminal on the RaspberryPi running
  [[wiki:Pi-hole]].
+ ~t openwrt~ → open a terminal on my router.
+ ~t builder~ → open a terminal on the Docker containter “builder”.

What’s going on here is that you (optionally) pass ~t~ the name of a
session that you’d like to start. The script will look up how to start
that session and then starts it. For good measure you can specify a
command to run instead of just logging in.

+ ~t -c "/share/ml-compile b9_0" builder~ → build the ~b9_0~ branch of
  MarkLogic server on the “builder” container

The ~t~ script was initially written in Perl and ran on both Linux and the Mac.
I recently convert it to Python in order to take advantage of the Python APIs
for [[wiki:ITerm2]], my prefered MacOS terminal.

* The iTerm2 API

I’m not doing anything very interested or sophisticated with
[[https://iterm2.com/python-api/][the iTerm2 API]]; I copied one of the examples and tinkered a bit.

** Call the API

In the main body of my script, I initialize the workspace and then
call ~run~ to execute the specified command with the named profile.

#+NAME: nsworkspace
#+BEGIN_SRC python -i
    # Launch the app with iTerm
    # pylint: disable=no-member
    AppKit.NSWorkspace.sharedWorkspace().launchApplication_("iTerm2")
    run(cmd, name)
#+END_SRC

I don’t really understand how ~AppKit~ works. It appears to be true,
as ~pylint~ warns me, that there is no member named ~NSWorkspace~, but
it does, in fact, work, so I tell ~pylint~ to leave me alone.

** Define the async callback

The ~run~ function defines the asynchronous function that’s part of
the iTerm2 API. The nested definition has a closure over the outer
function parameters so we don’t need the globals anymore!
([[https://so.nwalsh.com/2019/11/29/t#comment0001][Thanks]] Dave!)

The ~runiniterm~ function just implements part of the API; creating
a new window in which my command runs.

#+NAME: run_function
#+BEGIN_SRC python
def run(zshcommand, profile):
    """Execute the specified command in an iTerm2 window with
    the specified profile."""

    async def runiterm(connection):
        """Call the iTerm2 APIs."""
        app = await iterm2.async_get_app(connection)

        # Foreground the app
        await app.async_activate()

        if profile is not None:
            all_profiles = await iterm2.PartialProfile.async_query(connection)
            for prof in all_profiles:
                if prof.name == profile:
                    await prof.async_make_default()

        if zshcommand is None:
            zsh = "/usr/local/bin/zsh --login -i"
        else:
            zsh = '/usr/local/bin/zsh --login -i -c "%s"' % zshcommand
        await iterm2.Window.async_create(connection, command=zsh)

    iterm2.run_until_complete(runiterm, True)
#+END_SRC

** <<import-modules>>Import the relevant modules

In order to use these APIs, you must have imported them. I’ve done
that at the top of my script. The ~iterm2~ and ~AppKit~ modules
provide the ability to talk to ~iTerm2~.

#+NAME: iterm_imports
#+BEGIN_SRC python
import iterm2
import AppKit
#+END_SRC

* Defining sessions

There are (currently) four places where we can look to find the mapping
from a session name to  the underlying commands.

** Configured sessions

To start with, there’s a configuration file =~/etc/t-profiles.json=
for just this purpose:

#+BEGIN_SRC json :tangle no
{
  "default": {},
  "root": {
    "profile": "Root",
    "command": "sudo su -"
  },
  "openwrt": {
    "profile": "Root",
    "command": "ssh openwrt"
  },
  "aws": {
    "profile": "AWS",
    "command": "ssh aws"
  },
  "dunsinane": {
    "profile": "Dunsinane",
    "command": "ssh dunsinane"
  },
  "presentation": {
    "profile": "Presentation"
  }
}
#+END_SRC

Each key in the top-level map is a session name. The ~profile~
identifes the ~iTerm2~ profile to use and ~command~ specifies what
should be run in that session.

For example, the ~Root~ profile is [[wiki:The_Matrix][Matrix]] green-on-black, so running ~t root~
runs ~sudo su -~ in a new green-on-black terminal. The ~Presentation~
profile has a large font and high-contrast. It doesn’t specify a command so it’ll
just be a new [[wiki:Z_shell][zsh]] shell.

The ~check_profiles~ function looks in the configuration file.

#+NAME: check_profiles_function
#+BEGIN_SRC python
def check_profiles(term, command):
    """Search for the specified term in the global profiles."""
    name = None
    cmd = None

    try:
        with open("%s/etc/t-profiles.json" % HOME) as profile:
            profiles = json.load(profile)
    except FileNotFoundError:
        profiles = []
    except JSONDecodeError:
        profiles = []

    if term in profiles:
        if command is None and "command" in profiles[term]:
            cmd = profiles[term]["command"]
        if "profile" in profiles[term]:
            name = profiles[term]["profile"]

    return (name, cmd)
#+END_SRC

** Docker sessions

Another place we could look is among the running Docker containers.
The ~check_docker~ function looks there.

#+NAME: check_docker_function
#+BEGIN_SRC python
def check_docker(term, command):
    """Search for the specified term in the running Docker containers.
    This function relies on 'dps' a wrapper script that simplifies
    interaction with 'docker ps' and friends"""
    name = None
    cmd = None

    # Maybe a running container?
    proc = subprocess.run(args=["dps", "id", term], check=False, capture_output=True)
    if proc.returncode != 0:
        print("Ambigous container id: %s" % term, file=sys.stderr)
        sys.exit(1)
    cid = proc.stdout.decode("utf-8").strip()
    if cid != "":
        name = "Docker"
        if command is None:
            cmd = "dps login %s" % cid
        else:
            cmd = "dps exec %s %s" % (cid, command)

    return (name, cmd)
#+END_SRC

My ~dps~ script (not shown, but which I might feel inspired to write
in Python and turn into a module this script can use) will search for
containers by container name and image name. If we find a container
name that matches, then we construct an appropriate command.

** SSH sessions

Another place to look is in my =~/.ssh/config= file. You can use the
configuration to simplify SSH. For example, consider the following
fragment of my SSH config (fuzzed for privacy):

#+BEGIN_SRC
Host aws
  Hostname ec2-XX-XX-XX.us-XXX.compute.amazonaws.com
  User ec2-user
  IdentityFile ~/.ssh/AWS-EC2.pem
#+END_SRC

If I run ~ssh aws~, this configuration will connect to the correct
host, as the correct user, with the correct key. I no longer have to
type out the long form.

The ~check_ssh~ function looks for a matching SSH configuration.

#+NAME: check_ssh_function
#+BEGIN_SRC python
def check_ssh(term, command):
    """Search for the specified term in my ssh configuration."""
    name = None
    cmd = None

    # Maybe ssh?
    hostre = re.compile(r"^Host\s+%s" % term)
    with open("%s/.ssh/config" % HOME, "r") as config:
        for line in config:
            if hostre.match(line):
                name = "SSH"
                cmd = "ssh %s" % term
                if command is not None:
                    cmd = cmd + " " + command

    return (name, cmd)
#+END_SRC

** Host sessions

Last, but not least, we can look in ~/etc/hosts~. This is possibly
unnecessary or redundant. I could just treat any unrecognized session
as the name of a host. But at the moment, I don’t, so I look up things like
my Raspberry Pis and my NAS in ~/etc/hosts~.

That’s what ~check_hosts~ does.

#+NAME: check_hosts_function
#+BEGIN_SRC python
def check_hosts(term, command):
    """Search for the specified term in /etc/hosts."""
    name = None
    cmd = None

    hostre = re.compile(r"^\d+\.\d+\.\d+\.\d+\s+%s" % term)
    with open("/etc/hosts", "r") as hosts:
        for line in hosts:
            if hostre.match(line):
                name = "SSH"
                cmd = "ssh %s" % term
                if command is not None:
                    cmd = cmd + " " + command

    return (name, cmd)
#+END_SRC

* Odds and ends

There’s a utility function to get command line arguments. Mostly
because [[wiki:Pylint]] barked at me for having a ~main~ function that was
too long.

#+NAME: parse_command_line_function_1
#+BEGIN_SRC python
def parse_command_line():
    """Get the terminal selection and optional command from the command line."""
    parser = argparse.ArgumentParser()
    parser.add_argument("--command", "-c", action="store", help="Command to run")
#+END_SRC

Note that we specify ~default~ as the default profile. That’ll come up later.

#+NAME: parse_command_line_function_2
#+BEGIN_SRC python -i
    parser.add_argument(
        "profile", action="store", nargs="?", default="default", help="Profile name"
    )
    args = vars(parser.parse_args())
    term = args["profile"]
    command = args["command"]
    return (term, command)
#+END_SRC

We always start new terminals in the home directory (for consistency).
Near the top of the script, we store that in the ~HOME~ (for
convenience).

#+NAME: def_home
#+BEGIN_SRC python
HOME = os.environ["HOME"]
#+END_SRC

Right before we call the iTerm2 API, we change to that directory.

#+NAME: chdir
#+BEGIN_SRC python -i
    os.chdir(HOME)
#+END_SRC

* Main

Finally, we get to the ~main~ function.

#+NAME: main_function
#+BEGIN_SRC python
def main():
    """Run command in an iTerm window with profile"""
#+END_SRC

The initial session name and (optional) command come from the command line.
Recall that the ~session~ will always have a value because it defaults to ~default~.

#+NAME: parse_cli
#+BEGIN_SRC python -i
    (session, command) = parse_command_line()
#+END_SRC

Then we look for a matching configured, Docker, SSH or host session.
First one wins.

#+NAME: find_session
#+BEGIN_SRC python -i
    (name, cmd) = check_profiles(session, command)
    if name is None:
        (name, cmd) = check_docker(session, command)
    if name is None:
        (name, cmd) = check_ssh(session, command)
    if name is None:
        (name, cmd) = check_hosts(session, command)
#+END_SRC

If we /didn’t/ find the session, that’s ok if it’s the special session ~default~.
Otherwise, raise an error.

#+NAME: default_session
#+BEGIN_SRC python -i
    if name is None:
        if session == "default":
            pass
        else:
            print("Error: no such profile: %s" % session, file=sys.stderr)
            sys.exit(1)
#+END_SRC

If we got this far, we’ve figured out what to do so we’ll call the iTerm2 API
as described above and start the shell.

* Boilerplate

This is a completely standard Python3 program; it begins with an appropriate “[[wiki:Shebang][shebang]]”,
a [[wiki:Docstring][docstring]], and some standard imports.

#+NAME: envpython
#+BEGIN_SRC python
#!/usr/bin/env python3

"""Creates a new iTerm window to execute a command. Interrogates
Docker and SSH configurations to determine a reasonable default command.
Profiles can be configured for different environments.
"""

import re
import os
import sys
import json
import argparse
import subprocess
from json import JSONDecodeError
#+END_SRC

It [[import-modules][also imports]] the iTerm2 API modules.

On the other hand, if you import /this/ script as a module, then don’t
try to run it. Not that it’s designed to be used as a module, mind
you.

#+NAME: __main__
#+BEGIN_SRC python
if __name__ == "__main__":
    main()
#+END_SRC

That’s the script.

This isn’t a very sophisticated literate program, but it does
demonstrate how blocks can be reordered to aid exposition. (In this
particular case, I think I’ve done it a little bit carelessly in
places.)

Load this ~.org~ file up in Emacs:

((Cheat at pubishing time. Insert the .org file here))

Then ~C-c~ ~C-v~ ~t~ to produce exactly this pylint-satisfied,
[[https://black.readthedocs.io/en/stable/][black]]-compatible [[wiki:Python_(programming_language)][Python]] program.

((Cheat at publishing time. Replace the tangle shell with the
generated Python script))

#+begin_src python :noweb yes :tangle yes
<<envpython>>
<<iterm_imports>>

<<def_home>>


<<run_function>>


<<check_profiles_function>>


<<check_docker_function>>


<<check_ssh_function>>


<<check_hosts_function>>


<<parse_command_line_function_1>>
<<parse_command_line_function_2>>


<<main_function>>

<<parse_cli>>

<<find_session>>

<<default_session>>

<<chdir>>

<<nsworkspace>>


<<__main__>>
#+end_src

Sweet.

Then C-c C-v t to produce exactly this pylint-satisfied, black-compatible Python program.

#!/usr/bin/env python3

"""Creates a new iTerm window to execute a command. Interrogates
Docker and SSH configurations to determine a reasonable default command.
Profiles can be configured for different environments.
"""

import re
import os
import sys
import json
import argparse
import subprocess
from json import JSONDecodeError
import iterm2
import AppKit

HOME = os.environ["HOME"]


def run(zshcommand, profile):
    """Execute the specified command in an iTerm2 window with
    the specified profile."""

    async def runiterm(connection):
        """Call the iTerm2 APIs."""
        app = await iterm2.async_get_app(connection)

        # Foreground the app
        await app.async_activate()

        if profile is not None:
            all_profiles = await iterm2.PartialProfile.async_query(connection)
            for prof in all_profiles:
                if prof.name == profile:
                    await prof.async_make_default()

        if zshcommand is None:
            zsh = "/usr/local/bin/zsh --login -i"
        else:
            zsh = '/usr/local/bin/zsh --login -i -c "%s"' % zshcommand
        await iterm2.Window.async_create(connection, command=zsh)

    iterm2.run_until_complete(runiterm, True)


def check_profiles(term, command):
    """Search for the specified term in the global profiles."""
    name = None
    cmd = None

    try:
        with open("%s/etc/t-profiles.json" % HOME) as profile:
            profiles = json.load(profile)
    except FileNotFoundError:
        profiles = []
    except JSONDecodeError:
        profiles = []

    if term in profiles:
        if command is None and "command" in profiles[term]:
            cmd = profiles[term]["command"]
        if "profile" in profiles[term]:
            name = profiles[term]["profile"]

    return (name, cmd)


def check_docker(term, command):
    """Search for the specified term in the running Docker containers.
    This function relies on 'dps' a wrapper script that simplifies
    interaction with 'docker ps' and friends"""
    name = None
    cmd = None

    # Maybe a running container?
    proc = subprocess.run(args=["dps", "id", term], check=False, capture_output=True)
    if proc.returncode != 0:
        print("Ambigous container id: %s" % term, file=sys.stderr)
        sys.exit(1)
    cid = proc.stdout.decode("utf-8").strip()
    if cid != "":
        name = "Docker"
        if command is None:
            cmd = "dps login %s" % cid
        else:
            cmd = "dps exec %s %s" % (cid, command)

    return (name, cmd)


def check_ssh(term, command):
    """Search for the specified term in my ssh configuration."""
    name = None
    cmd = None

    # Maybe ssh?
    hostre = re.compile(r"^Host\s+%s" % term)
    with open("%s/.ssh/config" % HOME, "r") as config:
        for line in config:
            if hostre.match(line):
                name = "SSH"
                cmd = "ssh %s" % term
                if command is not None:
                    cmd = cmd + " " + command

    return (name, cmd)


def check_hosts(term, command):
    """Search for the specified term in /etc/hosts."""
    name = None
    cmd = None

    hostre = re.compile(r"^\d+\.\d+\.\d+\.\d+\s+%s" % term)
    with open("/etc/hosts", "r") as hosts:
        for line in hosts:
            if hostre.match(line):
                name = "SSH"
                cmd = "ssh %s" % term
                if command is not None:
                    cmd = cmd + " " + command

    return (name, cmd)


def parse_command_line():
    """Get the terminal selection and optional command from the command line."""
    parser = argparse.ArgumentParser()
    parser.add_argument("--command", "-c", action="store", help="Command to run")
    parser.add_argument(
        "profile", action="store", nargs="?", default="default", help="Profile name"
    )
    args = vars(parser.parse_args())
    term = args["profile"]
    command = args["command"]
    return (term, command)


def main():
    """Run command in an iTerm window with profile"""

    (session, command) = parse_command_line()

    (name, cmd) = check_profiles(session, command)
    if name is None:
        (name, cmd) = check_docker(session, command)
    if name is None:
        (name, cmd) = check_ssh(session, command)
    if name is None:
        (name, cmd) = check_hosts(session, command)

    if name is None:
        if session == "default":
            pass
        else:
            print("Error: no such profile: %s" % session, file=sys.stderr)
            sys.exit(1)

    os.chdir(HOME)

    # Launch the app with iTerm
    # pylint: disable=no-member
    AppKit.NSWorkspace.sharedWorkspace().launchApplication_("iTerm2")
    run(cmd, name)


if __name__ == "__main__":
    main()

Sweet.

Web mentions

Please provide your name and email address. Your email address will not be displayed and I won’t spam you, I promise. Your name and a link to your web address, if you provide one, will be displayed.

Your name:

Your email:

Homepage:

Do you comprehend the words on this page? (Please demonstrate that you aren't a mindless, screen-scraping robot.)

What is seven plus six?  (e.g. six plus two is 8)

Enter your comment in the box below. You may style your comment with the CommonMark flavor of Markdown.

All comments are moderated. I don’t promise to preserve all of your formatting and I reserve the right to remove comments for any reason.