I've implemented CommonJS Modules 1.1.1 as a simple bash script with a bit of help from M4.
I'm interested in comments on the shell scripting and the javascript output.
Some specific questions on bash scripting:
- How are backquotes different from $( ... ) as far as evaluating stuff in strings?
- Are there places where I have improperly quoted variables, where things could break
if path names have spaces or weird characters?
normalize_path
looks wrong to me, but I'm not sure how to correct it. - Is there a better way to increment/decrement variables?
- Is anything done in a bad style?
I'm also interested in comments on the javascript output (the anonymous bootstrap object, and its init
function).
Also, is this a good implementation of CommonJS Modules? It passes all of the unit tests, but it may still have flaws. One thing on my to-do list is adding a command line argument / environment variable for a list of include paths.
com4 - the main program.
#!/bin/bash
export program="CoM4"
export usage="Usage: 0ドル -i <in_file.js> -o <out_file.js>";
export com4_path="$( dirname $( readlink -f "0ドル" ))"
export included_files="included_files.tmp"
export tmp_path="_tmp"
export abs_path=""
export rel_path=""
export include=""
export in_file=""
export out_file=""
export fail=0;
export test_mode=0;
export html=0;
export var_global="window"
export var_main=""
. "$com4_path/include.sh"
get_options "$@"
mkdir "$tmp_path"
echo -n > "$out_file"
echo -n > "$included_files"
abs_path="`normalize_path $( dirname "$in_file" )`";
rel_path="$abs_path";
id="`basename "$in_file"`";
if [ $html = 1 ]; then
echo "<html><head><script>" >> "$out_file";
fi
if [ $test_mode = 1 ]; then
echo "function print(text){ console.log(text); }" >> "$out_file";
fi
if [ "$var_main" != "" ]; then
echo -n "$var_main = " >> "$out_file";
fi
echo "({modules:[ ">> "$out_file";
write_module "$id" 1 >> "$out_file";
for file in $( ls $tmp_path ); do
cat "$tmp_path/$file" >> "$out_file";
done
echo "
//
// $program bootstrap
//
0],init:function(){
var boot=this, exports=[], require=function(id){ return exports[id] || void boot.modules[id](require, id ? {id:id} : require.main, exports[id]={}) || exports[id]; };
require.main={id:0};
return require(0);
}}).init();" >> "$out_file";
if [ $html = 1 ]; then
echo "</script></head><body></body></html>" >> "$out_file";
fi
rm "$included_files"
rm -r "$tmp_path"
include.sh - helper functions.
get_options ()
{
while getopts "i:o:g:m:I:th" opt; do
case "$opt" in
i)
in_file="$OPTARG";;
o)
out_file="$OPTARG";;
g)
var_global="$OPTARG";;
I)
include="$OPTARG";; # TODO: -I / include dirs
t)
test_mode=1;;
h)
html=1;;
m)
var_main="$OPTARG";;
[?])
fail=1;;
esac
done
if [ "$in_file" = "" ]; then
echo >&2 "0ドル: No input file specified."
fail=1
fi
if [ "$out_file" = "" ]; then
echo >&2 "0ドル: No output file specified."
fail=1
fi
if [ $fail != 0 ]; then
echo >&2 "$usage"
exit $fail
fi
}
normalize_path ()
{
echo "$(cd $(dirname "1ドル"); echo $PWD/$(basename "1ドル"))"
}
write_module ()
{
if [ "`echo 1ドル | egrep '^\.\.?/'`" = "" ]; then
path=$(normalize_path "$abs_path/1ドル.js")
else
path=$(normalize_path "$rel_path/1ドル.js")
fi
module_name=`abs_to_rel "$abs_path" "$path"`
loaded=0
old_path="$rel_path"
rel_path=$( dirname "$path" )
count=0
if [ 2ドル ]; then
require="require";
else
require="[_require_]";
fi
for included in `cat $included_files`; do
count=$(( $count + 1 ))
if [ "$included" = "$path" ]; then
count=$(( $count - 1 ))
echo -n "$require($count)"
return
fi
done
tmp_file="$tmp_path/$count"
if [ "2ドル" = "" ]; then
echo -n "$require($count)"
fi
echo -n > "$tmp_file";
echo "$path" >> "$included_files"
echo "
//
// $module_name
//" >> "$tmp_file"
if [ -e "$path" ]; then
echo "function(require, module, exports){ " >> "$tmp_file"
m4 -P "$com4_path/macros.m4" "$path" >> "$tmp_file"
echo "}," >> "$tmp_file"
else
echo ",// Module \"$module_name\" not found." >> "$tmp_file"
echo "Warning: Module \"$module_name\" not found." >&2
fi
rel_path="$old_path"
}
# both 1ドル and 2ドル are absolute paths
# returns 2ドル relative to 1ドル
abs_to_rel ()
{
source=1ドル
target=2ドル
common_part=$source
back=
while [ "${target#$common_part}" = "${target}" ]; do
common_part=$(dirname $common_part)
back="../${back}"
done
echo ${back}${target#$common_part/}
}
macros.m4 - require
macro.
m4_changequote([_,_])m4_dnl
m4_define(require, [_m4_esyscmd(
if [ 1ドル ]; then
. "$com4_path/include.sh"
write_module 1ドル
else
echo -n "[_[_require_]_]"
fi
)_])m4_dnl
m4_define(GLOBAL, [_m4_esyscmd(
echo -n "$var_global"
)_])m4_dnl
Here is an example of the output from one of the unit tests.
The print
function is a debug-mode feature to make CommonJS unit tests work properly.
function print(text){ console.log(text); }
({modules:[
//
// ../program.js
//
function(require, module, exports){
var test = require(1);
var a = require(3);
var b = require(4);
test.assert(a.a, 'a exists');
test.assert(b.b, 'b exists')
test.assert(a.a().b === b.b, 'a gets b');
test.assert(b.b().a === a.a, 'b gets a');
test.print('DONE', 'info');
},
//
// ../test.js
//
function(require, module, exports){
exports.print = typeof print !== "undefined" ? print : function () {
var system = require(2);
var stdio = system.stdio;
stdio.print.apply(stdio, arguments);
};
exports.assert = function (guard, message) {
if (guard) {
exports.print('PASS ' + message, 'pass');
} else {
exports.print('FAIL ' + message, 'fail');
}
};
},
//
// ../system.js
//
,// Module "../system.js" not found.
//
// ../a.js
//
function(require, module, exports){
exports.a = function () {
return b;
};
var b = require(4);
},
//
// ../b.js
//
function(require, module, exports){
var a = require(3);
exports.b = function () {
return a;
};
},
//
// CoM4 bootstrap
//
0],init:function(){
var boot=this, exports=[], require=function(id){ return exports[id] || void boot.modules[id](require, id ? {id:id} : require.main, exports[id]={}) || exports[id]; };
require.main={id:0};
return require(0);
}}).init();
1 Answer 1
Looks good with a quick scan-over. Except I think you should learn to use the bash regular expressions operator instead of using egrep... Didn't really look at your code well enough but you've used it in a test operator so... Yeah, might be better as a bash solution: [[ $ARG =~ EXPR ]]
shopt -s extglob
also activates extended reg-exes in bash but the using can be a bit frustrating because it isn't "regex standard" or full featured, but with some patience you can learn to do things with bash other's have never dreamed of.
Script inputs
Try this instead:
<<ends M4
YOUR SCRIPT HERE
You can put variables and substitutions here too.
If you like you can pipe the output from m4 into cat, bash, seed, js, rhino
whatever. I love this little trick :)
ends
Substitutions: no difference. Only syntax and nesting.
Increment/Decrement (arithmetic):
declare -i i
i+=X; # -= requires the use of let for some reason or another..
let i++; # standard arithmetic command
(( i++ )) # just like let/expr..
$[ i++ ] # another way to let expr
You can also use a integer variables without the $ sign as an index to arrays or strings even with incremental/decremental operators. This helps with writing short & concise loop codes.
I love javascript. I imagine someday I will write my own command shell using javascript as the controller back end. But I'm partial to using SpiderMonkey. You cannot beat having access to the C API with a stick (from a shell script)
Happy hacking. Hope this helps. Your code looks good. But it will get better with many trials and errors. Hopefully someone with more insight into your JS tool can provide you a better answer. Just thought it was worth my two cents.
you can join my web and ask me anything about bash, or join my admin group. I have confidence in your skills.
-
\$\begingroup\$ m4 --- ugh... the ugly beast. I hate being forced into that syntax. Would have been much better with more syntax customization on the back end. I'm writing my own macro processor using awk ATM, I love being able to define the language to suit the language I'm working with. The only problem is most interpreters don't think someone might want to have a preprocessor run along side them, so they don't put the proper error handling interface into the code :( \$\endgroup\$user13387– user133872012年05月13日 05:55:07 +00:00Commented May 13, 2012 at 5:55
-
\$\begingroup\$ Thanks for the review! The heredoc idea looks cool, but I'm not sure where I'd use it exactly... are you suggesting to put the contents of the m4 script in a heredoc string in the bash script? I think I like it better as a separate script. Your comment about typed variables is very interesting, I had no idea this existed. I'm playing with it now. It seems that if you don't declare with -i,
+=
does string concatenation! As for m4, I agree the syntax is pretty bad, but it's powerful and gets the job done. I considered writing a simple text replacement script instead, but decided against... \$\endgroup\$Dagg– Dagg2012年05月13日 19:07:44 +00:00Commented May 13, 2012 at 19:07 -
\$\begingroup\$ If you need to do any preprocessing on your m4, awk, or sed scripts the here doc solution is quite viable. I usally set an alias like:
alias 'script:'='<<ends'
which looks way cooler!script: m4 | awk
catch my drift? Sure ya do!shopt -s expand_aliases
turns aliasing on for scripts. \$\endgroup\$user13387– user133872012年05月14日 01:47:58 +00:00Commented May 14, 2012 at 1:47 -
\$\begingroup\$ Sometimes i use the here-doc scripts to 'compile' some script in bash where the loop operates on a variable that won't change inside the loop.
source <(script: cat ... ends ... )
\$\endgroup\$user13387– user133872012年05月14日 01:50:25 +00:00Commented May 14, 2012 at 1:50