diff --git a/README.md b/README.md index 45c60ae..1acdcf2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ # tldr-bash-client -* version 0.3 -* https://github.com/pepa65/tldr-bash-client +* version 0.33 ### Bash client for tldr: community driven man-by-example **A fully-functional [bash](https://tiswww.case.edu/php/chet/bash/bashtop.html) client for the [tldr](https://github.com/tldr-pages/tldr) project, providing poignant examples of terminal commands.** -tldr list screenshot +tldr page gif This client can render both the old and the new tldr markup format. @@ -25,8 +24,8 @@ If the location is not in $PATH, you need to specify the path to run it. Alternately, you can do `sudo bpkg-install pepa65/tldr` if you have [bpkg](https://github.com/bpkg/bpkg) installed. -tldr page gif -[](tldr page screenshot) +tldr page screenshot +tldr list screenshot ### Prerequisites coreutils, less, grep, unzip, curl / wget @@ -73,6 +72,24 @@ Alternative location of pages cache tldr customize screenshot +# tldr-lint + +* version 0.11 + +### Linter for new syntax tldr source files + +``` +Usage: tldr-lint [-h|--help] [-V|--version] [-q|--quiet] [] [] + - All *.md files under and subdirectories are checked + - is checked regardless of extension + -q, --quiet: No output means check is OK + -V, --version: Display version + -h, --help: Display this help text +``` + +### Prerequisites +coreutils, sed, grep, find + ## Contributing Please file an issue for a question, a bug or a feature request. @@ -84,8 +101,8 @@ Or even better, send a pull request! ### License -Original client by Ray Lee http://github.com/raylee/tldr (MIT license) +Original tldr client in bash by Ray Lee http://github.com/raylee/tldr (MIT license) -Relicensed under GPLv3+ +The tldr-bash-client is relicensed under GPLv3+ and tldr-lint is GPLv3+ as well. tldr new markdown screenshot diff --git a/tldr b/tldr index 7934924..351eb46 100755 --- a/tldr +++ b/tldr @@ -1,8 +1,8 @@ #!/bin/bash set +vx -o pipefail [[ $- = *i* ]] && echo "Don't source this script!" && return 1 -version='0.3' -# tldr-bash-client version 0.3 +version='0.33' +# tldr-bash-client version 0.33 # 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 @@ -176,7 +176,7 @@ Recent(){ find "$1" -mtime -"${TLDR_EXPIRY// /}" >/dev/null 2>&1;} # Download index.json; Uses: index index_url base_url zip_url dl Update_index(){ $dl "$index" "$index_url" && Inf "Index file $I$index$XI freshly downloaded" || { - Err "Could not download index from $I$index_url$XI" + Err "Could not download index from $I$index_url$XI with $dl" exit 2 } } @@ -196,8 +196,8 @@ Config(){ version="tldr-bash-client version $version $XB$URL http://github.com/pepa65/tldr-bash-client$XURL" # Select download method - dl="$(type -p curl) -sfo" || { - dl="$(type -p wget) -qNO" || { + dl="$(type -p curl) -sLfo" || { + dl="$(type -p wget) --max-redirect=20 -qNO" || { Err "tldr requires$I curl$XI or$I wget $XI installed in your path" exit 3 } @@ -221,10 +221,10 @@ Config(){ [[ -f $index ]] && Recent "$index" || Update_index } -# $1: error message; Uses: md line ln +# $1: error message; Uses: md REPLY ln Unlinted(){ - Err "Page $I$md$XI not properly linted!$N${ERRSP}${ERR}Line $I$ln$XI [$XERR$U$line$XU$ERR]$N$ERRSP$ERR$1" - exit 4 + Err "Page $I$md$XI not properly linted!$N${ERRSP}${ERR}Line $I$ln$XI [$XERR$U$REPLY$XU$ERR]$N$ERRSP$ERR$1" + exit 5 } # $1: page; Uses: index index_url cachedir base_url platform os dl cached md @@ -238,10 +238,10 @@ Get_tldr(){ [[ $desc ]] || return # just not found - err=0 + error=0 if [[ $platform ]] then # platform given on commandline - [[ ! $desc =~ \"$platform\" ]] && notfound=$I$platform$XI && err=1 || md=$platform/$1.md + [[ ! $desc =~ \"$platform\" ]] && notfound=$I$platform$XI && error=1 || md=$platform/$1.md else # check common [[ $desc =~ \"common\" ]] && md=common/$1.md || { # not in common either [[ $notfound ]] && notfound+=" or " @@ -253,78 +253,80 @@ Get_tldr(){ [[ $desc =~ \"$os\" ]] && md=$os/$1.md } || { notfound+=" or $I$os$XI" - err=1 + error=1 } # if still no page found, get the first entry in index [[ $md ]] || md=$(cut -d "$Q" -f 8 <<<"$desc")/"$1.md" - ((err)) && Err "tldr page $I$1$XI not found in $notfound, from platform $U${md%/*}$XU instead" + ((error)) && Err "tldr page $I$1$XI not found in $notfound, from platform $U${md%/*}$XU instead" # return the local cached copy of the tldrpage, or retrieve and cache from github cached=$cachedir/$md Recent "$cached" || { mkdir -p "${cached%/*}" - $dl "$cached" "$base_url/$md" || Err "Could not download page $I$cached$XI from index $U$index_url$XU" + $dl "$cached" "$base_url/$md" || Err "Could not download page $I$cached$XI from index $U$index_url$XU with $dl" } } -# $1: text; Uses: page stdout; Sets: ln line +# $1: text; Uses: page stdout; Sets: ln REPLY Display_tldr(){ local newfmt len val - ln=0 line='' + ln=0 REPLY='' [[ $md ]] || md=$1 # Read full lines, and process even when no newline at the end - while read -r line || [[ $line ]] + while read -r || [[ $REPLY ]] do ((++ln)) ((ln==1)) && { - [[ ${line:0:1} = '#' ]] && newfmt=0 || newfmt=1 + [[ ${REPLY:0:1} = '#' ]] && newfmt=0 || newfmt=1 ((newfmt)) && { - [[ $line ]] || Unlinted "No title" - Out "$TNL$TSP$T$line$XT" - len=${#line} - read -r; ((++ln)) - [[ $line =~ [^=] ]] && Unlinted "Title underline must be equal signs" - ((len!=${#line})) && Unlinted "Underline length not equal to title's" - read -r; ((++ln)) + [[ $REPLY ]] || Unlinted "Empty title" + Out "$TNL$TSP$T$REPLY$XT" + len=${#REPLY} # title length + read -r + ((++ln)) + [[ $REPLY =~ [^=] ]] && Unlinted "Title underline must be equal signs" + ((len!=${#REPLY})) && Unlinted "Underline length not equal to title's" + read -r + ((++ln)) } } - case "${line:0:1}" in # first character + case "${REPLY:0:1}" in # first character '#') ((newfmt)) && Unlinted "Bad first character" - ((${#line} <= 2)) && Unlinted "No title" - [[ ! ${line:1:1} = ' ' ]] && Unlinted "2nd character no space" - Out "$TNL$TSP$T${line:2}$XT" ;; - '>') ((${#line} <= 3)) && Unlinted "No valid desciption" - [[ ! ${line:1:1} = ' ' ]] && Unlinted "2nd character no space" - [[ ! ${line: -1} = '.' ]] && Unlinted "Description doesn't end in full stop" - Out "$DNL$DSP$D${line:2}$XD" + ((${#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" - ((${#line} <= 2)) && Unlinted "No example content" - [[ ! ${line:1:1} = ' ' ]] && Unlinted "2nd character no space" - Out "$ENL$ESP$E${line:2}$XE" ;; + ((${#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" - ((${#line} <= 4)) && Unlinted "No valid code content" - [[ ${line:0:4} = ' ' ]] || Unlinted "No four spaces before code" - val=${line:4} + ((${#REPLY} <= 4)) && Unlinted "No valid code content" + [[ ${REPLY:0:4} = ' ' ]] || Unlinted "No four spaces before code" + val=${REPLY:4} # Value: convert {{value}} val=${val//\{\{/$CX$V} val=${val//\}\}/$XV$C} Out "$CNL$CSP$C$val$XC" ;; '`') ((newfmt)) && Unlinted "Bad first character" - ((${#line} <= 2)) && Unlinted "No valid code content" - [[ ! ${line: -1} = '`' ]] && Unlinted "Code doesn't end in backtick" - val=${line:1:-1} + ((${#REPLY} <= 2)) && Unlinted "No valid code content" + [[ ! ${REPLY: -1} = '`' ]] && Unlinted "Code doesn't end in backtick" + val=${REPLY:1:-1} # Value: convert {{value}} val=${val//\{\{/$CX$V} val=${val//\}\}/$XV$C} Out "$CNL$CSP$C$val$XC" ;; '') continue ;; *) ((newfmt)) || Unlinted "Bad first character" - [[ -z $line ]] && Unlinted "No example content" - Out "$ENL$EPS$E$line$XE" ;; + [[ -z $REPLY ]] && Unlinted "No example content" + Out "$ENL$ESP$E$REPLY$XE" ;; esac done <"$1" - trap 'less +Gg -~RXQFP"%pB\% tldr $I$page$XI - browse up/down, press Q to exit" <<<"$stdout"' EXIT + trap 'less -Gg -~RXQFP"%pB\% tldr $I$page$XI - browse up/down, press Q to exit" <<<"$stdout"' EXIT } # $1: exit code; Uses: platform index @@ -340,18 +342,21 @@ List_pages(){ # $1: exit code; Uses: dl cachedir zip_url Cache_fill(){ local tmp unzip + unzip="$(type -p unzip) -q" || { + Err "Unzip is necessary to fill the cache" + exit 6 + } 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" + Err "Could not download pages archive from $U$zip_url$XU with $dl" exit 7 } - $unzip "$tmp/pages.zip" -d "$tmp" 'pages/*' + $unzip "$tmp/pages.zip" -d "$tmp" 'pages/*' || { + rm -- "$tmp" + Err "Couldn't unzip the cache archive on $tmp/pages.zip" + exit 8 + } rm -rf -- "${cachedir:?}/"* mv -- "$tmp/pages/"* "${cachedir:?}/" rm -rf -- "$tmp" @@ -361,7 +366,7 @@ Cache_fill(){ # $@: commandline parameters; Uses: version cached; Sets: platform page Main(){ - local markdown err + local markdown err nomore='No more command line arguments allowed' Config markdown=0 err=0 case "$1" in @@ -369,48 +374,48 @@ Main(){ platform=$2 [[ ,common,linux,osx,sunos, = *,$platform,* ]] || { Err "Unknown platform $I$platform$XI" - Usage 8 + Usage 9 } - [[ $3 ]] && Err "No more command line arguments allowed" && err=9 + [[ $3 ]] && Err "$nomore" && err=10 } List_pages "$err" ;; - -c|--cache) [[ $2 ]] && Err "No more command line arguments allowed" && err=10 + -c|--cache) [[ $2 ]] && Err "$nomore" && err=11 Cache_fill "$err" ;; - -v|--version) [[ $2 ]] && Err "No more command line arguments allowed" && err=11 + -v|--version) [[ $2 ]] && Err "$nomore" && err=12 Inf "$version" exit "$err" ;; - -u|--update) [[ $2 ]] && Err "No more command line arguments allowed" && err=12 + -u|--update) [[ $2 ]] && Err "$nomore" && err=13 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 + -r|--render) [[ -z $2 ]] && Err "Specify a file to render" && Usage 14 + [[ $3 ]] && Err "$nomore" && err=15 [[ -f "$2" ]] && { Display_tldr "$2" && exit "$err" Err "A file error occured" - exit 15 - } || Err "No file:$I $2$XI" && exit 16 ;; + exit 16 + } || Err "No file:$I $2$XI" && exit 17 ;; -m|--markdown) shift page=$* - [[ -z $page ]] && Err "Specify a page to display" && Usage 17 + [[ -z $page ]] && Err "Specify a page to display" && Usage 18 [[ -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 + ''|-h|-\?|--help) [[ $2 ]] && Err "$nomore" && err=19 Usage "$err" ;; - -*) Err "Unrecognized option $I$1$XI"; Usage 19 ;; + -*) Err "Unrecognized option $I$1$XI"; Usage 20 ;; *) page=$* ;; esac - [[ -z $page ]] && Err "No command specified" && Usage 20 - [[ $page =~ ' -' || ${page:0:1} = '-' ]] && Err "Only one option allowed" && Usage 21 + [[ -z $page ]] && Err "No command specified" && Usage 21 + [[ $page =~ ' -' || ${page:0:1} = '-' ]] && Err "Only one option allowed" && Usage 22 [[ $page = */* ]] && platform=${page%/*} && page=${page##*/} [[ $platform && ,common,linux,osx,sunos, != *,$platform,* ]] && { Err "Unknown platform $I$platform$XI" - Usage 22 + Usage 23 } Get_tldr "${page// /-}" [[ ! -s $cached ]] && Err "tldr page for command $I$page$XI not found" \ - && Inf "Contribute new pages at:$XB$URL https://github.com/tldr-pages/tldr$XURL" && exit 23 + && Inf "Contribute new pages at:$XB$URL https://github.com/tldr-pages/tldr$XURL" && exit 24 ((markdown)) && Out "$(cat "$cached")" || Display_tldr "$cached" } diff --git a/tldr-lint b/tldr-lint new file mode 100755 index 0000000..798913d --- /dev/null +++ b/tldr-lint @@ -0,0 +1,161 @@ +#!/bin/bash +set +vx +[[ $- = *i* ]] && echo "Don't source this script!" && return 1 +version='0.11' +# tldr-lint version 0.11 +# Linter for new syntax tldr source files +# Old syntax files $f can be changed into new syntax files by: +# sed -i -e "1s/^# //" -e 's/^- //' -e 's/^`\(.*\)`$/ \1/' "$f" +# e=$(sed '1s/./=/g;q' "$f") sed -i "2i$e" "$f" +# Part of http://github.com/pepa65/tldr-bash-client +# Requirements: coreutils, sed, grep, find + +Help(){ # $1: optional message + # Use: self, version + cat <<-EOC + $B$O $self$X v$version + Usage: $self [-h|--help] [-V|--version] [-q|--quiet] [] [] + - All *.md files under and subdirectories are checked + - is checked regardless of extension + -q, --quiet: No output means check is OK + -V, --version: Output version + -h, --help: This help text + EOC + [[ $1 ]] && echo "$E ERROR:$X $1" + exit 1 +} + +Flag(){ # $1:message; $2:linenumber (0:not shown, missing:exit) + # Use: v, line, W, X, U, XU; Modify: flags + ((++flags)) + [[ $2 ]] && (($2)) && echo -e "$W FLAG:$X $1:\n$W$2$X:$U$line$XU" || + echo -e "$W FLAG:$X $1" +} + +Ok(){ # $1:message $2:OK(0),Not-OK(>0) + # Use: quiet, E, O, X + if (($2>0)) + then + echo "$E NOT-OK:$X $1" + else + ((quiet)) || echo "$O OK:$X $1" + fi +} + +Check(){ # $1:filename + # Use: quiet, E, O, W, X, U, XU, B, XB; Modify: line, flags + local n f lines name len_command check description examples + ((quiet)) || echo "$B File $O$1$X$XB" + IFS= read -rd '' f <"$1" + flags=0 + # general checks + lines="$(grep -n $'\r' <<<"$f" |\ + sed -e "s/^\([^:]*\):/$W\1$X:$U/g" -e "s/\r/$E^M$X/g" -e "s/$/$XU/g")" + [[ $lines ]] && Flag "Carriage returns $E^M$X not allowed:\n$lines" + lines="$(grep -n $'\t' <<<"$f" |\ + sed -e "s/^\([^:]*\):/$W\1$X:$U/g" -e "s/\t/$E^T$X/g" -e "s/$/$XU/g")" + [[ $lines ]] && Flag "Tabs $E^T$X not allowed:\n$lines" + lines="$(grep -n ' $' <<<"$f" |\ + sed -e "s/^\([^:]*\):/$W\1$X:$U/g" -e "s/$/$XU/g")" + [[ $lines ]] && Flag "Trailing spaces $U$E $X$XU not allowed:\n$lines" + n=0 + while true + do + # header + read -r && line=$REPLY && ((++n)) || break + check=$flags + grep -qn '^[a-zA-Z0-9_]\([a-zA-Z0-9_ .-]*[a-zA-Z0-9_]\)*$' <<<"$line" || + Flag "Command name not well-formed" $n + command=${line// /-} + name=${1##*/} name=${name%.*} + [[ $command = $name ]] || + Flag "Command name different from filename $U$name$XU" $n + Ok "Command name" $((flags-check)) + len_command=${#line} + check=$flags + read -r && line=$REPLY && ((++n)) || break + [[ $line =~ ^=+$ ]] || + Flag "Only equal signs $E=$X must underline the command" $n + ((${#line}!=len_command)) && + Flag "Command $U$command$XU and underline must have the same length" $n + Ok "Underline" $((flags-check)) + read -r && line=$REPLY && ((++n)) || break + [[ $line ]] && Flag "Empty line required after underline" $n + read -r && line=$REPLY && ((++n)) || break + # description + check=$flags + description=0 + while [[ ${line:0:2} = '> ' ]] + do + ((++description)) + [[ ${line:2:1} =~ [a-z] ]] && + Flag "Not allowed to start a description with lowercase$U a-z$XU" $n + [[ ${line:2:1} = ' ' ]] && + Flag "Only 1 space $U$E $X$XU after the greater-than symbol $E>$X allowed" $n + [[ ${line: -1} = '.' ]] || + Flag "Must have a dot $E.$X at the end of a command description" $n + read -r && line=$REPLY && ((++n)) || break 2 + done + ((description)) || + Flag "A command description '$U$E> $X---$E.$X$XU' is missing" + Ok "$description Command description lines" $((flags-check+!description)) + # examples + check=$flags + examples=0 + while true + do + [[ $line ]] && Flag "Empty line required before example description" $n || + read -r && line=$REPLY && ((++n)) || break 2 + [[ ${line:0:1} =~ [a-z] ]] && + Flag "Not allowed to start an example description with lowercase$U a-z$XU" $n + [[ ${line: -1} = ':' ]] || + Flag "A colon $E:$X must end each example description" $n + ((++examples)) + read -r && line=$REPLY && ((++n)) || break 2 + [[ $line ]] && Flag "Empty line required after example description" $n || + read -r && line=$REPLY && ((++n)) || break 2 + [[ ${line:0:4} = ' ' ]] || + Flag "Four spaces $E$U $XU$X must precede each example command" $n + [[ ${line:4:1} = ' ' ]] && + Flag "No more that 4 spaces $E$U $XU$X before example commands allowed" $n + read -r && line=$REPLY && ((++n)) || break 2 + done + done <<<"$f" + [[ $line ]] && Flag "Newline required at the end of a page" $n + Ok "$examples Examples" $((flags-check+!examples)) + Ok "$B$1$XB" $flags +} + +U=$'\e[4m' XU=$'\e[24m' B=$'\e[1m' XB=$'\e[0m' +E=$'\e[31m' W=$'\e[33m' O=$'\e[32m' X=$'\e[39m' +self=${0##*/} file='' dir='' line='' flags=0 quiet=0 + +(($#>2)) && Help "No more than 2 arguments allowed" +while (($#)) +do + case $1 in + -V|--version) echo "$version"; exit 0 ;; + -h|--help) Help ;; + -q|--quiet) quiet=1; shift ;; + *) if [[ -f $1 ]] + then + file=$1 + elif [[ -d $1 ]] + then + dir=$1 + else + Help "$U$1$XU is neither a file nor a directory" + fi + shift ;; + esac +done + +[[ $file || $dir ]] || Help "No valid file or directory given" +[[ $file ]] && Check "$file" +[[ $dir ]] && + while read -r + do + Check "$REPLY" + done < <(find "$dir" -name '*.md') + +exit 0 diff --git a/tldr-usage.jpg b/tldr-usage.jpg index 30e9b29..e3ad726 100644 Binary files a/tldr-usage.jpg and b/tldr-usage.jpg differ