mirror of
https://github.com/jorgebucaran/fisher
synced 2024-06-28 23:01:50 +02:00
SUMMARY This PR rewrites fisher from the ground up and adds new documentation. It introduces some breaking changes as described in the next section. For a historical background of this work see the original V3 proposal #307 and the more recent discussion about the future of the project #443. After much debate and careful consideration I decided it is in the best interest of the project to keep the CLI-based approach to dependency management as a facade to the fishfile-based approach originally proposed. The new `add` commands (previously `install`) and good ol' `rm` interactively update your fishfile and commit all your changes in one sweep. To the end user, it's as if you were adding or removing packages like you already do now. Internally, these commands affect how the fishfile is parsed and result in adding new or replacing/removing existing entries followed by a regular `fisher` run. INSTALLING - `install` has been renamed to `add` - Installing from a gist is no longer supported (but it will be back in a future release—removed only to simplify the rewrite) - To install a package from a tag or branch use an at symbol `@`—the colon `:` is deprecated LISTING - `ls` and `rm` are still available with a few minor differences - `ls` followed by a package name does not list specific package information (may be added back in a future release) - `ls` output format no longer displays a legend to indicate whether a package is a theme or a local package; now it's a flat dump of every installed package specifier - For local packages the full path is shown instead - I want to add a `--tree` option in to display packages in a tree-like format in the future - `ls-remote` has been removed as there is no longer a preferred organization to look for packages— there is no plan to add it back UPDATING - A new `self-update` command has been introduced to update fisher itself - fisher will be only updated when a new version is actually available - `update` has been removed - Everything is installed from scratch everytime you add or remove something, so there is no need to update specific packages—you're always up-to-date - To lock on a specific package version install from a tag/branch, e.g., `mypkg/foobar@1.3.2` UNINSTALLING - `self-uninstall` works as usual HELP & VERSION - `help` only displays fisher usage help - help is dumped to stdout instead of creating a man page on the fly and piping it to your pager `version` works as usual ENVIRONMENT - `$fish_path` been renamed to `$fisher_path` to make it clear that this is a fisher specific extension, not your shell's ECOSYSTEM - Oh My Fish! packages are still supported, albeit less attention is paid to them - Some packages that use Oh My Fish! specific environment variables or events might not work - Most of Oh My Fish! extensions are no longer necessary since fish 2.3, therefore it should be a simple matter to upgrade them to modern fish DEPENDENCIES - fisher can now run on fish 2.0 - It's a good idea to upgrade to at least fish 2.3 to use the string builtin and configuration snippets, but there's no reason for fisher to force you to use any fish version - `curl` is required for fetching packages - I am considering adding a fallback to `wget` if `curl` is not available on your system - `git` is optional - V3 fetches packages directly from github, gitlab and bitbucket, if you are using them - git is only used (implementation still wip) if you want to install a package from an unknown git host like your own git server
412 lines
14 KiB
Fish
412 lines
14 KiB
Fish
set -g fisher_version 3.0.2
|
|
|
|
type source >/dev/null; or function source; . $argv; end
|
|
|
|
if command which perl >/dev/null
|
|
function _fisher_now -a elapsed
|
|
command perl -MTime::HiRes -e 'printf("%.0f\n", (Time::HiRes::time() * 1000) - $ARGV[0])' $elapsed
|
|
end
|
|
else
|
|
function _fisher_now -a elapsed
|
|
command date "+%s%3N" | command awk "{ sub(/3N\$/,\"000\"); print \$0 - 0$elapsed }"
|
|
end
|
|
end
|
|
|
|
function fisher -a cmd -d "fish package manager"
|
|
if not command which curl >/dev/null
|
|
echo "curl is required to use fisher -- install curl and try again" >&2
|
|
return 1
|
|
end
|
|
|
|
test -z "$XDG_CACHE_HOME"; and set XDG_CACHE_HOME ~/.cache
|
|
test -z "$XDG_CONFIG_HOME"; and set XDG_CONFIG_HOME ~/.config
|
|
|
|
set -g fish_config $XDG_CONFIG_HOME/fish
|
|
set -g fisher_cache $XDG_CACHE_HOME/fisher
|
|
set -g fisher_config $XDG_CONFIG_HOME/fisher
|
|
|
|
test -z "$fisher_path"; and set -g fisher_path $fish_config
|
|
|
|
command mkdir -p {$fish_config,$fisher_path}/{functions,completions,conf.d} $fisher_cache
|
|
|
|
if test ! -e "$fisher_path/completions/fisher.fish"
|
|
echo "fisher self-complete" > $fisher_path/completions/fisher.fish
|
|
_fisher_self_complete
|
|
end
|
|
|
|
switch "$cmd"
|
|
case self-complete
|
|
_fisher_self_complete
|
|
case ls
|
|
_fisher_ls | command sed "s|$HOME|~|"
|
|
case -v {,--}version
|
|
_fisher_version (status -f)
|
|
case -h {,--}help
|
|
_fisher_help
|
|
case self-update
|
|
_fisher_self_update (status -f); or return
|
|
_fisher_self_complete
|
|
case self-uninstall
|
|
_fisher_self_uninstall
|
|
case "" add rm
|
|
if not isatty
|
|
while read -l i
|
|
set argv $argv $i
|
|
end
|
|
end
|
|
_fisher_commit $argv; or return
|
|
_fisher_self_complete
|
|
case \*
|
|
echo "unknown flag or command \"$cmd\" -- try fisher help" >&2
|
|
return 1
|
|
end
|
|
end
|
|
|
|
function _fisher_self_complete
|
|
complete -c fisher --erase
|
|
complete -xc fisher -n __fish_use_subcommand -a version -d "Show version"
|
|
complete -xc fisher -n __fish_use_subcommand -a help -d "Show help"
|
|
complete -xc fisher -n __fish_use_subcommand -a self-update -d "Update fisher"
|
|
complete -xc fisher -n __fish_use_subcommand -a ls -d "List installed packages"
|
|
complete -xc fisher -n __fish_use_subcommand -a rm -d "Remove packages"
|
|
complete -xc fisher -n __fish_use_subcommand -a add -d "Add packages"
|
|
for pkg in (_fisher_ls)
|
|
complete -xc fisher -n "__fish_seen_subcommand_from rm" -a $pkg
|
|
end
|
|
end
|
|
|
|
function _fisher_ls
|
|
set -l pkgs $fisher_config/*/*/*
|
|
for pkg in $pkgs
|
|
command readlink $pkg; and continue; or echo $pkg
|
|
end | command sed "s|$fisher_config/||;s|github\.com/||"
|
|
end
|
|
|
|
function _fisher_version -a file
|
|
echo "fisher version $fisher_version $file" | command sed "s|$HOME|~|"
|
|
end
|
|
|
|
function _fisher_help
|
|
echo "usage: fisher add <PACKAGES> add packages"
|
|
echo " fisher rm <PACKAGES> remove packages"
|
|
echo " fisher ls list installed packages"
|
|
echo " fisher self-update update fisher"
|
|
echo " fisher self-uninstall uninstall fisher and all packages"
|
|
echo " fisher help show this help"
|
|
echo " fisher version show version"
|
|
echo
|
|
echo "examples:"
|
|
echo " fisher add jethrokuan/z rafaelrinaldi/pure"
|
|
echo " fisher add gitlab.com/owner/foobar@v2"
|
|
echo " fisher add ~/myfish/mypkg"
|
|
echo " fisher rm rafaelrinaldi/pure"
|
|
echo " fisher ls | fisher rm"
|
|
end
|
|
|
|
function _fisher_self_update -a file
|
|
set -l url "https://raw.githubusercontent.com/jorgebucaran/fisher/master/fisher.fish"
|
|
echo "fetching $url" >&2
|
|
|
|
curl -s "$url?nocache" >$file@
|
|
|
|
set -l next_version (awk 'NR == 1 { print $4; exit }' < $file@)
|
|
switch "$next_version"
|
|
case "" $fisher_version
|
|
command rm -f $file@
|
|
if test -z "$next_version"
|
|
echo "cannot update fisher -- are you offline?" >&2
|
|
return 1
|
|
end
|
|
echo "fisher is already up-to-date" >&2
|
|
case \*
|
|
echo "linking $file" | command sed "s|$HOME|~|" >&2
|
|
command mv -f $file@ $file
|
|
source $file
|
|
echo "updated fisher to $fisher_version -- hooray!" >&2
|
|
end
|
|
end
|
|
|
|
function _fisher_self_uninstall
|
|
printf "removing %s\n" $fisher_config $fisher_cache $fish_config/fishfile $fisher_path/{functions,completions}/fisher.fish | command sed "s|$HOME|~|"
|
|
_fisher_pkg_remove_all $fisher_config/*/*/* >/dev/null
|
|
command rm -rf $fisher_config $fisher_cache 2>/dev/null
|
|
command rm $fisher_path/{functions,completions}/fisher.fish $fish_config/fishfile 2>/dev/null
|
|
set -e fisher_cache
|
|
set -e fisher_config
|
|
set -e fisher_path
|
|
set -e fisher_version
|
|
complete -c fisher --erase
|
|
functions -e (functions -a | command awk '/^_fisher/') fisher
|
|
end
|
|
|
|
function _fisher_commit
|
|
set -l elapsed (_fisher_now)
|
|
set -l fishfile $fish_config/fishfile
|
|
set -l added_pkgs
|
|
set -l updated_pkgs
|
|
set -l removed_pkgs
|
|
|
|
if test -z "$cmd" -a ! -e "$fishfile"
|
|
echo "fishfile not found -- need help? try fisher help" | command sed "s|$HOME|~|" >&2
|
|
return 1
|
|
end
|
|
command touch $fishfile
|
|
|
|
_fisher_fishfile_indent (echo -s $argv\;) < $fishfile > $fishfile@
|
|
|
|
command mv -f $fishfile@ $fishfile
|
|
command rm -f $fishfile@
|
|
|
|
set removed_pkgs (_fisher_pkg_remove_all $fisher_config/*/*/*)
|
|
command rm -rf $fisher_config
|
|
command mkdir -p $fisher_config
|
|
|
|
set added_pkgs (_fisher_pkg_fetch_all (_fisher_fishfile_load < $fishfile))
|
|
set updated_pkgs (
|
|
for pkg in $removed_pkgs
|
|
set pkg (echo $pkg | command sed "s|$fisher_config/||")
|
|
if contains -- $pkg $added_pkgs
|
|
echo $pkg
|
|
end
|
|
end)
|
|
|
|
if test ! -z "$added_pkgs$updated_pkgs$removed_pkgs"
|
|
echo (count $added_pkgs) (count $updated_pkgs) (count $removed_pkgs) (_fisher_now $elapsed) | _fisher_status_report >&2
|
|
end
|
|
end
|
|
|
|
function _fisher_pkg_remove_all
|
|
for pkg in $argv
|
|
echo $pkg
|
|
_fisher_pkg_uninstall $pkg
|
|
end
|
|
end
|
|
|
|
function _fisher_pkg_fetch_all
|
|
set -l pkg_jobs
|
|
set -l local_pkgs
|
|
set -l actual_pkgs
|
|
set -l fetched_pkgs
|
|
|
|
for name in $argv
|
|
switch $name
|
|
case \~\* /\*
|
|
set -l path (echo "$name" | command sed "s|~|$HOME|")
|
|
if test -e "$path"
|
|
set local_pkgs $local_pkgs $path
|
|
else
|
|
echo "cannot install $name -- is this a valid file?" >&2
|
|
end
|
|
continue
|
|
case https://\* ssh://\* {github,gitlab}.com/\* bitbucket.org/\*
|
|
case \*
|
|
set name "github.com/$name"
|
|
end
|
|
|
|
echo $name | command awk '{
|
|
split($0, tmp, /@/)
|
|
|
|
pkg = tmp[1]
|
|
tag = tmp[2] ? tmp[2] : "master"
|
|
name = tmp[split(pkg, tmp, "/")]
|
|
|
|
print (\
|
|
pkg ~ /^github\.com/ ? "https://codeload."pkg"/tar.gz/"tag : \
|
|
pkg ~ /^gitlab\.com/ ? "https://"pkg"/-/archive/"tag"/"name"-"tag".tar.gz" : \
|
|
pkg ~ /^bitbucket\.org/ ? "https://"pkg"/get/"tag".tar.gz" : pkg \
|
|
) "\t" pkg
|
|
}' | read -l url pkg
|
|
|
|
fish -c "
|
|
echo fetching $url >&2
|
|
command mkdir -p \"$fisher_config/$pkg\"
|
|
|
|
if curl -Ss $url 2>&1 | tar -xzf- -C \"$fisher_config/$pkg\" --strip-components=1 2>/dev/null
|
|
command mkdir -p \"$fisher_cache/$pkg\"
|
|
command cp -Rf \"$fisher_config/$pkg\" \"$fisher_cache/$pkg/..\"
|
|
else if test -d \"$fisher_cache/$pkg\"
|
|
echo cannot connect to server -- using data from \"$fisher_cache/$pkg\" | command sed 's|$HOME|~|' >&2
|
|
command cp -Rf \"$fisher_cache/$pkg\" \"$fisher_config/$pkg/..\"
|
|
else
|
|
command rm -rf \"$fisher_config/$pkg\"
|
|
echo cannot install \"$pkg\" -- are you offline\? >&2
|
|
end
|
|
" >/dev/null &
|
|
|
|
set pkg_jobs $pkg_jobs (_fisher_jobs --last)
|
|
set fetched_pkgs $fetched_pkgs "$pkg"
|
|
end
|
|
|
|
if test ! -z "$pkg_jobs"
|
|
_fisher_wait $pkg_jobs
|
|
for pkg in $fetched_pkgs
|
|
if test -d "$fisher_config/$pkg"
|
|
set actual_pkgs $actual_pkgs $pkg
|
|
_fisher_pkg_install $fisher_config/$pkg
|
|
end
|
|
end
|
|
end
|
|
|
|
for pkg in $local_pkgs
|
|
set -l path "local/$USER"
|
|
set -l name (echo "$pkg" | command sed 's|^.*/||')
|
|
|
|
command mkdir -p $fisher_config/$path
|
|
command ln -sf $pkg $fisher_config/$path
|
|
|
|
set actual_pkgs $actual_pkgs $path/$name
|
|
_fisher_pkg_install $fisher_config/$path/$name
|
|
end
|
|
|
|
if test ! -z "$actual_pkgs"
|
|
printf "%s\n" $actual_pkgs
|
|
_fisher_pkg_fetch_all (_fisher_pkg_get_deps $actual_pkgs | command sort --unique)
|
|
end
|
|
end
|
|
|
|
function _fisher_pkg_get_deps
|
|
for pkg in $argv
|
|
set -l path $fisher_config/$pkg
|
|
if test ! -d "$path"
|
|
echo $pkg
|
|
else if test -s "$path/fishfile"
|
|
_fisher_pkg_get_deps (_fisher_fishfile_indent < $path/fishfile | _fisher_fishfile_load)
|
|
end
|
|
end
|
|
end
|
|
|
|
function _fisher_pkg_install -a pkg
|
|
set -l name (echo $pkg | command sed "s|^.*/||")
|
|
for source in $pkg/{functions,completions,conf.d,}/*.fish
|
|
set -l target (echo "$source" | command sed 's|^.*/||')
|
|
switch $source
|
|
case $pkg/conf.d\*
|
|
set target $fisher_path/conf.d/$target
|
|
case $pkg/completions\*
|
|
set target $fisher_path/completions/$target
|
|
case $pkg/{functions,}\*
|
|
switch $target
|
|
case uninstall.fish
|
|
continue
|
|
case init.fish key_bindings.fish
|
|
set target $fisher_path/conf.d/$name\_$target
|
|
case \*
|
|
set target $fisher_path/functions/$target
|
|
end
|
|
end
|
|
echo "linking $target" | command sed "s|$HOME|~|" >&2
|
|
command ln -f $source $target
|
|
source $target >/dev/null 2>/dev/null
|
|
end
|
|
end
|
|
|
|
function _fisher_pkg_uninstall -a pkg
|
|
set -l name (echo $pkg | command sed "s|^.*/||")
|
|
for source in $pkg/{conf.d,completions,functions,}/*.fish
|
|
set -l target (echo "$source" | command sed 's|^.*/||')
|
|
set -l filename (echo "$target" | command sed 's|.fish||')
|
|
switch $source
|
|
case $pkg/conf.d\*
|
|
emit {$filename}_uninstall
|
|
command rm -f $fisher_path/conf.d/$target
|
|
case $pkg/completions\*
|
|
command rm -f $fisher_path/completions/$target
|
|
complete -ec $filename
|
|
case $pkg/{,functions}\*
|
|
switch $target
|
|
case uninstall.fish
|
|
source $source
|
|
continue
|
|
case init.fish key_bindings.fish
|
|
set target $fisher_path/conf.d/$name\_$target
|
|
case \*
|
|
set target $fisher_path/functions/$target
|
|
end
|
|
command rm -f $target
|
|
functions -e $filename
|
|
end
|
|
end
|
|
if not functions -q fish_prompt
|
|
source "$__fish_datadir/functions/fish_prompt.fish"
|
|
end
|
|
end
|
|
|
|
function _fisher_fishfile_indent -a pkgs
|
|
command awk -v PWD=$PWD -v HOME=$HOME -v PKGS="$pkgs" '
|
|
function normalize(s) {
|
|
gsub(/^[ \t]*|[ \t]*$|^https?:\/\/|github\.com\/|\.git$|\/$/, "", s)
|
|
sub(/^\.\//, PWD"/", s)
|
|
sub(HOME, "~", s)
|
|
return s
|
|
}
|
|
function get_pkg_name(s) {
|
|
split(s, tmp, /[@# ]+/)
|
|
return tmp[1]
|
|
}
|
|
BEGIN {
|
|
pkg_count = split(PKGS, pkgs, ";") - 1
|
|
cmd = pkgs[1]
|
|
for (i = 2; i <= pkg_count; i++) {
|
|
pkg_ids[i - 1] = get_pkg_name( pkgs[i] = normalize(pkgs[i]) )
|
|
}
|
|
} {
|
|
if (NF) {
|
|
nl = nl > 0 ? "" : nl
|
|
pkg_id = get_pkg_name( $0 = normalize($0) )
|
|
if (/^#/) print nl$0
|
|
else if (!seen[pkg_id]++) {
|
|
for (i = 1; i < pkg_count; i++) {
|
|
if (pkg_ids[i] == pkg_id) {
|
|
if (cmd == "rm") next
|
|
$0 = pkgs[i + 1]
|
|
break
|
|
}
|
|
}
|
|
print nl$0
|
|
}
|
|
nl = NF
|
|
} else if (nl) nl = (nl > 0 ? "" : nl)"\n"
|
|
}
|
|
END {
|
|
if (cmd == "rm" || pkg_count <= 1) exit
|
|
for (i = 2; i <= pkg_count; i++) {
|
|
if (!seen[pkg_ids[i - 1]]) print pkgs[i]
|
|
}
|
|
}
|
|
'
|
|
end
|
|
|
|
function _fisher_fishfile_load
|
|
command awk -v FS=\# '!/^#/ && NF { print $1 }'
|
|
end
|
|
|
|
function _fisher_status_report
|
|
command awk '
|
|
function msg(res, str, n) {
|
|
return (res ? res ", " : "") str " " n " package" (n > 1 ? "s" : "")
|
|
}
|
|
$1 = $1 - $2 { res = msg(res, "added", $1) }
|
|
$2 { res = msg(res, "updated", $2) }
|
|
$3 = $3 - $2 { res = msg(res, "removed", $3) }
|
|
{ printf((res ? res : "done") " in %.2fs\n", ($4 / 1000)) }
|
|
'
|
|
end
|
|
|
|
function _fisher_jobs
|
|
jobs $argv | command awk -v FS=\t '
|
|
/[0-9]+\t/ {
|
|
jobs[++n] = $1
|
|
} END {
|
|
for (i in jobs) print(jobs[i])
|
|
exit n == 0
|
|
}
|
|
'
|
|
end
|
|
|
|
function _fisher_wait
|
|
while for job in $argv
|
|
contains -- $job (_fisher_jobs); and break
|
|
end
|
|
end
|
|
end
|