Skip to content
Closed
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
1 change: 1 addition & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ new!(pub NODE_DISABLE_COMPILE_CACHE: string, "NODE_DISABLE_COMPILE_CACHE", {});
// child's CLI entrypoint checks this before anything else and hands off to
// C++ Bun__WebView__hostMain. Never returns — no JSC, no VM.
new!(pub BUN_INTERNAL_WEBVIEW_HOST: string, "BUN_INTERNAL_WEBVIEW_HOST", {});
new!(pub NODE_OPTIONS: string, "NODE_OPTIONS", {});
new!(pub NODE_PENDING_DEPRECATION: string, "NODE_PENDING_DEPRECATION", {});
new!(pub NODE_PRESERVE_SYMLINKS_MAIN: boolean, "NODE_PRESERVE_SYMLINKS_MAIN", { default: false });
new!(pub NODE_USE_SYSTEM_CA: boolean, "NODE_USE_SYSTEM_CA", { default: false });
Expand Down
40 changes: 34 additions & 6 deletions src/clap/comptime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ pub const fn count_single<Id>(params: &[Param<Id>]) -> usize {
let mut i = 0;
while i < params.len() {
if is_named(&params[i])
&& matches!(params[i].takes_value, Values::One | Values::OneOptional)
&& matches!(
params[i].takes_value,
Values::One
| Values::OneOptional
| Values::OneNoDashValue
| Values::OneOptionalNoDashValue
)
{
n += 1;
}
Expand Down Expand Up @@ -114,7 +120,10 @@ pub const fn convert_params_array<Id, const N: usize>(params: &[Param<Id>]) -> [
index = flags;
flags += 1;
}
Values::One | Values::OneOptional => {
Values::One
| Values::OneOptional
| Values::OneNoDashValue
| Values::OneOptionalNoDashValue => {
index = single;
single += 1;
}
Expand Down Expand Up @@ -361,7 +370,10 @@ impl ConvertedTable {
if p.names.long.is_some() || p.names.short.is_some() {
let ctr = match p.takes_value {
Values::None => &mut flags,
Values::One | Values::OneOptional => &mut single,
Values::One
| Values::OneOptional
| Values::OneNoDashValue
| Values::OneOptionalNoDashValue => &mut single,
Values::Many => &mut multi,
};
index = *ctr;
Expand Down Expand Up @@ -544,6 +556,7 @@ impl<Id> ComptimeClap<Id> {
state: streaming::State::Normal,
positional: None,
short_aliases: opt.short_aliases,
reject_bad_negations: opt.reject_bad_negations,
};

while let Some(arg) = stream.next()? {
Expand All @@ -567,7 +580,13 @@ impl<Id> ComptimeClap<Id> {
}
break;
}
} else if param.takes_value == Values::One || param.takes_value == Values::OneOptional {
} else if matches!(
param.takes_value,
Values::One
| Values::OneOptional
| Values::OneNoDashValue
| Values::OneOptionalNoDashValue
) {
debug_assert!(single_options.len() != 0);
if single_options.len() != 0 {
single_options[param.id] = Some(arg.value.unwrap_or(b""));
Expand Down Expand Up @@ -602,7 +621,10 @@ impl<Id> ComptimeClap<Id> {
pub fn flag(&self, name: &[u8]) -> bool {
let param = self.table.find(name);
debug_assert!(
param.takes_value == Values::None || param.takes_value == Values::OneOptional,
matches!(
param.takes_value,
Values::None | Values::OneOptional | Values::OneOptionalNoDashValue
),
"{} is an option and not a flag.",
bstr::BStr::new(name),
);
Expand Down Expand Up @@ -634,7 +656,13 @@ impl<Id> ComptimeClap<Id> {
bstr::BStr::new(name),
);
debug_assert!(
!(param.takes_value == Values::One || param.takes_value == Values::OneOptional),
!matches!(
param.takes_value,
Values::One
| Values::OneOptional
| Values::OneNoDashValue
| Values::OneOptionalNoDashValue
),
"{} takes one option, not multiple.",
bstr::BStr::new(name),
);
Expand Down
6 changes: 6 additions & 0 deletions src/clap/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ pub enum Error {
MissingValue,
#[error("InvalidArgument")]
InvalidArgument,
/// `--no-<x>` where `<x>` is known but carries a value, so there is
/// nothing to negate.
#[error("InvalidNegation")]
InvalidNegation,
#[error("WriteFailed")]
WriteFailed,
}
Expand All @@ -17,6 +21,7 @@ impl Error {
Self::DoesntTakeValue => "DoesntTakeValue",
Self::MissingValue => "MissingValue",
Self::InvalidArgument => "InvalidArgument",
Self::InvalidNegation => "InvalidNegation",
Self::WriteFailed => "WriteFailed",
}
}
Expand All @@ -40,6 +45,7 @@ impl From<crate::streaming::ArgError> for Error {
crate::streaming::ArgError::DoesntTakeValue => Self::DoesntTakeValue,
crate::streaming::ArgError::MissingValue => Self::MissingValue,
crate::streaming::ArgError::InvalidArgument => Self::InvalidArgument,
crate::streaming::ArgError::InvalidNegation => Self::InvalidNegation,
}
}
}
Expand Down
18 changes: 16 additions & 2 deletions src/clap/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,15 @@ pub enum Values {
One,
Many,
OneOptional,
/// Like [`Values::One`], but a separate following argument that starts
/// with '-' is never consumed as the value (Node's behavior for `-e`):
/// the parse fails with a missing value instead.
OneNoDashValue,
/// Like [`Values::OneOptional`], but a separate following argument is
/// consumed as the value when it does not start with '-' (Node's behavior
/// for `-p`), and in a short cluster the attached remainder stays part of
/// the cluster (`-pe` is `-p` followed by `-e`).
OneOptionalNoDashValue,
}

/// Represents a parameter for the command line.
Expand Down Expand Up @@ -417,6 +426,9 @@ pub struct ParseOptions<'a> {
/// flag, never to an option's value or a `--` target. Node keeps its own
/// aliases on exactly that branch (node_options-inl.h).
pub short_aliases: &'static [(&'static [u8], &'static [u8])],
/// Reject `--no-<x>` shapes Node rejects instead of ignoring them as an
/// unrecognized flag. Only the commands that stand in for `node` set this.
pub reject_bad_negations: bool,
}

// Help/usage/error rendering — none of this is on the cold-start hot chain
Expand Down Expand Up @@ -511,6 +523,7 @@ pub fn parse<Id: 'static>(
diagnostic: opt.diagnostic,
stop_after_positional_at: opt.stop_after_positional_at,
short_aliases: opt.short_aliases,
reject_bad_negations: opt.reject_bad_negations,
},
)?;
Ok(Args { clap, exe_arg })
Expand All @@ -532,6 +545,7 @@ pub fn parse_with_table<Id: 'static>(
diagnostic: opt.diagnostic,
stop_after_positional_at: opt.stop_after_positional_at,
short_aliases: opt.short_aliases,
reject_bad_negations: opt.reject_bad_negations,
},
)?;
Ok(Args { clap, exe_arg })
Expand Down Expand Up @@ -657,14 +671,14 @@ where
{
match param.takes_value {
Values::None => {}
Values::One => {
Values::One | Values::OneNoDashValue => {
write!(
w,
" <{}>",
bstr::BStr::new(value_text(context, param).map_err(Into::into)?)
)?;
}
Values::OneOptional => {
Values::OneOptional | Values::OneOptionalNoDashValue => {
write!(
w,
" <{}>?",
Expand Down
120 changes: 120 additions & 0 deletions src/clap/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ pub(crate) enum ArgError {
MissingValue,
#[error("InvalidArgument")]
InvalidArgument,
#[error("InvalidNegation")]
InvalidNegation,
}

/// Whether `takes_value` opts the param into Node's value-binding rules.
fn is_node_style(takes_value: clap::Values) -> bool {
matches!(
takes_value,
clap::Values::OneNoDashValue | clap::Values::OneOptionalNoDashValue
)
}

#[derive(Copy, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -62,6 +72,7 @@ pub struct StreamingClap<'p, 'a, Id, ArgIterator> {
pub positional: Option<&'p clap::Param<Id>>,
pub diagnostic: Option<&'p mut clap::Diagnostic>,
pub short_aliases: &'static [(&'static [u8], &'static [u8])],
pub reject_bad_negations: bool,
}

// ArgIterator is the
Expand Down Expand Up @@ -129,6 +140,13 @@ where
}));
}

if is_node_style(param.takes_value) {
return match self.node_style_value(param.takes_value, maybe_value) {
Ok(value) => Ok(Some(Arg { param, value })),
Err(e) => Err(self.err(arg, None, Some(name), e)),
};
}

let value = 'blk: {
if let Some(v) = maybe_value {
break 'blk v;
Expand All @@ -153,6 +171,23 @@ where
}));
}

