Skip to content
This repository has been archived by the owner on Jan 6, 2021. It is now read-only.

feat: add default value handling and PWD resolution #219

Closed
wants to merge 11 commits into from
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ of setting it properly.

- [Installation](#installation)
- [Usage](#usage)
- [Default Values](#default-values)
- [`cross-env` vs `cross-env-shell`](#cross-env-vs-cross-env-shell)
- [Windows Issues](#windows-issues)
- [Inspiration](#inspiration)
Expand Down Expand Up @@ -126,6 +127,43 @@ Pay special attention to the **triple backslash** `(\\\)` **before** the
**double quotes** `(")` and the **absence** of **single quotes** `(')`. Both of
these conditions have to be met in order to work both on Windows and UNIX.

Note: `${PWD}` and `$PWD` substitute for the current working directory.

## Default Values

To reference an environment variable and provide a default value if that
variable is empty or not-defined, use the syntax:

```bash
${ENV_VAR_NAME:-default value}
```

Also supports recursive replacement in the default value allowing you to test
multiple environment variables before showing a default

```bash
# Value of FIRST_VAR_TO_TEST or SECOND_VAR_TO_TEST environment variable if blank or undefined.
# If both are blank or undefined, replace with an empty string
${FIRST_VAR_TO_TEST:-$SECOND_VAR_TO_TEST}
```

Can nest as many as you want

```bash
# Value of FIRST_VAR_TO_TEST, then SECOND_VAR_TO_TEST then THIRD_VAR_TO_TEST environment variables.
# If all are blank or undefined, the string "default value if all are empty or undefined"
${FIRST_VAR_TO_TEST:-${SECOND_VAR_TO_TEST:-${THIRD_VAR_TO_TEST:-default value if all are empty or undefined}}}
```

Or even use multiple environment variables in the default values

```bash
${NODE_DEBUG_OPTIONS:---inspect=$NODE_DEBUG_PORT $NODE_DEFAULT_OPTIONS}
```

This follows the UNIX standard for environment variables and works for both
`cross-env` and `cross-env-shell`.

## `cross-env` vs `cross-env-shell`

The `cross-env` module exposes two bins: `cross-env` and `cross-env-shell`. The
Expand Down Expand Up @@ -173,8 +211,6 @@ easier for Windows users.

- [`env-cmd`](https://github.com/toddbluhm/env-cmd) - Reads environment
variables from a file instead
- [`@naholyr/cross-env`](https://www.npmjs.com/package/@naholyr/cross-env) -
`cross-env` with support for setting default values

## Contributors

Expand Down Expand Up @@ -208,6 +244,7 @@ Thanks goes to these people ([emoji key][emojis]):
<td align="center"><a href="https://nz.linkedin.com/in/jsonc11"><img src="https://avatars0.githubusercontent.com/u/5185660?v=4" width="100px;" alt="Jason Cooke"/><br /><sub><b>Jason Cooke</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=Jason-Cooke" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bibo5088"><img src="https://avatars0.githubusercontent.com/u/17709887?v=4" width="100px;" alt="bibo5088"/><br /><sub><b>bibo5088</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=bibo5088" title="Code">💻</a></td>
<td align="center"><a href="https://codefund.io"><img src="https://avatars2.githubusercontent.com/u/12481?v=4" width="100px;" alt="Eric Berry"/><br /><sub><b>Eric Berry</b></sub></a><br /><a href="#fundingFinding-coderberry" title="Funding Finding">🔍</a></td>
<td align="center"><a href="https://github.com/yinzara"><img src="https://avatars2.githubusercontent.com/u/671855?v=4" width="100px;" alt="Matt Morrissette"/><br /><sub><b>Matt Morrissette</b></sub></a><br /><a href="https://github.com/kentcdodds/cross-env/commits?author=yinzara" title="Code">💻</a><a href="https://github.com/kentcdodds/cross-env/commits?author=yinzara" title="Documentation">📖</a><a href="https://github.com/kentcdodds/cross-env/commits?author=yinzara" title="Tests">⚠️</a></td>
</tr>
</table>

Expand Down
53 changes: 52 additions & 1 deletion src/__tests__/command.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-template-curly-in-string */
const isWindowsMock = require('../is-windows')
const commandConvert = require('../command')

Expand Down Expand Up @@ -59,7 +60,7 @@ test(`leaves embedded variables unchanged when using correct operating system`,

test(`converts braced unix-style env variable usage for windows`, () => {
isWindowsMock.mockReturnValue(true)
// eslint-disable-next-line no-template-curly-in-string

expect(commandConvert('${test}', env)).toBe('%test%')
})

Expand All @@ -79,3 +80,53 @@ test(`normalizes command on windows`, () => {
// as `true` for command only
expect(commandConvert('./cmd.bat', env, true)).toBe('cmd.bat')
})

test(`evaluates default values for missing environment variables on windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(commandConvert('$test1/${foo:-bar}/$test2', env)).toBe(
'%test1%/bar/%test2%',
)
})

test(`evaluates default values for empty environment variables on windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(commandConvert('$test1/${empty_var:-bang}/$test2', env)).toBe(
'%test1%/bang/%test2%',
)
})

test(`evaluates default values recursively for empty environment variables on windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(
commandConvert('$test1/${empty_var:-${missing_var:-bang}}/$test2', env),
).toBe('%test1%/bang/%test2%')
})

test(`evaluates secondary values recursively for environment variables on windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(
commandConvert('$test1/${empty_var:-${test3:-bang}}/$test2', env),
).toBe('%test1%/%test3%/%test2%')
})

test('resolves the current working directory inside string in windows', () => {
isWindowsMock.mockReturnValue(true)
expect(commandConvert('cd ${PWD}\\ADirectory')).toBe(
`cd ${process.cwd()}\\ADirectory`,
)
})

test('resolves the current working directory inside string in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(commandConvert('cd ${PWD}/adir')).toBe('cd ${PWD}/adir')
})

test('All the escapes are belong to us in Windows', () => {
isWindowsMock.mockReturnValue(true)
expect(
commandConvert(
'start-\\${ESCAPED:-a-\\}-val}-\\$ESCAPED-\\\\${ESCAPED:-a-\\}-val}-\\\\$test1-end',
env,
),
).toBe('start-${ESCAPED:-a-\\}-val}-$ESCAPED-\\a-}-val-\\%test1%-end')
})
154 changes: 153 additions & 1 deletion src/__tests__/variable.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-template-curly-in-string */
const isWindowsMock = require('../is-windows')
const varValueConvert = require('../variable')

Expand All @@ -8,6 +9,7 @@ const JSON_VALUE = '{\\"foo\\":\\"bar\\"}'
beforeEach(() => {
process.env.VAR1 = 'value1'
process.env.VAR2 = 'value2'
process.env.EMPTY_VAR = ''
process.env.JSON_VAR = JSON_VALUE
})

Expand Down Expand Up @@ -76,7 +78,6 @@ test(`resolves an env variable value`, () => {

test(`resolves an env variable value with curly syntax`, () => {
isWindowsMock.mockReturnValue(true)
// eslint-disable-next-line no-template-curly-in-string
expect(varValueConvert('foo-${VAR1}')).toBe('foo-value1')
})

Expand Down Expand Up @@ -105,6 +106,26 @@ test(`does not resolve an env variable prefixed with \\ on Windows`, () => {
expect(varValueConvert('\\$VAR1')).toBe('$VAR1')
})

test(`does not resolve a $ without a word after it in UNIX`, () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('hello $ the case')).toBe('hello $ the case')
})

test(`does not resolve a $ without a word after it in Windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(varValueConvert('hello $ the case')).toBe('hello $ the case')
})

test(`does not resolve an env variable if it isn't prefix with a $ but is in curly braces \\ on UNIX`, () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('{VAR1}')).toBe('{VAR1}')
})

test(`does not resolve an env variable if it isn't prefix with a $ but is in curly braces \\ on Windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(varValueConvert('{VAR1}')).toBe('{VAR1}')
})

test(`does not resolve an env variable prefixed with \\ on UNIX`, () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('\\$VAR1')).toBe('$VAR1')
Expand All @@ -119,3 +140,134 @@ test(`resolves an env variable prefixed with \\\\ on UNIX`, () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('\\\\$VAR1')).toBe('\\value1')
})

test(`does not resolve an env variable prefixed with \\$\{ on Windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(varValueConvert('\\${VAR1}')).toBe('${VAR1}')
})

test(`does not resolve an env variable prefixed with on UNIX`, () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('\\${VAR1}')).toBe('${VAR1}')
})

test(`resolves an env variable prefixed with \\\\$\{ on Windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(varValueConvert('\\\\${VAR1}')).toBe('\\value1')
})

test(`resolves an env variable prefixed with \\\\$\{ on UNIX`, () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('\\\\${VAR1}')).toBe('\\value1')
})

test(`resolves default value for missing variable on UNIX`, () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('${UNKNOWN_UNIX_VAR:-defaultUnix}')).toBe(
'defaultUnix',
)
})

test(`resolves default value for missing variable on windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(varValueConvert('${UNKNOWN_WINDOWS_VAR:-defaultWindows}')).toBe(
'defaultWindows',
)
})

test(`resolves default value for empty string variable on UNIX`, () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('${EMPTY_VAR:-defaultUnix}')).toBe('defaultUnix')
})

test(`resolves default value for empty string variable on windows`, () => {
isWindowsMock.mockReturnValue(true)
expect(varValueConvert('${EMPTY_VAR:-defaultWindows}')).toBe('defaultWindows')
})

test('resolves default value recursively when primary and secondary doesnt exist in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(
varValueConvert('${EMPTY_VAR:-foobar${MISSING_VAR:-defaultUnix}}'),
).toBe('foobardefaultUnix')
})

