From f0af465bc83202580f12e585824587c892dcd2b3 Mon Sep 17 00:00:00 2001 From: Peter Heatwole Date: Fri, 29 Jan 2021 11:46:41 -0800 Subject: [PATCH 1/5] 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 --- README.rst | 46 +++++++++------ bin/pyenv-users | 147 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 144 insertions(+), 49 deletions(-) diff --git a/README.rst b/README.rst index b2b3c24..525dd1c 100644 --- a/README.rst +++ b/README.rst @@ -14,11 +14,11 @@ virtual environments, regardless of how they were created (``python -m venv Prerequisites ------------- -Requires ``find`` from GNU ``findutils`` to collect a list of candidates. +* BASH >= 4.3 (for ``nameref`` parameters) -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 ``:``. +* GNU coreutils (for ``readlink`` and ``realpath``) + +* GNU findutils (for ``find``) Installation @@ -52,20 +52,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 + + $ 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'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. +For example, to get a list of all versions linked to a virtual environment: -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``. +.. 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``. diff --git a/bin/pyenv-users b/bin/pyenv-users index 8577096..aea13c9 100755 --- a/bin/pyenv-users +++ b/bin/pyenv-users @@ -2,49 +2,130 @@ # # 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 ":" # # Scans [directory] for virtual environments whose `python` commands # are symlinks back into a pyenv version. Default: current directory. -if [ -n "$1" ]; then +set -e + +function collect_links () { + # Collect all symlinks named python that point into $PYENV_ROOT DIR="$1" -else + local -n _links=$2 + cmd="readlink -f '{}' | grep -q ${PYENV_ROOT}" + 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) +} + +function collect_pairs () { + # Turn each link into a (version, venv) string pair + local -n _links=$1 _versions=$2 _venvs=$3 + + # Regex to extract the pyenv version from the target string. The + # second group consumes the actual binary (`python`, `pypy3`, etc) + 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 ]] + version="${BASH_REMATCH[1]}" + # Only capture links outside PYENV_ROOT or inside pyenv-virtualenv venvs + if grep -v -q "$PYENV_ROOT" <<< "$linkpath" || \ + grep -q "$PYENV_ROOT/versions/$version/envs" <<< "$linkpath" + then + _versions[i]="$version" + _venvs[i++]="${link%/bin/python}" + fi + done +} + +function print_pairs () { + # Print each (version, venv) pair + local -n _versions=$1 _venvs=$2 + local -i K=${#_versions[@]} width=0 maxwidth=0 + + # Use the longest $version to setup the columns + 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 +} + +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 +} + +if [ -z "$PYENV_ROOT" ]; then + PYENV_ROOT=$(pyenv root) +fi + +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]}" fi -cmd="readlink -f '{}' | grep -q ${PYENV_ROOT}" -unset links 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) +# 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 + +collect_links "$DIR" links # Exit if no relevant venvs were found -if [[ $i -eq 0 ]]; then +if [ ${#links[@]} -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) -regex="${PYENV_ROOT}/versions/(.+)/bin/(.+)" - -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 ]] - version="${BASH_REMATCH[1]}" - # Only capture links outside PYENV_ROOT or inside pyenv-virtualenv venvs - if grep -v -q "$PYENV_ROOT" <<< "$linkpath" || \ - grep -q "$PYENV_ROOT/versions/$version/envs" <<< "$linkpath" - then - echo "$version":"${link%/bin/python}" - fi -done | sort | $output +collect_pairs links versions venvs +print_pairs versions venvs From 7042422b3c5250f14c8d86251189d9116540a512 Mon Sep 17 00:00:00 2001 From: Peter Heatwole Date: Fri, 29 Jan 2021 13:52:31 -0800 Subject: [PATCH 2/5] Only accept one positional argument --- bin/pyenv-users | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/pyenv-users b/bin/pyenv-users index aea13c9..d6212f9 100755 --- a/bin/pyenv-users +++ b/bin/pyenv-users @@ -113,6 +113,9 @@ if [[ "${#ARGUMENTS[@]}" == 0 ]]; then DIR="$PYENV_DIR" elif [[ "${#ARGUMENTS[@]}" == 1 ]]; then DIR="${ARGUMENTS[0]}" +else + pyenv help users + exit 1 fi # The `links` are the symlink pathnames, `versions` are pyenv version strings, From a2f4fc30ebd4e8d66a3d40a54745748e1456cc17 Mon Sep 17 00:00:00 2001 From: Peter Heatwole Date: Fri, 29 Jan 2021 13:53:15 -0800 Subject: [PATCH 3/5] Remove unnecessary check --- bin/pyenv-users | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bin/pyenv-users b/bin/pyenv-users index d6212f9..b4de998 100755 --- a/bin/pyenv-users +++ b/bin/pyenv-users @@ -124,11 +124,5 @@ fi declare -a links versions venvs collect_links "$DIR" links - -# Exit if no relevant venvs were found -if [ ${#links[@]} -eq 0 ]; then - exit 0 -fi - collect_pairs links versions venvs print_pairs versions venvs From ca173a51ed19df466451a4b2393239ab2fae7df2 Mon Sep 17 00:00:00 2001 From: Peter Heatwole Date: Fri, 29 Jan 2021 13:53:37 -0800 Subject: [PATCH 4/5] Tidy --- README.rst | 10 +--------- bin/pyenv-users | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 525dd1c..011215f 100644 --- a/README.rst +++ b/README.rst @@ -24,17 +24,9 @@ Prerequisites 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 diff --git a/bin/pyenv-users b/bin/pyenv-users index b4de998..160ad57 100755 --- a/bin/pyenv-users +++ b/bin/pyenv-users @@ -122,7 +122,6 @@ fi # 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 - collect_links "$DIR" links collect_pairs links versions venvs print_pairs versions venvs From fb86cb02b74cf2361a2a052796f6b59c9cfffa77 Mon Sep 17 00:00:00 2001 From: Peter Heatwole Date: Fri, 29 Jan 2021 22:05:39 -0800 Subject: [PATCH 5/5] Simplify by eliminating unnecessary functions The functions were only called once and only served to make the script more difficult to read. Relying on IN/OUT parameters in particular just wasn't worth the readability cost. --- README.rst | 2 - bin/pyenv-users | 109 ++++++++++++++++++++---------------------------- 2 files changed, 46 insertions(+), 65 deletions(-) diff --git a/README.rst b/README.rst index 011215f..f72094b 100644 --- a/README.rst +++ b/README.rst @@ -14,8 +14,6 @@ virtual environments, regardless of how they were created (``python -m venv Prerequisites ------------- -* BASH >= 4.3 (for ``nameref`` parameters) - * GNU coreutils (for ``readlink`` and ``realpath``) * GNU findutils (for ``find``) diff --git a/bin/pyenv-users b/bin/pyenv-users index 160ad57..f4dfa90 100755 --- a/bin/pyenv-users +++ b/bin/pyenv-users @@ -11,62 +11,6 @@ set -e -function collect_links () { - # Collect all symlinks named python that point into $PYENV_ROOT - DIR="$1" - local -n _links=$2 - cmd="readlink -f '{}' | grep -q ${PYENV_ROOT}" - 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) -} - -function collect_pairs () { - # Turn each link into a (version, venv) string pair - local -n _links=$1 _versions=$2 _venvs=$3 - - # Regex to extract the pyenv version from the target string. The - # second group consumes the actual binary (`python`, `pypy3`, etc) - 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 ]] - version="${BASH_REMATCH[1]}" - # Only capture links outside PYENV_ROOT or inside pyenv-virtualenv venvs - if grep -v -q "$PYENV_ROOT" <<< "$linkpath" || \ - grep -q "$PYENV_ROOT/versions/$version/envs" <<< "$linkpath" - then - _versions[i]="$version" - _venvs[i++]="${link%/bin/python}" - fi - done -} - -function print_pairs () { - # Print each (version, venv) pair - local -n _versions=$1 _venvs=$2 - local -i K=${#_versions[@]} width=0 maxwidth=0 - - # Use the longest $version to setup the columns - 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 -} - parse_options() { # Parse the command line options. Taken from `pyenv-virtualenv` OPTIONS=() @@ -91,10 +35,6 @@ parse_options() { done } -if [ -z "$PYENV_ROOT" ]; then - PYENV_ROOT=$(pyenv root) -fi - unset RAW parse_options "$@" for option in "${OPTIONS[@]}"; do @@ -114,14 +54,57 @@ if [[ "${#ARGUMENTS[@]}" == 0 ]]; then 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 -collect_links "$DIR" links -collect_pairs links versions venvs -print_pairs 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 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) + +# Turn each link into a (version, venv) string pair +regex="${PYENV_ROOT}/versions/(.+)/bin/(.+)" +unset i +for link in "${links[@]}"; do + linkpath=$(realpath -s "$link") + target=$(readlink -f "$link") + [[ "$target" =~ $regex ]] + version="${BASH_REMATCH[1]}" + # Only capture links outside PYENV_ROOT or inside pyenv-virtualenv venvs + if grep -v -q "$PYENV_ROOT" <<< "$linkpath" || \ + grep -q "$PYENV_ROOT/versions/$version/envs" <<< "$linkpath" + then + 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