1
\$\begingroup\$

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();
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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.

https://www.facebook.com/alt.bash

\$\endgroup\$
4
  • \$\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
    Commented 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
    Commented 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
    Commented 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
    Commented May 14, 2012 at 1:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.