Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shell detection #6

Merged
merged 19 commits into from
Jul 13, 2019
5 changes: 5 additions & 0 deletions .appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
skip_branch_with_pr: true
branches:
only:
- master

environment:
matrix:
- PYTHON: "C:\\Python27-x64"
Expand Down
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
language: python

branches:
only:
- master

matrix:
include:
- python: 2.7
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ deps =
pytest
commands =
python setup.py --quiet clean develop
pytest
pytest -v
5 changes: 2 additions & 3 deletions userpath/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .core import (
prepend, append, in_current_path, in_new_path, need_shell_restart, normpath
)
from .core import append, in_new_path, need_shell_restart, prepend
from .utils import in_current_path

__version__ = '1.1.0'
102 changes: 72 additions & 30 deletions userpath/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
import click

import userpath as up
from userpath.shells import DEFAULT_SHELLS, SHELLS


CONTEXT_SETTINGS = {
'help_option_names': ['-h', '--help'],
}
CONTEXT_SETTINGS = {'help_option_names': ['-h', '--help']}


def echo_success(text, nl=True):
Expand All @@ -28,12 +27,28 @@ def userpath():
pass


@userpath.command(context_settings=CONTEXT_SETTINGS,
short_help='Prepends to the user PATH')
@userpath.command(context_settings=CONTEXT_SETTINGS, short_help='Prepends to the user PATH')
@click.argument('locations', required=True, nargs=-1)
@click.option('-f', '--force', is_flag=True,
help='Update PATH even if it appears to be correct.')
def prepend(locations, force):
@click.option(
'-s',
'--shell',
'shells',
multiple=True,
type=click.Choice(sorted(SHELLS)),
help=(
'The shell in which PATH will be modified. This can be selected multiple times and has no '
'effect on Windows. The default shells are: {}'.format(', '.join(sorted(DEFAULT_SHELLS)))
),
)
@click.option(
'-a',
'--all-shells',
is_flag=True,
help='Update PATH of all supported shells. This has no effect on Windows as that is the standard behavior.',
)
@click.option('--home', help='Explicitly set the home directory.')
@click.option('-f', '--force', is_flag=True, help='Update PATH even if it appears to be correct.')
def prepend(locations, shells, all_shells, home, force):
"""Prepends to the user PATH. The shell must be restarted for the update to
take effect.
"""
Expand All @@ -46,27 +61,43 @@ def prepend(locations, force):
'the -f/--force flag.'.format(location)
))
sys.exit(2)
elif up.in_new_path(location):
elif up.in_new_path(location, shells=shells, all_shells=all_shells, home=home):
echo_warning((
'The directory `{}` is already in PATH, pending a shell '
'restart! If you are sure you want to proceed, try again '
'with the -f/--force flag.'.format(location)
))
sys.exit(2)

if up.prepend(locations):
if up.prepend(locations, shells=shells, all_shells=all_shells, home=home):
echo_success('Success!')
else:
echo_failure('An unexpected failure seems to have occurred.')
sys.exit(1)


@userpath.command(context_settings=CONTEXT_SETTINGS,
short_help='Appends to the user PATH')
@userpath.command(context_settings=CONTEXT_SETTINGS, short_help='Appends to the user PATH')
@click.argument('locations', required=True, nargs=-1)
@click.option('-f', '--force', is_flag=True,
help='Update PATH even if it appears to be correct.')
def append(locations, force):
@click.option(
'-s',
'--shell',
'shells',
multiple=True,
type=click.Choice(sorted(SHELLS)),
help=(
'The shell in which PATH will be modified. This can be selected multiple times and has no '
'effect on Windows. The default shells are: {}'.format(', '.join(sorted(DEFAULT_SHELLS)))
),
)
@click.option(
'-a',
'--all-shells',
is_flag=True,
help='Update PATH of all supported shells. This has no effect on Windows as that is the standard behavior.',
)
@click.option('--home', help='Explicitly set the home directory.')
@click.option('-f', '--force', is_flag=True, help='Update PATH even if it appears to be correct.')
def append(locations, shells, all_shells, home, force):
"""Appends to the user PATH. The shell must be restarted for the update to
take effect.
"""
Expand All @@ -79,38 +110,49 @@ def append(locations, force):
'the -f/--force flag.'.format(location)
))
sys.exit(2)
elif up.in_new_path(location):
elif up.in_new_path(location, shells=shells, all_shells=all_shells, home=home):
echo_warning((
'The directory `{}` is already in PATH, pending a shell '
'restart! If you are sure you want to proceed, try again '
'with the -f/--force flag.'.format(location)
))
sys.exit(2)

if up.append(locations):
if up.append(locations, shells=shells, all_shells=all_shells, home=home):
echo_success('Success!')
else:
echo_failure('An unexpected failure seems to have occurred.')
sys.exit(1)


