Skip to content

Commit

Permalink
Merge pull request #1 from pfheatwole/work
Browse files Browse the repository at this point in the history
Eliminate dependency on `column` and add raw output
  • Loading branch information
pfheatwole authored Jan 30, 2021
2 parents 6e38e32 + fb86cb0 commit c005853
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 48 deletions.
54 changes: 29 additions & 25 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,17 @@ virtual environments, regardless of how they were created (``python -m venv
Prerequisites
-------------

Requires ``find`` from GNU ``findutils`` to collect a list of candidates.
* GNU coreutils (for ``readlink`` and ``realpath``)

Uses ``column`` from the ``util-linux`` package (if available) for
pretty-printing the output. If ``column`` is not available, the output is the
simple form ``<version>:<venv path>``.
* GNU findutils (for ``find``)


Installation
------------

Verify that ``$PYENV_ROOT`` has been configured:

.. code-block:: bash
$ echo $PYENV_ROOT
Install the plugin:

.. code-block:: bash
$ git clone https://github.com/pyenv/pyenv-users.git "$PYENV_ROOT"/plugins/pyenv-users
$ git clone https://github.com/pyenv/pyenv-users.git "$(pyenv root)/plugins/pyenv-users"
Usage
Expand All @@ -52,20 +42,34 @@ search your home directory:
3.8.6 /home/peter/.cache/pypoetry/virtualenvs/my_project-KM_3YcvM-py3.8
pypy3.6-7.3.1 /home/peter/work/venvs/example1
For scripting, use the ``--raw`` option to output a list of ``:`` separated
items:

Disclaimer
----------
.. code-block:: bash
I'm not a script writer so it's probably a bit crude. It's not blazingly fast,
since it uses a brute force scan, but on my system it only takes 3 seconds,
and I like the simplicity.
$ pyenv users --raw ~
3.7.9:/home/peter/.cache/pypoetry/virtualenvs/my_project-KM_3YcvM-py3.7
3.7.9:/home/peter/work/venvs/long name with spaces
3.8.6:/home/peter/.cache/pypoetry/virtualenvs/my_project-KM_3YcvM-py3.8
pypy3.6-7.3.1:/home/peter/work/venvs/example1
I've been using it on Fedora 33 with no issues, but it could use more testing.
In particular, users on MacOS and Windows will probably prefer the addition of
a pretty-printing solution that doesn't rely on ``column``.
For example, to get a list of all versions linked to a virtual environment:

.. code-block:: bash
$ pyenv users --raw ~ | cut -d: -f1 | uniq
3.7.9
3.8.6
pypy3.6-7.3.1
Disclaimer
----------

Also, I seem to recall that not all versions of ``column`` support the ``-s``
parameter for setting the separator. I haven't had time to research that.
The plugin doesn't maintain a list of environments; it generates the list each
run by performing a brute force scan of the directory for symlinks named
`python`. This does mean it's not blazingly fast, but its simplicity provides
state-free reliability, and in practice ``find`` only takes a few seconds.

Tested with ``bash v5.0.17(1)``, ``find v4.7.0``, and ``column v2.36.1`` on
Fedora 33.
Also, it could really use more testing; so far I've been using it on Fedora 33
with no issues. Tested with ``bash v5.0.17(1)`` and ``find v4.7.0``.
106 changes: 83 additions & 23 deletions bin/pyenv-users
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,86 @@
#
# Summary: Find virtual environments that use pyenv-managed versions.
#
# Usage: pyenv users [directory]
# Usage: pyenv users [-r|--raw] [directory]
#
# -r/--raw Raw output strings as "<version>:<venv-path>"
#
# Scans [directory] for virtual environments whose `python` commands
# are symlinks back into a pyenv version. Default: current directory.

if [ -n "$1" ]; then
DIR="$1"
else
set -e

parse_options() {
# Parse the command line options. Taken from `pyenv-virtualenv`
OPTIONS=()
ARGUMENTS=()
local arg option index

for arg in "$@"; do
if [ "${arg:0:1}" = "-" ]; then
if [ "${arg:1:1}" = "-" ]; then
OPTIONS[${#OPTIONS[*]}]="${arg:2}"
else
index=1
while option="${arg:$index:1}"; do
[ -n "$option" ] || break
OPTIONS[${#OPTIONS[*]}]="$option"
index=$((index+1))
done
fi
else
ARGUMENTS[${#ARGUMENTS[*]}]="$arg"
fi
done
}

unset RAW
parse_options "$@"
for option in "${OPTIONS[@]}"; do
case "$option" in
"r" | "raw" )
RAW=true
;;
"h" | "help" )
pyenv help users
exit 0
;;
esac
done

if [[ "${#ARGUMENTS[@]}" == 0 ]]; then
DIR="$PYENV_DIR"
elif [[ "${#ARGUMENTS[@]}" == 1 ]]; then
DIR="${ARGUMENTS[0]}"
else
echo -e "\nToo many directory arguments.\n"
pyenv help users
exit 1
fi

# ----------------------------------------------------------------------------
# Finished parsing the arguments. Begin the actual functionality.

# The `links` are the symlink pathnames, `versions` are pyenv version strings,
# and `venvs` are venv pathnames. Using parallel arrays since arrays-of-arrays
# are a pain in bash. Keeping versions and venvs separate avoids needing awk.
declare -a links versions venvs

if [ -z "$PYENV_ROOT" ]; then
PYENV_ROOT=$(pyenv root)
fi

# Collect all symlinks named `python` that point into $PYENV_ROOT
cmd="readlink -f '{}' | grep -q ${PYENV_ROOT}"
unset links i
unset i
while IFS= read -r -d $'\0' file; do
links[i++]="$file"
done < <(find -H "$DIR" -name "python" -type l -exec sh -c "$cmd" \; -print0)

# Exit if no relevant venvs were found
if [[ $i -eq 0 ]]; then
exit 0
fi

# Use columnar output if convenient.
if [ -n "$(command -v column)" ]; then
output="column -t -s ':'"
else
output="cat"
fi

# Regex to extract the pyenv version from the target string. The
# second group consumes the actual binary (`python`, `pypy3`, etc)
# Turn each link into a (version, venv) string pair
regex="${PYENV_ROOT}/versions/(.+)/bin/(.+)"

unset i
for link in "${links[@]}"; do
# `$link` is the `python` symlink, and `$target` is its target.
linkpath=$(realpath -s "$link")
target=$(readlink -f "$link")
[[ "$target" =~ $regex ]]
Expand All @@ -45,6 +90,21 @@ for link in "${links[@]}"; do
if grep -v -q "$PYENV_ROOT" <<< "$linkpath" || \
grep -q "$PYENV_ROOT/versions/$version/envs" <<< "$linkpath"
then
echo "$version":"${link%/bin/python}"
versions[i]="$version"
venvs[i++]="${link%/bin/python}"
fi
done

# Print each (version, venv) pair
declare -i K=${#versions[@]} width=0 maxwidth=0
for (( k=0; k < K; k++ )); do
width=${#versions[k]}
if (( width > maxwidth )); then maxwidth=$width; fi
done
for (( k=0; k < K; k++ )); do
if [ -z "$RAW" ]; then
printf "%-*s %s\n" "$maxwidth" "${versions[$k]}" "${venvs[k]}"
else
echo "${versions[$k]}":"${venvs[$k]}"
fi
done | sort | $output
done | sort

0 comments on commit c005853

Please # to comment.