├── .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 | [](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 |
--------------------------------------------------------------------------------