Running MongoDB scripts with command line arguments

17-May-2020 Like this? Dislike this? Let me know

MongoDB provides a command line interface (CLI), conveniently called mongo, much like all other databases or subsystems in general. From this CLI you can perform queries, perform administration, examine stats, etc.:

$ mongo
MongoDB Enterprise rs0:PRIMARY> use testX
switched to db testX
MongoDB Enterprise rs0:PRIMARY> show collections
product
trade
MongoDB Enterprise rs0:PRIMARY> db.trade.count();
63
MongoDB Enterprise rs0:PRIMARY> db.trade.findOne();
{
"_id" : ObjectId("5d2fcc8b983742c7e2b8df12"),
"cid" : "ABC123",
"product" : "A",
"amount" : NumberDecimal("100.000000000000"),
"tdate" : ISODate("2018-01-01T00:00:00Z")
}
The CLI also allows you to run javascript scripts, both from the invoking shell and the interpreter via the load() function. Note that there are a few differences (mostly conveniences) built into the CLI input handler that are not available in scripts; they are summarized here. Below is a small example:
$ cat myscript.js

// Whatever db is defaulted upon startup, getSiblingSB() is a way to switch over
// to a new target DB.  You must have permissions of course, but this function
// from a javascript basis is effectively "use testX", which is *not* a valid 
// javascript expression.  "use" and "show" are intercepted by the CLI before 
// passing them to the javascript engine.  Expressions read from a script, 
// however, are passed directly to the javascript engine:
db = db.getSiblingDB("testX"); 
c = db.trade.find({"product":"D"}).sort({"tdate":-1}).limit(1);
while(c.hasNext()) {
    var item = c.next();
    printjson(item);
}
$ mongo myscript.js
{
"_id" : ObjectId("5d2fcc8b983742c7e2b8df1d"),
"cid" : "JDU876",
"product" : "D",
"amount" : NumberDecimal("300.000000000000"),
"tdate" : ISODate("2018-03-01T00:00:00Z")
}

$ mongo 
MongoDB Enterprise rs0:PRIMARY> load("myscript.js")
{
"_id" : ObjectId("5d2fcc8b983742c7e2b8df1d"),
"cid" : "JDU876",
"product" : "D",
"amount" : NumberDecimal("300.000000000000"),
"tdate" : ISODate("2018-03-01T00:00:00Z")
}
Very often it is useful to run the CLI with arguments from the command line -- but there is no built-in way to do this. You can, however, "mimic" the behavior by combining the eval option along with a script name. The javascript (NOT JSON; more on this in just a bit) material presented in the eval option is evaluated before the script is loaded; therefore, you can set up a javascript object of information to be consumed by the script. Let's adopt args by convention for the name of this object:
$ cat myscript.js

target = "A";  // default

if(typeof args !== 'undefined') {  // defend against no command line args passed via --eval
    target = args.product; // pick up product arg
}

db = db.getSiblingDB("testX");
c = db.trade.find({"product":target}).sort({"tdate":-1}).limit(1);
while(c.hasNext()) {
    var item = c.next();
    printjson(item);
}

$ mongo myscript.js --eval 'args={"product":"D"}'
{
"_id" : ObjectId("5d2fcc8b983742c7e2b8df1d"),
"cid" : "JDU876",
"product" : "D",
"amount" : NumberDecimal("300.000000000000"),
"tdate" : ISODate("2018-03-01T00:00:00Z")
}
If you prefer, you can put options first, before the script argument e.g.:

$ mongo --host "mongodb://machine:port" --eval 'args={"product":"D"}' myscript.js 

Nice -- but there is real value in this approach. Since args is javascript, with a single variable you can pass in multiple scalars and non-scalars in a type-safe way to the script:
$ cat myscript.js

targets = ["A"];  // Ah!  Now the default is an *array* of one item, "A"
order = 1;        // default

if(typeof args !== 'undefined') {
    targets = args.products;
    order = args.order;
}

db = db.getSiblingDB("testX");
c = db.trade.find({"product":{$in:targets}}).sort({"tdate":order}); // take limit off...
while(c.hasNext()) {
    var item = c.next();
    printjson(item);
}
$ mongo myscript.js --eval 'args={"products":["A","D"],order:1}' 
{
"_id" : ObjectId("5d2fcc8b983742c7e2b8df12"),
"cid" : "ABC123",
"product" : "A",
"amount" : NumberDecimal("100.000000000000"),
"tdate" : ISODate("2018-01-01T00:00:00Z")
}
{
"_id" : ObjectId("5d2fcc8b983742c7e2b8df15"),
"cid" : "ABC123",
"product" : "D",
"amount" : NumberDecimal("98.000000000000"),
"tdate" : ISODate("2018-01-01T00:00:00Z")
}
...
It should be clear that any arbitrarily complex javascript structure can be easily passed on the command line in this fashion and is consumable in a way that is in fact easier than traditional command line arg passing. Just beware of proper handling and escaping of single and double quotes and environment variable substitution.

For example, how might we pass a penny-precise decimal arg on the command line? Assuming we change the script and the call to find to this:

vamt = new NumberDecimal("100");

if(typeof args !== 'undefined') {
    if(typeof args.products !== 'undefined') { products = args.products; }
    if(typeof args.order !== 'undefined') { order = args.order; }
    if(typeof args.amount !== 'undefined') { vamt = args.amount; }
}


c = db.trade.find({"product":{$in:products} , amount:{$lt:vamt}   }  ).sort({"tdate":order});
then we can do this; note that the less-than ($lt) operator filters out the 100.0 from the result given a javascript argument of NumberDecimal("100"). The use of the NumberDecimal class (which is already defined in the mongodb CLI javascript engine) with a string constructor ensures that the variable picked up inside the script is a true decimal type and does not succumb to the problems of string-to-float parsing of "regular" numbers, e.g. amount:1.09 turning into amount:1.08999999999999999.
$ mongo myscript.js --eval 'args={"products":["A","D"],order:1,amount:new NumberDecimal("100")}'
{
"_id" : ObjectId("5d2fcc8b983742c7e2b8df15"),
"cid" : "ABC123",
"product" : "D",
"amount" : NumberDecimal("98.000000000000"),
"tdate" : ISODate("2018-01-01T00:00:00Z")
}
...
This approach also works for dates. It means that your script does not have to worry about conversion from string (which is what the command line is) to the proper type in the script; it "arrives" as the proper type:

$ mongo myscript.js --eval 'args={"products":["A","D"],order:1,amount:new NumberDecimal("100"),startDate:new ISODate("2020-02-01")}'

NOTE: Be sure to be safe and use new to construct an object:
    BAD!  Both amount and startDate will be presented in myscript.js as strings; without
    new, an object is constructed and toString() called!
    $ mongo myscript.js --eval 'args={"products":["A","D"],order:1,amount:NumberDecimal("100"),startDate:ISODate("2020-02-01")}'

    CORRECT!  Use new:
    $ mongo myscript.js --eval 'args={"products":["A","D"],order:1,amount:new NumberDecimal("100"),startDate:new ISODate("2020-02-01")}'    
    
This is a simple script with little in the way of optional arg handling. The reader can use their own best practices to work through the incoming args variable to set up their resources. A simple way is to cascade the checks for existence, e.g.:
// DEFAULTS:
products = ["A"];  // default
order = -1;

if(typeof args !== 'undefined') {
    if(typeof args.products !== 'undefined') { products = args.products; }
    if(typeof args.order !== 'undefined') { order = args.order; }
}

Like this? Dislike this? Let me know


Site copyright © 2013-2024 Buzz Moschetti. All rights reserved