#!/usr/bin/env bash # vim: set filetype=sh: PREFIX="${DESK_DIR:-$HOME/.desk}" DESKS="${DESK_DESKS_DIR:-$PREFIX/desks}" DESKFILE_NAME=Deskfile ## Commands cmd_version() { echo "◲ desk 0.6.0" } cmd_usage() { cmd_version echo cat <<_EOF Usage: $PROGRAM List the current desk and any associated aliases. If no desk is being used, display available desks. $PROGRAM init Initialize desk configuration. $PROGRAM (list|ls) List all desks along with a description. $PROGRAM (.|go) [ [shell-args...]] Activate a desk. Extra arguments are passed onto shell. If called with no arguments, look for a Deskfile in the current directory. If not a recognized desk, try as a path to directory containing a Deskfile. $PROGRAM run Run a command within a desk's environment then exit. Think '\$SHELL -c'. $PROGRAM edit [desk-name] Edit (or create) a deskfile with the name specified, otherwise edit the active deskfile. $PROGRAM help Show this text. $PROGRAM version Show version information. Since desk spawns a shell, to deactivate and "pop" out a desk, you simply need to exit or otherwise end the current shell process. _EOF } cmd_init() { if [ -d "$PREFIX" ]; then echo "Desk dir already exists at ${PREFIX}" exit 1 fi read -p "Where do you want to store your deskfiles? (default: ${PREFIX}): " \ NEW_PREFIX [ -z "${NEW_PREFIX}" ] && NEW_PREFIX="$PREFIX" if [ ! -d "${NEW_PREFIX}" ]; then echo "${NEW_PREFIX} doesn't exist, attempting to create." mkdir -p "$NEW_PREFIX/desks" fi local SHELLTYPE=$(get_running_shell) case "${SHELLTYPE}" in bash) local SHELLRC="${HOME}/.bashrc" ;; fish) local SHELLRC="${HOME}/.config/fish/config.fish" ;; zsh) local SHELLRC="${HOME}/.zshrc" ;; esac read -p "Where's your shell rc file? (default: ${SHELLRC}): " \ USER_SHELLRC [ -z "${USER_SHELLRC}" ] && USER_SHELLRC="$SHELLRC" if [ ! -f "$USER_SHELLRC" ]; then echo "${USER_SHELLRC} doesn't exist" exit 1 fi printf "\n# Hook for desk activation\n" >> "$USER_SHELLRC" # Since the hook is appended to the rc file, its exit status becomes # the exit status of `source $USER_SHELLRC` which typically # indicates if something went wrong. If $DESK_ENV is void, `test` # sets exit status to 1. That, however, is part of desk's normal # operation, so we clear exit status after that. if [ "$SHELLTYPE" == "fish" ]; then echo "test -n \"\$DESK_ENV\"; and . \"\$DESK_ENV\"; or true" >> "$USER_SHELLRC" else echo "[ -n \"\$DESK_ENV\" ] && source \"\$DESK_ENV\" || true" >> "$USER_SHELLRC" fi echo "Done. Start adding desks to ${NEW_PREFIX}/desks!" } cmd_go() { # TODESK ($1) may either be # # - the name of a desk in $DESKS/ # - a path to a Deskfile # - a directory containing a Deskfile # - empty -> `./Deskfile` # local TODESK="$1" local DESKEXT=$(get_deskfile_extension) local DESKPATH="$(find "${DESKS}/" -name "${TODESK}${DESKEXT}" 2>/dev/null)" local POSSIBLE_DESKFILE_DIR="${TODESK%$DESKFILE_NAME}" if [ -z "$POSSIBLE_DESKFILE_DIR" ]; then POSSIBLE_DESKFILE_DIR="." fi # If nothing could be found in $DESKS/, check to see if this is a path to # a Deskfile. if [[ -z "$DESKPATH" && -d "$POSSIBLE_DESKFILE_DIR" ]]; then if [ ! -f "${POSSIBLE_DESKFILE_DIR}/${DESKFILE_NAME}" ]; then echo "No Deskfile found in '${POSSIBLE_DESKFILE_DIR}'" exit 1 fi local REALPATH=$( cd $POSSIBLE_DESKFILE_DIR && pwd ) DESKPATH="${REALPATH}/${DESKFILE_NAME}" TODESK=$(basename "$REALPATH") fi # Shift desk name so we can forward on all arguments to the shell. shift; if [ -z "$DESKPATH" ]; then echo "Desk $TODESK (${TODESK}${DESKEXT}) not found in $DESKS" exit 1 else local SHELL_EXEC="$(get_running_shell)" DESK_NAME="${TODESK}" DESK_ENV="${DESKPATH}" exec "${SHELL_EXEC}" "$@" fi } cmd_run() { local TODESK="$1" shift; cmd_go "$TODESK" -ic "$@" } # Usage: desk list [options] # Description: List all desks along with a description # --only-names: List only the names of the desks # --no-format: Use ' - ' to separate names from descriptions cmd_list() { if [ ! -d "${DESKS}/" ]; then echo "No desk dir! Run 'desk init'." exit 1 fi local SHOW_DESCRIPTIONS DESKEXT AUTO_ALIGN name desc len longest out while [[ $# -gt 0 ]]; do case "$1" in --only-names) SHOW_DESCRIPTIONS=false && AUTO_ALIGN=false ;; --no-format) AUTO_ALIGN=false ;; esac shift done DESKEXT=$(get_deskfile_extension) out="" longest=0 while read -d '' -r f; do name=$(basename "${f/${DESKEXT}//}") if [[ "$SHOW_DESCRIPTIONS" = false ]]; then out+="$name"$'\n' else desc=$(echo_description "$f") out+="$name - $desc"$'\n' len=${#name} (( len > longest )) && longest=$len fi done < <(find "${DESKS}/" -name "*${DESKEXT}" -print0) if [[ "$AUTO_ALIGN" != false ]]; then print_aligned "$out" "$longest" else printf "%s" "$out" fi } # Usage: desk [options] # Description: List the current desk and any associated aliases. If no desk is being used, display available desks # --no-format: Use ' - ' to separate alias/export/function names from their descriptions cmd_current() { if [ -z "$DESK_ENV" ]; then printf "No desk activated.\n\n%s" "$(cmd_list)" exit 1 fi local DESKPATH=$DESK_ENV local CALLABLES=$(get_callables "$DESKPATH") local AUTO_ALIGN len longest out while [[ $# -gt 0 ]]; do case "$1" in --no-format) AUTO_ALIGN=false ;; esac shift done printf "%s - %s\n" "$DESK_NAME" "$(echo_description "$DESKPATH")" if [[ -n "$CALLABLES" ]]; then longest=0 out=$'\n' for NAME in $CALLABLES; do # Last clause in the grep regexp accounts for fish functions. len=$((${#NAME} + 2)) (( len > longest )) && longest=$len local DOCLINE=$( grep -B 1 -E \ "^(alias ${NAME}=|export ${NAME}=|(function )?${NAME}( )?\()|function $NAME" "$DESKPATH" \ | grep "#") if [ -z "$DOCLINE" ]; then out+=" ${NAME}"$'\n' else out+=" ${NAME} - ${DOCLINE##\# }"$'\n' fi done if [[ "$AUTO_ALIGN" != false ]]; then print_aligned "$out" "$longest" else printf "%s" "$out" fi fi } cmd_edit() { if [ $# -eq 0 ]; then if [ "$DESK_NAME" == "" ]; then echo "No desk activated." exit 3 fi local DESKNAME_TO_EDIT="$DESK_NAME" elif [ $# -eq 1 ]; then local DESKNAME_TO_EDIT="$1" else echo "Usage: ${PROGRAM} edit [desk-name]" exit 1 fi local DESKEXT=$(get_deskfile_extension) local EDIT_PATH="${DESKS}/${DESKNAME_TO_EDIT}${DESKEXT}" ${EDITOR:-vi} "$EDIT_PATH" } ## Utilities FNAME_CHARS='[a-zA-Z0-9_-]' # Echo the description of a desk. $1 is the deskfile. echo_description() { local descline=$(grep -E "#\s+Description" "$1") echo "${descline##*Description: }" } # Echo a list of aliases, exports, and functions for a given desk get_callables() { local DESKPATH=$1 grep -E "^(alias |export |(function )?${FNAME_CHARS}+ ?\()|function $NAME" "$DESKPATH" \ | sed 's/alias \([^= ]*\)=.*/\1/' \ | sed 's/export \([^= ]*\)=.*/\1/' \ | sed -E "s/(function )?(${FNAME_CHARS}+) ?\(\).*/\2/" \ | sed -E "s/function (${FNAME_CHARS}+).*/\1/" } get_running_shell() { # Echo the name of the parent shell via procfs, if we have it available. # Otherwise, try to use ps with bash's parent pid. if [ -e /proc ]; then # Get cmdline procfile of the process running this script. local CMDLINE_FILE="/proc/$(grep PPid /proc/$$/status | cut -f2)/cmdline" if [ -f "$CMDLINE_FILE" ]; then # Strip out any verion that may be attached to the shell executable. # Strip leading dash for login shells. local CMDLINE_SHELL=$(sed -r -e 's/\x0.*//' -e 's/^-//' "$CMDLINE_FILE") fi else # Strip leading dash for login shells. # If the parent process is a login shell, guess bash. local CMDLINE_SHELL=$(ps -o args -p $PPID | tail -1 | sed -e 's/login/bash/' -e 's/^-//') fi if [ ! -z "$CMDLINE_SHELL" ]; then basename "$CMDLINE_SHELL" exit fi # Fall back to $SHELL otherwise. basename "$SHELL" exit } # Echo the extension of recognized deskfiles (.fish for fish) get_deskfile_extension() { if [ "$(get_running_shell)" == "fish" ]; then echo '.fish' else echo '.sh' fi } # Echo first argument as key/value pairs aligned into two columns; second argument is the longest key print_aligned() { local out=$1 longest=$2 printf "%s" "$out" | awk -v padding="$longest" -F' - ' '{ printf "%-*s %s\n", padding, $1, substr($0, index($0, " - ")+2, length($0)) }' } PROGRAM="${0##*/}" case "$1" in init) shift; cmd_init "$@" ;; help|--help) shift; cmd_usage "$@" ;; version|--version) shift; cmd_version "$@" ;; ls|list) shift; cmd_list "$@" ;; go|.) shift; cmd_go "$@" ;; run) shift; cmd_run "$@" ;; edit) shift; cmd_edit "$@" ;; *) cmd_current "$@" ;; esac exit 0