Skip to content

Instantly share code, notes, and snippets.

@goedel-gang
Last active May 25, 2019 14:45
Show Gist options
  • Save goedel-gang/fe51f674e36523fe2a0351e366c6da0e to your computer and use it in GitHub Desktop.
Save goedel-gang/fe51f674e36523fe2a0351e366c6da0e to your computer and use it in GitHub Desktop.
source bourne-apparish
# shellcheck disable=SC2155,SC2181,SC2016 shell=bash
# vim: ft=sh ts=4 sw=0 sts=-1 et
# 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
# Vim modeline to try and keep the indentation in check.
# FIGMENTIZE: apparish
# .__ .__
# _____ ______ ______ _____ _______ |__| ______| |__
# \__ \ \____ \ \____ \\__ \ \_ __ \| | / ___/| | \
# / __ \_| |_> >| |_> >/ __ \_| | \/| | \___ \ | Y \
# (____ /| __/ | __/(____ /|__| |__|/____ >|___| /
# \/ |__| |__| \/ \/ \/
# bookmarks for the command line with comprehensive tab completion on target
# content
# works for bash and zsh
#
# Quick Guide:
# - save this file in $HOME/.bourne-apparish
# - issue 'source $HOME/.bourne-apparish'
# - issue 'apparix-init'
# - 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.
#
# 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-init and 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.
#
# apparix-init initialise apparix (needed only once)
# ---
# 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. If you use zsh, you may need to additionally
# put the lines
#
# autoload -Uz compinit
# compinit
#
# (without outcommenting them as above) *BEFORE* the line where you source
# $HOME/.bourne-apparish. This is not the case for example if you use
# oh-my-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.
#
# TODO: allow commas and newlines in directory names
# TODO: maybe write these as scripts with shebangs, and keep shell functions to
# very hollow wrapper, to prevent the constant checks for bash/zsh and make it
# easier to extend to other shells.
APPARIXHOME="${APPARIXHOME:=$HOME}"
APPARIXRC="${APPARIXRC:=$APPARIXHOME/.apparixrc}"
APPARIXEXPAND="${APPARIXEXPAND:=$APPARIXHOME/.apparixexpand}"
APPARIXLOG="${APPARIXLOG:=$APPARIXHOME/.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='vi "$APPARIXRC"'
else
>&2 echo "Apparish: not aliasing via"
fi
function apparix-init() {
already=""
if [[ -e "$APPARIXRC" && -e "$APPARIXEXPAND" ]]; then
already=" already"
fi
true >> "$APPARIXRC"
true >> "$APPARIXEXPAND"
echo "Apparish is up and running$already"
}
function apparish() {
if [[ 0 == "$#" ]]; then
cat -- "$APPARIXRC" "$APPARIXEXPAND" | tr ', ' '\t_' | column -t
return
fi
local mark="$1"
local list="$(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"
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="$(wc -l <<< "$list")"
listtail="$(tail -n 2 <<< "$list")"
ellipsis=""
if (( listsize > 2 )); then ellipsis="\n(...)"; fi
if (( listsize > 0 )); then
echo -e "Bookmark $mark exists" \
"($listsize total):$ellipsis\n$listtail"
fi
echo "$PWD (added)"
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"
grep '^e,' -- "$APPARIXRC" | cut -f 2 -d , | \
while read -r parentdir; do
# This unfortunately depends on GNU find's printf
# could maybe be rewritten in a hackier way to remove the ./
# prefixes.
# find . -maxdepth 1 -mindepth 1 -type d -printf "%f\0" | \
# while IFS='' read -r -d '' subdir; do
# echo "j,$subdir,$parentdir/$subdir" >> "$APPARIXEXPAND"
# done
# this might break if you have more directories than ARGMAX
# 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() {
if [[ 2 == "$#" ]]; then
loc="$(apparish "$1" "$2")"
elif [[ 1 == "$#" ]]; then
loc="$(apparish "$1")"
fi
if [[ "$?" == 0 ]]; then
ls "$loc"
fi
}
# apparix search bookmark
function amibm() {
grep -- ",$(grepsanitise "$PWD")$" "$APPARIXRC" | \
cut -f 2 -d ',' | paste -s -d ' ' -
}
# apparix search bookmark
function bmgrep() {
pat="${1?Need a pattern to search}"
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
apparix-init Use one time after installing apparix
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 izaak_array < "$1"
}
elif silent version_assert 3 0 0; then
function read_array() {
izaak_array=()
while IFS='' read -r line; do
izaak_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
izaak_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
# Directories (add -S / for slash separator):
compgen -d -- "$cur_file"
elif elemOf "$caller" "${APPARIX_FILE_FUNCTIONS[@]}"; then
# complete on filenames. this is a little harder to do nicely.
_all_files_compgen "$cur_file"
else
>&2 echo "Unknown caller: Izaak has probably messed something up"
return 1
fi
}
# generate completions for a bookmark
# this is currently case sensitive. Good? Bad? Who knows!
function _apparix_compgen_bm() {
cut -f2 -d, -- "$APPARIXRC" "$APPARIXEXPAND" | sort |\
\grep -i -- "^$(grepsanitise "$1")"
if [[ -n "$1" ]]; then
cut -f2 -d, -- "$APPARIXRC" "$APPARIXEXPAND" | sort |\
\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")
COMPREPLY=( "${izaak_array[@]}" )
else
local cur_file="${COMP_WORDS[2]}"
local app_dir="$(apparish "$tag" 2>/dev/null)"
if [[ -d "$app_dir" ]]; then
# run in subshell so cd isn't permanent
read_array <(cd -- "$app_dir" &&
_apparix_comp_file "$1" "$cur_file")
COMPREPLY=( "${izaak_array[@]}" )
else
COMPREPLY=()
fi
fi
if (( ${#COMPREPLY[@]} > 0 )); then
# The line below makes all known cases with spaces in directory
# names work.
read_array <(printf "%q\n" "${COMPREPLY[@]}")
COMPREPLY=( "${izaak_array[@]}" )
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
function _apparix_file() {
_arguments \
'1:mark:_values "\n" $(cut -d, -f2 "$APPARIXRC" "$APPARIXEXPAND")' \
'2:file:_path_files -W "$(apparish "$words[2]" 2>/dev/null)"'
}
function _apparix_directory() {
_arguments \
'1:mark:_values "\n" $(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
TAB: menu-complete
set show-all-if-ambiguous on
.PHONY: bash
SHELL = bash
APPARIX_ENV = APPARIXRC=$(CURDIR)/apparixrc APPARIXEXPAND=$(CURDIR)/apparixexpand
BASH_ENV = INPUTRC=inputrc
all: bash
apparixrc:
$(APPARIX_ENV) bash -c "source bourne-apparish; apparix-init"
apparixexpand: apparixrc
animal_facts:
mkdir -p $@
$(APPARIX_ENV) bash -c "source bourne-apparish; cd $@; bm otters"
dj_khaled:
mkdir -p $@
$(APPARIX_ENV) bash -c "source bourne-apparish; cd $@; bm another"
ricegum:
mkdir -p $@
$(APPARIX_ENV) bash -c "source bourne-apparish; cd $@; bm irrelevant"
bash: apparixrc apparixexpand animal_facts dj_khaled ricegum
bash --version
$(BASH_ENV) $(APPARIX_ENV) bash --rcfile bashrc
clean:
rmdir */
rm -f apparixrc apparixexpand
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment