diff --git a/bin/generate_readme.sh b/bin/generate_readme.sh index d02052a..ac4d34f 100755 --- a/bin/generate_readme.sh +++ b/bin/generate_readme.sh @@ -1,13 +1,111 @@ #!/usr/bin/env bash -README="README.md" -_setup() { - INVALID_CHARS="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" +_usage() { + printf " +Script to autogenerate markdown based on bash source code.\n +The script generates table of contents and bashdoc and update it the given markdown template.\n +Usage:\n %s [options.. ]\n +Options:\n + -f | --file - Relative or absolute path to the README.md file. + -p | --sh-dir - path to the bash script source folder to generate shdocs.\n + -l | --toc-level - Minimum level of header to print in Table of Contents.\n + -d | --toc-depth - Maximum depth of tree to print in Table of Contents.\n + -h | --help - Display usage instructions.\n" "${0##*/}" + exit 0 +} + +_setup_arguments() { + + unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR MINLEVEL=1 MAXLEVEL=3 - SCRIPT_FILE=$(basename "${0}") - rm "$README" - cp readme-template.md "$README" + SCRIPT_FILE="${0##*/}" + SOURCE_MARKDOWN="../README.md" + SOURCE_SCRIPT_DIR="../src" + + SHORTOPTS="hp:f:m:d:-:" + + while getopts "${SHORTOPTS}" OPTION; do + case "${OPTION}" in + -) + _check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; } + case "${OPTARG}" in + help) + _usage + ;; + file) + _check_longoptions "${!OPTIND}" + SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + toc-level) + _check_longoptions "${!OPTIND}" + MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + toc-depth) + _check_longoptions "${!OPTIND}" + MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + sh-dir) + _check_longoptions "${!OPTIND}" + SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1)) + ;; + '') + _usage + ;; + *) + printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + esac + ;; + h) + _usage + ;; + f) + SOURCE_MARKDOWN="${OPTARG}" + ;; + m) + MINLEVEL="${OPTARG}" + ;; + d) + MAXLEVEL="${OPTARG}" + ;; + p) + SOURCE_SCRIPT_DIR="${OPTARG}" + ;; + :) + printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + ?) + printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1 + ;; + esac + done + shift "$((OPTIND - 1))" + + if [[ -w "${SOURCE_MARKDOWN}" ]]; then + declare src_file src_extension + src_file="${SOURCE_MARKDOWN##*/}" + src_extension="${src_file##*.}" + if [[ "${src_extension,,}" != "md" ]]; then + printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1 + fi + else + printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1 + fi + + if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then + printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1 + fi + + re='^[0-9]+$' + if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then + echo "error: Not a number" >&2 + exit 1 + fi + if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then + printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1 + fi + } _setup_tempfile() { @@ -20,56 +118,64 @@ _setup_tempfile() { _generate_shdoc() { declare file file="$(realpath $1)" - ./shdoc.awk < $file >> "$2" + ./shdoc.awk < "${file}" >> "$2" } _insert_shdoc_to_file() { - declare shdoc_tmp_file="$1" - declare dest_file="$2" + declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc + shdoc_tmp_file="$1" + source_markdown="$2" - declare start_shdoc="" - declare info_shdoc="" - declare end_shdoc="" + start_shdoc="" + info_shdoc="" + end_shdoc="" - sed -i "1s/^/$info_shdoc\n/" "$shdoc_tmp_file" + sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}" - if grep --color=always -Pzl "(?s)$start_shdoc.*\n.*$end_shdoc" $dest_file &> /dev/null; then + if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" $source_markdown &> /dev/null; then # src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file - sed -i -ne "/$start_shdoc/ {p; r $shdoc_tmp_file" -e ":a; n; /$end_shdoc/ {p; b}; ba}; p" $dest_file - echo -e "\nUpdated shdoc content to $dest_file successfully\n" + sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}" + echo -e "Updated shdoc content to ${source_markdown} successfully\n" else - printf "%s\n" "${start_shdoc}" >> "${dest_file}" - cat "${shdoc_tmp_file}" >> "${dest_file}" - printf "%s\n" "${end_shdoc}" >> "${dest_file}" - echo -e "\nCreated shdoc content to $dest_file successfully\n" + { + printf "%s\n" "${start_shdoc}" + cat "${shdoc_tmp_file}" + printf "%s\n" "${end_shdoc}" + } >> "${source_markdown}" + echo -e "Created shdoc content to ${source_markdown} successfully\n" fi } _process_sh_files() { - declare shdoc_tmp_file=$(_setup_tempfile) - find ../src -name '*.sh' -print0 | sort -z | + declare shdoc_tmp_file source_script_dir source_markdown + source_markdown="${1}" + source_script_dir="${2}" + shdoc_tmp_file=$(_setup_tempfile) + find "${source_script_dir}" -name '*.sh' -print0 | sort -z | while IFS= read -r -d '' line; do - _generate_shdoc "$line" "$shdoc_tmp_file" + _generate_shdoc "${line}" "${shdoc_tmp_file}" done - _insert_shdoc_to_file "$shdoc_tmp_file" "$README" - rm "$shdoc_tmp_file" + _insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}" + rm "${shdoc_tmp_file}" } _generate_toc() { - declare line level title anchor output counter temp_output + declare line level title anchor output counter temp_output invalid_chars + + invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—" + while IFS='' read -r line || [[ -n "${line}" ]]; do + level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" + title="$(echo "${line}" | sed -E 's/^#+ //')" + [[ "${title}" = "Table of Contents" ]] && continue - while IFS='' read -r line || [[ -n "$line" ]]; do - level="$(echo "$line" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')" - title="$(echo "$line" | sed -E 's/^#+ //')" # tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text - anchor="$(echo "$title" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "$INVALID_CHARS")" + anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")" # check new line introduced is not duplicated, if is duplicated, introduce a number at the end - # copying doctoc behavior temp_output=$output"$level- [$title](#$anchor)\n" counter=1 while true; do @@ -86,7 +192,7 @@ _generate_toc() { # grep: filter header candidates to be included in toc # sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment - done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "$1" | tr -d '\r' | sed "s/^#\{$(($MINLEVEL - 1))\}//g")" + done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((${MINLEVEL} - 1))\}//g")" # when in toc we have two `--` quit one output="$(echo "$output" | sed 's/--*/-/g')" @@ -97,63 +203,68 @@ _generate_toc() { _insert_toc_to_file() { - declare toc_text="$2" + declare source_markdown toc_text start_toc info_toc end_toc utext_ampersand utext_slash + source_markdown="${1}" + toc_text="${2}" - # inspired in doctoc lines - declare start_toc="" - declare info_toc="" - declare end_toc="" + start_toc="" + info_toc="" + end_toc="" toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc" # temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command utext_ampersand="id8234923000230gzz" utext_slash="id9992384923423gzz" - toc_block="$(echo "$toc_block" | sed "s,\&,$utext_ampersand,g")" - toc_block="$(echo "$toc_block" | sed "s,\/,$utext_slash,g")" + toc_block="$(echo "${toc_block}" | sed "s,\&,${utext_ampersand},g")" + toc_block="$(echo "${toc_block}" | sed "s,\/,${utext_slash},g")" # search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705 # grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches - if grep --color=always -Pzl "(?s)$start_toc.*\n.*$end_toc" $1 &> /dev/null; then + if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then # src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace - sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" $1 - echo -e "\nUpdated TOC content in $1 succesfully\n" + sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}" + echo -e "Updated TOC content in ${source_markdown} succesfully\n" else - sed -i 1i"$toc_block" "$1" - echo -e "\nCreated TOC in $1 succesfully\n" + sed -i 1i"$toc_block" "${source_markdown}" + echo -e "Created TOC in ${source_markdown} succesfully\n" fi # undo symbol replacements - sed -i "s,$utext_ampersand,\&,g" $1 - sed -i "s,$utext_slash,\/,g" $1 + sed -i "s,${utext_ampersand},\&,g" "${source_markdown}" + sed -i "s,${utext_slash},\/,g" "${source_markdown}" } _process_toc() { - declare toc_temp_file=$(_setup_tempfile) + declare toc_temp_file source_markdown + source_markdown="${1}" - sed '/```/,/```/d' "$README" > "$toc_temp_file" + toc_temp_file=$(_setup_tempfile) + + sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}" declare level=$MINLEVEL - while [[ $(grep -E "^#{$level} " "$toc_temp_file" | wc -l) -le 1 ]]; do + while [[ $(grep -E "^#{$level} " "${toc_temp_file}" | wc -l) -le 1 ]]; do level=$(($level + 1)) done - if [[ $MINLEVEL -ne $level ]]; then - echo -e "\nnote: detected all headers (maybe except 1) in level $level, switching to that level of headers to fill table of contents" - fi - MINLEVEL=$level - toc_text=$(_generate_toc "$toc_temp_file") - rm "$toc_temp_file" - _insert_toc_to_file "$README" "$toc_text" + MINLEVEL=${level} + toc_text=$(_generate_toc "${toc_temp_file}") + rm "${toc_temp_file}" + + _insert_toc_to_file "${source_markdown}" "${toc_text}" } main() { - _setup - _process_sh_files - _process_toc + trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT + set -o errexit -o noclobber -o pipefail + + _setup_arguments "${@}" + _process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}" + _process_toc "${SOURCE_MARKDOWN}" "${MINLEVEL}" "${MAXLEVEL}" } -main "$@" +main "${@}"