Skip to content

Commit 40f4a99

Browse files
committed
Refactor venv install to add options for installing packages (#3)
1 parent 30c501c commit 40f4a99

File tree

1 file changed

+144
-31
lines changed

1 file changed

+144
-31
lines changed

src/venv-cli/venv.sh

Lines changed: 144 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -219,50 +219,115 @@ venv::delete() {
219219

220220
venv::install() {
221221
if venv::_check_if_help_requested "$1"; then
222-
echo "venv install [<requirements file>] [--skip-lock|-s] [<install args>]"
222+
echo "venv install [requirement specifiers] [OPTIONS]"
223223
echo
224-
echo "Clear the environment, then install requirements from <requirements file>,"
225-
echo "like 'requirements.txt' or 'requirements.lock'."
226-
echo "Installed packages are then locked into the corresponding .lock-file,"
227-
echo "e.g. 'venv install requirements.txt' will lock packages into 'requirements.lock'."
228-
echo "This step is skipped if '--skip-lock' or '-s' is specified, or when installing"
229-
echo "directly from a .lock-file."
224+
echo "Clear the environment, then install requirements from a requirements file, like 'requirements.txt' or 'requirements.lock'."
225+
echo "Installed packages are then locked into the corresponding .lock-file, e.g. 'venv install -r requirements.txt'" will lock packages into
226+
echo "'requirements.lock'. This step is skipped if '--skip-lock' or '-s' is specified, or when installing directly from a .lock-file."
230227
echo
231-
echo "The <requirements file> must have file extension '.txt' or '.lock'."
232-
echo "If no arguments are passed, a default file name of 'requirements.txt'"
233-
echo "will be used."
228+
echo "Optionally, additional requirements can be passed as [requirement specifiers], e.g. 'numpy' or 'pandas >= 2.0'."
229+
echo "These will first be added to the requirements file, and then the full set of requirements will be installed from the requirements file."
234230
echo
235-
echo "Additional <install args> are passed on to 'pip install'."
231+
echo "The requirements file must have file extension '.txt' or '.lock'."
232+
echo "If no arguments are passed, a default file name of 'requirements.txt' will be used."
233+
echo
234+
echo "Options:"
235+
echo " -h, --help Show this help and exit."
236+
echo " -r, --requirement <requirements file> Install from the given requirements file. If requirement specifiers"
237+
echo " are passed, they will be added to the requirements file before installation."
238+
echo " If not specified, will default to using 'requirements.txt'."
239+
echo " -s, --skip-lock Skip locking packages to a .lock-file after installation."
240+
echo " --pip-args <ARGS> Additional arguments to pass through to pip install."
236241
echo
237242
echo "Examples:"
238243
echo "$ venv install"
244+
echo "$ venv install -r requirements.txt"
245+
echo "This will install requirements from 'requirements.txt' and lock them into 'requirements.lock'."
239246
echo
240-
echo "$ venv install dev-requirements.txt"
247+
echo "$ venv install numpy"
248+
echo "$ venv install numpy -r requirements.txt"
249+
echo "This will add 'numpy' to 'requirements.txt', then install all requirements from 'requirements.txt'."
241250
echo
242-
echo "$ venv install requirements/dev-all.txt --skip-lock|-s --no-cache"
251+
echo "$ venv install numpy 'pandas >= 2.0' -r requirements/dev-requirements.txt -s --pip-args='--no-cache --pre'"
252+
echo "This will add 'numpy' and 'pandas >= 2.0' to 'requirements/dev-requirements.txt', then install"
253+
echo "all requirements from 'dev-requirements.txt' without locking them."
254+
echo "The arguments '--no-cache' and '--pre' are passed on to 'pip install'."
243255
return "${_success}"
244256
fi
245257

246-
local requirements_file
247-
if [ -z "$1" ] || [ "$1" = "--skip-lock" ] || [ "$1" = "-s" ]; then
248-
# If no filename was passed
249-
requirements_file="requirements.txt"
258+
# Parse arguments. Fail if invalid arguments are passed
259+
local TEMP=$(getopt -o 'r:s' --long 'requirement:,skip-lock,pip-args::' -n 'venv install' -- "$@")
260+
local _exit="$?"
261+
if [ "${_exit}" -ne 0 ]; then
262+
return "${_exit}"
263+
fi
250264

251-
else
252-
if ! venv::_check_install_requirements_file "$1"; then
253-
# Fail if file name doesn't match required format
254-
return "${_fail}"
265+
local package_args=() # List of packages to install
266+
local requirements_file=""
267+
local skip_lock=false
268+
local pip_args=""
269+
270+
eval set -- "$TEMP" # Unpack the arguments in $TEMP into the positional parameters #1, #2, ...
271+
272+
# Parse arguments
273+
while true; do
274+
# -- marks the end of the options, and anything after it is treated as a positional argument.
275+
# If "$*" = "--", there are no optional parameters left, and we can break the loop
276+
if [ "$*" = "--" ]; then
277+
shift
278+
break
255279
fi
256280

257-
# If full requirements file (.txt or .lock) passed
258-
requirements_file="$1"
259-
shift
281+
case "$1" in
282+
"-r" | "--requirement")
283+
requirements_file="$2"
284+
shift 2
285+
;;
286+
"-s" | "--skip-lock")
287+
skip_lock=true
288+
shift
289+
;;
290+
"--pip-args")
291+
pip_args="$2"
292+
shift 2
293+
;;
294+
--)
295+
# -- marks the end of the options, and anything after it is treated as a positional argument.
296+
# For venv install, positional arguments are package specifiers
297+
shift
298+
package_args+=( "$@" )
299+
break
300+
;;
301+
*)
302+
if [ -z "$1" ]; then
303+
break
304+
fi
305+
;;
306+
esac
307+
done
308+
309+
# Check the specified requirements file
310+
if [ -z "${requirements_file}" ]; then
311+
requirements_file="requirements.txt"
312+
venv::color_echo "${_yellow}" "No requirements file specified, using requirements.txt"
313+
fi
314+
if ! venv::_check_install_requirements_file "${requirements_file}"; then
315+
# Fail if file name doesn't match required format
316+
return "${_fail}"
260317
fi
261318