test('resolves secondary value recursively when primary doesnt exist in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('${EMPTY_VAR:-bang${VAR1:-defaultUnix}}')).toBe(
'bangvalue1',
)
})

test('resolves default value recursively when primary and secondary doesnt exist in windows', () => {
isWindowsMock.mockReturnValue(true)
expect(
varValueConvert('${EMPTY_VAR:-foobar${MISSING_VAR:-defaultWindows}}'),
).toBe('foobardefaultWindows')
})

test('resolves secondary value recursively when primary doesnt exist in windows', () => {
isWindowsMock.mockReturnValue(true)
expect(varValueConvert('${EMPTY_VAR:-bang${VAR1:-defaultWindows}}')).toBe(
'bangvalue1',
)
})

test('resolves the current working directory inside string in windows', () => {
isWindowsMock.mockReturnValue(true)
expect(varValueConvert('${PWD}\\ADirectory')).toBe(
`${process.cwd()}\\ADirectory`,
)
})

test('resolves the current working directory inside string in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('${PWD}/adir')).toBe(`${process.cwd()}/adir`)
})

test('resolves a very complex string with defaults and PWD in Windows', () => {
isWindowsMock.mockReturnValue(true)
expect(
varValueConvert(
'start-${PWD}-${EMPTY_VAR:-${NO_VAR1:-$VAR1}}-${NO_VAR2:-$VAR2}-${NO_VAR3:-${EMPTY_VAR:-value3}}-${EMPTY_VAR:-$PWD}-end',
),
).toBe(`start-${process.cwd()}-value1-value2-value3-${process.cwd()}-end`)
})