// `--no-<x>` where `<x>` is a known option that carries a
// value cannot mean anything, and Node rejects it
// (src/node_options-inl.h). An *unknown* `--no-<x>` is left
// alone: Bun ignores unrecognized flags on purpose so the many
// Node options it does not implement (--no-global-search-paths,
// --no-extra-info-on-fatal-exception, …) stay harmless.
if self.reject_bad_negations {
if let Some(negated) = name.strip_prefix(b"no-") {
let negates_a_value = params.iter().any(|p| {
p.names.matches_long(negated) && p.takes_value != clap::Values::None
});
if negates_a_value {
return Err(self.err(arg, None, Some(name), ArgError::InvalidNegation));
}
}
}

// unrecognized command
// if flag else arg
if arg_info.kind == ArgKind::Long || arg_info.kind == ArgKind::Short {
Expand Down Expand Up @@ -250,6 +285,37 @@ where
return Ok(Some(Arg { param, value: None }));
}

if is_node_style(param.takes_value) {
if next_is_eql {
return match self
.node_style_value(param.takes_value, Some(&arg[next_index + 1..]))
{
Ok(value) => Ok(Some(Arg { param, value })),
Err(e) => Err(self.err(arg, Some(short), None, e)),
};
}
if arg.len() > next_index {
// Text attached without '=' stays part of the cluster for
// the optional form, so "-pe 42" is -p followed by -e 42
// rather than -p with the value "e".
if param.takes_value == clap::Values::OneOptionalNoDashValue {
self.state = State::Chaining(Chaining {
arg,
index: next_index,
});
return Ok(Some(Arg { param, value: None }));
}
return Ok(Some(Arg {
param,
value: Some(&arg[next_index..]),
}));
}
return match self.node_style_value(param.takes_value, None) {
Ok(value) => Ok(Some(Arg { param, value })),
Err(e) => Err(self.err(arg, Some(short), None, e)),
};
}