262-
local skip_lock=false
263-
if [ "$1" = "--skip-lock" ] || [ "$1" = "-s" ]; then
264-
skip_lock=true
265-
shift
319+
# Add package specifiers to requirements file if they are not already there
320+
if [ "${#package_args[@]}" -gt 0 ]; then
321+
# Create the requirements file if it doesn't already exist, otherwise the next command will fail
322+
if [ ! -f "${requirements_file}" ]; then
323+
touch "${requirements_file}"
324+
fi
325+
326+
# Make a temporary backup of the requirements file, in case something fails
327+
venv::_create_backup_file "${requirements_file}"
328+
if ! venv::_add_packages_to_requirements "${requirements_file}" "${package_args[@]}"; then
329+
return "${_fail}"
330+
fi
266331
fi
267332

268333
# Clear the environment before running pip install to avoid orphaned packages
@@ -272,20 +337,68 @@ venv::install() {
272337
fi
273338

274339
venv::color_echo "${_green}" "Installing requirements from ${requirements_file}"
275-
if ! pip install --require-virtualenv --use-pep517 -r "${requirements_file}" "$@"; then
340+
# ${pip_args} is unquoted on purpose so it is not passed as a single string argument, but several arguments
341+
if ! pip install --require-virtualenv --use-pep517 -r "${requirements_file}" ${pip_args}; then
276342
return "${_fail}"
277343
fi
278344

345+
# Lock the installed packages into a .lock-file
279346
local lock_file="$(venv::_get_lock_from_requirements "${requirements_file}")"
280347
if "${skip_lock}" || [ "${requirements_file}" == "${lock_file}" ]; then
281348
venv::color_echo "${_yellow}" "Skipping locking packages to ${lock_file}"
282349
return "${_success}"
283350
fi
284-
285351
venv::lock "${lock_file}"
286-
return "$?" # Return exit status from venv::lock command
352+
353+
# Remove the backup file if everything went well
354+
venv::_remove_backup_file "${requirements_file}"
287355
}
288356

357+
venv::_add_packages_to_requirements() {
358+
local requirements_file="$1"
359+
local package_args=("${@:2}")
360+
361+
local package_spec
362+
for package_spec in "${package_args[@]}"; do
363+
# Use the sed command to remove everything after the package name in the 'package_spec' string
364+
local package_name="$(echo "${package_spec}" | sed -n 's|^\([a-zA-Z][a-zA-Z0-9_-]*\).*$|\1|p')"
365+
if [ -z "${package_name}" ]; then
366+
# Append the package spec directly to the requirements file if the package name could not be extracted
367+
venv::color_echo "${_yellow}" "Could not extract package name from '${package_spec}', adding directly to ${requirements_file}"
368+
echo "${package_spec}" >> "${requirements_file}"
369+
continue
370+
fi
371+
372+
# Look for the package name in the requirements file
373+
if command grep -q "^${package_name}" "${requirements_file}"; then
374+
# Replace package from requirements file if it's already there
375+
echo "Replacing existing ${package_name} requirement with '${package_spec}' in ${requirements_file}"
376+
sed -i "s|^${package_name}.*$|${package_spec}|g" "${requirements_file}"
377+
else
378+
# Add package to requirements file if it's not already there
379+
echo "Adding '${package_spec}' to ${requirements_file}"
380+
echo "${package_spec}" >> "${requirements_file}"
381+
fi
382+
done
383+
384+
# Sort requirements file after adding packages. LC_COLLATE is set to C to ensure lines beginning with '-'
385+
# are sorted first, instad of being ignored
386+
LC_COLLATE=C sort --ignore-case --stable -o "${requirements_file}" "${requirements_file}"
387+
}
388+
389+
venv::_create_backup_file() {
390+
local file="$1"
391+
if [ -f "${file}" ]; then
392+
cp "${file}" "${file}.bak"
393+
fi
394+
}
395+
396+
venv::_remove_backup_file() {
397+
local backup_file="$1"
398+
if [ -f "${backup_file}.bak" ]; then
399+
rm "${backup_file}.bak"
400+
fi
401+
}
289402

290403
venv::lock() {
291404
if venv::_check_if_help_requested "$1"; then

0 commit comments

Comments
 (0)