@userpath.command(context_settings=CONTEXT_SETTINGS,
short_help='Checks if locations are in the user PATH')
@userpath.command(context_settings=CONTEXT_SETTINGS, short_help='Checks if locations are in the user PATH')
@click.argument('locations', required=True, nargs=-1)
def verify(locations):
@click.option(
'-s',
'--shell',
'shells',
multiple=True,
type=click.Choice(sorted(SHELLS)),
help=(
'The shell in which PATH will be modified. This can be selected multiple times and has no '
'effect on Windows. The default shells are: {}'.format(', '.join(sorted(DEFAULT_SHELLS)))
),
)
@click.option(
'-a',
'--all-shells',
is_flag=True,
help='Update PATH of all supported shells. This has no effect on Windows as that is the standard behavior.',
)
@click.option('--home', help='Explicitly set the home directory.')
def verify(locations, shells, all_shells, home):
"""Checks if locations are in the user PATH."""
for location in locations:
if up.in_current_path(location):
echo_success((
'The directory `{}` is in PATH!'.format(location)
))
elif up.in_new_path(location):
echo_warning((
'The directory `{}` is in PATH, pending a shell restart!'.format(location)
))
echo_success('The directory `{}` is in PATH!'.format(location))
elif up.in_new_path(location, shells=shells, all_shells=all_shells, home=home):
echo_warning('The directory `{}` is in PATH, pending a shell restart!'.format(location))
sys.exit(2)
else:
echo_failure((
'The directory `{}` is not in PATH!'.format(location)
))
echo_failure('The directory `{}` is not in PATH!'.format(location))
sys.exit(1)
140 changes: 14 additions & 126 deletions userpath/core.py
Original file line number Diff line number Diff line change
@@ -1,134 +1,22 @@
import os
import platform
import subprocess
from .interface import Interface
from .utils import in_current_path

ON_WINDOWS = os.name == 'nt' or platform.system() == 'Windows'

def prepend(location, app_name=None, shells=None, all_shells=False, home=None):
interface = Interface(shells=shells, all_shells=all_shells, home=home)
return interface.put(location, front=True, app_name=app_name)

def normpath(location):
return os.path.abspath(os.path.expanduser(location.strip(';:')))

def append(location, app_name=None, shells=None, all_shells=False, home=None):
interface = Interface(shells=shells, all_shells=all_shells, home=home)
return interface.put(location, front=False, app_name=app_name)

def location_in_path(location, path):
return normpath(location) in (
os.path.normpath(p) for p in path.split(os.pathsep)
)

def in_new_path(location, shells=None, all_shells=False, home=None):
interface = Interface(shells=shells, all_shells=all_shells, home=home)
return interface.location_in_new_path(location)

if ON_WINDOWS:
def get_new_path():
output = subprocess.check_output([
'powershell', '-Command', "& {[Environment]::GetEnvironmentVariable('PATH', 'User')}"
], shell=True).decode().strip()

# We do this because the output may contain new lines.
return ''.join(output.splitlines())

def put(location, front=True, app_name=None):
location = normpath(location)

# PowerShell will always be available on Windows 7 or later.
try:
old_path = get_new_path()
head, tail = (location, old_path) if front else (old_path, location)
new_path = '{}{}{}'.format(head, os.pathsep, tail)

subprocess.check_call([
'powershell',
'-Command',
"& {{[Environment]::SetEnvironmentVariable('PATH', '{}', 'User')}}".format(new_path)
], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError:
try:
head, tail = (location, '%~a') if front else ('%~a', location)
new_path = '{}{}{}'.format(head, os.pathsep, tail)

# https://superuser.com/a/601034/766960
subprocess.check_call((
'for /f "skip=2 tokens=3*" %a in (\'reg query HKCU\Environment '
'/v PATH\') do @if [%b]==[] ( @setx PATH "{new_path}" ) else '
'( @setx PATH "{new_path} %~b" )'.format(new_path=new_path)
), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError:
return False

new_path = get_new_path()
return all(location_in_path(l, new_path) for l in location.split(os.pathsep))

else:
from datetime import datetime

INIT_FILES = {
os.path.expanduser('~/.profile'): 'PATH="{}"\n',
os.path.expanduser('~/.bashrc'): 'export PATH="{}"\n',
# macOS seems to need this.
os.path.expanduser('~/.bash_profile'): 'export PATH="{}"\n',
}

def get_new_path():
return subprocess.check_output(['bash', '--login', '-c', 'echo $PATH']).decode().strip()

def put(location, front=True, app_name=None):
# This function is probably insufficient even though it works in
# most situations. Please improve this to succeed more broadly!
location = normpath(location)

try:
head, tail = (location, '$PATH') if front else ('$PATH', location)
new_path = '{}{}{}'.format(head, os.pathsep, tail)

for file in INIT_FILES:
if os.path.exists(file):
with open(file, 'r') as f:
lines = f.readlines()
else:
lines = []

lines.extend([
'# Created by `{}` on {}\n'.format(
app_name or 'userpath', datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
),
INIT_FILES[file].format(new_path)
])
with open(file, 'w') as f:
f.writelines(lines)
except (OSError, PermissionError):
return False

new_path = get_new_path()
return all(location_in_path(l, new_path) for l in location.split(os.pathsep))


def prepend(location, app_name=None):
if isinstance(location, list) or isinstance(location, tuple):
location = os.pathsep.join(normpath(l) for l in location)
return put(location, front=True, app_name=app_name)


def append(location, app_name=None):
if isinstance(location, list) or isinstance(location, tuple):
location = os.pathsep.join(normpath(l) for l in location)
return put(location, front=False, app_name=app_name)


def in_current_path(location):
current_path = os.environ.get('PATH', '')
if isinstance(location, list) or isinstance(location, tuple):
return all(location_in_path(l, current_path) for l in location)
else:
return location_in_path(location, current_path)


def in_new_path(location):
new_path = get_new_path()
if isinstance(location, list) or isinstance(location, tuple):
return all(location_in_path(l, new_path) for l in location)
else:
return location_in_path(location, new_path)


def need_shell_restart(location):
if isinstance(location, list) or isinstance(location, tuple):
return any(not in_current_path(l) and in_new_path(l) for l in location)
else:
return not in_current_path(location) and in_new_path(location)
def need_shell_restart(location, shells=None, all_shells=False, home=None):
interface = Interface(shells=shells, all_shells=all_shells, home=home)
return not in_current_path(location) and interface.location_in_new_path(location)
Loading