From 39616b70b11c07c1ed86110e4da7ee0b7986b3f8 Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Wed, 17 Aug 2016 16:24:01 -0500 Subject: [PATCH] Removed -e/--env flags, allow # comment lines in env file, bug fixes - Removed the -e/--env flags in favor of just requiring the first arg to env-cmd to be the relative path to the env file - Comments are now valid in env files using the # - Empty lines in env file are now ignored - Fixed bug env file no longer needs to end on a newline --- .npmignore | 1 - CHANGELOG.md | 6 ++++ README.md | 11 +++--- lib/index.js | 76 +++++++++++++++++++++++----------------- package.json | 2 +- test/.env | 3 -- test/.env-malformed | 3 -- test/test.js | 85 ++++++++++++++++++++++++++++++--------------- 8 files changed, 115 insertions(+), 72 deletions(-) delete mode 100644 test/.env delete mode 100644 test/.env-malformed diff --git a/.npmignore b/.npmignore index 3f7ab23..76096a2 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,3 @@ -.gitignore coverage/ node_modules/ test/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a31c608..54eee2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.0.0 +- BREAKING: Removed the `-e` and `--env` flags! Now it just expects the first arg to `env-cmd` to be the relative path to the env file: `env-cmd env_file command carg1 carg2` +- Changed ParseEnvFile over to more generic name: ParseEnvString +- ParseEnvString: Ignore comment lines (lines starting with '#') +- ParseEnvString: Ignore empty lines in env file + ## 1.0.1 - Fixed badges - Added .npmignore diff --git a/README.md b/README.md index 5483045..3ae1e0f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A simple node program for executing commands using an environment from an env fi ## Usage **Environment file ``./test/.env`** ``` +# This is a comment ENV1=THANKS ENV2=FORALL ENV4=THEFISH @@ -22,7 +23,7 @@ ENV4=THEFISH ```js { "scripts": { - "test": "env-cmd -e ./test/.env mocha -R spec" + "test": "env-cmd ./test/.env mocha -R spec" } } ``` @@ -30,15 +31,15 @@ or **Terminal** ```sh -./node_modules/.bin/env-cmd -e ./test/.env node index.js +./node_modules/.bin/env-cmd ./test/.env node index.js ``` ## Why -Because sometimes it just too cumbersome passing tons of environment variables to scripts. Its usually just easier to have a file with all the vars in them, especially for development. +Because sometimes its just too cumbersome passing lots of environment variables to scripts. Its usually just easier to have a file with all the vars in them, especially for development and testing. -**Do not commit sensitive data to a public git!** +**Do not commit sensitive env data to a public git repo!** ## Special Thanks -Special thanks to [cross-env](https://github.com/kentcdodds/cross-env) for inspiration (use's the same `cross-spawn` lib underneath). +Special thanks to [cross-env](https://github.com/kentcdodds/cross-env) for inspiration (use's the same `cross-spawn` lib underneath too). diff --git a/lib/index.js b/lib/index.js index 11d5b71..a7b6c36 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,8 +5,21 @@ const path = require('path') const fs = require('fs') function EnvCmd (args) { + // Parse the args from the command line const parsedArgs = ParseArgs(args) - const env = ParseEnvFile(parsedArgs.envFilePath) + + // Attempt to open the provided file + let file + try { + file = fs.readFileSync(parsedArgs.envFilePath, { encoding: 'utf8' }) + } catch (e) { + throw new Error(`Error! Could not find or read file at ${parsedArgs.envFilePath}`) + } + + // Parse the env file string + const env = ParseEnvString(file) + + // Execute the command with the given environment variables if (parsedArgs.command) { const proc = spawn(parsedArgs.command, parsedArgs.commandArgs, { stdio: 'inherit', @@ -19,30 +32,25 @@ function EnvCmd (args) { } function ParseArgs (args) { - if (args.length < 3) { + if (args.length < 2) { throw new Error('Error! Too few arguments passed to env-cmd.') } - const envFileFlags = /(^\-e$|^\-\-env$)/g let envFilePath let command let commandArgs = args.slice() while (commandArgs.length) { const arg = commandArgs.shift() - // if this is the env file flag the get the file - if (arg.match(envFileFlags)) { - envFilePath = path.resolve(process.cwd(), commandArgs.shift()) + // assume the first arg is the env file + if (!envFilePath) { + envFilePath = path.resolve(process.cwd(), arg) } else { command = arg break } } - if (!envFilePath) { - throw new Error('Error! No -e or --env flag passed.') - } - return { envFilePath, command, @@ -50,38 +58,44 @@ function ParseArgs (args) { } } -function ParseEnvFile (envFilePath) { - let file = fs.readFileSync(envFilePath, { encoding: 'utf8' }) +function ParseEnvString (envFileString) { const envs = Object.assign({}, process.env) - while (file.length) { - // Get the full line - const line = file.slice(0, file.indexOf('\n') + 1) + while (envFileString.length) { + // The the last index of the line using the newline delimiter + let endOfLineIndex = envFileString.indexOf('\n') - // Shrink the file by 1 line - file = file.slice(line.length) - - // Parse the line - const equalSign = line.indexOf('=') - - if (equalSign === -1) { - throw new Error(`Error! Malformed line in ${path.parse(envFilePath).base}.`) + // If no newline, then assume end of file + if (endOfLineIndex === -1) { + endOfLineIndex = envFileString.length } - // Set then new env var - envs[line.slice(0, equalSign)] = line.slice(line.indexOf('=') + 1, -1) + // Get the full line + const line = envFileString.slice(0, endOfLineIndex + 1) + + // Shrink the file by 1 line + envFileString = envFileString.slice(line.length) + + // Only parse lines that are not empty and don't begin with # + if (line.length > 1 && line[0] !== '#') { + // Parse the line + const equalSign = line.indexOf('=') + + if (equalSign === -1) { + throw new Error('Error! Malformed line in env file.') + } + + // Set then new env var + envs[line.slice(0, equalSign)] = line.slice(equalSign + 1, endOfLineIndex) + } } return envs } function PrintHelp () { return ` -Usage: env-cmd -e [file] command [command options] +Usage: env-cmd env_file command [command options] A simple application for running a cli application using an env config file - -Options: - - -e, --env Relative path to the env file ` } @@ -98,7 +112,7 @@ process.on('uncaughtException', HandleUncaughtExceptions) module.exports = { EnvCmd, ParseArgs, - ParseEnvFile, + ParseEnvString, PrintHelp, HandleUncaughtExceptions } diff --git a/package.json b/package.json index 28c8569..47f7236 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "env-cmd", - "version": "1.0.1", + "version": "2.0.0", "description": "Executes a command using the envs in the provided env file", "main": "lib/index.js", "bin": { diff --git a/test/.env b/test/.env deleted file mode 100644 index 397890e..0000000 --- a/test/.env +++ /dev/null @@ -1,3 +0,0 @@ -BOB=COOL -NODE_ENV=dev -NICE=42 diff --git a/test/.env-malformed b/test/.env-malformed deleted file mode 100644 index 3a1857b..0000000 --- a/test/.env-malformed +++ /dev/null @@ -1,3 +0,0 @@ -BOB=COOL -NODE_ENV dev -NICE=42 diff --git a/test/test.js b/test/test.js index c907b27..8ea7d19 100644 --- a/test/test.js +++ b/test/test.js @@ -5,14 +5,18 @@ const describe = require('mocha').describe const it = require('mocha').it const afterEach = require('mocha').afterEach const beforeEach = require('mocha').beforeEach +const before = require('mocha').before +const after = require('mocha').after const path = require('path') const proxyquire = require('proxyquire') const sinon = require('sinon') +const fs = require('fs') const spawnStub = sinon.spy(() => ({ on: sinon.stub(), exit: sinon.stub() })) + const lib = proxyquire('../lib', { 'cross-spawn': { spawn: spawnStub @@ -20,29 +24,29 @@ const lib = proxyquire('../lib', { }) const EnvCmd = lib.EnvCmd const ParseArgs = lib.ParseArgs -const ParseEnvFile = lib.ParseEnvFile +const ParseEnvString = lib.ParseEnvString const PrintHelp = lib.PrintHelp const HandleUncaughtExceptions = lib.HandleUncaughtExceptions describe('env-cmd', function () { describe('ParseArgs', function () { it('should parse out the -e envfile path', function () { - const parsedArgs = ParseArgs(['-e', './test/envFile', 'command', 'cmda1', 'cmda2']) + const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2']) assert(parsedArgs.envFilePath === path.join(__dirname, 'envFile')) }) it('should parse out the --env envfile path', function () { - const parsedArgs = ParseArgs(['--env', './test/envFile', 'command', 'cmda1', 'cmda2']) + const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2']) assert(parsedArgs.envFilePath === path.join(__dirname, 'envFile')) }) it('should parse out the command', function () { - const parsedArgs = ParseArgs(['-e', './test/envFile', 'command', 'cmda1', 'cmda2']) + const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2']) assert(parsedArgs.command === 'command') }) it('should parse out the command args', function () { - const parsedArgs = ParseArgs(['-e', './test/envFile', 'command', 'cmda1', 'cmda2']) + const parsedArgs = ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2']) assert(parsedArgs.commandArgs.length === 2) assert(parsedArgs.commandArgs[0] === 'cmda1') assert(parsedArgs.commandArgs[1] === 'cmda2') @@ -50,55 +54,80 @@ describe('env-cmd', function () { it('should error out if incorrect number of args passed', function () { try { - ParseArgs(['-e', './test/envFile']) + ParseArgs(['./test/envFile']) } catch (e) { assert(e.message === 'Error! Too few arguments passed to env-cmd.') return } - assert(!'No exepection thrown') - }) - - it('should error out if no envFile flag found', function () { - try { - ParseArgs(['./test/envFile', 'command', 'cmda1', 'cmda2']) - } catch (e) { - assert(e.message === 'Error! No -e or --env flag passed.') - return - } - assert(!'No exepection thrown') + assert(!'No exception thrown') }) }) - describe('ParseEnvFile', function () { - it('should parse out vars in the environment variable file', function () { - const env = ParseEnvFile(path.join(__dirname, '/.env')) + describe('ParseEnvString', function () { + it('should parse out vars in the environment variable string', function () { + const env = ParseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n') assert(env.BOB === 'COOL') assert(env.NODE_ENV === 'dev') - assert(env.NICE === '42') + assert(env.ANSWER === '42') }) - it('should parse out vars in the environment variable file', function () { + it('should ignore comment lines (starting with \'#\') and empty lines', function () { + const env = ParseEnvString('#BOB=COOL\nNODE_ENV=dev\n\n#ANSWER=42\n') + assert(env.BOB === undefined) + assert(env.NODE_ENV === 'dev') + assert(env.ANSWER === undefined) + }) + + it('should parse out env vars even if string does not end in \'\\n\'', function () { + const env = ParseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42') + assert(env.BOB === 'COOL') + assert(env.NODE_ENV === 'dev') + assert(env.ANSWER === '42') + }) + + it('should throw parse error due to malformed env var string', function () { try { - ParseEnvFile(path.join(__dirname, '/.env-malformed')) + ParseEnvString('BOB=COOL\nNODE_ENV dev\nANSWER=42\n') } catch (e) { - assert(e.message === 'Error! Malformed line in .env-malformed.') + assert(e.message === 'Error! Malformed line in env file.') return } - assert(!'No exepection thrown') + assert(!'No exception thrown') }) }) describe('EnvCmd', function () { + before(function () { + this.readFileStub = sinon.stub(fs, 'readFileSync') + }) + after(function () { + this.readFileStub.restore() + }) afterEach(function () { spawnStub.reset() + this.readFileStub.reset() }) it('should spawn a new process with the env vars set', function () { - EnvCmd(['-e', './test/.env', 'echo', '$BOB']) + this.readFileStub.returns('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n') + EnvCmd(['./test/.env', 'echo', '$BOB']) + assert(this.readFileStub.args[0][0] === path.join(process.cwd(), 'test/.env')) assert(spawnStub.args[0][0] === 'echo') assert(spawnStub.args[0][1][0] === '$BOB') assert(spawnStub.args[0][2].env.BOB === 'COOL') assert(spawnStub.args[0][2].env.NODE_ENV === 'dev') - assert(spawnStub.args[0][2].env.NICE === '42') + assert(spawnStub.args[0][2].env.ANSWER === '42') + }) + + it('should throw error if file does not exist', function () { + this.readFileStub.restore() + try { + EnvCmd(['./test/.non-existent-file', 'echo', '$BOB']) + } catch (e) { + const resolvedPath = path.join(process.cwd(), 'test/.non-existent-file') + assert(e.message === `Error! Could not find or read file at ${resolvedPath}`) + return + } + assert(!'No exception thrown') }) }) @@ -108,7 +137,7 @@ describe('env-cmd', function () { assert(typeof helpText === 'string') assert(helpText.match(/Usage/g).length !== 0) assert(helpText.match(/env-cmd/).length !== 0) - assert(helpText.match(/-e/).length !== 0) + assert(helpText.match(/env_file/).length !== 0) }) }) describe('HandleUncaughtExceptions', function () {