
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