@@ -219,50 +219,115 @@ venv::delete() {
219219
220220venv::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
290403venv::lock () {
291404 if venv::_check_if_help_requested " $1 " ; then
0 commit comments