A more literate version of “t”
My second attempt to use org-mode and tangle: much improved!
This post replaces a previous post.
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 theb9_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.