fisher/fisher.fish
Jorge Bucaran 54170ef1c3
breaking: implement fisher V3 (#445)
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
2018-10-05 20:20:31 +09:00

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