#!/bin/bash set +vx -o pipefail [[ $- = *i* ]] && echo "Don't source this script!" && return 1 VERSION='0.1a' # tldr-bash-client version 0.1a # Bash client for tldr: community driven man-by-example # - forked from Ray Lee, http://github.com/raylee/tldr # - modified and expanded by pepa65: http://github.com/pepa65/tldr-bash-client # Requiring: coreutils, less, grep, unzip, curl/wget # The 5 elements in TLDR markup that can be styled with these colors and # backgrounds (last one specified will be used) and modes (more can apply): # Colors: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White # BG: BlackBG, RedBG, GreenBG, YellowBG, BlueBG, MagentaBG, CyanBG, WhiteBG # Modes: Bold, Underline, Italic, Inverse # 'Newline' can be added to the style list to add a newline before the element # and 'Space' to add a space at the start of the line (not for Value element) # (The style items are separated by space, lower or uppercase mixed allowed.) : ${TLDR_TITLE_STYLE:= Newline Space Bold Yellow } : ${TLDR_DESCRIPTION_STYLE:= Space Yellow } : ${TLDR_EXAMPLE_STYLE:= Newline Space Bold Green } : ${TLDR_CODE_STYLE:= Space Bold Blue } : ${TLDR_VALUE_STYLE:= Space Bold Cyan } # Color and/or background (Newline and Space also allowed) for error messages : ${TLDR_ERROR_COLOR:= Space Red } # How long before an attempt will be made to re-download a page : ${TLDR_EXPIRY:= 60 } Usage(){ # $1: optional exit code SELF=$(basename "$0") local exit=${1:-0} Out "$(cat <<-EOF $version USAGE: $GRE$B$SELF$XB$DEF [$YEL${B}option$XB$DEF] [$BLU${B}platform$XB/$DEF]$CYA$B$XB$DEF $BLU$B platform$XB/$CYA${B}command$XB$DEF: Show page for$CYA$B command$XB$DEF (from$BLU$B platform$XB$DEF) $BLU$B platform$XB$DEF is optionally one of:$YEL common$DEF,$YEL linux$DEF,$YEL osx$DEF,$YEL sunos$DEF $YEL$B option$XB$DEF is optionally one of: $B$YEL-l$DEF,$YEL --list$DEF $BLU[platform]$DEF$XB: Show all available pages (from$BLU$B platform$XB$DEF) $B$YEL-r$DEF,$YEL --render$DEF$XB $MAG$B$XB$DEF: Render a local$B$MAG file$DEF$XB as tldr markdown $B$YEL-m$DEF,$YEL --markdown$DEF$XB $CYA$B$XB$DEF: Show the markdown source for$B$CYA command$DEF$XB $B$YEL-c$DEF,$YEL --cache$DEF$XB: Cache all pages by downloading archive from repo $B$YEL-u$DEF,$YEL --update$DEF$XB: Re-download index file from repo $B$YEL-v$DEF,$YEL --version$DEF$XB: Version number and repo location [$B$YEL-h$DEF,$YEL -?$DEF,$YEL --help$DEF$XB]: This help overview Element styling:$T Title$XT$D Description$XD$E Example$XD$C Code$XC$V Value$XV All pages and the index are cached locally under $YEL$configdir$DEF. By default, the cached copies will be re-downloaded after $YEL${TLDR_EXPIRY// /}$DEF days. EOF )" exit "$exit" } Err(){ STDERR+=$ERRNL$ERRSP$ERR$B$1$XB$XERR$N;} # $1: keep error messages Out(){ STDOUT+=$1$N;} # $1: keep output Style(){ # $1: Style specification local style STYLES='' XSTYLES='' COLOR='' XCOLOR='' NL='' SP='' for style in $1 do [[ ${style,,} = newline ]] && NL=$N [[ ${style,,} = space ]] && SP=' ' COLOR+=${color[${style,,}]:-}${bg[${style,,}]:-} XCOLOR=${xbg[${style,,}]:-}${xcolor[${style,,}]:-}$XCOLOR STYLES+=${color[${style,,}]:-}${bg[${style,,}]:-}${mode[${style,,}]:-} XSTYLES=${xmode[${style,,}]:-}${xbg[${style,,}]:-}${xcolor[${style,,}]:-}$XSTYLES done } Init_term(){ [[ -t 2 ]] && { # only if interactive session (stderr open) B=$'\e[1m' # $(tput bold || tput md) # Start bold XB=$'\e[0m' # End bold (no tput code...) U=$'\e[4m' # $(tput smul || tput us) # Start underline XU=$'\e[24m' # $(tput rmul || tput ue) # End underline I=$'\e[3m' # $(tput sitm || tput ZH) # Start italic XI=$'\e[23m' # $(tput ritm || tput ZR) # End italic R=$'\e[7m' # $(tput smso || tput so) # Start reverse XR=$'\e[27m' # $(tput rmso || tput se) # End reverse X=$'\e[0m' # $(tput sgr0 || tput me) # End all [[ $TERM != *-m ]] && { BLA=$'\e[30m' # $(tput setaf 0 || tput AF 0) RED=$'\e[31m' # $(tput setaf 1 || tput AF 1) GRE=$'\e[32m' # $(tput setaf 2 || tput AF 2) YEL=$'\e[33m' # $(tput setaf 3 || tput AF 3) BLU=$'\e[34m' # $(tput setaf 4 || tput AF 4) MAG=$'\e[35m' # $(tput setaf 5 || tput AF 5) CYA=$'\e[36m' # $(tput setaf 6 || tput AF 6) WHI=$'\e[37m' # $(tput setaf 7 || tput AF 7) DEF=$'\e[39m' # $(tput op) BLAB=$'\e[40m' # $(tput setab 0 || tput AB 0) REDB=$'\e[41m' # $(tput setab 1 || tput AB 1) GREB=$'\e[42m' # $(tput setab 2 || tput AB 2) YELB=$'\e[43m' # $(tput setab 3 || tput AB 3) BLUB=$'\e[44m' # $(tput setab 4 || tput AB 4) MAGB=$'\e[45m' # $(tput setab 5 || tput AB 5) CYAB=$'\e[46m' # $(tput setab 6 || tput AB 6) WHIB=$'\e[47m' # $(tput setab 7 || tput AB 7) DEFB=$'\e[49m' # $(tput op) } } declare -A color=(['black']=$BLA ['red']=$RED ['green']=$GRE ['yellow']=$YEL \ ['blue']=$BLU ['magenta']=$MAG ['cyan']=$CYA ['white']=$WHI) declare -A xcolor=(['black']=$DEF ['red']=$DEF ['green']=$DEF ['yellow']=$DEF \ ['blue']=$DEF ['magenta']=$DEF ['cyan']=$DEF ['white']=$DEF) declare -A bg=(['blackbg']=$BLAB ['redbg']=$REDB ['greenbg']=$GREB ['yellowbg']=$YELB \ ['bluebg']=$BLUB ['magentabg']=$MAGB ['cyanbg']=$CYAB ['whitebg']=$WHIB) declare -A xbg=(['blackbg']=$DEFB ['redbg']=$DEFB ['greenbg']=$DEFB ['yellowbg']=$DEFB \ ['bluebg']=$DEFB ['magentabg']=$DEFB ['cyanbg']=$DEFB ['whitebg']=$DEFB) declare -A mode=(['bold']=$B ['underline']=$U ['italic']=$I ['inverse']=$R) declare -A xmode=(['bold']=$XB ['underline']=$XU ['italic']=$XI ['inverse']=$XR) # the 5 main tldr page styles and error message colors Style "$TLDR_TITLE_STYLE" T=$STYLES XT=$XSTYLES TNL=$NL TSP=$SP Style "$TLDR_DESCRIPTION_STYLE" D=$STYLES XD=$XSTYLES DNL=$NL DSP=$SP Style "$TLDR_EXAMPLE_STYLE" E=$STYLES XE=$XSTYLES ENL=$NL ESP=$SP Style "$TLDR_CODE_STYLE" C=$STYLES XC=$XSTYLES CNL=$NL CSP=$SP Style "$TLDR_VALUE_STYLE" V=$STYLES XV=$XSTYLES Style "$TLDR_ERROR_COLOR" ERR=$COLOR XERR=$XCOLOR ERRNL=$NL ERRSP=$SP } Recent(){ find "$1" -mtime -"${TLDR_EXPIRY// /}" &>/dev/null;} # $1: page Update_index(){ # Download index.json $DL "$index" "$index_url" && Out "${GRE}Index file $I$index$XI re-downloaded$DEF" || { Err "Could not download index from $I$index_url$XI" exit 2 } } Config(){ # Initialize globals, check the environment PLATFORM=common STDOUT='' STDERR='' Q='"' N=$'\n' case "$(uname -s)" in Darwin) PLATFORM='osx' ;; Linux) PLATFORM='linux' ;; SunOS) PLATFORM='sunos' ;; esac Init_term trap 'less -RXQFP"Press Q to exit " <<<"$STDOUT$STDERR"' EXIT version="$GRE$B tldr-bash-client version $VERSION$XB $YEL http://github.com/pepa65/tldr-bash-client$DEF" # Select download method DL="$(type -p curl) -sfo" || { DL="$(type -p wget) -qNO" || { Err "tldr requires$I curl$XI or$I wget $XI installed in your path" exit 3 } } base_url='https://raw.githubusercontent.com/tldr-pages/tldr/master/pages' zip_url='http://tldr-pages.github.io/assets/tldr.zip' index_url='http://tldr-pages.github.io/assets/index.json' [[ -d ~/.config ]] && configdir=~/.config/tldr || configdir=~/.tldr [[ -d "$configdir" ]] || mkdir -p "$configdir" index=$configdir/index.json # update if the file doesn't exists, or if it's older than $TLDR_EXPIRY [[ -f $index ]] && Recent "$index" || Update_index } Unlinted(){ # $1: error message Err "Page $I$PAGE$XI not properly linted!\nLine $I$L$XI:[$U$REPLY$XU]$N$ERR$B$1" exit 4 } Get_tldr(){ # $1: page # convert the local platform name to tldr's version # extract the platform key from index.json, return preferred subpath to page local desc=$(tr '{' '\n' <$index |grep "\"name\":\"$1\"") # results in, eg, "name":"netstat","platform":["linux","osx"]}, [[ $desc ]] || return # just not found local err=0 if [[ $platform ]] then # platform given on commandline [[ ! $desc =~ \"$platform\" ]] && notfound=$I$platform$XI && err=1 || PAGE=$platform/$1.md else # check common [[ $desc =~ \"common\" ]] && PAGE=common/$1.md || { # not in common either [[ $notfound ]] && notfound+=" or " notfound+=${I}common$XI } fi # if no page found yet, try the system platform [[ $PAGE ]] || [[ $platform = "$PLATFORM" ]] || { [[ $desc =~ \"$PLATFORM\" ]] && PAGE=$PLATFORM/$1.md } || { notfound+=" or $I$PLATFORM$XI" err=1 } # if still no page found, get the first entry in index [[ $PAGE ]] || PAGE=$(cut -d "$Q" -f 8 <<<"$desc")/"$1.md" ((err)) && Err "tldr page $I$1$XI not found in $notfound, page from platform $U${PAGE%/*}$XU instead" # return the local cached copy of the tldrpage, or retrieve and cache from github CACHED=$configdir/$PAGE Recent "$CACHED" || { mkdir -p "${CACHED%/*}" $DL "$CACHED" "$base_url/$PAGE" || Err "Could not download page $I$CACHED$XI from index $U$index_url$XU" } } Display_tldr(){ # read one line at a time, don't strip whitespace ('IFS='), and # process last line even if it doesn't have a newline at the end L=0 local newfmt len line while read -r || [[ $REPLY ]] do ((++L)) ((L==1)) && { [[ ${REPLY:0:1} = '#' ]] && newfmt=0 || newfmt=1 ((newfmt)) && { [[ $REPLY ]] || Unlinted "No title" Out "$TNL$TSP$T$REPLY$XT" len=${#REPLY} read -r; ((++L)) [[ $REPLY =~ [^=] ]] && Unlinted "Title underline must be equal signs" ((len!=${#REPLY})) && Unlinted "Underline length not equal to title's" read -r; ((++L)) } } case "${REPLY:0:1}" in # first character '#') ((newfmt)) && Unlinted "Bad first character" ((${#REPLY} <= 2)) && Unlinted "No title" [[ ! ${REPLY:1:1} = ' ' ]] && Unlinted "2nd character no space" Out "$TNL$TSP$T${REPLY:2}$XT" ;; '>') ((${#REPLY} <= 3)) && Unlinted "No valid desciption" [[ ! ${REPLY:1:1} = ' ' ]] && Unlinted "2nd character no space" [[ ! ${REPLY: -1} = '.' ]] && Unlinted "Description doesn't end in full stop" Out "$DNL$DSP$D${REPLY:2}$XD" DNL='' ;; '-') ((newfmt)) && Unlinted "Bad first character" ((${#REPLY} <= 2)) && Unlinted "No example content" [[ ! ${REPLY:1:1} = ' ' ]] && Unlinted "2nd character no space" Out "$ENL$ESP$E${REPLY:2}$XE" ;; ' ') ((newfmt)) || Unlinted "Bad first character" ((${#REPLY} <= 4)) && Unlinted "No valid code content" [[ ${REPLY:0:4} = ' ' ]] || Unlinted "No four spaces before code" line=${REPLY:4} # Value: convert {{value}} line=${line//\{\{/$CX$V} line=${line//\}\}/$XV$C} Out "$CNL$CSP$C$line$XC" ;; '`') ((newfmt)) && Unlinted "Bad first character" ((${#REPLY} <= 2)) && Unlinted "No valid code content" [[ ! ${REPLY: -1} = '`' ]] && Unlinted "Code doesn't end in backtick" line=${REPLY:1:-1} # Value: convert {{value}} line=${line//\{\{/$CX$V} line=${line//\}\}/$XV$C} Out "$CNL$CSP$C$line$XC" ;; '') continue ;; *) ((newfmt)) || Unlinted "Bad first character" [[ -z $REPLY ]] && Unlinted "No example content" Out "$ENL$EPS$E$REPLY$XE" ;; esac done trap 'less +Gg -RXQFP"%pB\% tldr $I$page$XI - press Q to exit" <<<"$STDOUT$STDERR"' EXIT } List_pages(){ # $1: exit code local platformtext [[ $platform ]] && platformtext=" from platform $I$platform$XI" Out "${GRE}Known tldr pages$platformtext:" Out "$(while read -r c1 c2 c3; do printf "%-19s %-19s %-19s %-19s$N" "$c1" "$c2" "$c3"; done \ <<<$(tr '{' '\n' <"$index" |grep "$platform" |cut -d "$Q" -f4))" exit "$1" } Cache_fill(){ # $1: exit code local tmp=$(mktemp -d) $DL "$tmp/pages.zip" "$zip_url" || { rm -- "$tmp" Err "Could not download pages archive from $U$zip_url$XU" exit 6 } unzip="$(type -p unzip) -q" || { rm -- "$tmp" Err "Unzip is necessary to fill the cache" exit 7 } $unzip "$tmp/pages.zip" -d "$tmp" 'pages/*' rm -rf -- "${configdir:?}/"* mv -- "$tmp/pages/"* "${configdir:?}/" rm -rf -- "$tmp" Out "${GRE}Pages cached in $U$configdir$XU$DEF" exit "$1" } Config markdown=0 err=0 arg=$1 case "$arg" in -l|--list) [[ $2 ]] && { platform=$2 [[ ,common,linux,osx,sunos, = *,$platform,* ]] || { Err "Unknown platform $I$platform$XI" Usage 8 } [[ $3 ]] && Err "No more command line arguments allowed" && err=9 } List_pages "$err" ;; -c|--cache) [[ $2 ]] && Err "No more command line arguments allowed" && err=10 Cache_fill "$err" ;; -v|--version) [[ $2 ]] && Err "No more command line arguments allowed" && err=11 Out "$version" exit "$err" ;; -u|--update) [[ $2 ]] && Err "No more command line arguments allowed" && err=12 Update_index exit "$err" ;; -r|--render) [[ -z $2 ]] && Err "Specify a file to render" && Usage 13 [[ $3 ]] && Err "No more command line arguments allowed" && err=14 [[ -f "$2" ]] && { Display_tldr <"$2" && exit "$err" Err "A file error occured" exit 15 } || Err "No file:$I $2$XI" && exit 16 ;; -m|--markdown) shift page=$* [[ -z $page ]] && Err "Specify a page to display" && Usage 17 [[ -f "$page" && ${page: -3:3} = .md ]] && Out "$(cat "$page")" && exit 0 markdown=1 ;; ''|-h|-\?|--help) [[ $2 ]] && Err "No more command line arguments allowed" && err=18 Usage "$err" ;; -*) Err "Unrecognized option $I$1$XI"; Usage 19 ;; *) page=$* ;; esac [[ -z $page ]] && Err "No command specified" && Usage 20 [[ $page =~ ' -' || ${page:0:1} = '-' ]] && Err "Only one option allowed" && Usage 21 [[ $page = */* ]] && platform=${page%/*} && page=${page##*/} [[ $platform && ,common,linux,osx,sunos, != *,$platform,* ]] && { Err "Unknown platform $I$platform$XI" Usage 22 } Get_tldr "${page// /-}" [[ ! -s $CACHED ]] && Err "tldr page for command $I$page$XI not found" && exit 23 ((markdown)) && Out "$(cat "$CACHED")" || Display_tldr <"$CACHED" # The error trap will output the accumulated stdout and stderr exit 0