Add fish_add_path, a simple way to add to $PATH

This is a function you can either execute once, interactively, or
stick in config.fish, and it will do the right thing.

Some options are included to choose some slightly different behavior,
like setting $PATH directly instead of $fish_user_paths, or moving
already existing components to the front/back instead of ignoring
them, or appending new components instead of prepending them.

The defaults were chosen because they are the most safe, and
especially because they allow it to be idempotent - running it again
and again and again won't change anything, it won't even run the
actual `set` because it skips that if all components are already in.

Fixes #6960.
This commit is contained in:
Fabian Homborg 2020-05-21 13:33:17 +02:00
parent ba116f1d3b
commit 9354dd6971
3 changed files with 207 additions and 0 deletions

View File

@ -0,0 +1,66 @@
.. _cmd-fish_add_path:
fish_add_path - add to the path
==============================================================
Synopsis
--------
::
fish_add_path [paths...]
fish_add_path (-h | --help)
fish_add_path [(-g | --global) | (-U | --universal) | (-P | --path)] [(-m | --move)] [(-a | --append) | (-p | --prepend)] [(-v | --verbose) | (-n | --dry-run)] [paths...]
Description
-----------
``fish_add_path`` is a simple way to add more components to fish's $PATH. It does this by adding the components either to $fish_user_paths or directly to $PATH (if the ``--path`` switch is given).
It is (by default) safe to use ``fish_add_path`` in config.fish, or it can be used once, interactively, and the paths will stay in future because of :ref:`universal variables <variables-universal>`. This is a "do what I mean" style command, if you need more control, consider modifying the variable yourself.
Components are normalized by :ref:`realpath <cmd-realpath>`. This means that trailing slashes are ignored and symlinks are resolved, and relative paths are made absolute. If a component already exists, it is not added again and stays in the same place unless the ``--move`` switch is given.
Components are added in the order they are given, and they are prepended to the path unless ``--append`` is given (if $fish_user_paths is used, that means they are last in $fish_user_paths, which is itself prepended to $PATH, so they still stay ahead of the system paths).
If no component is new, the variable ($fish_user_paths or $PATH) is not set again or otherwise modified, so variable handlers are not triggered.
If a component is not an existing directory, ``fish_add_path`` ignores it.
Options
-------
- ``-a`` or ``--append`` causes the components to be added to the *end* of the variable
- ``-p`` or ``--prepend`` causes the components to be added to the *front* of the variable (this is the default)
- ``-g`` or ``--global`` means to use a global $fish_user_paths
- ``-U`` or ``--universal`` means to use a universal $fish_user_paths - this is the default if it doesn't already exist
- ``-P`` or ``--path`` means to use $PATH directly
- ``-m`` or ``--move`` means to move already existing components to the place they would be added - by default they would be left in place and not added again
- ``-v`` or ``--verbose`` means to print the :ref:`set <cmd-set>` command used
- ``-n`` or ``--dry-run`` means to print the ``set`` command that would be used without executing it
If ``--move`` is used, it may of course lead to the path swapping order, so you should be careful doing that in config.fish.
Example
-------
::
# I just installed mycoolthing and need to add it to the path to use it.
fish_add_path /opt/mycoolthing/bin
# I want my ~/.local/bin to be checked first.
fish_add_path -m ~/.local/bin
# I prefer using a global fish_user_paths
fish_add_path -g ~/.local/bin ~/.otherbin /usr/local/sbin
# I want to append to the entire $PATH because this directory contains fallbacks
fish_add_path -aP /opt/fallback/bin
# I want to add the bin/ directory of my current $PWD (say /home/nemo/)
> fish_add_path -v bin/
set fish_user_paths /home/nemo/bin /usr/bin /home/nemo/.local/bin

View File

