├── .gitignore ├── .travis.yml ├── HISTORY.md ├── Makefile ├── README.md ├── index.js ├── lib ├── index.js ├── tableize.js └── to_args.js ├── package.json └── test ├── fixtures └── test.ledger ├── lib ├── index_test.js ├── tableize_test.js └── to_args_test.js ├── mocha.opts └── setup.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | ghc: 5 | - 7.8 6 | addons: 7 | apt: 8 | packages: 9 | - hledger 10 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## [v1.1.1] 2 | > Aug 8, 2016 3 | 4 | - Support `{ mode: 'list' }` option. 5 | 6 | [v1.1.1]: https://github.com/rstacruz/node-hledger/compare/v1.0.0...v1.1.1 7 | 8 | ## v1.0.0 9 | > Dec 6, 2015 10 | 11 | - Initial release. 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Use mdx to update readme.md 2 | update: README.md 3 | README.md: lib/index.js lib/tableize.js 4 | ( sed '//q' $@; \ 5 | echo; \ 6 | ./node_modules/.bin/mdx $^ --format markdown; \ 7 | sed -n '//,$$p' $@ ) > $@_ 8 | @mv $@_ $@ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-hledger 2 | 3 | Node.js API for [hledger]. 4 | 5 | ``` 6 | npm install --save-exact hledger 7 | ``` 8 | 9 | [![Status](https://travis-ci.org/rstacruz/node-hledger.svg?branch=master)](https://travis-ci.org/rstacruz/node-hledger "See test builds") 10 | 11 | ## API 12 | 13 | 14 | 15 | ### hledger 16 | 17 | > `hledger(args, options)` 18 | 19 | Invokes hledger and returns a promise. It resolves into the CSV output as a 20 | 2D array. 21 | 22 | ```js 23 | var hledger = require('hledger') 24 | 25 | hledger(['bal', 'Assets']) 26 | .then((data) => ...) 27 | 28 | [ [ 'account', 'balance' ], 29 | [ 'Assets', '$200' ], 30 | [ 'Assets:Savings', '$150' ], 31 | [ 'Assets:Checking', '$50' ] ] 32 | ``` 33 | 34 | You can invoke it with a string: 35 | 36 | ```js 37 | hledger('bal Assets') 38 | ``` 39 | 40 | You can then use functions to make better sense of them: 41 | 42 | ```js 43 | hledger(['bal', 'Assets']) 44 | .then(hledger.tableize) 45 | .then((data) => ...) 46 | 47 | [ { account: 'Assets', balance: '$200' }, 48 | { account: 'Assets:Savings', balance: '$150' }, 49 | ... ] 50 | ``` 51 | 52 | You may pass the option `{ mode: 'list' }` to support commands that don't 53 | have CSV output (eg, `accounts`). 54 | 55 | ```js 56 | hledger('accounts', { mode: 'list' }) 57 | .then(data => ...) 58 | 59 | [ 'Assets:Savings', 60 | 'Assets:Checking' 61 | ] 62 | 63 | ``` 64 | 65 | ### hledger.tableize 66 | 67 | > `tableize(list)` 68 | 69 | hledger.tableize: 70 | Turns a CSV-based array into an table list. 71 | 72 | ```js 73 | input = [ 74 | ['account', 'amount'], 75 | ['Savings', '$100'], 76 | ['Checking', '$150'] 77 | ] 78 | 79 | tableize(input) 80 | // [ { account: 'Savings', amount: '$100' }, 81 | // { account: 'Checking', amount: '$200' } ] 82 | ``` 83 | 84 | Used for piping into `hledger()`'s promise output: 85 | 86 | ```js 87 | hledger('...') 88 | .then(hledger.tableize) 89 | .then((data) => ...) 90 | ``` 91 | 92 | 93 | [hledger]: http://hledger.org/ 94 | 95 | ## Thanks 96 | 97 | **node-hledger** © 2015+, Rico Sta. Cruz. Released under the [MIT] License.
98 | Authored and maintained by Rico Sta. Cruz with help from contributors ([list][contributors]). 99 | 100 | > [ricostacruz.com](http://ricostacruz.com)  ·  101 | > GitHub [@rstacruz](https://github.com/rstacruz)  ·  102 | > Twitter [@rstacruz](https://twitter.com/rstacruz) 103 | 104 | [MIT]: http://mit-license.org/ 105 | [contributors]: http://github.com/rstacruz/node-hledger/contributors 106 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index') 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Promise = require('pinkie-promise') 4 | var spawn = require('child_process').spawn 5 | var csvParse = require('csv-parse') 6 | var tableize = require('./tableize') 7 | var toArgs = require('./to_args') 8 | 9 | /** 10 | * Invokes hledger and returns a promise. It resolves into the CSV output as a 11 | * 2D array. 12 | * 13 | * var hledger = require('hledger') 14 | * 15 | * hledger(['bal', 'Assets']) 16 | * .then((data) => ...) 17 | * 18 | * [ [ 'account', 'balance' ], 19 | * [ 'Assets', '$200' ], 20 | * [ 'Assets:Savings', '$150' ], 21 | * [ 'Assets:Checking', '$50' ] ] 22 | * 23 | * You can invoke it with a string: 24 | * 25 | * hledger('bal Assets') 26 | * 27 | * You can then use functions to make better sense of them: 28 | * 29 | * hledger(['bal', 'Assets']) 30 | * .then(hledger.tableize) 31 | * .then((data) => ...) 32 | * 33 | * [ { account: 'Assets', balance: '$200' }, 34 | * { account: 'Assets:Savings', balance: '$150' }, 35 | * ... ] 36 | * 37 | * You may pass the option `{ mode: 'list' }` to support commands that don't 38 | * have CSV output (eg, `accounts`). 39 | * 40 | * hledger('accounts', { mode: 'list' }) 41 | * .then(data => ...) 42 | * 43 | * [ 'Assets:Savings', 44 | * 'Assets:Checking' 45 | * ] 46 | */ 47 | 48 | function hledger (args, options) { 49 | var HLEDGER_BIN = process.env['HLEDGER_BIN'] || 'hledger' 50 | var mode = (options && options.mode) || 'csv' 51 | args = toArgs(args) 52 | 53 | if (mode === 'csv') { 54 | args = args.concat(['-O', 'csv']) 55 | } 56 | 57 | return new Promise(function (resolve, reject) { 58 | var child = spawn(HLEDGER_BIN, args) 59 | var stderr = '' 60 | var stdout = '' 61 | 62 | child.stdout.on('data', function (data) { stdout += data.toString() }) 63 | child.stderr.on('data', function (data) { stderr += data.toString() }) 64 | child.on('error', (err) => reject(err)) 65 | 66 | child.on('close', function (code) { 67 | if (code !== 0) { 68 | var err = new Error(stderr.trim()) 69 | err.code = code 70 | reject(err) 71 | } else if (mode === 'csv') { 72 | csvParse(stdout, function (err, data) { 73 | if (err) throw reject(err) 74 | resolve(data) 75 | }) 76 | } else { 77 | resolve(stdout.trim().split('\n')) 78 | } 79 | }) 80 | }) 81 | } 82 | 83 | // 84 | // Exports 85 | // 86 | 87 | hledger.tableize = require('./tableize') 88 | 89 | module.exports = hledger 90 | -------------------------------------------------------------------------------- /lib/tableize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * hledger.tableize: 3 | * Turns a CSV-based array into an table list. 4 | * 5 | * input = [ 6 | * ['account', 'amount'], 7 | * ['Savings', '$100'], 8 | * ['Checking', '$150'] 9 | * ] 10 | * 11 | * tableize(input) 12 | * // [ { account: 'Savings', amount: '$100' }, 13 | * // { account: 'Checking', amount: '$200' } ] 14 | * 15 | * Used for piping into `hledger()`'s promise output: 16 | * 17 | * hledger('...') 18 | * .then(hledger.tableize) 19 | * .then((data) => ...) 20 | */ 21 | 22 | function tableize (list) { 23 | if (!Array.isArray(list)) throw new Error('tableize: expected an array') 24 | 25 | const keys = list[0] 26 | return list.slice(1).map((row) => { 27 | return keys.reduce((item, key, idx) => { 28 | item[key] = row[idx] 29 | return item 30 | }, {}) 31 | }) 32 | } 33 | 34 | module.exports = tableize 35 | -------------------------------------------------------------------------------- /lib/to_args.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Internal: helper to convert `args` into an arguments array. 3 | * Takes in an array or a string. 4 | */ 5 | 6 | module.exports = function toArgs (args) { 7 | if (typeof args === 'string') { 8 | var shellquote = require('shell-quote') 9 | return shellquote.parse(args).map(function (arg) { 10 | if (arg && arg.op) return arg.op 11 | return arg 12 | }) 13 | } 14 | 15 | return args 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hledger", 3 | "description": "Node.js API for hledger.", 4 | "version": "1.1.1", 5 | "author": "Rico Sta. Cruz ", 6 | "bugs": { 7 | "url": "https://github.com/rstacruz/node-hledger/issues" 8 | }, 9 | "dependencies": { 10 | "csv-parse": "1.0.1", 11 | "pinkie-promise": "2.0.0", 12 | "shell-quote": "1.4.3" 13 | }, 14 | "devDependencies": { 15 | "expect": "1.13.0", 16 | "mdx": "0.2.2", 17 | "mocha": "2.3.4" 18 | }, 19 | "directories": { 20 | "test": "test" 21 | }, 22 | "homepage": "https://github.com/rstacruz/node-hledger#readme", 23 | "keywords": [ 24 | "api", 25 | "hledger", 26 | "ledger" 27 | ], 28 | "license": "MIT", 29 | "main": "index.js", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/rstacruz/node-hledger.git" 33 | }, 34 | "scripts": { 35 | "test": "mocha", 36 | "prepublish": "make -B README.md" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/fixtures/test.ledger: -------------------------------------------------------------------------------- 1 | 2015/01/01 * Opening balances 2 | Assets:Savings $100 3 | Assets:Checking $150 4 | Equity:Opening balances 5 | -------------------------------------------------------------------------------- /test/lib/index_test.js: -------------------------------------------------------------------------------- 1 | var hl = require('../../index') 2 | 3 | describe('hledger', function () { 4 | describe('basic test', function () { 5 | before(function () { 6 | return hl(['-f', 'test/fixtures/test.ledger', 'bal']) 7 | .then(hl.tableize) 8 | .then((data) => { 9 | this.data = data 10 | }) 11 | }) 12 | 13 | it('parses 1st account', function () { 14 | expect(this.data[0]) 15 | .toEqual({ account: 'Assets', balance: '$250' }) 16 | }) 17 | 18 | it('parses 2nd account', function () { 19 | expect(this.data[1]) 20 | .toEqual({ account: 'Assets:Checking', balance: '$150' }) 21 | }) 22 | }) 23 | 24 | describe('errors when hledger is not found', function () { 25 | before(function () { 26 | process.env.HLEDGER_BIN = 'nonexistentexecutable' 27 | }) 28 | 29 | after(function () { 30 | delete process.env.HLEDGER_BIN 31 | }) 32 | 33 | it('are handled', function () { 34 | return invert(hl(['...'])) 35 | .then((err) => { 36 | expect(err.message).toEqual('spawn nonexistentexecutable ENOENT') 37 | }) 38 | }) 39 | }) 40 | 41 | describe('handles accounts', function () { 42 | it('works', function () { 43 | return hl(['-f', 'test/fixtures/test.ledger', 'accounts'], { mode: 'list' }) 44 | .then((data) => { 45 | expect(data).toEqual([ 46 | 'Assets:Checking', 47 | 'Assets:Savings', 48 | 'Equity:Opening balances' 49 | ]) 50 | }) 51 | }) 52 | }) 53 | 54 | describe('errors with unknown flags', function () { 55 | it('are handled', function () { 56 | return invert(hl(['-X'])) 57 | .then((err) => { 58 | expect(err.message).toInclude('hledger: Unknown flag: -X') 59 | }) 60 | }) 61 | 62 | it('includes the code', function () { 63 | return invert(hl(['-X'])) 64 | .then((err) => { 65 | expect(err.code).toEqual(1) 66 | }) 67 | }) 68 | }) 69 | }) 70 | 71 | /* 72 | * Inverts a promise 73 | */ 74 | 75 | function invert (promise) { 76 | return new Promise((resolve, reject) => { 77 | promise 78 | .then((data) => { 79 | reject(new Error('Promise succeeded when expected to fail')) 80 | }) 81 | .catch((err) => { 82 | resolve(err) 83 | }) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /test/lib/tableize_test.js: -------------------------------------------------------------------------------- 1 | const tableize = require('../../lib/tableize') 2 | 3 | describe('tableize', function () { 4 | it('works', function () { 5 | const input = [ 6 | ['account', 'amount'], 7 | ['Savings', '$100'], 8 | ['Checking', '$150'] 9 | ] 10 | 11 | expect(tableize(input)).toEqual([ 12 | { account: 'Savings', amount: '$100' }, 13 | { account: 'Checking', amount: '$150' } 14 | ]) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/lib/to_args_test.js: -------------------------------------------------------------------------------- 1 | var toArgs = require('../../lib/to_args') 2 | 3 | describe('to args', function () { 4 | it('works with arrays', function () { 5 | expect(toArgs(['a', 'b'])).toEqual(['a', 'b']) 6 | }) 7 | 8 | it('works with strings', function () { 9 | expect(toArgs('a b')).toEqual(['a', 'b']) 10 | }) 11 | 12 | it('works with strings with quotes', function () { 13 | expect(toArgs('a "b c" d')).toEqual(['a', 'b c', 'd']) 14 | }) 15 | 16 | it('works with strings with control chars', function () { 17 | expect(toArgs('a "b c" > d')).toEqual(['a', 'b c', '>', 'd']) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/setup 2 | --recursive 3 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | global.expect = require('expect') 2 | --------------------------------------------------------------------------------