Pure bash program for auto-filling a template file (Part 2)Pure bash program for auto-filling a template file...
Can a German sentence have two subjects?
Why doesn't Newton's third law mean a person bounces back to where they started when they hit the ground?
If Manufacturer spice model and Datasheet give different values which should I use?
What is the white spray-pattern residue inside these Falcon Heavy nozzles?
How did the USSR manage to innovate in an environment characterized by government censorship and high bureaucracy?
Why CLRS example on residual networks does not follows its formula?
Can an x86 CPU running in real mode be considered to be basically an 8086 CPU?
Example of a relative pronoun
What are these boxed doors outside store fronts in New York?
How is it possible for user's password to be changed after storage was encrypted? (on OS X, Android)
Is it possible to make sharp wind that can cut stuff from afar?
My colleague's body is amazing
How do I create uniquely male characters?
How can bays and straits be determined in a procedurally generated map?
How is this relation reflexive?
least quadratic residue under GRH: an EXPLICIT bound
Why is an old chain unsafe?
Infinite past with a beginning?
Why did the Germans forbid the possession of pet pigeons in Rostov-on-Don in 1941?
Why can't I see bouncing of a switch on an oscilloscope?
declaring a variable twice in IIFE
A newer friend of my brother's gave him a load of baseball cards that are supposedly extremely valuable. Is this a scam?
How is the claim "I am in New York only if I am in America" the same as "If I am in New York, then I am in America?
I see my dog run
Pure bash program for auto-filling a template file (Part 2)
Pure bash program for auto-filling a template file with ENV variables'find' template for handling any filebash script for printer administrationAn atexit for BashBash CGI Upload FileLocating the Bash history file for a userBash - Compile C++ File Functionbash script - sed - template file processingS3 Bash Tools Part 2S3 Bash Tools Part 3Pure bash program for auto-filling a template file with ENV variables
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{ margin-bottom:0;
}
$begingroup$
This is my rewrite of the program posted here, based on janos's comments. I also added the ability to read from stdin.
I've also included a test script. I wanted something lightweight I could easily re-run, but didn't want a dependency or full test framework like bats. Comments on the test script are also welcome.
New Program
#!/bin/bash
set -euo pipefail
die() {
printf '%sn' "$1" >&2
exit 1
}
show_help() {
>&2 echo "Usage:
$ $0 <filename>
The program will read from stdin if <filename> is not given.
Description:
Fills in template files with ENV variables. <filename> is assumed to be a
template file whose variables are enclosed in double braces like:
Some content with {{ MY_VAR1 }}
or {{ MY_VAR1 }} and {{ MY_VAR2 }}
where MY_VAR1 and MY_VAR2 and ENV variables. Assuming that the ENV variables
are set:
$ export MY_VAR1=value_1
$ export MY_VAR2=value_2
then executing this script on a file with the above content will output:
Some content with value_1
or value_1 and value_2
"
}
if [[ $# -gt 0 && ( $1 = '-h' || $1 = '--help' ) ]]; then
show_help
exit 0
fi
# If given, ensure arg is a file:
if [[ $# -gt 0 && ! -f $1 ]]; then
die "'$1' is not a file."
fi
# If we're reading from stdin, save its contents to a temp file
# This is because we need to read it twice: first to extract
# the required vars, then again to replace them.
if [[ $# -eq 0 ]]; then
# Read stdin into a temp file
tmpfile=$(mktemp)
cat /dev/stdin > "$tmpfile"
# Clean it up, saving to FDs to read from
exec 3< "$tmpfile"
exec 4< "$tmpfile"
rm "$tmpfile"
else
exec 3< "$1"
exec 4< "$1"
fi
# Gather all the required template variables
vars=()
while IFS= read -r line; do
vars+=( "$line" )
done < <( grep -Eo '{{ ([-_[:alnum:]]*) }}' <&3 |
grep -Eo '([-_[:alnum:]]*)' |
sort -u )
# Verify that all template variables exist
missing=()
for var in "${vars[@]}"; do
if [[ -z ${!var+x} ]]; then
missing+=( "$var" )
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
>&2 echo "The following required variables have not been exported:"
for var in "${missing[@]}"; do
>&2 echo "${var}"
done
exit 1
fi
# Dynamically construct the sed cmd to do the replacement
sed_cmd=
for var in "${vars[@]}"; do
# sanitize the user's input (ie, the var's value) by prepending a backslash
# to our sed delimiter (#) as well as to backslashes themselves, to prevent
# the input from being interpreted by sed as a special character like a
# backreference (1) or a tab (t), etc
escaped_val=$(printf "%s" "${!var}" | sed -E 's|([#])|\1|g')
sed_cmd+="s#\{\{ ${var} }}#${escaped_val}#g;"
done
sed -E "${sed_cmd}" <&4
Test Script
#!/bin/bash
# shellcheck disable=SC2030,SC2031
# disabling because we're modifying in a subshell by design
set -euo pipefail
# Do everything in a sub-process to keep parent process clean
(
new_test() {
echo "$1"
unset MY_VAR1
unset MY_VAR2
}
basic_tmpl='
hello there {{ MY_VAR1 }}
some other stuff
foo: {{ MY_VAR2 }}
line with both: {{ MY_VAR1 }} and {{ MY_VAR2 }}'
# Each test goes in a subshell too
(
new_test "Should error when tmpl variables are missing"
printf "%s" "$basic_tmpl" | ./fill_template 2>/dev/null
&& echo 'FAIL' || echo 'PASS'
)
(
new_test "Should succeed when tmpl variables are set"
export MY_VAR1=val1
export MY_VAR2=val2
printf "%s" "$basic_tmpl" | ./fill_template >/dev/null 2>&1
&& echo 'PASS' || echo 'FAIL'
)
(
new_test "Basic template should produce expected output"
export MY_VAR1=val1
export MY_VAR2=val2
result=$(printf '%s' "$basic_tmpl" | ./fill_template 2> /dev/null)
expected='
hello there val1
some other stuff
foo: val2
line with both: val1 and val2'
[[ "$result" = "$expected" ]] && echo 'PASS' || echo 'FAIL'
)
(
new_test "Values with spaces/slashes/metachars still work"
export MY_VAR1='/some/path/and_1_t+_'
export MY_VAR2='blah _\_ baz'
result=$(printf '%snn' "$basic_tmpl" | ./fill_template 2> /dev/null)
expected='
hello there /some/path/and_1_t+_
some other stuff
foo: blah _\_ baz
line with both: /some/path/and_1_t+_ and blah _\_ baz'
[[ "$result" = "$expected" ]] && echo 'PASS' || echo 'FAIL'
)
)
bash
$endgroup$
add a comment |
$begingroup$
This is my rewrite of the program posted here, based on janos's comments. I also added the ability to read from stdin.
I've also included a test script. I wanted something lightweight I could easily re-run, but didn't want a dependency or full test framework like bats. Comments on the test script are also welcome.
New Program
#!/bin/bash
set -euo pipefail
die() {
printf '%sn' "$1" >&2
exit 1
}
show_help() {
>&2 echo "Usage:
$ $0 <filename>
The program will read from stdin if <filename> is not given.
Description:
Fills in template files with ENV variables. <filename> is assumed to be a
template file whose variables are enclosed in double braces like:
Some content with {{ MY_VAR1 }}
or {{ MY_VAR1 }} and {{ MY_VAR2 }}
where MY_VAR1 and MY_VAR2 and ENV variables. Assuming that the ENV variables
are set:
$ export MY_VAR1=value_1
$ export MY_VAR2=value_2
then executing this script on a file with the above content will output:
Some content with value_1
or value_1 and value_2
"
}
if [[ $# -gt 0 && ( $1 = '-h' || $1 = '--help' ) ]]; then
show_help
exit 0
fi
# If given, ensure arg is a file:
if [[ $# -gt 0 && ! -f $1 ]]; then
die "'$1' is not a file."
fi
# If we're reading from stdin, save its contents to a temp file
# This is because we need to read it twice: first to extract
# the required vars, then again to replace them.
if [[ $# -eq 0 ]]; then
# Read stdin into a temp file
tmpfile=$(mktemp)
cat /dev/stdin > "$tmpfile"
# Clean it up, saving to FDs to read from
exec 3< "$tmpfile"
exec 4< "$tmpfile"
rm "$tmpfile"
else
exec 3< "$1"
exec 4< "$1"
fi
# Gather all the required template variables
vars=()
while IFS= read -r line; do
vars+=( "$line" )
done < <( grep -Eo '{{ ([-_[:alnum:]]*) }}' <&3 |
grep -Eo '([-_[:alnum:]]*)' |
sort -u )
# Verify that all template variables exist
missing=()
for var in "${vars[@]}"; do
if [[ -z ${!var+x} ]]; then
missing+=( "$var" )
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
>&2 echo "The following required variables have not been exported:"
for var in "${missing[@]}"; do
>&2 echo "${var}"
done
exit 1
fi
# Dynamically construct the sed cmd to do the replacement
sed_cmd=
for var in "${vars[@]}"; do
# sanitize the user's input (ie, the var's value) by prepending a backslash
# to our sed delimiter (#) as well as to backslashes themselves, to prevent
# the input from being interpreted by sed as a special character like a
# backreference (1) or a tab (t), etc
escaped_val=$(printf "%s" "${!var}" | sed -E 's|([#])|\1|g')
sed_cmd+="s#\{\{ ${var} }}#${escaped_val}#g;"
done
sed -E "${sed_cmd}" <&4
Test Script
#!/bin/bash
# shellcheck disable=SC2030,SC2031
# disabling because we're modifying in a subshell by design
set -euo pipefail
# Do everything in a sub-process to keep parent process clean
(
new_test() {
echo "$1"
unset MY_VAR1
unset MY_VAR2
}
basic_tmpl='
hello there {{ MY_VAR1 }}
some other stuff
foo: {{ MY_VAR2 }}
line with both: {{ MY_VAR1 }} and {{ MY_VAR2 }}'
# Each test goes in a subshell too
(
new_test "Should error when tmpl variables are missing"
printf "%s" "$basic_tmpl" | ./fill_template 2>/dev/null
&& echo 'FAIL' || echo 'PASS'
)
(
new_test "Should succeed when tmpl variables are set"
export MY_VAR1=val1
export MY_VAR2=val2
printf "%s" "$basic_tmpl" | ./fill_template >/dev/null 2>&1
&& echo 'PASS' || echo 'FAIL'
)
(
new_test "Basic template should produce expected output"
export MY_VAR1=val1
export MY_VAR2=val2
result=$(printf '%s' "$basic_tmpl" | ./fill_template 2> /dev/null)
expected='
hello there val1
some other stuff
foo: val2
line with both: val1 and val2'
[[ "$result" = "$expected" ]] && echo 'PASS' || echo 'FAIL'
)
(
new_test "Values with spaces/slashes/metachars still work"
export MY_VAR1='/some/path/and_1_t+_'
export MY_VAR2='blah _\_ baz'
result=$(printf '%snn' "$basic_tmpl" | ./fill_template 2> /dev/null)
expected='
hello there /some/path/and_1_t+_
some other stuff
foo: blah _\_ baz
line with both: /some/path/and_1_t+_ and blah _\_ baz'
[[ "$result" = "$expected" ]] && echo 'PASS' || echo 'FAIL'
)
)
bash
$endgroup$
$begingroup$
Can you clarify what you mean by "pure" Bash? I assumed it meant using only builtins, but I seegrep
,sed
,mktemp
,sort
and more.sed
in particular, being a program interpreter, seems far from any definition of "pure Bash" than I can conceive.
$endgroup$
– Toby Speight
2 days ago
$begingroup$
sed, awk, etc are fine.
$endgroup$
– Jonah
2 days ago
add a comment |
$begingroup$
This is my rewrite of the program posted here, based on janos's comments. I also added the ability to read from stdin.
I've also included a test script. I wanted something lightweight I could easily re-run, but didn't want a dependency or full test framework like bats. Comments on the test script are also welcome.
New Program
#!/bin/bash
set -euo pipefail
die() {
printf '%sn' "$1" >&2
exit 1
}
show_help() {
>&2 echo "Usage:
$ $0 <filename>
The program will read from stdin if <filename> is not given.
Description:
Fills in template files with ENV variables. <filename> is assumed to be a
template file whose variables are enclosed in double braces like:
Some content with {{ MY_VAR1 }}
or {{ MY_VAR1 }} and {{ MY_VAR2 }}
where MY_VAR1 and MY_VAR2 and ENV variables. Assuming that the ENV variables
are set:
$ export MY_VAR1=value_1
$ export MY_VAR2=value_2
then executing this script on a file with the above content will output:
Some content with value_1
or value_1 and value_2
"
}
if [[ $# -gt 0 && ( $1 = '-h' || $1 = '--help' ) ]]; then
show_help
exit 0
fi
# If given, ensure arg is a file:
if [[ $# -gt 0 && ! -f $1 ]]; then
die "'$1' is not a file."
fi
# If we're reading from stdin, save its contents to a temp file
# This is because we need to read it twice: first to extract
# the required vars, then again to replace them.
if [[ $# -eq 0 ]]; then
# Read stdin into a temp file
tmpfile=$(mktemp)
cat /dev/stdin > "$tmpfile"
# Clean it up, saving to FDs to read from
exec 3< "$tmpfile"
exec 4< "$tmpfile"
rm "$tmpfile"
else
exec 3< "$1"
exec 4< "$1"
fi
# Gather all the required template variables
vars=()
while IFS= read -r line; do
vars+=( "$line" )
done < <( grep -Eo '{{ ([-_[:alnum:]]*) }}' <&3 |
grep -Eo '([-_[:alnum:]]*)' |
sort -u )
# Verify that all template variables exist
missing=()
for var in "${vars[@]}"; do
if [[ -z ${!var+x} ]]; then
missing+=( "$var" )
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
>&2 echo "The following required variables have not been exported:"
for var in "${missing[@]}"; do
>&2 echo "${var}"
done
exit 1
fi
# Dynamically construct the sed cmd to do the replacement
sed_cmd=
for var in "${vars[@]}"; do
# sanitize the user's input (ie, the var's value) by prepending a backslash
# to our sed delimiter (#) as well as to backslashes themselves, to prevent
# the input from being interpreted by sed as a special character like a
# backreference (1) or a tab (t), etc
escaped_val=$(printf "%s" "${!var}" | sed -E 's|([#])|\1|g')
sed_cmd+="s#\{\{ ${var} }}#${escaped_val}#g;"
done
sed -E "${sed_cmd}" <&4
Test Script
#!/bin/bash
# shellcheck disable=SC2030,SC2031
# disabling because we're modifying in a subshell by design
set -euo pipefail
# Do everything in a sub-process to keep parent process clean
(
new_test() {
echo "$1"
unset MY_VAR1
unset MY_VAR2
}
basic_tmpl='
hello there {{ MY_VAR1 }}
some other stuff
foo: {{ MY_VAR2 }}
line with both: {{ MY_VAR1 }} and {{ MY_VAR2 }}'
# Each test goes in a subshell too
(
new_test "Should error when tmpl variables are missing"
printf "%s" "$basic_tmpl" | ./fill_template 2>/dev/null
&& echo 'FAIL' || echo 'PASS'
)
(
new_test "Should succeed when tmpl variables are set"
export MY_VAR1=val1
export MY_VAR2=val2
printf "%s" "$basic_tmpl" | ./fill_template >/dev/null 2>&1
&& echo 'PASS' || echo 'FAIL'
)
(
new_test "Basic template should produce expected output"
export MY_VAR1=val1
export MY_VAR2=val2
result=$(printf '%s' "$basic_tmpl" | ./fill_template 2> /dev/null)
expected='
hello there val1
some other stuff
foo: val2
line with both: val1 and val2'
[[ "$result" = "$expected" ]] && echo 'PASS' || echo 'FAIL'
)
(
new_test "Values with spaces/slashes/metachars still work"
export MY_VAR1='/some/path/and_1_t+_'
export MY_VAR2='blah _\_ baz'
result=$(printf '%snn' "$basic_tmpl" | ./fill_template 2> /dev/null)
expected='
hello there /some/path/and_1_t+_
some other stuff
foo: blah _\_ baz
line with both: /some/path/and_1_t+_ and blah _\_ baz'
[[ "$result" = "$expected" ]] && echo 'PASS' || echo 'FAIL'
)
)
bash
$endgroup$
This is my rewrite of the program posted here, based on janos's comments. I also added the ability to read from stdin.
I've also included a test script. I wanted something lightweight I could easily re-run, but didn't want a dependency or full test framework like bats. Comments on the test script are also welcome.
New Program
#!/bin/bash
set -euo pipefail
die() {
printf '%sn' "$1" >&2
exit 1
}
show_help() {
>&2 echo "Usage:
$ $0 <filename>
The program will read from stdin if <filename> is not given.
Description:
Fills in template files with ENV variables. <filename> is assumed to be a
template file whose variables are enclosed in double braces like:
Some content with {{ MY_VAR1 }}
or {{ MY_VAR1 }} and {{ MY_VAR2 }}
where MY_VAR1 and MY_VAR2 and ENV variables. Assuming that the ENV variables
are set:
$ export MY_VAR1=value_1
$ export MY_VAR2=value_2
then executing this script on a file with the above content will output:
Some content with value_1
or value_1 and value_2
"
}
if [[ $# -gt 0 && ( $1 = '-h' || $1 = '--help' ) ]]; then
show_help
exit 0
fi
# If given, ensure arg is a file:
if [[ $# -gt 0 && ! -f $1 ]]; then
die "'$1' is not a file."
fi
# If we're reading from stdin, save its contents to a temp file
# This is because we need to read it twice: first to extract
# the required vars, then again to replace them.
if [[ $# -eq 0 ]]; then
# Read stdin into a temp file
tmpfile=$(mktemp)
cat /dev/stdin > "$tmpfile"
# Clean it up, saving to FDs to read from
exec 3< "$tmpfile"
exec 4< "$tmpfile"
rm "$tmpfile"
else
exec 3< "$1"
exec 4< "$1"
fi
# Gather all the required template variables
vars=()
while IFS= read -r line; do
vars+=( "$line" )
done < <( grep -Eo '{{ ([-_[:alnum:]]*) }}' <&3 |
grep -Eo '([-_[:alnum:]]*)' |
sort -u )
# Verify that all template variables exist
missing=()
for var in "${vars[@]}"; do
if [[ -z ${!var+x} ]]; then
missing+=( "$var" )
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
>&2 echo "The following required variables have not been exported:"
for var in "${missing[@]}"; do
>&2 echo "${var}"
done
exit 1
fi
# Dynamically construct the sed cmd to do the replacement
sed_cmd=
for var in "${vars[@]}"; do
# sanitize the user's input (ie, the var's value) by prepending a backslash
# to our sed delimiter (#) as well as to backslashes themselves, to prevent
# the input from being interpreted by sed as a special character like a
# backreference (1) or a tab (t), etc
escaped_val=$(printf "%s" "${!var}" | sed -E 's|([#])|\1|g')
sed_cmd+="s#\{\{ ${var} }}#${escaped_val}#g;"
done
sed -E "${sed_cmd}" <&4
Test Script
#!/bin/bash
# shellcheck disable=SC2030,SC2031
# disabling because we're modifying in a subshell by design
set -euo pipefail
# Do everything in a sub-process to keep parent process clean
(
new_test() {
echo "$1"
unset MY_VAR1
unset MY_VAR2
}
basic_tmpl='
hello there {{ MY_VAR1 }}
some other stuff
foo: {{ MY_VAR2 }}
line with both: {{ MY_VAR1 }} and {{ MY_VAR2 }}'
# Each test goes in a subshell too
(
new_test "Should error when tmpl variables are missing"
printf "%s" "$basic_tmpl" | ./fill_template 2>/dev/null
&& echo 'FAIL' || echo 'PASS'
)
(
new_test "Should succeed when tmpl variables are set"
export MY_VAR1=val1
export MY_VAR2=val2
printf "%s" "$basic_tmpl" | ./fill_template >/dev/null 2>&1
&& echo 'PASS' || echo 'FAIL'
)
(
new_test "Basic template should produce expected output"
export MY_VAR1=val1
export MY_VAR2=val2
result=$(printf '%s' "$basic_tmpl" | ./fill_template 2> /dev/null)
expected='
hello there val1
some other stuff
foo: val2
line with both: val1 and val2'
[[ "$result" = "$expected" ]] && echo 'PASS' || echo 'FAIL'
)
(
new_test "Values with spaces/slashes/metachars still work"
export MY_VAR1='/some/path/and_1_t+_'
export MY_VAR2='blah _\_ baz'
result=$(printf '%snn' "$basic_tmpl" | ./fill_template 2> /dev/null)
expected='
hello there /some/path/and_1_t+_
some other stuff
foo: blah _\_ baz
line with both: /some/path/and_1_t+_ and blah _\_ baz'
[[ "$result" = "$expected" ]] && echo 'PASS' || echo 'FAIL'
)
)
bash
bash
asked Apr 1 at 11:19
JonahJonah
3,516718
3,516718
$begingroup$
Can you clarify what you mean by "pure" Bash? I assumed it meant using only builtins, but I seegrep
,sed
,mktemp
,sort
and more.sed
in particular, being a program interpreter, seems far from any definition of "pure Bash" than I can conceive.
$endgroup$
– Toby Speight
2 days ago
$begingroup$
sed, awk, etc are fine.
$endgroup$
– Jonah
2 days ago
add a comment |
$begingroup$
Can you clarify what you mean by "pure" Bash? I assumed it meant using only builtins, but I seegrep
,sed
,mktemp
,sort
and more.sed
in particular, being a program interpreter, seems far from any definition of "pure Bash" than I can conceive.
$endgroup$
– Toby Speight
2 days ago
$begingroup$
sed, awk, etc are fine.
$endgroup$
– Jonah
2 days ago
$begingroup$
Can you clarify what you mean by "pure" Bash? I assumed it meant using only builtins, but I see
grep
, sed
, mktemp
, sort
and more. sed
in particular, being a program interpreter, seems far from any definition of "pure Bash" than I can conceive.$endgroup$
– Toby Speight
2 days ago
$begingroup$
Can you clarify what you mean by "pure" Bash? I assumed it meant using only builtins, but I see
grep
, sed
, mktemp
, sort
and more. sed
in particular, being a program interpreter, seems far from any definition of "pure Bash" than I can conceive.$endgroup$
– Toby Speight
2 days ago
$begingroup$
sed, awk, etc are fine.
$endgroup$
– Jonah
2 days ago
$begingroup$
sed, awk, etc are fine.
$endgroup$
– Jonah
2 days ago
add a comment |
1 Answer
1
active
oldest
votes
$begingroup$
The grep
matches a lot of strings that can't be valid bash identifiers ("alphanumeric characters and underscores, beginning with an alphabetic character or an underscore"). There are some magical exceptions like $*
and $@
but I'm assuming you aren't trying to support those in your templates.
Reading inputs twice is not necessary; a single-pass approach would remove a lot of complexity from your program, and make it possible to process infinitely large inputs on STDIN. The only real drawback is that it will exit on the first undefined value, instead of producing a list.
And finally, everyone has their own definitions, but—to me—"pure bash" means "without the use of external programs." Making your program meet this bar not only makes it faster, it removes even more complexity, because you don't need to filter values through an external program that might misunderstand them. It also means your template values need not be exported. They only need to be visible to the current shell.
This version, implemented as a function, uses only bash builtins and passes your tests once ./fill_template
is replaced by fill_template
:
fill_template() {
(( ${#@} )) || set -- /dev/stdin
local file line eof original name value
for file; do
while true; do
read -r line
eof=$?
while [[ $line =~ {{" "*([a-zA-Z_][_a-zA-Z0-9]*)" "*}} ]]; do
original=$BASH_REMATCH
name=${BASH_REMATCH[1]}
value=${!name?"unset template variable: $name"}
line=${line//$original/$value}
done
printf -- %s "$line"
(( eof )) && break || printf "n"
done <$file
done
}
$endgroup$
1
$begingroup$
Very nice. I do still like the idea of printing out all missing ENV vars (I think it will be an easy thing to forget when using the script) but I much prefer the simplicity of the single pass. I'm thinking a better design would be a single pass like your suggestion as the default (with die on first error), and then a separate option to list all missing vars.
$endgroup$
– Jonah
Apr 2 at 1:06
$begingroup$
You could switch modes on error -- proceed normally until the first failure; on failure set a flag that suppresses normal template output and only prints the names of unset variables.
$endgroup$
– Oh My Goodness
Apr 3 at 22:54
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f216644%2fpure-bash-program-for-auto-filling-a-template-file-part-2%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
The grep
matches a lot of strings that can't be valid bash identifiers ("alphanumeric characters and underscores, beginning with an alphabetic character or an underscore"). There are some magical exceptions like $*
and $@
but I'm assuming you aren't trying to support those in your templates.
Reading inputs twice is not necessary; a single-pass approach would remove a lot of complexity from your program, and make it possible to process infinitely large inputs on STDIN. The only real drawback is that it will exit on the first undefined value, instead of producing a list.
And finally, everyone has their own definitions, but—to me—"pure bash" means "without the use of external programs." Making your program meet this bar not only makes it faster, it removes even more complexity, because you don't need to filter values through an external program that might misunderstand them. It also means your template values need not be exported. They only need to be visible to the current shell.
This version, implemented as a function, uses only bash builtins and passes your tests once ./fill_template
is replaced by fill_template
:
fill_template() {
(( ${#@} )) || set -- /dev/stdin
local file line eof original name value
for file; do
while true; do
read -r line
eof=$?
while [[ $line =~ {{" "*([a-zA-Z_][_a-zA-Z0-9]*)" "*}} ]]; do
original=$BASH_REMATCH
name=${BASH_REMATCH[1]}
value=${!name?"unset template variable: $name"}
line=${line//$original/$value}
done
printf -- %s "$line"
(( eof )) && break || printf "n"
done <$file
done
}
$endgroup$
1
$begingroup$
Very nice. I do still like the idea of printing out all missing ENV vars (I think it will be an easy thing to forget when using the script) but I much prefer the simplicity of the single pass. I'm thinking a better design would be a single pass like your suggestion as the default (with die on first error), and then a separate option to list all missing vars.
$endgroup$
– Jonah
Apr 2 at 1:06
$begingroup$
You could switch modes on error -- proceed normally until the first failure; on failure set a flag that suppresses normal template output and only prints the names of unset variables.
$endgroup$
– Oh My Goodness
Apr 3 at 22:54
add a comment |
$begingroup$
The grep
matches a lot of strings that can't be valid bash identifiers ("alphanumeric characters and underscores, beginning with an alphabetic character or an underscore"). There are some magical exceptions like $*
and $@
but I'm assuming you aren't trying to support those in your templates.
Reading inputs twice is not necessary; a single-pass approach would remove a lot of complexity from your program, and make it possible to process infinitely large inputs on STDIN. The only real drawback is that it will exit on the first undefined value, instead of producing a list.
And finally, everyone has their own definitions, but—to me—"pure bash" means "without the use of external programs." Making your program meet this bar not only makes it faster, it removes even more complexity, because you don't need to filter values through an external program that might misunderstand them. It also means your template values need not be exported. They only need to be visible to the current shell.
This version, implemented as a function, uses only bash builtins and passes your tests once ./fill_template
is replaced by fill_template
:
fill_template() {
(( ${#@} )) || set -- /dev/stdin
local file line eof original name value
for file; do
while true; do
read -r line
eof=$?
while [[ $line =~ {{" "*([a-zA-Z_][_a-zA-Z0-9]*)" "*}} ]]; do
original=$BASH_REMATCH
name=${BASH_REMATCH[1]}
value=${!name?"unset template variable: $name"}
line=${line//$original/$value}
done
printf -- %s "$line"
(( eof )) && break || printf "n"
done <$file
done
}
$endgroup$
1
$begingroup$
Very nice. I do still like the idea of printing out all missing ENV vars (I think it will be an easy thing to forget when using the script) but I much prefer the simplicity of the single pass. I'm thinking a better design would be a single pass like your suggestion as the default (with die on first error), and then a separate option to list all missing vars.
$endgroup$
– Jonah
Apr 2 at 1:06
$begingroup$
You could switch modes on error -- proceed normally until the first failure; on failure set a flag that suppresses normal template output and only prints the names of unset variables.
$endgroup$
– Oh My Goodness
Apr 3 at 22:54
add a comment |
$begingroup$
The grep
matches a lot of strings that can't be valid bash identifiers ("alphanumeric characters and underscores, beginning with an alphabetic character or an underscore"). There are some magical exceptions like $*
and $@
but I'm assuming you aren't trying to support those in your templates.
Reading inputs twice is not necessary; a single-pass approach would remove a lot of complexity from your program, and make it possible to process infinitely large inputs on STDIN. The only real drawback is that it will exit on the first undefined value, instead of producing a list.
And finally, everyone has their own definitions, but—to me—"pure bash" means "without the use of external programs." Making your program meet this bar not only makes it faster, it removes even more complexity, because you don't need to filter values through an external program that might misunderstand them. It also means your template values need not be exported. They only need to be visible to the current shell.
This version, implemented as a function, uses only bash builtins and passes your tests once ./fill_template
is replaced by fill_template
:
fill_template() {
(( ${#@} )) || set -- /dev/stdin
local file line eof original name value
for file; do
while true; do
read -r line
eof=$?
while [[ $line =~ {{" "*([a-zA-Z_][_a-zA-Z0-9]*)" "*}} ]]; do
original=$BASH_REMATCH
name=${BASH_REMATCH[1]}
value=${!name?"unset template variable: $name"}
line=${line//$original/$value}
done
printf -- %s "$line"
(( eof )) && break || printf "n"
done <$file
done
}
$endgroup$
The grep
matches a lot of strings that can't be valid bash identifiers ("alphanumeric characters and underscores, beginning with an alphabetic character or an underscore"). There are some magical exceptions like $*
and $@
but I'm assuming you aren't trying to support those in your templates.
Reading inputs twice is not necessary; a single-pass approach would remove a lot of complexity from your program, and make it possible to process infinitely large inputs on STDIN. The only real drawback is that it will exit on the first undefined value, instead of producing a list.
And finally, everyone has their own definitions, but—to me—"pure bash" means "without the use of external programs." Making your program meet this bar not only makes it faster, it removes even more complexity, because you don't need to filter values through an external program that might misunderstand them. It also means your template values need not be exported. They only need to be visible to the current shell.
This version, implemented as a function, uses only bash builtins and passes your tests once ./fill_template
is replaced by fill_template
:
fill_template() {
(( ${#@} )) || set -- /dev/stdin
local file line eof original name value
for file; do
while true; do
read -r line
eof=$?
while [[ $line =~ {{" "*([a-zA-Z_][_a-zA-Z0-9]*)" "*}} ]]; do
original=$BASH_REMATCH
name=${BASH_REMATCH[1]}
value=${!name?"unset template variable: $name"}
line=${line//$original/$value}
done
printf -- %s "$line"
(( eof )) && break || printf "n"
done <$file
done
}
edited Apr 1 at 18:40
answered Apr 1 at 17:35
Oh My GoodnessOh My Goodness
2,202315
2,202315
1
$begingroup$
Very nice. I do still like the idea of printing out all missing ENV vars (I think it will be an easy thing to forget when using the script) but I much prefer the simplicity of the single pass. I'm thinking a better design would be a single pass like your suggestion as the default (with die on first error), and then a separate option to list all missing vars.
$endgroup$
– Jonah
Apr 2 at 1:06
$begingroup$
You could switch modes on error -- proceed normally until the first failure; on failure set a flag that suppresses normal template output and only prints the names of unset variables.
$endgroup$
– Oh My Goodness
Apr 3 at 22:54
add a comment |
1
$begingroup$
Very nice. I do still like the idea of printing out all missing ENV vars (I think it will be an easy thing to forget when using the script) but I much prefer the simplicity of the single pass. I'm thinking a better design would be a single pass like your suggestion as the default (with die on first error), and then a separate option to list all missing vars.
$endgroup$
– Jonah
Apr 2 at 1:06
$begingroup$
You could switch modes on error -- proceed normally until the first failure; on failure set a flag that suppresses normal template output and only prints the names of unset variables.
$endgroup$
– Oh My Goodness
Apr 3 at 22:54
1
1
$begingroup$
Very nice. I do still like the idea of printing out all missing ENV vars (I think it will be an easy thing to forget when using the script) but I much prefer the simplicity of the single pass. I'm thinking a better design would be a single pass like your suggestion as the default (with die on first error), and then a separate option to list all missing vars.
$endgroup$
– Jonah
Apr 2 at 1:06
$begingroup$
Very nice. I do still like the idea of printing out all missing ENV vars (I think it will be an easy thing to forget when using the script) but I much prefer the simplicity of the single pass. I'm thinking a better design would be a single pass like your suggestion as the default (with die on first error), and then a separate option to list all missing vars.
$endgroup$
– Jonah
Apr 2 at 1:06
$begingroup$
You could switch modes on error -- proceed normally until the first failure; on failure set a flag that suppresses normal template output and only prints the names of unset variables.
$endgroup$
– Oh My Goodness
Apr 3 at 22:54
$begingroup$
You could switch modes on error -- proceed normally until the first failure; on failure set a flag that suppresses normal template output and only prints the names of unset variables.
$endgroup$
– Oh My Goodness
Apr 3 at 22:54
add a comment |
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f216644%2fpure-bash-program-for-auto-filling-a-template-file-part-2%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
$begingroup$
Can you clarify what you mean by "pure" Bash? I assumed it meant using only builtins, but I see
grep
,sed
,mktemp
,sort
and more.sed
in particular, being a program interpreter, seems far from any definition of "pure Bash" than I can conceive.$endgroup$
– Toby Speight
2 days ago
$begingroup$
sed, awk, etc are fine.
$endgroup$
– Jonah
2 days ago