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

Conversation

yinzara
Copy link

@yinzara yinzara commented Nov 18, 2019

What:

Adds support for specifying default values for environment variables using the standard POSIX/bash syntax ${ENV_VAR_NAME:-default value}. Also support recursive resolution through defaults (i.e. ${ENV_VAR_NAME:-${OTHER_ENV_VAR:-default if both not set}})

Also adds support for resolution of ${PWD}/$PWD into the current working directory.

Why:

Currently there is no way with the library to specify a default value for an environment variable when not present. There is a reference in the readme to another library that seems to be a fork of this library with the added feature however I can't find the source for it and the documentation for the library incorrectly references this library. Additionally how they suggest you specify a default value (i.e. ${ENV_VAR_NAME:default value} ) doesn't follow standard POSIX/bash so there is no portability of this syntax beyond cross-env.

In most linux/unix shells the PWD environment variable points to the current working directory but in Windows no such environment variable exists. This PR also adds resolution in windows using the NodeJS process.cwd() function.

How:

This required a rewrite of the primary regular expression based implementation of value replacement to a combination iterative/recursive algorithm.

To implement the recursive replacement in the same fashion as Bash does (so that if the value was evaluated in Bash before it wouldn't cause functional changes), I had to use a brace matching iterative state machine over the characters of the string that recursed on the default values calculation.

This allowed me to replace the double implementation (in command.js and variable.js) of the string replacement algorithm. Previously there were two nearly identical implementations that only varied in Windows and for the command.js only.

I've created a new "env-replace.js" that exports a function that achieves this for both. It's documented in the file. All existing test cases pass with no changes and a new test case has been added for an extremely complex case:
start-${PWD}-${EMPTY_VAR:-${NO_VAR:-$VAR1}}-${NO_VAR:-$VAR2}-${NO_VAR:-${EMPTY_VAR:-value3}}-${EMPTY_VAR:-$PWD}-end which now passes. I will provide screen shots of this evaluating in both PowerShell (through a npm script) and Mac OS Bash (both in the console in via an npm script).

Checklist:

  • [X ] Documentation
  • [X ] Tests
  • [X ] Ready to be merged

@codecov
Copy link

codecov bot commented Nov 18, 2019

Codecov Report

Merging #219 into master will not change coverage.
The diff coverage is 100%.

Impacted file tree graph

@@          Coverage Diff          @@
##           master   #219   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           4      5    +1     
  Lines          78    132   +54     
  Branches       18     35   +17     
=====================================
+ Hits           78    132   +54
Impacted Files Coverage Δ
src/env-replace.js 100% <100%> (ø)
src/command.js 100% <100%> (ø) ⬆️
src/variable.js 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 67f21c3...318f2f7. Read the comment docs.

@yinzara yinzara changed the title feat: add default value handling feat: add default value handling and PWD resolution Nov 19, 2019
@kentcdodds
Copy link
Owner

Thanks for this!

I'm concerned that the shell may eagerly replace these values before they get to cross-env (so people will have to use cross-env-shell instead). Could you test this out locally to make sure it works as expected? You should be able to simply run: node ./src/bin/cross-env.js to get cross-env running locally, then try out a few combinations and provide a screenshot or something.

@franklin-ross
Copy link

@kentcdodds, does that practically matter? If the shell converts them, all good, if you're not on a shell that understands that syntax, cross-env will convert them. Everyone's a winner! :-D

@kentcdodds
Copy link
Owner

I had that thought as well. But I just want to be sure.

@yinzara
Copy link
Author

yinzara commented Nov 20, 2019

Yeah I agree. I'll make an example in both Windows PowerShell and Bash in Linux and we'll see if it comes out with the same results but I'm pretty sure it will. However, as stated, since I'm just duplicating the functionality of what Bash already supports, I can't see how it wouldn't function identically.

@franklin-ross
Copy link

I guess the only issue would be if the timing matters for some reason. Like if some insane person does BOB=5 cross-env BOB=${BOB:10}, then I think bash would replace it early, meaning BOB would be 10, whereas cross-env would see BOB as already set, and then it would be 5. I'm no bash wizard, but that seems like what would happen to me. It's a pretty unusual case though.

@yinzara
Copy link
Author

yinzara commented Nov 20, 2019

Good news and bad news.

Good news. CMD does not expand the variables. If you run cross-env directly from PowerShell, it DOES eagerly expands them but it does this already (i.e. cross-env already doesn't work correctly with PowerShell directly) because variables in PowerShell start with a $. However, if you use cross-env from within a package.json "scripts" script from within PowerShell it does not eagerly expand the variables. Since this is the primary use of cross-env I'm not concerned.

However, there is bad news. When I started to use more complex examples of replacements it turns out the regex style approach I was using has examples that break. I will add test cases to cover this and see if I can rework the approach.

@yinzara
Copy link
Author

yinzara commented Nov 20, 2019

Not bad. I need to add some test cases to keep us at 100% (I won't say this is good until then).

@yinzara
Copy link
Author

yinzara commented Nov 20, 2019

CMD.exe
Screen Shot 2019-11-20 at 2 56 46 AM

@yinzara
Copy link
Author

yinzara commented Nov 20, 2019

Windows PowerShell
Screen Shot 2019-11-20 at 2 56 01 AM

@yinzara
Copy link
Author

yinzara commented Nov 20, 2019

Mac OS X Bash
Screen Shot 2019-11-20 at 2 49 44 AM

@yinzara
Copy link
Author

yinzara commented Nov 22, 2019

Any chance I could get some love on this? I really want to use this with my team but keeping the custom version is a maintenance nightmare.

Copy link
Owner

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one last ask.

**/
// state machine requires slightly more complexity than normally required
// eslint-disable-next-line complexity
function envReplace(value, env = process.env, winEnvReplace = false) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing, but I really don't want to maintain this function. And environment variable expansion syntax is not something I'm very familiar with.

Would you agree to help maintain this code if I merge?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative might be to use a PEG parser generator or something. Let me see if I can come up with something, just to see what it looks like. Total props for writing this though @yinzara, not having a dig at all. I've written my fare share of this kind of thing, and they are hard!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I'm happy to help maintain what I've written. Love to see your PEG variation if you would like. Up you you

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, got something mostly working now, but need to support escape characters and stuff.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I've got in mind. I'm not totally done yet and I've got to head out now for a bit, but this is pretty close. Just need to sort out a few final cases like ${BOB:-hello " there"} and make sure it behaves like bash. You can load this up and play with it here: https://pegjs.org/online.

{
  const process = {
    env: {
      BOB: "Hello!"
    }
  };
  
  function getEnv(name, fallback) {
     const env = process.env[name];
     return (env == null || env === '')
     	? (fallback == null ? '' : fallback)
        : env;
  }
}

start
  = String

String = QuotedString / UnquotedString

EnvVar
  = "${" _ name:VarName _ (":-" / ":=") fallback:FallbackValue "}" { return getEnv(name, fallback); }
  / "${" _ name:VarName _ "}" { return getEnv(name); }
  / "$" name:VarName { return getEnv(name); }
  / "$" { return "$"; }

FallbackValue
  = QuotedString
  / val:((BracketStringChar / EnvVar)*) { return val.join('').trim(); }

VarName "environment variable name"
  = [a-zA-Z_][a-zA-z_0-9]+ { return text(); }

_ "whitespace"
  = [ \t\n\r\v]*

QuotedString
  = '"' val:(DoubleStringChar / EnvVar)* '"' { return val.join(''); }
  / "'" val:(SingleStringChar / EnvVar)* "'" { return val.join(''); }
  
UnquotedString
  = val:(BareStringChar / EnvVar)* { return val.join(''); }
  
DoubleStringChar
  = '\\' escaped:("$" / '"') { return escaped; }
  / !("$" / '"') char:. { return char; }

SingleStringChar
  = "\\" escaped:("$" / "'") { return escaped; }
  / !("$" / "'") char:. { return char; }

BracketStringChar
  = "\\" escaped:("$" / "}") { return escaped; }
  / !("$" / "}") char:. { return char; }

BareStringChar
  = "\\" escaped:("$") { return escaped; }
  / !("$") char:. { return char; }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great to hear, thanks! I've been wanting an excuse to build a parser for a while, so I'm happy to have found a good reason.

I agree that setting the value as part of the stringify function would be wrong, but my thought was to create a few different methods for walking the parse tree, one of which would be a more "interactive" style thing to set environment along the way. That's the next thing I'm gonna look at.

The brace matching should all be handled automatically by the grammar. There's something like this (I'm simplifying a bit) under the Variable production rule: "${" VarName ":-" Fallback "}", and the Fallback can be 0 or more Word / Quoted / Variable, so they're mutually recursive and can nest indefinitely. If for some reason it fails to match then it falls back on just reading it as plain text, although bash refuses to parse, so I'm still on the fence about which way to go there.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of helping, I'd love that, but maybe in a little bit once I get things settled 😜. I'm still working pretty rough and ready at the moment, but hope to get it into a relatively stable state soon.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it. I think this will drastically simplify cross-env (and any other library that attempts to do something with args like this).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great, thanks a lot for the feedback! I sometimes find it difficult to tell whether my vision for something is as good as I think it is, or I'm just getting lost in the woods, so the validation is great. This feels pretty nice and clean though, so I'm happy to hear that.

I'll keep plugging away at it, and probably try and integrate it with a cross-env branch next week to see how that goes.

There's got to be somewhere better to talk about it than this PR though, but Github doesn't really have room for random chats. I might make a "cross-env integration" issue or something in the package that we can all continue talking on. Would that be better?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can continue here: franklin-ross/bash-env-parser#7

@derekgreer
Copy link

Any updates on this?

@franklin-ross
Copy link

Hey, my apologies. I got a bit lost in my new workplace. I'm going to jump back on this and create a PR.

@franklin-ross
Copy link

I have to admit that I initially balked when I realised exactly how messy this all is. I believe the library I've written is quite true to bash, but the nature of cross-env running within an arbitrary shell which may or may not parse some of the input before we get to it makes it quite hard to decide on any kind of concrete acceptance criteria.

I need to get over that and accept that cross-env is practically very useful despite that messiness, and that this will make it more useful.

I'm afraid that this will increase the (already significant) number of issues raised to the tune of "this doesn't work for this corner case". I think it's still worth it, but I want to point that out.

@derekgreer
Copy link

Would be great if we could get this feature merged.

@kentcdodds
Copy link
Owner

#257

@kentcdodds kentcdodds closed this Dec 1, 2020
@yinzara
Copy link
Author

yinzara commented Dec 1, 2020

Thanks @kentcdodds for all your work. Totally understand about this.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants