
2 changed files with 6 additions and 366 deletions
@ -1,366 +0,0 @@ |
|||||
#!/bin/sh |
|
||||
# vim: set ts=4: |
|
||||
#---help--- |
|
||||
# USAGE: |
|
||||
# esh [options] [--] <input> [<variable>...] |
|
||||
# esh <-h | -V> |
|
||||
# |
|
||||
# Process and evaluate an ESH template. |
|
||||
# |
|
||||
# ARGUMENTS: |
|
||||
# <input> Path of the template file or "-" to read from STDIN. |
|
||||
# <variable> Variable(s) specified as <name>=<value> to pass into the |
|
||||
# template (the have higher priority than environment |
|
||||
# variables). |
|
||||
# |
|
||||
# OPTIONS: |
|
||||
# -d Don't evaluate template, just dump a shell script. |
|
||||
# -o <file> Output file or "-" for STDOUT. Defaults to "-". |
|
||||
# -s <shell> Command name or path of the shell to use for template |
|
||||
# evaluation. It must not contain spaces. |
|
||||
# Defaults to "/bin/sh". |
|
||||
# -h Show this help message and exit. |
|
||||
# -V Print version and exit. |
|
||||
# |
|
||||
# ENVIRONMENT: |
|
||||
# ESH_AWK Command name of path of the awk program to use. |
|
||||
# It must not contain spaces. Defaults to "awk". |
|
||||
# ESH_MAX_DEPTH Maximum include depth. Defaults to 3. |
|
||||
# ESH_SHELL Same as -s. |
|
||||
# |
|
||||
# EXIT STATUS: |
|
||||
# 0 Clean exit, no error has encountered. |
|
||||
# 1 Generic error. |
|
||||
# 10 Invalid usage. |
|
||||
# 11 ESH syntax error. |
|
||||
# 12 Include error: file not found. |
|
||||
# 13 Include error: exceeded max include depth (ESH_MAX_DEPTH). |
|
||||
# |
|
||||
# Please report bugs at <https://github.com/jirutka/esh/issues>. |
|
||||
#---help--- |
|
||||
set -eu |
|
||||
|
|
||||
readonly PROGNAME='esh' |
|
||||
readonly VERSION='0.3.0' |
|
||||
readonly SCRIPTPATH="$0" |
|
||||
|
|
||||
AWK_CONVERTER=$(cat <<'AWK' |
|
||||
function fail(code, msg) { |
|
||||
state = "ERROR" |
|
||||
# FIXME: /dev/stderr is not portable |
|
||||
printf("%s: %s\n", line_info(), msg) > "/dev/stderr" |
|
||||
exit code |
|
||||
} |
|
||||
function line_info() { |
|
||||
return FILENAME ? (filenames[depth] ":" linenos[depth]) : "(init)" # (init) if inside BEGIN |
|
||||
} |
|
||||
# IMPORTANT: This is the only function that should print a newline. |
|
||||
function puts(str) { |
|
||||
print(line_info()) > MAP_FILE |
|
||||
print(str) |
|
||||
} |
|
||||
function fputs(str) { |
|
||||
printf("%s", str) |
|
||||
} |
|
||||
function trim(str) { |
|
||||
gsub(/^[ \t\r\n]+|[ \t\r\n]+$/, "", str) |
|
||||
return str |
|
||||
} |
|
||||
function read(len, _str) { |
|
||||
if (len == "") { |
|
||||
_str = buff |
|
||||
buff = "" |
|
||||
} else if (len > 0) { |
|
||||
_str = substr(buff, 1, len) |
|
||||
buff = substr(buff, len + 1, length(buff)) |
|
||||
} |
|
||||
return _str |
|
||||
} |
|
||||
function skip(len) { |
|
||||
buff = substr(buff, len + 1, length(buff)) |
|
||||
} |
|
||||
function flush(len, _str) { |
|
||||
_str = read(len) |
|
||||
|
|
||||
if (state == "TEXT") { |
|
||||
gsub("'", "'\\''", _str) |
|
||||
} |
|
||||
if (state != "COMMENT") { |
|
||||
fputs(_str) |
|
||||
} |
|
||||
} |
|
||||
function file_exists(filename, _junk) { |
|
||||
if ((getline _junk < filename) >= 0) { |
|
||||
close(filename) |
|
||||
return 1 |
|
||||
} |
|
||||
return 0 |
|
||||
} |
|
||||
function dirname(path) { |
|
||||
return sub(/\/[^\/]+\/*$/, "/", path) ? path : "" |
|
||||
} |
|
||||
function include(filename) { |
|
||||
if (index(filename, "/") != 1) { # if does not start with "/" |
|
||||
filename = dirname(filenames[depth]) filename |
|
||||
} |
|
||||
if (!file_exists(filename)) { |
|
||||
fail(12, "cannot include " filename ": not a file or not readable") |
|
||||
} |
|
||||
if (depth > MAX_DEPTH) { |
|
||||
fail(13, "cannot include " filename ": exceeded maximum depth of " MAX_DEPTH) |
|
||||
} |
|
||||
buffs[depth] = buff |
|
||||
states[depth] = state |
|
||||
filenames[depth + 1] = filename |
|
||||
depth++ |
|
||||
|
|
||||
init() |
|
||||
while ((getline buff < filename) > 0) { |
|
||||
if (print_nl && state != "COMMENT") { |
|
||||
puts("") |
|
||||
} |
|
||||
process_line() |
|
||||
} |
|
||||
end_text() |
|
||||
close(filename) |
|
||||
|
|
||||
depth-- |
|
||||
buff = buffs[depth] |
|
||||
state = states[depth] |
|
||||
} |
|
||||
function init() { |
|
||||
buff = "" |
|
||||
linenos[depth] = 0 |
|
||||
print_nl = 0 |
|
||||
start_text() |
|
||||
} |
|
||||
function start_text() { |
|
||||
puts("") |
|
||||
fputs("printf '%s' '") |
|
||||
state = "TEXT" |
|
||||
} |
|
||||
function end_text() { |
|
||||
if (state != "TEXT") { return } |
|
||||
puts("' #< " line_info()) |
|
||||
state = "UNDEF" |
|
||||
} |
|
||||
function process_line() { |
|
||||
linenos[depth]++ |
|
||||
|
|
||||
while (buff != "") { |
|
||||
print_nl = 1 |
|
||||
|
|
||||
if (state == "TEXT" && match(buff, /<%/)) { |
|
||||
flush(RSTART - 1) # print buff before "<%" |
|
||||
skip(2) # skip "<%" |
|
||||
|
|
||||
flag = substr(buff, 1, 1) |
|
||||
if (flag != "%") { |
|
||||
end_text() |
|
||||
} |
|
||||
if (flag == "%") { # <%% |
|
||||
skip(1) |
|
||||
fputs("<%") |
|
||||
} else if (flag == "=") { # <%= |
|
||||
skip(1) |
|
||||
fputs("__print ") |
|
||||
state = "TAG" |
|
||||
} else if (flag == "+") { # <%+ |
|
||||
if (!match(buff, /[^%]%>/)) { |
|
||||
fail(11, "syntax error: <%+ must be closed on the same line") |
|
||||
} |
|
||||
filename = trim(substr(buff, 2, match(buff, /.-?%>/) - 1)) |
|
||||
skip(RSTART) |
|
||||
include(filename) |
|
||||
state = "TAG" |
|
||||
} else if (flag == "#") { # <%# |
|
||||
state = "COMMENT" |
|
||||
} else { |
|
||||
state = "TAG" |
|
||||
} |
|
||||
} else if (state != "TEXT" && match(buff, /%>/)) { |
|
||||
flag = RSTART > 1 ? substr(buff, RSTART - 1, 1) : "" |
|
||||
|
|
||||
if (flag == "%") { # %%> |
|
||||
flush(RSTART - 2) |
|
||||
skip(1) |
|
||||
flush(2) |
|
||||
} else if (flag == "-") { # -%> |
|
||||
flush(RSTART - 2) |
|
||||
skip(3) |
|
||||
print_nl = 0 |
|
||||
} else { # %> |
|
||||
flush(RSTART - 1) |
|
||||
skip(2) |
|
||||
} |
|
||||
if (flag != "%") { |
|
||||
start_text() |
|
||||
} |
|
||||
} else { |
|
||||
flush() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
BEGIN { |
|
||||
FS = "" |
|
||||
depth = 0 |
|
||||
|
|
||||
puts("#!" (SHELL ~ /\// ? SHELL : "/usr/bin/env " SHELL)) |
|
||||
puts("set -eu") |
|
||||
puts("if ( set -o pipefail 2>/dev/null ); then set -o pipefail; fi") |
|
||||
puts("__print() { printf '%s' \"$*\"; }") |
|
||||
|
|
||||
split(VARS, _lines, /\n/) |
|
||||
for (_i in _lines) { |
|
||||
puts(_lines[_i]) |
|
||||
} |
|
||||
init() |
|
||||
} |
|
||||
{ |
|
||||
if (NR == 1) { |
|
||||
filenames[0] = FILENAME # this var is not defined in BEGIN so we must do it here |
|
||||
} |
|
||||
buff = $0 |
|
||||
process_line() |
|
||||
|
|
||||
if (print_nl && state != "COMMENT") { |
|
||||
puts("") |
|
||||
} |
|
||||
} |
|
||||
END { |
|
||||
end_text() |
|
||||
} |
|
||||
AWK |
|
||||
) |
|
||||
AWK_ERR_FILTER=$(cat <<'AWK' |
|
||||
function line_info(lno, _line, _i) { |
|
||||
while ((getline _line < MAPFILE) > 0 && _i++ < lno) { } |
|
||||
close(MAPFILE) |
|
||||
return _line |
|
||||
} |
|
||||
{ |
|
||||
if (match($0, "^" SRCFILE ":( line)? ?[0-9]+:") && match(substr($0, 1, RLENGTH), /[0-9]+:$/)) { |
|
||||
lno = substr($0, RSTART, RLENGTH - 1) + 0 |
|
||||
msg = substr($0, RSTART + RLENGTH + 1) # v-- some shells duplicate filename |
|
||||
msg = index(msg, SRCFILE ":") == 1 ? substr(msg, length(SRCFILE) + 3) : msg |
|
||||
print(line_info(lno) ": " msg) |
|
||||
} else if ($0 != "") { |
|
||||
print($0) |
|
||||
} |
|
||||
} |
|
||||
AWK |
|
||||
) |
|
||||
readonly AWK_CONVERTER AWK_ERR_FILTER |
|
||||
|
|
||||
print_help() { |
|
||||
sed -En '/^#---help---/,/^#---help---/p' "$SCRIPTPATH" | sed -E 's/^# ?//; 1d;$d;' |
|
||||
} |
|
||||
|
|
||||
filter_shell_stderr() { |
|
||||
$ESH_AWK \ |
|
||||
-v SRCFILE="$1" \ |
|
||||
-v MAPFILE="$2" \ |
|
||||
-- "$AWK_ERR_FILTER" |
|
||||
} |
|
||||
|
|
||||
evaluate() { |
|
||||
local srcfile="$1" |
|
||||
local mapfile="$2" |
|
||||
|
|
||||
# This FD redirection magic is for swapping stdout/stderr back and forth. |
|
||||
exec 3>&1 |
|
||||
{ set +e; $ESH_SHELL "$srcfile" 2>&1 1>&3; echo $? >>"$mapfile"; } \ |
|
||||
| filter_shell_stderr "$srcfile" "$mapfile" >&2 |
|
||||
exec 3>&- |
|
||||
|
|
||||
return $(tail -n 1 "$mapfile") |
|
||||
} |
|
||||
|
|
||||
convert() { |
|
||||
local input="$1" |
|
||||
local vars="$2" |
|
||||
local map_file="${3:-"/dev/null"}" |
|
||||
|
|
||||
$ESH_AWK \ |
|
||||
-v MAX_DEPTH="$ESH_MAX_DEPTH" \ |
|
||||
-v SHELL="$ESH_SHELL" \ |
|
||||
-v MAP_FILE="$map_file" \ |
|
||||
-v VARS="$vars" \ |
|
||||
-- "$AWK_CONVERTER" "$input" |
|
||||
} |
|
||||
|
|
||||
process() { |
|
||||
local input="$1" |
|
||||
local vars="$2" |
|
||||
local evaluate="${3:-yes}" |
|
||||
local ret=0 tmpfile mapfile |
|
||||
|
|
||||
if [ "$evaluate" = yes ]; then |
|
||||
tmpfile=$(mktemp) |
|
||||
mapfile=$(mktemp) |
|
||||
|
|
||||
convert "$input" "$vars" "$mapfile" > "$tmpfile" || ret=$? |
|
||||
test $ret -ne 0 || evaluate "$tmpfile" "$mapfile" || ret=$? |
|
||||
|
|
||||
rm -f "$tmpfile" "$mapfile" |
|
||||
else |
|
||||
convert "$input" "$vars" || ret=$? |
|
||||
fi |
|
||||
return $ret |
|
||||
} |
|
||||
|
|
||||
: ${ESH_AWK:="awk"} |
|
||||
: ${ESH_MAX_DEPTH:=3} |
|
||||
: ${ESH_SHELL:="/bin/sh"} |
|
||||
EVALUATE='yes' |
|
||||
OUTPUT='' |
|
||||
|
|
||||
while getopts ':dho:s:V' OPT; do |
|
||||
case "$OPT" in |
|
||||
d) EVALUATE=no;; |
|
||||
h) print_help; exit 0;; |
|
||||
o) OUTPUT="$OPTARG";; |
|
||||
s) ESH_SHELL="$OPTARG";; |
|
||||
V) echo "$PROGNAME $VERSION"; exit 0;; |
|
||||
'?') echo "$PROGNAME: unknown option: -$OPTARG" >&2; exit 10;; |
|
||||
esac |
|
||||
done |
|
||||
shift $(( OPTIND - 1 )) |
|
||||
|
|
||||
if [ $# -eq 0 ]; then |
|
||||
printf "$PROGNAME: %s\n\n" 'missing argument <input>' >&2 |
|
||||
print_help >&2 |
|
||||
exit 10 |
|
||||
fi |
|
||||
|
|
||||
INPUT="$1"; shift |
|
||||
[ "$INPUT" != '-' ] || INPUT='' |
|
||||
if [ "$INPUT" ] && ! [ -f "$INPUT" -a -r "$INPUT" ]; then |
|
||||
echo "$PROGNAME: can't read $INPUT: not a file or not readable" >&2; exit 10 |
|
||||
fi |
|
||||
|
|
||||
# Validate arguments. |
|
||||
for arg in "$@"; do |
|
||||
case "$arg" in |
|
||||
*=*) ;; |
|
||||
*) echo "$PROGNAME: illegal argument: $arg" >&2; exit 10;; |
|
||||
esac |
|
||||
done |
|
||||
|
|
||||
# Format variables into shell variable assignments. |
|
||||
vars=''; for item in "$@"; do |
|
||||
vars="$vars\n${item%%=*}='$( |
|
||||
printf %s "${item#*=}" | $ESH_AWK "{ gsub(/'/, \"'\\\\\\\''\"); print }" |
|
||||
)'" |
|
||||
done |
|
||||
|
|
||||
export ESH="$0" |
|
||||
|
|
||||
if [ "${OUTPUT#-}" ]; then |
|
||||
tmpfile="$(mktemp)" |
|
||||
trap "rm -f '$tmpfile'" EXIT HUP INT TERM |
|
||||
process "$INPUT" "$vars" "$EVALUATE" > "$tmpfile" |
|
||||
mv "$tmpfile" "$OUTPUT" |
|
||||
else |
|
||||
process "$INPUT" "$vars" "$EVALUATE" |
|
||||
fi |
|
Loading…
Reference in new issue