|
|
@ -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 |