test('sequential variables resolves in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('start-$VAR1$VAR2-end')).toBe('start-value1value2-end')
})

test('sequential variables with brackets resolves in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('start-${VAR1}$VAR2-end')).toBe(
'start-value1value2-end',
)
})

test('sequential variables resolves in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('start-$VAR1$VAR2-end')).toBe('start-value1value2-end')
})

test('reversed sequential variables with brackets resolves in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('start-$VAR1${VAR2}-end')).toBe(
'start-value1value2-end',
)
})

test('escape closed curly brace in default value in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(varValueConvert('${EMPTY_VAR:-${UNKNOWN:-ba\\}r}}')).toBe('ba}r')
})

test('All the escapes are belong to us in UNIX', () => {
isWindowsMock.mockReturnValue(false)
expect(
varValueConvert(
'start-\\${ESCAPED:-a-\\}-val}-\\$ESCAPED-\\\\${ESCAPED:-a-\\}-val}-\\\\$VAR1-end',
),
).toBe('start-${ESCAPED:-a-\\}-val}-$ESCAPED-\\a-}-val-\\value1-end')
})
Empty file modified src/bin/cross-env-shell.js
100644 → 100755
Empty file.
Empty file modified src/bin/cross-env.js
100644 → 100755
Empty file.
11 changes: 2 additions & 9 deletions src/command.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const path = require('path')
const isWindows = require('./is-windows')
const envReplace = require('./env-replace')

module.exports = commandConvert

Expand All @@ -15,15 +16,7 @@ function commandConvert(command, env, normalize = false) {
if (!isWindows()) {
return command
}
const envUnixRegex = /\$(\w+)|\${(\w+)}/g // $my_var or ${my_var}
const convertedCmd = command.replace(envUnixRegex, (match, $1, $2) => {
const varName = $1 || $2
// In Windows, non-existent variables are not replaced by the shell,
// so for example "echo %FOO%" will literally print the string "%FOO%", as
// opposed to printing an empty string in UNIX. See kentcdodds/cross-env#145
// If the env variable isn't defined at runtime, just strip it from the command entirely
return env[varName] ? `%${varName}%` : ''
})
const convertedCmd = envReplace(command, env, true)
// Normalization is required for commands with relative paths
// For example, `./cmd.bat`. See kentcdodds/cross-env#127
// However, it should not be done for command arguments.
Expand Down
Loading