488 lines
11 KiB
Bash
Executable file
488 lines
11 KiB
Bash
Executable file
#!/bin/bash
|
|
shopt -s extglob
|
|
|
|
|
|
#
|
|
# RFC's are created from ./doc/devel/template/new_rfc.sh and stored in
|
|
# ./doc/devel/rfc/ . There are directories for the various states
|
|
# ./doc/devel/rfc_final, ./doc/devel/rfc_pending, ./doc/devel/rfc_parked,
|
|
# ./doc/devel/rfc_dropped . Which contain symlinks back to ../rfc/
|
|
#
|
|
|
|
|
|
function usage()
|
|
{
|
|
grep -v '^// ' <<"EOF" | less -F
|
|
Lumiera RFC maintenance script
|
|
==============================
|
|
// Note: the source of this documentation is maintained
|
|
// directly admin/rfc.sh in its usage() function
|
|
// edit it only there and then regenerate
|
|
// doc/devel/technical/infra/ with:
|
|
// ./admin/rfc.sh help >doc/technical/infra/rfcsh.txt
|
|
|
|
Usage
|
|
-----
|
|
|
|
./admin/rfc.sh <command> [options]
|
|
|
|
Commands (with <mandatory> and [optional] parameters)
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
find <rfcs> [regex]:: List all matching RFC's (matching `regex`)
|
|
show <rfcs> [regex]:: Read RFC's (matching `regex`)
|
|
create <title>:: Create a new RFC
|
|
edit <rfc> [chapter]:: Edit RFC at chapter
|
|
asciidoc <rfc>:: pass the rfc.txt to `asciidoc` command
|
|
comment <rfc>:: Add a new comment to a RFC
|
|
draft <rfc>:: Change RFC to Draft state
|
|
final <rfc>:: Change RFC to Final state
|
|
park <rfc>:: Change RFC to Parked state
|
|
drop <rfc>:: Change RFC to Dropped state
|
|
supersede <rfc> <new>:: Supersede RFC with a new RFC
|
|
discard <rfc>:: Delete an RFC
|
|
help:: Show this help
|
|
process:: Do automatic maintenance work
|
|
wrap <rfcs>:: canonical reformatting
|
|
|
|
Option types
|
|
~~~~~~~~~~~~
|
|
|
|
title:: Quoted string used as RFC title
|
|
rfc:: Name of the RFC, smart matched, unique
|
|
rfcs:: Name of the RFC, smart matched, multiple
|
|
regex:: Regex matched against the content of a RFC
|
|
chapter:: Heading of a section
|
|
|
|
|
|
Smart matching
|
|
--------------
|
|
|
|
RFC names don't need to be given exactly, they use a globbing pattern.
|
|
This is:
|
|
|
|
* case insensitive
|
|
* whitespaces are ignored
|
|
* `*` stands for any number of parameters
|
|
* `?` is any single character
|
|
* when starting with `/` they are matched against the begin of the name
|
|
* some regex operators work too
|
|
|
|
`find` and `show` can operate on many matches so the given rfc name doesn't
|
|
need to be unique. The other commands will complain when the RFC name given
|
|
doesn't resolve to one unique RFC.
|
|
|
|
When `less` presents multiple files one can go forth and back with the `:n`
|
|
and `:p` commands.
|
|
|
|
The tile for `create` should be a normal document title. Possibly quoted
|
|
since it may contain spaces and not too long. The filename is this title
|
|
in CamelCase with all spaces and special characters removed.
|
|
|
|
Chapter machching single lines containing this word, special asciidoc
|
|
comments in the form `//word:.*` and asciidoc block attributes `[word.*]`
|
|
on a single line. When a chapter pattern is not unique, the last one is
|
|
picked.
|
|
|
|
rfc.sh executes git add/rm/mv commands, but never commits. One should do a
|
|
commit as soon he finished editing.
|
|
|
|
EOF
|
|
}
|
|
|
|
|
|
function camel_case()
|
|
{
|
|
sed -e 's/[^[:alnum:]]/ /g;s/ \+\(.\)/\U\1/g' <<<" $1"
|
|
}
|
|
|
|
|
|
|
|
function untabify()
|
|
{
|
|
local in="$1"
|
|
local tmp
|
|
local out
|
|
|
|
while [[ "$in" ]]; do
|
|
tmp="${in%%$'\t'*}"
|
|
in="${in:${#tmp}+1}"
|
|
out="$out$tmp "
|
|
out="${out:0:${#out}/8*8}"
|
|
done
|
|
out="${out%%*( )}"
|
|
|
|
echo "$out"
|
|
}
|
|
|
|
|
|
|
|
function word_wrap()
|
|
{
|
|
local linelength="${1:-80}"
|
|
local indent=""
|
|
local text
|
|
local tab=$'\t'
|
|
local rest
|
|
local oldindent
|
|
local coderx='//|/\*|#'
|
|
local meld=yes
|
|
|
|
while IFS='' read -r line; do
|
|
line="$(untabify "$line")"
|
|
# remove DOS linebreaks
|
|
line="${line//$'\r'/}"
|
|
|
|
text="${line##*([![:alnum:]])}"
|
|
oldindent="$indent"
|
|
indent="${line:0:${#line}-${#text}}"
|
|
if [[ "$text" ]]; then
|
|
|
|
# avoid melding text after // /* and # comments
|
|
if [[ "$indent" =~ $coderx ]]; then
|
|
meld=no
|
|
fi
|
|
|
|
if [[ $meld = yes && ! "${indent// /}" && ${#oldindent} -eq ${#indent} ]]; then
|
|
line="${rest:+$rest }$text"
|
|
else
|
|
if [[ "$rest" ]]; then
|
|
echo "$rest"
|
|
fi
|
|
fi
|
|
indent="${indent//?/ }"
|
|
|
|
while [[ "${#line}" -ge "${linelength}" ]]; do
|
|
local wrapped
|
|
local tmpline="${line:0:$linelength}"
|
|
wrapped="${tmpline% *}"
|
|
if [[ "${#wrapped}" -gt "${#indent}" ]]; then
|
|
line="${indent}${line:${#wrapped}+1}"
|
|
else
|
|
wrapped="$line"
|
|
line=''
|
|
fi
|
|
echo "$wrapped"
|
|
done
|
|
|
|
rest="${line}"
|
|
else
|
|
meld=yes
|
|
if [[ "$rest" ]]; then
|
|
echo "$rest"
|
|
rest=""
|
|
fi
|
|
echo "$line"
|
|
fi
|
|
done
|
|
if [[ "$rest" ]]; then
|
|
echo "$rest"
|
|
fi
|
|
}
|
|
|
|
|
|
function smart_wrap()
|
|
{
|
|
# calling on a file
|
|
word_wrap $2 <"$1" >"$1.$$.tmp"
|
|
if ! cmp "$1" "$1.$$.tmp" &>/dev/null ; then
|
|
cat "$1.$$.tmp" >"$1"
|
|
git add "$1"
|
|
fi
|
|
rm "$1.$$.tmp"
|
|
}
|
|
|
|
|
|
|
|
function find_rfc()
|
|
{
|
|
# find rfc by shortname
|
|
local file
|
|
local match="$1"
|
|
match="${match// /}"
|
|
match="${match//./\.}"
|
|
match="${match//\?/.}"
|
|
match="${match//\*/.*}"
|
|
|
|
local globstate=$(shopt -p nocasematch)
|
|
shopt -s nocasematch
|
|
|
|
for file in $(find ./doc/devel/rfc/ -name '*.txt');
|
|
do
|
|
local name="/${file##*/}"
|
|
if [[ "$name" =~ $match ]]; then
|
|
echo "$file"
|
|
fi
|
|
done
|
|
$globstate
|
|
}
|
|
|
|
|
|
|
|
function find_chapter()
|
|
{
|
|
# find chapter
|
|
local file="$1"
|
|
local chapter="$2"
|
|
local found=$(grep -n -i "^\($chapter\|//$chapter:.*\|\[$chapter.*\]\)\$" "$file" | tail -1)
|
|
if [[ "$found" ]]; then
|
|
echo "${found%%:*}"
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
function process_file()
|
|
{
|
|
local file="$1"
|
|
local path="${1%/*}"
|
|
local basename="${1##*/}"
|
|
local linkdest="$path"
|
|
local state=$(grep '^\*State\* *' "$file")
|
|
|
|
case "$state" in
|
|
*Final*)
|
|
linkdest="./doc/devel/rfc_final"
|
|
;;
|
|
*Idea*|*Draft*)
|
|
linkdest="./doc/devel/rfc_pending"
|
|
;;
|
|
*Parked*)
|
|
linkdest="./doc/devel/rfc_parked"
|
|
;;
|
|
*Dropped*)
|
|
linkdest="./doc/devel/rfc_dropped"
|
|
;;
|
|
*)
|
|
echo "Unknown State: '$state'" >&2
|
|
exit 1
|
|
esac
|
|
|
|
local oldpath
|
|
for oldpath in ./doc/devel/rfc_*/$basename; do :; done
|
|
|
|
if [[ -h "$oldpath" ]]; then
|
|
if [[ "$oldpath" != "$linkdest/$basename" ]]; then
|
|
git mv "$oldpath" "$linkdest/$basename"
|
|
fi
|
|
elif [[ ! -s "$linkdest/$basename" ]]; then
|
|
ln -s "../rfc/$basename" "$linkdest/"
|
|
git add "$linkdest/$basename"
|
|
fi
|
|
}
|
|
|
|
|
|
function edit()
|
|
{
|
|
# filename lineoffset chapter
|
|
EDITOR="${EDITOR:-$(git config --get core.editor)}"
|
|
EDITOR="${EDITOR:-$VISUAL}"
|
|
|
|
local file="$1"
|
|
local line=0
|
|
|
|
if [[ "$3" ]]; then
|
|
line=$(find_chapter "$file" "$3")
|
|
fi
|
|
|
|
$EDITOR +$(($line+${2:-1})) $file
|
|
}
|
|
|
|
|
|
function unique_name()
|
|
{
|
|
local files=($(find_rfc "$1"))
|
|
|
|
if [[ ${#files[*]} -gt 1 ]]; then
|
|
echo "multiple matches:" >&2
|
|
(
|
|
IFS=$'\n'
|
|
echo "${files[*]}" >&2
|
|
)
|
|
elif [[ ${#files[*]} -eq 0 ]]; then
|
|
echo "no matches" >&2
|
|
else
|
|
echo ${files[0]}
|
|
fi
|
|
}
|
|
|
|
|
|
function add_comment()
|
|
{
|
|
local name="$1"
|
|
local nl=$'\n'
|
|
local comment="//edit comment$nl$nl$(git config --get user.name):: '$(date +%c)' ~<$(git config --get user.email)>~$nl"
|
|
|
|
ed "$name" 2>/dev/null <<EOF
|
|
/endof_comments:/-1i
|
|
$comment
|
|
.
|
|
wq
|
|
EOF
|
|
|
|
edit "$name" -4 "endof_comments"
|
|
}
|
|
|
|
function edit_state()
|
|
{
|
|
local name="$1"
|
|
local state="$2"
|
|
local comment="$3"
|
|
|
|
ed "$name" 2>/dev/null <<EOF
|
|
/^\*State\*/s/\(\*State\* *_\).*_/\1${state}_/
|
|
wq
|
|
EOF
|
|
|
|
if [[ "$comment" ]]; then
|
|
|
|
ed "$name" 2>/dev/null <<EOF
|
|
/endof_comments:/-1i
|
|
$comment
|
|
.
|
|
wq
|
|
EOF
|
|
|
|
fi
|
|
}
|
|
|
|
|
|
function change_state()
|
|
{
|
|
#rfcname state
|
|
local name="$1"
|
|
local state="$2"
|
|
|
|
local nl=$'\n'
|
|
local comment=".State -> $state$nl//add reason$nl$nl$(git config --get user.name):: '$(date +%c)' ~<$(git config --get user.email)>~$nl"
|
|
edit_state "$name" "$state" "$comment"
|
|
edit "$name" -4 "endof_comments"
|
|
process_file "$name"
|
|
}
|
|
|
|
|
|
command="$1"
|
|
shift
|
|
|
|
case "$command" in
|
|
process)
|
|
# for all rfc's
|
|
for file in $(find ./doc/devel/rfc -name '*.txt');
|
|
do
|
|
echo "process $file"
|
|
process_file "$file"
|
|
done
|
|
:
|
|
;;
|
|
search)
|
|
grep -r -C3 -n "$1" ./doc/devel/rfc | less -F
|
|
;;
|
|
find|list|ls)
|
|
if [[ "$2" ]]; then
|
|
find_rfc "$1" | xargs grep -i -C3 -n "$2"
|
|
else
|
|
find_rfc "$1"
|
|
fi
|
|
;;
|
|
show|less|more)
|
|
if [[ "$2" ]]; then
|
|
less $(find_rfc "$1" | xargs grep -i -l "$2")
|
|
else
|
|
less $(find_rfc "$1")
|
|
fi
|
|
;;
|
|
create)
|
|
TITLE="$@"
|
|
name=$(camel_case "$TITLE")
|
|
if [[ -f "./doc/devel/rfc/${name}.txt" ]]; then
|
|
echo "$name.txt exists already"
|
|
else
|
|
source ./doc/devel/template/new_rfc.sh >"./doc/devel/rfc/${name}.txt"
|
|
edit "./doc/devel/rfc/${name}.txt" 2 abstract
|
|
git add "./doc/devel/rfc/${name}.txt"
|
|
process_file "./doc/devel/rfc/${name}.txt"
|
|
fi
|
|
;;
|
|
edit)
|
|
name=$(unique_name "$1")
|
|
if [[ "$name" ]]; then
|
|
edit "${name}" 2 "$2"
|
|
process_file "$name"
|
|
fi
|
|
;;
|
|
asciidoc)
|
|
name=$(unique_name "$1")
|
|
if [[ "$name" ]]; then
|
|
asciidoc "${name}"
|
|
fi
|
|
;;
|
|
draft)
|
|
name=$(unique_name "$1")
|
|
if [[ "$name" ]]; then
|
|
change_state "$name" Draft
|
|
fi
|
|
;;
|
|
park)
|
|
name=$(unique_name "$1")
|
|
if [[ "$name" ]]; then
|
|
change_state "$name" Parked
|
|
fi
|
|
;;
|
|
final)
|
|
name=$(unique_name "$1")
|
|
if [[ "$name" ]]; then
|
|
change_state "$name" Final
|
|
fi
|
|
;;
|
|
supersede)
|
|
name=$(unique_name "$1")
|
|
newname=$(unique_name "$2")
|
|
newname="${newname##*/}"
|
|
newname="${newname%.txt}"
|
|
if [[ "$name" && "$newname" ]]; then
|
|
change_state "$name" "Superseded by $newname"
|
|
fi
|
|
;;
|
|
drop)
|
|
name=$(unique_name "$1")
|
|
if [[ "$name" ]]; then
|
|
change_state "$name" Dropped
|
|
fi
|
|
;;
|
|
comment)
|
|
name=$(unique_name "$1")
|
|
if [[ "$name" ]]; then
|
|
add_comment "${name}"
|
|
process_file "$name"
|
|
fi
|
|
;;
|
|
discard)
|
|
name=$(unique_name "$1")
|
|
|
|
if [[ "$name" ]]; then
|
|
for link in ./doc/devel/rfc_*/${name##*/}; do :; done
|
|
|
|
if [[ -h "$link" ]]; then
|
|
git rm -f "${link}" || rm "${link}"
|
|
fi
|
|
|
|
git rm -f "${name}" || rm "${name}"
|
|
fi
|
|
;;
|
|
wrap)
|
|
find_rfc "$1" | while read file; do
|
|
smart_wrap "$file" 80
|
|
process_file "$file"
|
|
done
|
|
;;
|
|
smart_wrap)
|
|
# multipurpose smart wrap
|
|
smart_wrap "$1" ${2:-80}
|
|
;;
|
|
help|*)
|
|
usage
|
|
;;
|
|
esac
|
|
|
|
|
|
|