Skip to content

Commit

Permalink
fixing #24 and changing shebangs
Browse files Browse the repository at this point in the history
  • Loading branch information
Sylvain Viart committed Jun 7, 2019
1 parent cd05e64 commit 0ad78be
Show file tree
Hide file tree
Showing 30 changed files with 310 additions and 114 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ Read the help and version messages from standard input (`docopts` found in `$PAT
make build_doc: include [examples/legacy_bash/rock_hello_world.sh](examples/legacy_bash/rock_hello_world.sh)

```bash
#!/usr/bin/env bash
# Example from README.md

PATH=$PATH:../..
eval "$(docopts -V - -h - : "$@" <<EOF
Usage: rock [options] <argv>...
Expand Down Expand Up @@ -172,6 +176,10 @@ command line arguments:
make build_doc: include [examples/legacy_bash/rock_hello_world_with_grep.sh](examples/legacy_bash/rock_hello_world_with_grep.sh)

```bash
#!/usr/bin/env bash
# Example from README.md
PATH=$PATH:../..

#? rock 0.1.0
#? Copyright (C) 200X Thomas Light
#? License RIT (Robot Institute of Technology)
Expand Down
138 changes: 97 additions & 41 deletions build_doc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,95 +2,151 @@
#
# helper that insert external source into README.md content
#
# Usage: ./build_doc.sh README_FILE > README.tmp
# Usage: ./build_doc.sh [-d] [-i] README_FILE
#
# Options:
# -i Edit in place README_FILE is modified
# -d diff: README_FILE is unmodified diff is outputed
#
# Behavior:
# parse README_FILE and look for marker matching: ^make build_doc
#
# make build_doc: `markdown bash command`
# make build_doc: `markdown link to a local file`
# make build_doc: include [markdown link](to/a/local file)
#
# bash command : will be executed and inserted verbatim
# local file : imported verbatim
#
# The README_FILE modifed text flow is outputed on stdout.
# The generated text from README_FILE is outputed on stdout.
#
# The markup "make build_doc: *" is conserved.
#
# README_FILE is unmodified.
#
# some files: /tmp/content.* are created

README_FILE=$1
# blank line above ^^

# return a colon separated $start_line:$end_line:$type:$shell_cmd
# Input parser:
# return a @ separated $start_line@$end_line@$type@$shell_cmd
get_build_doc() {
local input=$1
local cmd num_line start_line end_line type
# read the output of the grep at the end of the while
# which is a splited grep -n output
while read num_line source
do
type=""
# ${var:0:1} extract first char of a string in bash
if [[ ${source:0:1} == '`' ]] ; then
# free bash command

# extract inner text, removing first char and last char
cmd=${source:1:$((${#source} - 2))}
type=""
elif [[ ${source:0:1} == '[' ]] ; then
elif [[ ${source:0:7} == 'include' ]] ; then
# local file link

# remove prefix including openinng '('
cmd=${source#*(}
# remove last char, supposed to be the closing ')'
cmd=${cmd:0:$((${#cmd} - 1))}
cmd="cat $cmd"
local src=$(markdown_extract_link "$source")
cmd="include '$src' $num_line"

# detect file type
if grep -q 'shell script' <<< "$(file $cmd)" ; then
if grep -q 'shell script' <<< "$(file $src)" ; then
type="bash"
fi
else
# unknown command
cmd="echo '$num_line: not recognized command: $source'"
fi

# num_line+2 is supposed to be the starting ```
start_line=$(($num_line + 2))

# find closing ``` : first matching ``` after $start_line
end_line=$(awk "NR > $start_line && /^\`{3}/ {print NR; exit}" $README_FILE)
end_line=$(awk "NR > $start_line && /^\`{3}/ {print NR; exit}" $input)

# display result
echo "$start_line:$end_line:$type:$cmd"
echo "$start_line@$end_line@$type@$cmd"

done < <(grep -n '^make build_doc' $README_FILE | awk -F':' '{$2=""; print $0}')
done < <(grep -n '^make build_doc' $input | awk -F':' '{$2=""; print $0}')
}

# transform a string to a valid filename identified
# transform a free string to a valid filename identifier
# remove quote, space and so on
to_filename() {
echo "$1" | tr -d '/. `"'"'"'$,^:|\\'
echo "$1" | tr -d '/. `"'"'"'$,^:|\\{}[]()&#\\'
}

# given $1 contains a markdown link: [text](path/to/local/file)
# return the path/to/local/file
# or no_match is not found
markdown_extract_link() {
local regexp='\[[^]]+\]\(([^)]+)\)'
if [[ $1 =~ $regexp ]] ; then
echo "${BASH_REMATCH[1]}"
else
echo "no_match"
fi
}

# search position of $2 in $1
# return -1 if not found
# 0 to n-1
strpos() {
# remove $2 from $1 and any character following to the end
local x="${1%%$2*}"
[[ "$x" = "$1" ]] && echo -1 || echo "${#x}"
}

# formating helpers

# extract text bloc starting at Usage to the first blank line.
# remove Usage and blank line.
get_usage() {
sed -n '/^Usage:/,/^$/ p' | sed -e '1d' -e '$d'
}

# include the given file
include() {
local local_file="$1"
local num_line=$2
if [[ $local_file == no_match ]] ; then
echo "include: file not matched at line '$num_line'"
else
cat "$local_file"
fi
}

################## main

build=$(get_build_doc)

# NOTE: changing ISF wil alter get_build_doc parsing, it must be done after.
IFS=$':\n'
# the loop combine the parsed input into a valid sed command
sed_cmd=""
while read start end format_type mycmd
do
content=/tmp/content.$(to_filename "$mycmd")
# merge multiple output
{ echo '```'"$format_type" ; eval "$mycmd" ; echo -e '```\n'; } > $content
# the start end computation must keep the line itself or no content will be ouptputed
sed_cmd="$sed_cmd -e '$((start -1)),$end d' -e '$((end +1)) r $content'"
done <<<"$build"

tmp=README_FILE.tmp
cp $README_FILE $tmp
echo "sed -i $sed_cmd $tmp"
# eval is required to preserve multiple args quoting and passing multiple actions to one sed process.
eval "sed -i $sed_cmd $tmp"
diff -u $README_FILE $tmp
main_build_doc() {
README_FILE=$1

build=$(get_build_doc $README_FILE)

# NOTE: changing IFS will alter get_build_doc parsing, it must be done after.
IFS=$'@\n'
# the loop combine the parsed input into a valid sed command
# building a oneliner sed allow us to keep lines number while deleting old content for replacing it.
sed_cmd=""
while read start end format_type mycmd
do
content=/tmp/content.$(to_filename "$mycmd")
# merge multiple output
{ echo '```'"$format_type" ; eval "$mycmd" ; echo -e '```\n'; } > $content
# the start end computation must keep the line itself or no content will be outputed
sed_cmd="$sed_cmd -e '$((start -1)),$end d' -e '$((end +1)) r $content'"
done <<<"$build"

# eval is required to preserve multiple args quoting and passing multiple actions to one sed process.
if $ARGS_i ; then
# edit inplace
eval "sed -i $sed_cmd $README_FILE"
elif $ARGS_d ; then
diff -u $README_FILE <(eval "sed $sed_cmd $README_FILE")
else
eval "sed $sed_cmd $README_FILE"
fi
}

if [[ $0 == $BASH_SOURCE ]] ; then
source ./docopts.sh --auto -G "$@"
main_build_doc "$ARGS_README_FILE"
fi
47 changes: 26 additions & 21 deletions docopts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
# docopts helper for bash, provides some functions for common usages
#
# Usage:
# # raw usage
# source path/to/docopts.sh
# docopts -A ARGS -h "$help" -V $version : "$@"
# usage=$(docopt_get_help_string "$0")
# version=$(docopt_get_version_string "$0")
# eval $(docopts -A ARGS -h "$usage" -V $version : "$@")
# myarray=( $(docopt_get_values ARGS FILE") )
#
# # or for auto parsing the caller script comment (bash 4 associative array):
# source path/to/docopts.sh --auto "$@"
#
# # or for using globals variables (bash 3.2 compatible):
# source path/to/docopts.sh --auto -G "$@"
# source path/to/docopts.sh --auto -G "$@"
#
# Conventions:
# Conventions:
# The prefix docopt_* is used to export globals and functions
# docopt_auto_parse() modify $HELP and $ARGS or populate $ARGS_* globals.
#
Expand All @@ -24,35 +28,32 @@
# source path/to/docopts.sh
# docopts -G ARGS -h "$help" -V $version : "$@"

# compute this file's absolute paths
docopt_sh_dir="$( cd "$( dirname ${BASH_SOURCE[0]} 2>/dev/null)" && pwd 2>/dev/null)"
docopt_sh_me="$docopt_sh_dir/${BASH_SOURCE[0]}"

# fetch Usage: from the given filename
# usually $0 in the main level script
docopt_get_help_string() {
local myfname=$1
# filter the block (/!\ all blocks) starting at a "# Usage:" and ending
# at an empty line, one level of comment markup is removed
# filter the block (/!\ ALL blocks) starting at a "# Usage:" and ending
# at an empty line, one level of comment markup is removed.
#
## sed -n -e '/^# Usage:/,/\(^# \?----\|^$\)/ { /----/ d; s/^# \?//p }' rock_no-stdin_example.sh

# sed gory details:
# -n : no print output
# -e : pass sed code inline
# /^# Usage:/,/^$/ : filter range blocks from '# Usage:' to empty line
# s/^# \{0,1\}// : substitute comment marker and an optional space (POSIX regex)
# p : print
sed -n -e '/^# Usage:/,/^$/ s/^# \{0,1\}//p' < $myfname
# /^# Usage:/,/^$/ : filter range blocks from '# Usage:' to empty line
# s/^# \{0,1\}// : substitute comment marker and an optional space (POSIX regex)
# p : print
sed -n -e '/^# Usage:/,/^$/ s/^# \{0,1\}//p' < "$myfname"
}

# fetch version information from the given filename or string
# usually $0 in the main level script, or the help string extracted
# Fetch version information from the given filename or string.
# Usually $0 in the main level script, or the help string extracted
# by docopt_get_help_string()
#
# Use standard delimiter ----
docopt_get_version_string() {
if [[ -f "$1" ]] ; then
# filter the block (all blocks) starting at a "# Usage:" and ending
# at an empty line, one level of comment markup is removed
sed -n -e '/^# ----/,/^$/ s/^# \{0,1\}//p' < "$1"
sed -n -e '/^# ----/,/^$/ { 1d; s/^# \{0,1\}//; /----/ d; p }' < "$1"
else
# use docopts --separator behavior
echo "$1"
Expand Down Expand Up @@ -143,19 +144,23 @@ docopt_get_raw_value() {
awk -F= "\$1 == \"$kstr\" {sub(\"^[^=]+=\", \"\", \$0);print}" <<<"$docopts_out"
}

# Debug, prints env varible ARGS or $1 formatted as a bash 4 assoc array
# or with -G [prefix] greps ${prefix}_ variables from environment
# Debug, prints env variable ARGS or $1 formatted
# Usage: docopt_print_ARGS [ASSOC_ARRAY_NAME]
# docopt_print_ARGS -G [VARIABLE_PREFIX]
# with -G VARIABLE_PREFIX grep ${VARIABLE_PREFIX}_ variables from environment
docopt_print_ARGS() {
local use_associative=true
if [[ $1 == '-G' ]] ; then
use_associative=false
shift
fi
# $1 can be the name of the global assoc array, or the prefix if -G is given
local assoc="$1"
if [[ -z $assoc ]] ; then
assoc=ARGS
fi

local a
if $use_associative ; then
# bash dark magic copying $assoc argument to a local myassoc array
# inspired by:
Expand All @@ -165,11 +170,11 @@ docopt_print_ARGS() {

# loop on keys
echo "docopt_print_ARGS => $assoc"
local a
for a in ${!myassoc[@]} ; do
printf "%20s = %s\n" $a "${myassoc[$a]}"
done
else
echo "docopt_print_ARGS -G => ${assoc}_*"
set | grep "^${assoc}_"
fi
}
Expand Down
6 changes: 3 additions & 3 deletions examples/arguments_example.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
#!/usr/bin/env bash
#
# Usage: arguments_example.sh [-vqrh] [FILE] ...
# arguments_example.sh (--left | --right) CORRECTION FILE
# arguments_example.sh (--left | --right) CORRECTION FILE
#
# Process FILE and optionally apply correction to either left-hand side or
# right-hand side.
Expand All @@ -27,7 +27,7 @@ source docopts.sh --auto "$@"

# docopt_auto_parse use ARGS bash 4 globla assoc array
# main code
# on assoc array '!' before nane gike hash keys
# on bash assoc array a '!' before name gives hash keys list
for a in ${!ARGS[@]} ; do
echo "$a = ${ARGS[$a]}"
done
2 changes: 1 addition & 1 deletion examples/calculator_example.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Not a serious example.
#
# Usage:
Expand Down
2 changes: 1 addition & 1 deletion examples/cat-n_wrapper_example.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# cat -n all files
#
Expand Down
2 changes: 1 addition & 1 deletion examples/counted_example.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# Usage: counted_example.sh --help
# counted_example.sh -v...
Expand Down
2 changes: 1 addition & 1 deletion examples/docopts_auto_example.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# sample of script using auto-parser.
#
Expand Down
2 changes: 1 addition & 1 deletion examples/legacy_bash/cat-n_wrapper_example.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# cat -n all files
#
Expand Down
7 changes: 5 additions & 2 deletions examples/legacy_bash/naval_fate.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Naval Fate.
#
# Usage:
Expand Down Expand Up @@ -27,7 +27,10 @@ source docopts.sh
# no version support in docopt_auto_parse() so we call docopts directly
usage=$(docopt_get_help_string "$0")
parsed="$(docopts -G ARGS -V "$VERSION" -h "$usage" : "$@")"
echo "============ parsed output"
echo "$parsed"
# now vars are populated at global scope
eval "$parsed"

# docopt_print_ARGS
echo "============== docopt_print_ARGS"
docopt_print_ARGS -G
2 changes: 1 addition & 1 deletion examples/legacy_bash/rock_hello_world.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Example from README.md

PATH=$PATH:../..
Expand Down
Loading

0 comments on commit 0ad78be

Please sign in to comment.