if arg.len() <= next_index {
let value = match self.iter.next() {
Some(v) => v,
Expand Down Expand Up @@ -342,6 +408,58 @@ where
}))
}

/// Bind the value of a param declared with Node's value semantics
/// ([`clap::Values::OneNoDashValue`] / [`clap::Values::OneOptionalNoDashValue`]).
///
/// Mirrors nodejs/node v26.3.0 `src/node_options-inl.h`:
///
/// * An `=`-attached value binds verbatim. For the required form an empty
/// one is an error rather than an empty value (`node --eval=` exits 9);
/// the optional form is a boolean upstream, so `--print=` is no value.
/// * A separate following argument that starts with '-' is never the
/// value; it is a missing value instead (`node -e -p` exits 9). This is
/// why an expression like `-42` must be passed as `--eval=-42`.
/// * A separate following argument may escape that rule with a leading
/// backslash, which is then stripped: `node -p "\-42"` prints -42. The
/// `=` form does not unescape, so `--eval=\-42` keeps the backslash.
/// * For the optional form (upstream's `--print <arg>` alias) an *empty*
/// following argument is additionally not consumed. It stays a
/// positional, which is why `node -p "" -e 42` prints `undefined`: the
/// positional ends option parsing before `-e` is seen.
fn node_style_value(
&mut self,
takes_value: clap::Values,
attached: Option<&'a [u8]>,
) -> Result<Option<&'a [u8]>, ArgError> {
if let Some(value) = attached {
if !value.is_empty() {
return Ok(Some(value));
}
if takes_value == clap::Values::OneOptionalNoDashValue {
return Ok(None);
}
return Err(ArgError::MissingValue);
}

let usable = self.iter.remain().first().is_some_and(|next| {
!next.starts_with(b"-")
&& !(takes_value == clap::Values::OneOptionalNoDashValue && next.is_empty())
});
if !usable {
if takes_value == clap::Values::OneNoDashValue {
return Err(ArgError::MissingValue);
}
return Ok(None);
}

// `usable` only holds when the iterator has a next argument.
let value = self.iter.next().unwrap_or_default();
Ok(Some(match value.strip_prefix(b"\\") {
Some(rest) if rest.starts_with(b"-") => rest,
_ => value,
}))
}

fn err(&mut self, arg: &[u8], short: Option<u8>, long: Option<&[u8]>, e: ArgError) -> ArgError {
if let Some(d) = self.diagnostic.as_deref_mut() {
// `Diagnostic` owns
Expand Down Expand Up @@ -369,6 +487,7 @@ mod tests {
};
let mut c = StreamingClap::<u8, args::SliceIterator> {
short_aliases: &[],
reject_bad_negations: false,
params,
iter: &mut iter,
state: State::Normal,
Expand Down Expand Up @@ -402,6 +521,7 @@ mod tests {
};
let mut c = StreamingClap::<u8, args::SliceIterator> {
short_aliases: &[],
reject_bad_negations: false,
params,
iter: &mut iter,
state: State::Normal,
Expand Down
Loading
Loading