Skip to content

Commit

Permalink
Implement detection for shell run via interpreter
Browse files Browse the repository at this point in the history
Fix #26, close #27.

Co-Authored-By: granitosaurus <tinarg@protonmail.com>
  • Loading branch information
uranusjr and granitosaurus committed Feb 12, 2020
1 parent 295fc30 commit 38062ad
Showing 1 changed file with 46 additions and 14 deletions.
60 changes: 46 additions & 14 deletions src/shellingham/posix/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re

from .._core import SHELL_NAMES, ShellDetectionFailure
from . import proc, ps
Expand All @@ -19,20 +20,16 @@ def _get_process_mapping():
raise ShellDetectionFailure('compatible proc fs or ps utility is required')


def _iter_process_command(mapping, pid, max_depth):
"""Iterator to traverse up the tree, yielding `argv[0]` of each process.
def _iter_process_args(mapping, pid, max_depth):
"""Iterator to traverse up the tree, yielding each process's argument list.
"""
for _ in range(max_depth):
try:
proc = mapping[pid]
except KeyError: # We've reached the root process. Give up.
break
try:
cmd = proc.args[0]
except IndexError: # Process has no name? Whatever, ignore it.
pass
else:
yield cmd
if proc.args: # Persumably the process should always have a name?
yield proc.args
pid = proc.ppid # Go up one level.


Expand All @@ -47,15 +44,50 @@ def _get_login_shell(proc_cmd):
return (os.path.basename(proc_cmd).lower(), proc_cmd)


_INTERPRETER_SHELL_NAMES = [
(re.compile(r'^python(\d+(\.\d+)?)?$'), {'xonsh'}),
]


def _get_interpreter_shell(proc_name, proc_args):
"""Get shell invoked via an interpreter.
Some shells are implemented on, and invoked with an interpreter, e.g. xonsh
is commonly executed with an executable Python script. This detects what
script the interpreter is actually running, and check whether that looks
like a shell.
See sarugaku/shellingham#26 for rational.
"""
for pattern, shell_names in _INTERPRETER_SHELL_NAMES:
if not pattern.match(proc_name):
continue
for arg in proc_args:
name = os.path.basename(arg).lower()
if os.path.isfile(arg) and name in shell_names:
return (name, arg)
return None


def _get_shell(cmd, *args):
if cmd.startswith('-'): # Login shell! Let's use this.
return _get_login_shell(cmd)
name = os.path.basename(cmd).lower()
if name in SHELL_NAMES: # Command looks like a shell.
return (name, cmd)
shell = _get_interpreter_shell(name, args)
if shell:
return shell
return None


def get_shell(pid=None, max_depth=6):
"""Get the shell that the supplied pid or os.getpid() is running in.
"""
pid = str(pid or os.getpid())
mapping = _get_process_mapping()
for proc_cmd in _iter_process_command(mapping, pid, max_depth):
if proc_cmd.startswith('-'): # Login shell! Let's use this.
return _get_login_shell(proc_cmd)
name = os.path.basename(proc_cmd).lower()
if name in SHELL_NAMES: # The inner-most (non-login) shell.
return (name, proc_cmd)
for proc_args in _iter_process_args(mapping, pid, max_depth):
shell = _get_shell(*proc_args)
if shell:
return shell
return None

0 comments on commit 38062ad

Please sign in to comment.