Skip to content

Commit f0af465

Browse files
committed
Overhaul
* Eliminated the dependency on `column` from `util-linux` * Added the general framework for adding command line options * Add a `-r|--raw` option to make scripting easier
1 parent 6e38e32 commit f0af465

File tree

2 files changed

+144
-49
lines changed

2 files changed

+144
-49
lines changed

README.rst

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ virtual environments, regardless of how they were created (``python -m venv
1414
Prerequisites
1515
-------------
1616

17-
Requires ``find`` from GNU ``findutils`` to collect a list of candidates.
17+
* BASH >= 4.3 (for ``nameref`` parameters)
1818

19-
Uses ``column`` from the ``util-linux`` package (if available) for
20-
pretty-printing the output. If ``column`` is not available, the output is the
21-
simple form ``<version>:<venv path>``.
19+
* GNU coreutils (for ``readlink`` and ``realpath``)
20+
21+
* GNU findutils (for ``find``)
2222

2323

2424
Installation
@@ -52,20 +52,34 @@ search your home directory:
5252
3.8.6 /home/peter/.cache/pypoetry/virtualenvs/my_project-KM_3YcvM-py3.8
5353
pypy3.6-7.3.1 /home/peter/work/venvs/example1
5454
55+
For scripting, use the ``--raw`` option to output a list of ``:`` separated
56+
items:
5557

56-
Disclaimer
57-
----------
58+
.. code-block:: bash
59+
60+
$ pyenv users --raw ~
61+
3.7.9:/home/peter/.cache/pypoetry/virtualenvs/my_project-KM_3YcvM-py3.7
62+
3.7.9:/home/peter/work/venvs/long name with spaces
63+
3.8.6:/home/peter/.cache/pypoetry/virtualenvs/my_project-KM_3YcvM-py3.8
64+
pypy3.6-7.3.1:/home/peter/work/venvs/example1
5865
59-
I'm not a script writer so it's probably a bit crude. It's not blazingly fast,
60-
since it uses a brute force scan, but on my system it only takes 3 seconds,
61-
and I like the simplicity.
66+
For example, to get a list of all versions linked to a virtual environment:
6267

63-
I've been using it on Fedora 33 with no issues, but it could use more testing.
64-
In particular, users on MacOS and Windows will probably prefer the addition of
65-
a pretty-printing solution that doesn't rely on ``column``.
68+
.. code-block:: bash
69+
70+
$ pyenv users --raw ~ | cut -d: -f1 | uniq
71+
3.7.9
72+
3.8.6
73+
pypy3.6-7.3.1
74+
75+
76+
Disclaimer
77+
----------
6678

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

70-
Tested with ``bash v5.0.17(1)``, ``find v4.7.0``, and ``column v2.36.1`` on
71-
Fedora 33.
84+
Also, it could really use more testing; so far I've been using it on Fedora 33
85+
with no issues. Tested with ``bash v5.0.17(1)`` and ``find v4.7.0``.

bin/pyenv-users

Lines changed: 114 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,130 @@
22
#
33
# Summary: Find virtual environments that use pyenv-managed versions.
44
#
5-
# Usage: pyenv users [directory]
5+
# Usage: pyenv users [-r|--raw] [directory]
6+
#
7+
# -r/--raw Raw output strings as "<version>:<venv-path>"
68
#
79
# Scans [directory] for virtual environments whose `python` commands
810
# are symlinks back into a pyenv version. Default: current directory.
911

10-
if [ -n "$1" ]; then
12+
set -e
13+
14+
function collect_links () {
15+
# Collect all symlinks named python that point into $PYENV_ROOT
1116
DIR="$1"
12-
else
17+
local -n _links=$2
18+
cmd="readlink -f '{}' | grep -q ${PYENV_ROOT}"
19+
unset i
20+
while IFS= read -r -d $'\0' file; do
21+
_links[i++]="$file"
22+
done < <(find -H "$DIR" -name "python" -type l -exec sh -c "$cmd" \; -print0)
23+
}
24+
25+
function collect_pairs () {
26+
# Turn each link into a (version, venv) string pair
27+
local -n _links=$1 _versions=$2 _venvs=$3
28+
29+
# Regex to extract the pyenv version from the target string. The
30+
# second group consumes the actual binary (`python`, `pypy3`, etc)
31+
regex="${PYENV_ROOT}/versions/(.+)/bin/(.+)"
32+
33+
unset i
34+
for link in "${_links[@]}"; do
35+
# `$link` is the `python` symlink, and `$target` is its target.
36+
linkpath=$(realpath -s "$link")
37+
target=$(readlink -f "$link")
38+
[[ "$target" =~ $regex ]]
39+
version="${BASH_REMATCH[1]}"
40+
# Only capture links outside PYENV_ROOT or inside pyenv-virtualenv venvs
41+
if grep -v -q "$PYENV_ROOT" <<< "$linkpath" || \
42+
grep -q "$PYENV_ROOT/versions/$version/envs" <<< "$linkpath"
43+
then
44+
_versions[i]="$version"
45+
_venvs[i++]="${link%/bin/python}"
46+
fi
47+
done
48+
}
49+
50+
function print_pairs () {
51+
# Print each (version, venv) pair
52+
local -n _versions=$1 _venvs=$2
53+
local -i K=${#_versions[@]} width=0 maxwidth=0
54+
55+
# Use the longest $version to setup the columns
56+
for (( k=0; k < K; k++ )); do
57+
width=${#_versions[k]}
58+
if (( width > maxwidth )); then maxwidth=$width; fi
59+
done
60+
61+
for (( k=0; k < K; k++ )); do
62+
if [ -z "$RAW" ]; then
63+
printf "%-*s %s\n" "$maxwidth" "${_versions[$k]}" "${_venvs[k]}"
64+
else
65+
echo "${_versions[$k]}":"${_venvs[$k]}"
66+
fi
67+
done | sort
68+
}
69+
70+
parse_options() {
71+
# Parse the command line options. Taken from `pyenv-virtualenv`
72+
OPTIONS=()
73+
ARGUMENTS=()
74+
local arg option index
75+
76+
for arg in "$@"; do
77+
if [ "${arg:0:1}" = "-" ]; then
78+
if [ "${arg:1:1}" = "-" ]; then
79+
OPTIONS[${#OPTIONS[*]}]="${arg:2}"
80+
else
81+
index=1
82+
while option="${arg:$index:1}"; do
83+
[ -n "$option" ] || break
84+
OPTIONS[${#OPTIONS[*]}]="$option"
85+
index=$((index+1))
86+
done
87+
fi
88+
else
89+
ARGUMENTS[${#ARGUMENTS[*]}]="$arg"
90+
fi
91+
done
92+
}
93+
94+
if [ -z "$PYENV_ROOT" ]; then
95+
PYENV_ROOT=$(pyenv root)
96+
fi
97+
98+
unset RAW
99+
parse_options "$@"
100+
for option in "${OPTIONS[@]}"; do
101+
case "$option" in
102+
"r" | "raw" )
103+
RAW=true
104+
;;
105+
"h" | "help" )
106+
pyenv help users
107+
exit 0
108+
;;
109+
esac
110+
done
111+
112+
if [[ "${#ARGUMENTS[@]}" == 0 ]]; then
13113
DIR="$PYENV_DIR"
114+
elif [[ "${#ARGUMENTS[@]}" == 1 ]]; then
115+
DIR="${ARGUMENTS[0]}"
14116
fi
15117

16-
cmd="readlink -f '{}' | grep -q ${PYENV_ROOT}"
17-
unset links i
18-
while IFS= read -r -d $'\0' file; do
19-
links[i++]="$file"
20-
done < <(find -H "$DIR" -name "python" -type l -exec sh -c "$cmd" \; -print0)
118+
# The `links` are the symlink pathnames, `versions` are pyenv version strings,
119+
# and `venvs` are venv pathnames. Using parallel arrays since arrays-of-arrays
120+
# are a pain in bash. Keeping versions and venvs separate avoids needing awk.
121+
declare -a links versions venvs
122+
123+
collect_links "$DIR" links
21124

22125
# Exit if no relevant venvs were found
23-
if [[ $i -eq 0 ]]; then
126+
if [ ${#links[@]} -eq 0 ]; then
24127
exit 0
25128
fi
26129

27-
# Use columnar output if convenient.
28-
if [ -n "$(command -v column)" ]; then
29-
output="column -t -s ':'"
30-
else
31-
output="cat"
32-
fi
33-
34-
# Regex to extract the pyenv version from the target string. The
35-
# second group consumes the actual binary (`python`, `pypy3`, etc)
36-
regex="${PYENV_ROOT}/versions/(.+)/bin/(.+)"
37-
38-
for link in "${links[@]}"; do
39-
# `$link` is the `python` symlink, and `$target` is its target.
40-
linkpath=$(realpath -s "$link")
41-
target=$(readlink -f "$link")
42-
[[ "$target" =~ $regex ]]
43-
version="${BASH_REMATCH[1]}"
44-
# Only capture links outside PYENV_ROOT or inside pyenv-virtualenv venvs
45-
if grep -v -q "$PYENV_ROOT" <<< "$linkpath" || \
46-
grep -q "$PYENV_ROOT/versions/$version/envs" <<< "$linkpath"
47-
then
48-
echo "$version":"${link%/bin/python}"
49-
fi
50-
done | sort | $output
130+
collect_pairs links versions venvs
131+
print_pairs versions venvs

0 commit comments

Comments
 (0)