Skip to content

Commit d1a186d

Browse files
authored
Merge pull request #166 from cisagov/improvement/allow_setup-env_to_specify_python
Allow setup-env to specify Python version
2 parents 01c9e11 + a9c6ed8 commit d1a186d

File tree

2 files changed

+159
-52
lines changed

2 files changed

+159
-52
lines changed

CONTRIBUTING.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@ There are a few ways to do this, but we prefer to use
4646
create and manage a Python virtual environment specific to this
4747
project.
4848

49-
If you already have `pyenv` and `pyenv-virtualenv` configured you can
50-
take advantage of the `setup-env` tool in this repo to automate the
51-
entire environment configuration process.
49+
We recommend using the `setup-env` script located in this repository,
50+
as it automates the entire environment configuration process. The
51+
dependencies required to run this script are
52+
[GNU `getopt`](https://github.com/util-linux/util-linux/blob/master/misc-utils/getopt.1.adoc),
53+
[`pyenv`](https://github.com/pyenv/pyenv), and [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv).
54+
If these tools are already configured on your system, you can simply run the
55+
following command:
5256

5357
```console
5458
./setup-env
@@ -57,27 +61,34 @@ entire environment configuration process.
5761
Otherwise, follow the steps below to manually configure your
5862
environment.
5963

60-
#### Installing and using `pyenv` and `pyenv-virtualenv` ####
64+
#### Installing and using GNU `getopt`, `pyenv`, and `pyenv-virtualenv` ####
6165

62-
On the Mac, we recommend installing [brew](https://brew.sh/). Then
63-
installation is as simple as `brew install pyenv pyenv-virtualenv` and
66+
On macOS, we recommend installing [brew](https://brew.sh/). Then
67+
installation is as simple as `brew install gnu-getopt pyenv pyenv-virtualenv` and
6468
adding this to your profile:
6569

6670
```bash
71+
# GNU getopt must be explicitly added to the path since it is
72+
# keg-only (https://docs.brew.sh/FAQ#what-does-keg-only-mean)
73+
export PATH="$(brew --prefix)/opt/gnu-getopt/bin:$PATH"
74+
75+
# Setup pyenv
6776
export PYENV_ROOT="$HOME/.pyenv"
6877
export PATH="$PYENV_ROOT/bin:$PATH"
6978
eval "$(pyenv init --path)"
7079
eval "$(pyenv init -)"
7180
eval "$(pyenv virtualenv-init -)"
7281
```
7382

74-
For Linux, Windows Subsystem for Linux (WSL), or on the Mac (if you
83+
For Linux, Windows Subsystem for Linux (WSL), or macOS (if you
7584
don't want to use `brew`) you can use
7685
[pyenv/pyenv-installer](https://github.com/pyenv/pyenv-installer) to
7786
install the necessary tools. Before running this ensure that you have
7887
installed the prerequisites for your platform according to the
7988
[`pyenv` wiki
8089
page](https://github.com/pyenv/pyenv/wiki/common-build-problems).
90+
GNU `getopt` is included in most Linux distributions as part of the
91+
[`util-linux`](https://github.com/util-linux/util-linux) package.
8192

8293
On WSL you should treat your platform as whatever Linux distribution
8394
you've chosen to install.

setup-env

Lines changed: 141 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,68 +9,84 @@ USAGE=$(
99
Configure a development environment for this repository.
1010
1111
It does the following:
12+
- Allows the user to specify the Python version to use for the virtual environment.
13+
- Allows the user to specify a name for the virtual environment.
1214
- Verifies pyenv and pyenv-virtualenv are installed.
13-
- Creates a Python virtual environment.
15+
- Creates the Python virtual environment.
1416
- Configures the activation of the virtual enviroment for the repo directory.
1517
- Installs the requirements needed for development.
1618
- Installs git pre-commit hooks.
17-
- Configures git upstream remote "lineage" repositories.
19+
- Configures git remotes for upstream "lineage" repositories.
1820
1921
Usage:
20-
setup-env [options] [virt_env_name]
22+
setup-env [--venv-name venv_name] [--python-version python_version]
2123
setup-env (-h | --help)
2224
2325
Options:
24-
-f --force Delete virtual enviroment if it already exists.
25-
-h --help Show this message.
26-
-i --install-hooks Install hook environments for all environments in the
27-
pre-commit config file.
26+
-f | --force Delete virtual enviroment if it already exists.
27+
-h | --help Show this message.
28+
-i | --install-hooks Install hook environments for all environments in the
29+
pre-commit config file.
30+
-l | --list-versions List available Python versions and select one interactively.
31+
-v | --venv-name Specify the name of the virtual environment.
32+
-p | --python-version Specify the Python version for the virtual environment.
2833
2934
END_OF_LINE
3035
)
3136

37+
# Display pyenv's installed Python versions
38+
python_versions() {
39+
pyenv versions --bare --skip-aliases --skip-envs
40+
}
41+
3242
# Flag to force deletion and creation of virtual environment
3343
FORCE=0
3444

35-
# Positional parameters
36-
PARAMS=""
45+
# Initialize the other flags
46+
INSTALL_HOOKS=0
47+
LIST_VERSIONS=0
48+
PYTHON_VERSION=""
49+
VENV_NAME=""
3750

38-
# Parse command line arguments
39-
while (("$#")); do
40-
case "$1" in
41-
-f | --force)
42-
FORCE=1
43-
shift
44-
;;
45-
-h | --help)
46-
echo "${USAGE}"
47-
exit 0
48-
;;
49-
-i | --install-hooks)
50-
INSTALL_HOOKS=1
51-
shift
52-
;;
53-
-*) # unsupported flags
54-
echo "Error: Unsupported flag $1" >&2
55-
exit 1
56-
;;
57-
*) # preserve positional arguments
58-
PARAMS="$PARAMS $1"
59-
shift
60-
;;
61-
esac
62-
done
51+
# Define long options
52+
LONGOPTS="force,help,install-hooks,list-versions,python-version:,venv-name:"
53+
54+
# Define short options for getopt
55+
SHORTOPTS="fhilp:v:"
56+
57+
# Check for GNU getopt by matching a specific pattern ("getopt from util-linux")
58+
# in its version output. This approach presumes the output format remains stable.
59+
# Be aware that format changes could invalidate this check.
60+
if [[ $(getopt --version 2> /dev/null) != *"getopt from util-linux"* ]]; then
61+
cat << 'END_OF_LINE'
62+
63+
Please note, this script requires GNU getopt due to its enhanced
64+
functionality and compatibility with certain script features that
65+
are not supported by the POSIX getopt found in some systems, particularly
66+
those with a non-GNU version of getopt. This distinction is crucial
67+
as a system might have a non-GNU version of getopt installed by default,
68+
which could lead to unexpected behavior.
6369
64-
# set positional arguments in their proper place
65-
eval set -- "$PARAMS"
70+
On macOS, we recommend installing brew (https://brew.sh/). Then installation
71+
is as simple as `brew install gnu-getopt` and adding this to your
72+
profile:
73+
74+
export PATH="$(brew --prefix)/opt/gnu-getopt/bin:$PATH"
75+
76+
GNU getopt must be explicitly added to the PATH since it
77+
is keg-only (https://docs.brew.sh/FAQ#what-does-keg-only-mean).
78+
79+
END_OF_LINE
80+
exit 1
81+
fi
6682

6783
# Check to see if pyenv is installed
6884
if [ -z "$(command -v pyenv)" ] || { [ -z "$(command -v pyenv-virtualenv)" ] && [ ! -f "$(pyenv root)/plugins/pyenv-virtualenv/bin/pyenv-virtualenv" ]; }; then
6985
echo "pyenv and pyenv-virtualenv are required."
7086
if [[ "$OSTYPE" == "darwin"* ]]; then
7187
cat << 'END_OF_LINE'
7288
73-
On the Mac, we recommend installing brew, https://brew.sh/. Then installation
89+
On macOS, we recommend installing brew, https://brew.sh/. Then installation
7490
is as simple as `brew install pyenv pyenv-virtualenv` and adding this to your
7591
profile:
7692
@@ -81,7 +97,7 @@ END_OF_LINE
8197

8298
fi
8399
cat << 'END_OF_LINE'
84-
For Linux, Windows Subsystem for Linux (WSL), or on the Mac (if you don't want
100+
For Linux, Windows Subsystem for Linux (WSL), or macOS (if you don't want
85101
to use "brew") you can use https://github.com/pyenv/pyenv-installer to install
86102
the necessary tools. Before running this ensure that you have installed the
87103
prerequisites for your platform according to the pyenv wiki page,
@@ -100,16 +116,88 @@ END_OF_LINE
100116
exit 1
101117
fi
102118

103-
set +o nounset
119+
# Use GNU getopt to parse options
120+
if ! PARSED=$(getopt --options $SHORTOPTS --longoptions $LONGOPTS --name "$0" -- "$@"); then
121+
echo "Error parsing options"
122+
exit 1
123+
fi
124+
eval set -- "$PARSED"
125+
126+
while true; do
127+
case "$1" in
128+
-f | --force)
129+
FORCE=1
130+
shift
131+
;;
132+
-h | --help)
133+
echo "$USAGE"
134+
exit 0
135+
;;
136+
-i | --install-hooks)
137+
INSTALL_HOOKS=1
138+
shift
139+
;;
140+
-l | --list-versions)
141+
LIST_VERSIONS=1
142+
shift
143+
;;
144+
-p | --python-version)
145+
PYTHON_VERSION="$2"
146+
shift 2
147+
# Check the Python versions being passed in.
148+
if [ -n "${PYTHON_VERSION+x}" ]; then
149+
if python_versions | grep -E "^${PYTHON_VERSION}$" > /dev/null; then
150+
echo Using Python version "$PYTHON_VERSION"
151+
else
152+
echo Error: Python version "$PYTHON_VERSION" is not installed.
153+
echo Installed Python versions are:
154+
python_versions
155+
exit 1
156+
fi
157+
fi
158+
;;
159+
-v | --venv-name)
160+
VENV_NAME="$2"
161+
shift 2
162+
;;
163+
--)
164+
shift
165+
break
166+
;;
167+
*)
168+
# Unreachable due to GNU getopt handling all options
169+
echo "Programming error"
170+
exit 64
171+
;;
172+
esac
173+
done
174+
104175
# Determine the virtual environment name
105-
if [ "$1" ]; then
176+
if [ -n "$VENV_NAME" ]; then
106177
# Use the user-provided environment name
107-
env_name=$1
178+
env_name="$VENV_NAME"
108179
else
109180
# Set the environment name to the last part of the working directory.
110181
env_name=${PWD##*/}
111182
fi
112-
set -o nounset
183+
184+
# List Python versions and select one interactively.
185+
if [ $LIST_VERSIONS -ne 0 ]; then
186+
echo Available Python versions:
187+
python_versions
188+
# Read the user's desired Python version.
189+
# -r: treat backslashes as literal, -p: display prompt before input.
190+
read -r -p "Enter the desired Python version: " PYTHON_VERSION
191+
# Check the Python versions being passed in.
192+
if [ -n "${PYTHON_VERSION+x}" ]; then
193+
if python_versions | grep -E "^${PYTHON_VERSION}$" > /dev/null; then
194+
echo Using Python version "$PYTHON_VERSION"
195+
else
196+
echo Error: Python version "$PYTHON_VERSION" is not installed.
197+
exit 1
198+
fi
199+
fi
200+
fi
113201

114202
# Remove any lingering local configuration.
115203
if [ $FORCE -ne 0 ]; then
@@ -118,7 +206,7 @@ if [ $FORCE -ne 0 ]; then
118206
elif [[ -f .python-version ]]; then
119207
cat << 'END_OF_LINE'
120208
An existing .python-version file was found. Either remove this file yourself
121-
or re-run with --force option to have it deleted along with the associated
209+
or re-run with the --force option to have it deleted along with the associated
122210
virtual environment.
123211
124212
rm .python-version
@@ -128,10 +216,18 @@ END_OF_LINE
128216
fi
129217

130218
# Create a new virtual environment for this project
131-
if ! pyenv virtualenv "${env_name}"; then
219+
#
220+
# If $PYTHON_VERSION is undefined then the current pyenv Python version will be used.
221+
#
222+
# We can't quote ${PYTHON_VERSION:=} below since if the variable is
223+
# undefined then we want nothing to appear; this is the reason for the
224+
# "shellcheck disable" line below.
225+
#
226+
# shellcheck disable=SC2086
227+
if ! pyenv virtualenv ${PYTHON_VERSION:=} "${env_name}"; then
132228
cat << END_OF_LINE
133229
An existing virtual environment named $env_name was found. Either delete this
134-
environment yourself or re-run with --force option to have it deleted.
230+
environment yourself or re-run with the --force option to have it deleted.
135231
136232
pyenv virtualenv-delete ${env_name}
137233

0 commit comments

Comments
 (0)