Permalink
Cannot retrieve contributors at this time
# 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 |