Skip to content

Commit add37a1

Browse files
author
Kai Gritun
committed
fix: improve boolean env var coercion and add NO_ prefix negation support
Fixes yargs/yargs#2501 Two issues addressed: 1. Boolean string coercion now accepts common truthy/falsy values: - 'true', '1', 'yes', 'on' → true - 'false', '0', 'no', 'off' → false Previously only 'true' was recognized as truthy. 2. Environment variables with NO_ prefix are now treated as boolean negation (consistent with --no-* CLI behavior): - MY_APP_NO_DO_THING=true → doThing: false - MY_APP_NO_DO_THING=false → doThing: true (double negation) The NO_ prefix respects the 'negation-prefix' and 'boolean-negation' configuration options.
1 parent 60f2db5 commit add37a1

File tree

2 files changed

+118
-2
lines changed

2 files changed

+118
-2
lines changed

lib/yargs-parser.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,18 @@ export class YargsParser {
617617

618618
// handle parsing boolean arguments --foo=true --bar false.
619619
if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
620-
if (typeof val === 'string') val = val === 'true'
620+
if (typeof val === 'string') {
621+
const lower = val.toLowerCase()
622+
// Support common truthy/falsy string representations
623+
if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on') {
624+
val = true
625+
} else if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'off') {
626+
val = false
627+
} else {
628+
// For other strings, treat non-empty as truthy (backwards compat edge case)
629+
val = val === 'true'
630+
}
631+
}
621632
}
622633

623634
let value = Array.isArray(val)
@@ -728,19 +739,48 @@ export class YargsParser {
728739
if (typeof envPrefix === 'undefined') return
729740

730741
const prefix = typeof envPrefix === 'string' ? envPrefix : ''
742+
// Convert negation-prefix to env var format (e.g., 'no-' -> 'NO_')
743+
const negationPrefix = (configuration['negation-prefix'] || 'no-').toUpperCase().replace(/-/g, '_')
731744
const env = mixin.env()
732745
Object.keys(env).forEach(function (envVar) {
733746
if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) {
734747
// get array of nested keys and convert them to camel case
748+
let isNegated = false
735749
const keys = envVar.split('__').map(function (key, i) {
736750
if (i === 0) {
737751
key = key.substring(prefix.length)
752+
// Check for negation prefix (e.g., NO_DO_THING -> doThing with negation)
753+
if (configuration['boolean-negation'] && key.lastIndexOf(negationPrefix, 0) === 0) {
754+
key = key.substring(negationPrefix.length)
755+
isNegated = true
756+
}
738757
}
739758
return camelCase(key)
740759
})
741760

742761
if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && !hasKey(argv, keys)) {
743-
setArg(keys.join('.'), env[envVar])
762+
let value: any = env[envVar]
763+
// Handle negated boolean env vars
764+
if (isNegated && checkAllAliases(keys.join('.'), flags.bools)) {
765+
// For negated booleans: NO_FOO=true means foo=false, NO_FOO=false means foo=true
766+
const lower = String(value).toLowerCase()
767+
if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on') {
768+
value = false
769+
} else if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'off') {
770+
value = true
771+
} else {
772+
value = false // Default negated to false for non-standard values
773+
}
774+
setKey(argv, keys, value)
775+
// Also set aliases
776+
if (flags.aliases[keys.join('.')]) {
777+
flags.aliases[keys.join('.')].forEach(function (x) {
778+
setKey(argv, x.split('.'), value)
779+
})
780+
}
781+
} else {
782+
setArg(keys.join('.'), value)
783+
}
744784
}
745785
}
746786
})

test/yargs-parser.mjs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2306,6 +2306,82 @@ describe('yargs-parser', function () {
23062306
bar: 'bar'
23072307
})
23082308
})
2309+
2310+
it('should coerce boolean env var "1" to true', function () {
2311+
process.env.TEST_BOOL_DO_THING = '1'
2312+
const result = parser([], {
2313+
envPrefix: 'TEST_BOOL',
2314+
boolean: ['doThing']
2315+
})
2316+
result.doThing.should.equal(true)
2317+
delete process.env.TEST_BOOL_DO_THING
2318+
})
2319+
2320+
it('should coerce boolean env var "0" to false', function () {
2321+
process.env.TEST_BOOL_ZERO = '0'
2322+
const result = parser([], {
2323+
envPrefix: 'TEST_BOOL',
2324+
boolean: ['zero']
2325+
})
2326+
result.zero.should.equal(false)
2327+
delete process.env.TEST_BOOL_ZERO
2328+
})
2329+
2330+
it('should coerce boolean env var "yes" to true', function () {
2331+
process.env.TEST_BOOL_YES = 'yes'
2332+
const result = parser([], {
2333+
envPrefix: 'TEST_BOOL',
2334+
boolean: ['yes']
2335+
})
2336+
result.yes.should.equal(true)
2337+
delete process.env.TEST_BOOL_YES
2338+
})
2339+
2340+
it('should coerce boolean env var "no" to false', function () {
2341+
process.env.TEST_BOOL_NO = 'no'
2342+
const result = parser([], {
2343+
envPrefix: 'TEST_BOOL',
2344+
boolean: ['no']
2345+
})
2346+
result.no.should.equal(false)
2347+
delete process.env.TEST_BOOL_NO
2348+
})
2349+
2350+
it('should handle NO_ prefix in env vars for boolean negation', function () {
2351+
process.env.MY_APP_NO_DO_THING = 'true'
2352+
const result = parser([], {
2353+
envPrefix: 'MY_APP_',
2354+
boolean: ['doThing'],
2355+
default: { doThing: true }
2356+
})
2357+
// NO_DO_THING=true should negate doThing, making it false
2358+
result.doThing.should.equal(false)
2359+
delete process.env.MY_APP_NO_DO_THING
2360+
})
2361+
2362+
it('should handle NO_ prefix with "1" value for boolean negation', function () {
2363+
process.env.MY_APP_NO_FEATURE = '1'
2364+
const result = parser([], {
2365+
envPrefix: 'MY_APP_',
2366+
boolean: ['feature'],
2367+
default: { feature: true }
2368+
})
2369+
// NO_FEATURE=1 should negate feature, making it false
2370+
result.feature.should.equal(false)
2371+
delete process.env.MY_APP_NO_FEATURE
2372+
})
2373+
2374+
it('should handle NO_ prefix with "false" value (double negation)', function () {
2375+
process.env.MY_APP_NO_OPT = 'false'
2376+
const result = parser([], {
2377+
envPrefix: 'MY_APP_',
2378+
boolean: ['opt'],
2379+
default: { opt: false }
2380+
})
2381+
// NO_OPT=false means do NOT negate, so opt should be true
2382+
result.opt.should.equal(true)
2383+
delete process.env.MY_APP_NO_OPT
2384+
})
23092385
})
23102386

23112387
describe('configuration', function () {

0 commit comments

Comments
 (0)