ofek / userpath

Cross-platform tool for adding locations to the user PATH, no elevated privileges required!
MIT License
152 stars 21 forks source link

userpath append errors on Alpine linux: "No such file or directory: 'bash'" #60

Open deronnax opened 3 months ago

deronnax commented 3 months ago

Easily reproducible with docker:

$ docker run -it --rm alpine:3.20 sh -c "apk add -v py3-pip && pip install --break-system-package userpath && userpath append /opt/brew"

[...]
Traceback (most recent call last):
  File "/usr/bin/userpath", line 8, in <module>
    sys.exit(userpath())
             ^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/userpath/cli.py", line 122, in append
    elif up.in_new_path(location, shells=shells, all_shells=all_shells, home=home):
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/userpath/core.py", line 17, in in_new_path
    return interface.location_in_new_path(location, check=check)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/userpath/interface.py", line 116, in location_in_new_path
    new_path = get_flat_output(show_path_command)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/site-packages/userpath/utils.py", line 33, in get_flat_output
    process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/subprocess.py", line 1026, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/lib/python3.11/subprocess.py", line 1953, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'bash'

I think it's a bug but I don't know. Anyway, it makes fail pipx ensurepath on Alpine linux.

matthewhughes934 commented 3 months ago

If you're looking for a workaround you can set the SHELL environment variable and userpath will pick that up (i.e. the added --env argument):

docker run --env 'SHELL=/bin/sh' -it --rm alpine:3.20 sh -c "apk add -v py3-pip && pip install --break-system-package userpath && userpath append /opt/brew"

this feels like a bug, the detect_shells function: https://github.com/ofek/userpath/blob/981085be7669815a186420e1211ed9944ab928ba/userpath/interface.py#L95 will fail to detect the shell when:

In this case it falls back to trying the default shells https://github.com/ofek/userpath/blob/981085be7669815a186420e1211ed9944ab928ba/userpath/shells.py#L4 which includes bash. Maybe these default should be reduced to just sh since that's defined in POSIX and is generally more likely to be available than bash

Another approach for unix systems might be to try pw_shell before falling back:

import os
import pwd

def shell_from_pwd():
    try:
        pw = pwd.getpwuid(os.getuid())
    except KeyError:
        return None
    else:
        return pw.pw_shell

Which should return /bin/sh on Alpine Linux