Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions lib/yargs-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,18 @@ export class YargsParser {

// handle parsing boolean arguments --foo=true --bar false.
if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
if (typeof val === 'string') val = val === 'true'
if (typeof val === 'string') {
const lower = val.toLowerCase()
// Support common truthy/falsy string representations
if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on') {
val = true
} else if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'off') {
val = false
} else {
// For other strings, treat non-empty as truthy (backwards compat edge case)
val = val === 'true'
Comment on lines +628 to +629
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// For other strings, treat non-empty as truthy (backwards compat edge case)
val = val === 'true'
// For other strings, treat as false (backwards compat edge case)
val = false

}
}
}

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

const prefix = typeof envPrefix === 'string' ? envPrefix : ''
// Convert negation-prefix to env var format (e.g., 'no-' -> 'NO_')
const negationPrefix = (configuration['negation-prefix'] || 'no-').toUpperCase().replace(/-/g, '_')
const env = mixin.env()
Object.keys(env).forEach(function (envVar) {
if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) {
// get array of nested keys and convert them to camel case
let isNegated = false
const keys = envVar.split('__').map(function (key, i) {
if (i === 0) {
key = key.substring(prefix.length)
// Check for negation prefix (e.g., NO_DO_THING -> doThing with negation)
if (configuration['boolean-negation'] && key.lastIndexOf(negationPrefix, 0) === 0) {
key = key.substring(negationPrefix.length)
isNegated = true
}
}
return camelCase(key)
})

if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && !hasKey(argv, keys)) {
setArg(keys.join('.'), env[envVar])
let value: any = env[envVar]
// Handle negated boolean env vars
if (isNegated && checkAllAliases(keys.join('.'), flags.bools)) {
// For negated booleans: NO_FOO=true means foo=false, NO_FOO=false means foo=true
const lower = String(value).toLowerCase()
if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on') {
value = false
} else if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'off') {
value = true
} else {
value = false // Default negated to false for non-standard values
}
setKey(argv, keys, value)
// Also set aliases
if (flags.aliases[keys.join('.')]) {
flags.aliases[keys.join('.')].forEach(function (x) {
setKey(argv, x.split('.'), value)
})
}
} else {
setArg(keys.join('.'), value)
}
}
}
})
Expand Down
76 changes: 76 additions & 0 deletions test/yargs-parser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,82 @@ describe('yargs-parser', function () {
bar: 'bar'
})
})

it('should coerce boolean env var "1" to true', function () {
process.env.TEST_BOOL_DO_THING = '1'
const result = parser([], {
envPrefix: 'TEST_BOOL',
boolean: ['doThing']
})
result.doThing.should.equal(true)
delete process.env.TEST_BOOL_DO_THING
})

it('should coerce boolean env var "0" to false', function () {
process.env.TEST_BOOL_ZERO = '0'
const result = parser([], {
envPrefix: 'TEST_BOOL',
boolean: ['zero']
})
result.zero.should.equal(false)
delete process.env.TEST_BOOL_ZERO
})

it('should coerce boolean env var "yes" to true', function () {
process.env.TEST_BOOL_YES = 'yes'
const result = parser([], {
envPrefix: 'TEST_BOOL',
boolean: ['yes']
})
result.yes.should.equal(true)
delete process.env.TEST_BOOL_YES
})

it('should coerce boolean env var "no" to false', function () {
process.env.TEST_BOOL_NO = 'no'
const result = parser([], {
envPrefix: 'TEST_BOOL',
boolean: ['no']
})
result.no.should.equal(false)
delete process.env.TEST_BOOL_NO
})

it('should handle NO_ prefix in env vars for boolean negation', function () {
process.env.MY_APP_NO_DO_THING = 'true'
const result = parser([], {
envPrefix: 'MY_APP_',
boolean: ['doThing'],
default: { doThing: true }
})
// NO_DO_THING=true should negate doThing, making it false
result.doThing.should.equal(false)
delete process.env.MY_APP_NO_DO_THING
})

it('should handle NO_ prefix with "1" value for boolean negation', function () {
process.env.MY_APP_NO_FEATURE = '1'
const result = parser([], {
envPrefix: 'MY_APP_',
boolean: ['feature'],
default: { feature: true }
})
// NO_FEATURE=1 should negate feature, making it false
result.feature.should.equal(false)
delete process.env.MY_APP_NO_FEATURE
})

it('should handle NO_ prefix with "false" value (double negation)', function () {
process.env.MY_APP_NO_OPT = 'false'
const result = parser([], {
envPrefix: 'MY_APP_',
boolean: ['opt'],
default: { opt: false }
})
// NO_OPT=false means do NOT negate, so opt should be true
result.opt.should.equal(true)
delete process.env.MY_APP_NO_OPT
})
})

describe('configuration', function () {
Expand Down
Loading