@ -0,0 +1,85 @@
function fish_add_path --description "Add paths to the PATH"
# This is meant to be the easy one-stop shop to adding stuff to $PATH.
# By default it'll prepend the given paths to a universal $fish_user_paths, excluding the already-included ones.
#
# That means it can be executed once in an interactive session, or stuffed in config.fish,
# and it will do The Right Thing.
#
# The options:
# --prepend or --append to select whether to put the new paths first or last
# --global or --universal to decide whether to use a universal or global fish_user_paths
# --path to set $PATH instead
# --move to move existing entries instead of ignoring them
# --verbose to print the set-command used
# --dry-run to print the set-command without running it
# We do not allow setting $PATH universally.
#
# It defaults to keeping $fish_user_paths or creating a universal, prepending and ignoring existing entries.
argparse -x g,U -x P,U -x a,p g/global U/universal P/path p/prepend a/append h/help m/move v/verbose n/dry-run -- $argv
or return
if set -q _flag_help
__fish_print_help fish_add_path
return 0
end
set -l scope $_flag_global $_flag_universal
if not set -q scope[1]; and not set -q fish_user_paths
set scope -U
end
set -l var fish_user_paths
set -q _flag_path; and set var PATH
set -l mode $_flag_prepend $_flag_append
set -q mode[1]; or set mode -p
# To keep the order of our arguments, go through and save the ones we want to keep.
set -l newpaths
set -l indexes
for path in $argv
# Realpath allows us to canonicalize the path, which is needed for deduplication.
# We could add a non-canonical version of the given path if no duplicate exists, but tbh that's a recipe for disaster.
# realpath complains if a parent directory does not exist, so we silence stderr.
set -l p (builtin realpath -- $path 2>/dev/null)
# Ignore non-existing paths
test -d "$p"; or continue
if set -l ind (contains -i -- $p $$var)
# In move-mode, we remove it from its current position and add it back.
if set -q _flag_move
set -a indexes $ind
set -a newpaths $p
end
else
# Without move, we only add it if it's not in.
set -a newpaths $p
end
end
# Ensure the variable is only set once, by constructing a new variable before.
# This is to stop any handlers or anything from firing more than once.
set -l newvar $$var
if set -q _flag_move; and set -q indexes[1]
# We remove in one step, so the indexes don't move.
set -e newvar[$indexes]
end
set $mode newvar $newpaths
# Finally, only set if there is anything *to* set.
# This saves us from setting, especially in the common case of someone putting this in config.fish
# to ensure a path is in $PATH.
if set -q newpaths[1]; or set -q indexes[1]
if set -q _flag_verbose; or set -q _flag_n
# The escape helps make it unambiguous - so you see whether an argument includes a space or something.
echo (string escape -- set $scope $var $newvar)
end
not set -q _flag_n
and set $scope $var $newvar
return 0
else
return 1
end
end

View File

@ -0,0 +1,56 @@
# RUN: %fish %s
#
# This deals with $PATH manipulation. We need to be careful not to step on anything.
set -l tmpdir (mktemp -d)
mkdir $tmpdir/bin
mkdir $tmpdir/sbin
mkdir $tmpdir/etc
ln -s $tmpdir/bin $tmpdir/link
# We set fish_user_paths to an empty global to have a starting point
set -g fish_user_paths
fish_add_path -v $tmpdir/bin
# CHECK: set fish_user_paths {{.*}}/bin
echo $status
# CHECK: 0
# Confirm that it actually ends up in $PATH
contains -- (builtin realpath $tmpdir/bin) $PATH
and echo Have bin
# CHECK: Have bin
# Not adding duplicates and not triggering variable handlers
function checkpath --on-variable PATH --on-variable fish_user_paths; echo CHECKPATH: $argv; end
set PATH $PATH
# CHECK: CHECKPATH: VARIABLE SET PATH
fish_add_path -v $tmpdir/bin
# Nothing happened, so the status failed.
echo $status
# CHECK: 1
functions --erase checkpath
# Not adding a link either
fish_add_path -v $tmpdir/link
echo $status
# CHECK: 1
fish_add_path -a $tmpdir/sbin
# Not printing anything because it's not verbose, the /sbin should be added at the end.
string replace -- $tmpdir '' $fish_user_paths | string join ' '
# CHECK: /bin /sbin
fish_add_path -m $tmpdir/sbin
string replace -- $tmpdir '' $fish_user_paths | string join ' '
# CHECK: /sbin /bin
set -l oldpath "$PATH"
fish_add_path -nP $tmpdir/etc | string replace -- $tmpdir ''
# Should print a set command to prepend /etc to $PATH, but not actually do it
# CHECK: set PATH /etc{{.*}}
# Confirm that $PATH didn't change.
test "$oldpath" = "$PATH"
or echo "PATH CHANGED!!!" >&2
exit 0