Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
607 lines (559 sloc) 20.4 KB
# shellcheck disable=SC2155,SC2181,SC2016 shell=bash
# vim: ft=sh ts=4 sw=0 sts=-1 et
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# A P P A R I S H
#
# bookmarks for the command line with comprehensive tab completion on target
# content
# works for bash and zsh
# Authors:
# Stijn van Dongen
# Sitaram Chamarty
# Izaak van Dongen
# Quick Guide:
# - save this file in $HOME/.bourne-apparish
# - issue 'source $HOME/.bourne-apparish'
# - go to a directory and issue 'bm foo'
# - you can now go to that directory by issuing 'to foo'
# - try tab completion and command substitution, see the examples below.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ignore errors about:
# - testing $?, because that's useful when you have branches
# - declaring and assigning at the same time because I know what I'm doing
# (fingers crossed)
# - unexpanded substitutions in single quotes for similar reasons
# Apparish is a pure shell implementation of an older system, apparix, written
# partly in C. For both systems the bookmarking commands are implemented as
# shell functions. The names of these functions are the same between the two
# implementations and the function definitions are very similar. The apparix
# shell functions invoke a C executable. Apparish uses another shell funtion
# to mimic this C program and apparish provides two additional funcctions,
# apparix-list. The pivotal commands however are 'bm' (bookmark) and 'to' (go
# to mark). You can change from apparix to apparish and vice versa, as they use
# the same resource files.
#
# ---
# bm <tag> create bookmark <tag> for current directory
# ---
# to <tag> jump to the directory marked <tag>
# to <tag> <TAB> tab-complete on subdirectories of <tag>
# to <tag> s<TAB> tab-complete on subdirectories of <tag> starting
# with s
# to <tag> foo/<TAB> tab-complete in subdirectory foo of <tag>
# to <tag> foo/bar<TAB> et cetera et cetera
#
# --- the commands below allow tab-completion identical to 'to' above.
# als <tag> list contents of <tag> directory
# ald <tag> list subdirectories of <tag> directory
# amd <tag> NAME issue mkdir in <tag> directory
# amd <tag> PATH/<TAB> amd allows tab completion
# ae <tag> FILE edit FILE in <tag> directory
# ae <tag> FI<TAB> complete on FI in <tag> directory
# a <tag> s<TAB> echo the location of the <tag> directory or
# content.
# This is useful in command substitution, e.g.
# 'cp somefile ($a tag src)'
#
# --- apparix uses by default the most recent <tag> if identical tags exist.
# It can e.g. be useful to use 'now' as an often-changing tag.
# apparix-list <tag> list all destinations marked <tag>
# whence <tag> Enter menu to select destination
#
# --- the functionality below mimics bash CDPATH.
# portal add all subdirectory names as mark
# portal-expand refresh the portal subdirectory cache
#
# If you use 'ae', make sure $EDITOR is set to the name of an available editor.
# I find it useful to have this alias:
#
# alias a=apparish
#
# as I use it in command substitution, e.g.
#
# echo cp myfile $(a bm)
# cp myfile $(a bm)
#
# This is a big decision from a Huffman point of view. If you want to remove
# it, go to all the places in the lines below where the name Huffman is
# mentioned and remove the relevant part.
#
# Apparish (this file) implements apparix functionality in shell code,
# compatible with apparix resource files. You can either use old apparix
# (compiling and installing the application apparix) in conjunction with
# sourcing .bourne-apparix, or you can simply source this file without needing
# to install apparix. This file implements nearly all apparix functionality
# in shell code. It uses a apparish in place of apparix.
#
# BASH and ZSH functions
#
# Apparish should work for modern bourne-style shells, not including the
# bourne shell. Name this file for example .bourne-apparish in your $HOME
# directory, and put the line 'source $HOME/.bourne-apparish' (without quotes)
# in the file $HOME/.bashrc or $HOME/.bash_login if you use bash, in the file
# $HOME/.zshrc if you use zsh.
#
# Thanks to Sitaram Chamarty for all the important parts of the bash completion
# code, and thanks to Izaak van Dongen for figuring out the zsh completion
# code, subsequently improving and standardising the bash completion code, and
# suggesting the name apparish.
#
APPARIXHOME="${APPARIXHOME:=$HOME}"
# ensure APPARIXHOME exists
mkdir -p "$APPARIXHOME"
APPARIXRC="${APPARIXRC:=$APPARIXHOME/.apparixrc}"
APPARIXEXPAND="${APPARIXEXPAND:=$APPARIXHOME/.apparixexpand}"
APPARIXLOG="${APPARIXLOG:=$APPARIXHOME/.apparixlog}"
# ensure set up and log files exist
touch "$APPARIXRC"
touch "$APPARIXEXPAND"
touch "$APPARIXLOG"
APPARIX_FILE_FUNCTIONS=( a ae av aget toot apparish ) # Huffman (remove a)
APPARIX_DIR_FUNCTIONS=( to als ald amd todo rme )
# these are some helper functions. With my system they are mostly redundant
# copies of functions I already have defined, but I aim to make this file
# stand-alone.
# sanitise $1 so that it becomes suitable for use with your basic grep
function grepsanitise() {
sed 's/[].*^$]\|\[/\\&/g' <<< "$1"
}
# vim-like: totally silence the given command, with less of the tedium. doesn't
# affect return status, so can be used inside if statements.
# black magic: "$@" expands to each argument as a separate word.
# gotcha: this won't expand any aliases you have. This is probably preferred
# functionality anyway, though (at least for me)
silent() {
"$@" > /dev/null 2> /dev/null
}
# Huffman (remove this paragraph, or just alias "a" yourself)
if ! silent command -v a; then
alias a='apparish'
else
>&2 echo "Apparish: not aliasing a"
fi
if ! silent command -v via; then
alias via='"${EDITOR:-vim}" "$APPARIXRC"'
else
>&2 echo "Apparish: not aliasing via"
fi
function apparish() {
if [[ 0 == "$#" ]]; then
cat -- "$APPARIXRC" "$APPARIXEXPAND" | tr ', ' '\t_' | column -t
return
fi
local mark="$1"
local list="$(command grep -F ",$mark," "$APPARIXRC" "$APPARIXEXPAND")"
if [[ -z "$list" ]]; then
>&2 echo "Mark '$mark' not found"
return 1
fi
local target="$( (tail -n 1 | cut -f 3 -d ',') <<< "$list")"
if [[ 2 == "$#" ]]; then
echo "$target/$2"
else
echo "$target"
fi
}
function apparix-list() {
if [[ 0 == "$#" ]]; then
>&2 echo "Need mark"
return 1
fi
local mark="$1"
command grep -F ",$mark," -- "$APPARIXRC" "$APPARIXEXPAND" | cut -f 3 -d ','
}
function bm() {
if [[ 0 == "$#" ]]; then
>&2 echo Need mark
return 1
fi
local mark="$1"
local list="$(apparix-list "$mark")"
echo "j,$mark,$PWD" | tee -a -- "$APPARIXLOG" >> "$APPARIXRC"
if [[ -n "$list" ]]; then
listsize=$((1 + $(wc -l <<< "$list") ))
echo "$PWD added, $listsize total"
fi
}
function to() {
if [[ 2 == "$#" ]]; then
loc="$(apparish "$1" "$2")"
elif [[ 1 == "$#" ]]; then
if [[ "$1" == '-' ]]; then
loc="-"
else
loc="$(apparish "$1")"
fi
else
>&2 echo "Usage: to MARK [SUBDIR1/[SUBDIR2/[etc]]]"
return 1
fi
if [[ "$?" == 0 ]]; then
cd -- "$loc" || return 1
fi
}
function portal() {
echo "e,$PWD" >> "$APPARIXRC"
portal-expand
}
function portal-expand() {
local parentdir
rm -f -- "$APPARIXEXPAND"
true > "$APPARIXEXPAND"
command grep '^e,' -- "$APPARIXRC" | cut -f 2 -d , | \
while read -r parentdir; do
# run in an explicit bash subshell to be able to locally set the
# right options
parentdir="$parentdir" APPARIXEXPAND="$APPARIXEXPAND" bash <<EOF
cd -- "\$parentdir" || return 1
shopt -s nullglob
shopt -u dotglob
shopt -u failglob
GLOBIGNORE="./:../"
for _subdir in */ .*/; do
subdir="\${_subdir%/}"
echo "j,\$subdir,\$parentdir/\$subdir" >> "\$APPARIXEXPAND"
done
EOF
done
}
function whence() {
if [[ 0 == "$#" ]]; then
>&2 echo "Need mark"
return 1
fi
local mark="$1"
select target in $(apparix-list "$mark"); do
cd -- "$target" || return 1
break
done
}
function toot() {
if [[ 3 == "$#" ]]; then
file="$(apparish "$1" "$2")/$3"
elif [[ 2 == "$#" ]]; then
file="$(apparish "$1")/$2"
else
>&2 echo "toot tag dir file OR toot tag file"
return 1
fi
if [[ "$?" == 0 ]]; then
"${EDITOR:-vim}" "$file"
fi
}
function todo() {
toot "$@" TODO
}
function rme() {
toot "$@" README
}
# apparix listing of directories of mark
function ald() {
if [[ 2 == "$#" ]]; then
loc="$(apparish "$1" "$2")"
elif [[ 1 == "$#" ]]; then
loc="$(apparish "$1")"
fi
if [[ "$?" == 0 ]]; then
ls -d "$loc"/*
fi
}
# apparix ls of mark
function als() {
local narg=$#
local lastarg=${!narg}
local options=""
local loc=""
if [[ $lastarg == -* ]]; then options=$lastarg; narg=$((narg-1)); fi
if (( 2 == narg )); then
loc="$(apparish "$1" "$2")"
elif (( 1 == narg )); then
loc="$(apparish "$1")"
fi
if [[ "$?" == 0 ]]; then
ls $options $loc
fi
}
# apparix search bookmark
# TODO: fix when cwd has newlines in
function amibm() {
command grep -- ",$(grepsanitise "$PWD")$" "$APPARIXRC" | \
cut -f 2 -d ',' | paste -s -d ' ' -
}
# apparix search bookmark
function bmgrep() {
pat="${1?Need a pattern to search}"
command grep -- "$pat" "$APPARIXRC" | cut -f 2,3 -d ',' | tr ',' '\t' | column -t
}
# apparix get; get something from a mark
function aget() {
if [[ 2 == "$#" ]]; then
loc="$(apparish "$1" "$2")"
elif [[ 1 == "$#" ]]; then
loc="$(apparish "$1")"
fi
if [[ "$?" == 0 ]]; then
cp "$loc" .
fi
}
# apparix mkdir in mark
function amd() {
if [[ 2 == "$#" ]]; then
loc="$(apparish "$1" "$2")"
elif [[ 1 == "$#" ]]; then
loc="$(apparish "$1")"
fi
if [[ "$?" == 0 ]]; then
mkdir -p -- "$loc"
fi
}
# apparix edit of file in mark or subdirectory of mark
function av() {
if [[ 2 == "$#" ]]; then
loc="$(apparish "$1" "$2")"
elif [[ 1 == "$#" ]]; then
loc="$(apparish "$1")"
fi
if [[ "$?" == 0 ]]; then
view -- "$loc"
fi
}
# apparix edit of file in mark or subdirectory of mark
function ae() {
if [[ 2 == "$#" ]]; then
loc="$(apparish "$1" "$2")"
elif [[ 1 == "$#" ]]; then
loc="$(apparish "$1")"
fi
if [[ "$?" == 0 ]]; then
"${EDITOR:-vim}" "$loc"
fi
}
function apparish_ls() {
cat <<EOH
bm MARK Bookmark current directory as mark
to MARK [SUBDIR] Jump to mark or a subdirectory of mark
ald MARK [SUBDIR] List subdirectories of mark directory or subdir
als MARK [SUBDIR] List mark directory or subdir
amd MARK [SUBDIR] Make directory in mark
ae MARK [SUBDIR/]FILE Edit file in mark
av MARK [SUBDIR/]FILE View file in mark
amibm See if the current directory is a bookmark
bmgrep PATTERN List all marks and targets where target matches
PATTERN
todo MARK [SUBDIR] Edit TODO file in mark directory
rme MARK [SUBDIR] Edit README file
whence MARK Menu-based selection for mark with multiple targets
portal Add current directory as portal (subdirectories are
mark names)
portal-expand Re-expand all portals
apparix-list MARK List all targets for bookmark mark
EOH
}
if [[ -n "$BASH_VERSION" ]]; then
# bash specific helper functions
# assert that bash version is at least $1.$2.$3
version_assert() {
for i in {1..3}; do
if ((BASH_VERSINFO[$((i - 1))] > ${!i})); then
return 0
elif ((BASH_VERSINFO[$((i - 1))] < ${!i})); then
# echo "Your bash is older than $1.$2.$3" >&2
return 1
fi
done
return 0
}
# define a function to read lines from a file into an array
# https://github.com/koalaman/shellcheck/wiki/SC2207
if silent version_assert 4 0 0; then
function read_array() {
mapfile -t goedel_array < "$1"
}
elif silent version_assert 3 0 0; then
function read_array() {
goedel_array=()
while IFS='' read -r line; do
goedel_array+=("$line");
done < "$1"
}
else
>&2 echo "really, bash 2 isn't cool enough to run apparix"
function read_array() {
local IFS=$'\n'
# this is a bad fallback implementation on purpose
# shellcheck disable=SC2207
goedel_array=( $(cat -- "$1") )
}
fi
# complete sensibly on filenames and directories
# https://stackoverflow.com/questions/12933362/getting-compgen-to-include...
# -slashes-on-directories-when-looking-for-files
function _all_files_compgen() {
local cur="$1"
# The outcommented code splits directories and files but then treats
# them the same. Previously, it used to add a slash for directories, but
# this makes completing actually harder; Manually adding a slash is a
# good way of instigating the next level of completion. Anyway, I've
# kept this around in case people want to change this behaviour. I use
# comm because old greps have an issue where -v does not treat an empty
# file with -f correctly.
# $ comm -3 <(compgen -f -- "$cur" | sort) \
# <(compgen -d -- "$cur" | sort) # | sed -e 's/$/ /'
# Directories (add -S / for slash separator):
# $ compgen -d -- "$cur"
compgen -f -- "$cur"
}
# https://stackoverflow.com/questions/3685970/check-if-a-bash-array-...
# contains-a-value
function elemOf() {
local e match="$1"
shift
for e; do [[ "$e" == "$match" ]] && return 0; done
return 1
}
# a file, used by _apparix_comp
function _apparix_comp_file() {
local caller="$1"
local cur_file="$2"
if elemOf "$caller" "${APPARIX_DIR_FUNCTIONS[@]}"; then
if [[ -n "$BERTRAND_RUSSEL" ]]; then
# # Directories (add -S / for slash separator):
compgen -d -- "$cur_file"
else
goedel_compfile "$cur_file" d
fi
elif elemOf "$caller" "${APPARIX_FILE_FUNCTIONS[@]}"; then
# complete on filenames. this is a little harder to do nicely.
if [[ -n "$BERTRAND_RUSSEL" ]]; then
_all_files_compgen "$cur_file"
else
goedel_compfile "$cur_file" f
fi
else
>&2 echo "Unknown caller: Izaak has probably messed something up"
return 1
fi
}
# the existence of this function is a counterexample to Gödel's little known
# incompletion theorem: there's no such thing as good completion on files in
# Bash
function goedel_compfile() {
local part_esc="$1"
case "$2" in
f)
local find_files=true;;
d) ;;
*) >&2 echo "Specify file type"; return 1;;
esac
local part_unesc="$(bash -c "printf '%s' $part_esc")"
local part_dir="$(dirname "$part_unesc")"
COMPREPLY=()
# can't pipe to while because that's a subshell and we need to modify
# COMREPLY.
while IFS='' read -r -d '' result; do
# this is a bit of a weird hack because printf "%q\n" with no
# arguments prints ''. It should be robust, because any actual
# single quotes will have been escaped by printf.
if [[ "$result" != "''" ]]; then
COMPREPLY+=("$result")
fi
# use an explicit bash subshell to set some glob flags.
done < <(part_dir="$part_dir" part_unesc="$part_unesc" \
find_files="$find_files" bash <<EOF
shopt -s nullglob
shopt -s extglob
shopt -u dotglob
shopt -u failglob
GLOBIGNORE="./:../"
if [[ "\$part_dir" == "." ]]; then
find_name_prefix="./"
fi
# here we delay the %q escaping because I want to strip trailing /s
if [[ -d "\$part_unesc" ]]; then
if [[ "\$part_unesc" != +(/) ]]; then
part_unesc="\${part_unesc%%+(/)}"
fi
if [[ "\$find_files" == true ]]; then
printf "%q\\0" "\$part_unesc"/* "\$part_unesc"/*/
else
printf "%q\\0" "\$part_unesc"/*/
fi
else
if [[ "\$find_files" == true ]]; then
printf "%q\\0" "\$part_unesc"*/ "\$part_unesc"*
else
printf "%q\\0" "\$part_unesc"*/
fi
fi
EOF
)
}
# generate completions for a bookmark
# this is currently case sensitive. Good? Bad? Who knows!
function _apparix_compgen_bm() {
cut -f2 -d, -- "$APPARIXRC" "$APPARIXEXPAND" | sort |\
command grep -i -- "^$(grepsanitise "$1")"
if [[ -n "$1" ]]; then
cut -f2 -d, -- "$APPARIXRC" "$APPARIXEXPAND" | sort |\
command grep -i -- "^..*$(grepsanitise "$1")"
fi
}
# complete an apparix tag followed by a file inside that tag's
# directory
function _apparix_comp() {
local tag="${COMP_WORDS[1]}"
COMPREPLY=()
if [[ "$COMP_CWORD" == 1 ]]; then
read_array <(_apparix_compgen_bm "$tag" | \
xargs -d $'\n' printf "%q\n")
COMPREPLY=( "${goedel_array[@]}" )
else
local cur_file="${COMP_WORDS[2]}"
local app_dir="$(apparish "$tag" 2>/dev/null)"
if [[ -d "$app_dir" ]]; then
# can't run in subshell as _apparix_comp_file modifies COMREPLY.
# Just hope that nothing goes wrong, basically
silent pushd -- "$app_dir"
_apparix_comp_file "$1" "$cur_file"
silent popd
else
COMPREPLY=()
fi
fi
return 0
}
# register completions
# nospace prevents bash putting a space after partially completed paths
# nosort prevents bash from messing up the bespoke order in which bookmarks
# are completed
if version_assert 4 4 0; then
complete -o nospace -o nosort -F _apparix_comp \
"${APPARIX_FILE_FUNCTIONS[@]}" "${APPARIX_DIR_FUNCTIONS[@]}"
else
# >&2 echo "(Apparish: Can't disable alphabetic sorting of completions)"
complete -o nospace -F _apparix_comp \
"${APPARIX_FILE_FUNCTIONS[@]}" "${APPARIX_DIR_FUNCTIONS[@]}"
fi
elif [[ -n "$ZSH_VERSION" ]]; then
# Use zsh's completion system, as this seems a lot more robust, rather
# than using bashcompinit to reuse the bash code but really this wasn't
# a hassle to write
autoload -Uz compinit
compinit
function _apparix_file() {
IFS=$'\n'
_arguments \
'1:mark:($(cut -d, -f2 "$APPARIXRC" "$APPARIXEXPAND"))' \
'2:file:_path_files -W "$(apparish "$words[2]" 2>/dev/null)"'
}
function _apparix_directory() {
IFS=$'\n'
_arguments \
'1:mark:($(cut -d, -f2 "$APPARIXRC" "$APPARIXEXPAND"))' \
'2:file:_path_files -/W "$(apparish "$words[2]" 2>/dev/null)"'
}
compdef _apparix_file "${APPARIX_FILE_FUNCTIONS[@]}"
compdef _apparix_directory "${APPARIX_DIR_FUNCTIONS[@]}"
else
>&2 echo "Apparish: I don't know how to generate completions"
fi