Skip to content
1 change: 1 addition & 0 deletions src/install/PackageManager/CommandLineArguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const SHARED_PARAMS: &[ParamType] = &[
),
clap::param!("-g, --global Install globally"),
clap::param!("--cwd <STR> Set a specific cwd"),
clap::param!("--env-file <STR>..."),
Comment thread
robobun marked this conversation as resolved.
BACKEND_PARAM,
clap::param!(
"--registry <STR> Use a specific registry by default, overriding .npmrc, bunfig.toml and environment variables"
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/cli/bunx_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ impl Options {
ctx.debug.run_in_bun = true;
} else if positional == b"--no-install" {
opts.no_install = true;
} else if positional == b"--cwd" || positional == b"--env-file" {
// Step past the value so it isn't mistaken for the package
// name. `--cwd` before `x` was applied by the caller's
// `apply_leading_cwd()`; after `x` it is not honored yet.
Comment thread
robobun marked this conversation as resolved.
i += 1;
Comment thread
claude[bot] marked this conversation as resolved.
} else if positional == b"--package" || positional == b"-p" {
// Next argument should be the package name
i += 1;
Expand Down
8 changes: 5 additions & 3 deletions src/runtime/cli/create_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,13 @@ impl CreateOptions {
..Default::default()
};

if opts.positionals.len() >= 1
&& (opts.positionals[0] == b"c" || opts.positionals[0] == b"create")
if let Some(i) = opts
.positionals
.iter()
.position(|&p| p == b"c" || p == b"create")
{
let mut v = core::mem::take(&mut opts.positionals).into_vec();
v.remove(0);
v.drain(..=i);
opts.positionals = v.into_boxed_slice();
}

Expand Down
121 changes: 81 additions & 40 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,11 @@
/// This is set `true` during `Command.which()` if argv0 is "bunx"
static IS_BUNX_EXE: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);

/// argv index of the subcommand keyword as located by `Command::which()`.
/// `bun test …` → 1; `bun --cwd ./dir test …` → 3.
Comment thread
robobun marked this conversation as resolved.
static SUBCOMMAND_ARGV_INDEX: core::sync::atomic::AtomicUsize =
core::sync::atomic::AtomicUsize::new(1);

bun_core::declare_scope!(CLI, hidden);

pub(crate) type LoaderColonList =
Expand Down Expand Up @@ -746,20 +751,8 @@

#[cold]
pub(crate) fn exec() -> crate::Result<()> {
let mut command_name: &[u8] = b"";
for (i, arg) in bun::argv().iter().enumerate() {
if i == 0 {
continue;
}
if arg.len() > 1 && arg[0] == b'-' {
continue;
}
command_name = arg;
break;
}
if command_name.is_empty() {
command_name = bun::argv().get(1).map(|z| z.as_bytes()).unwrap_or(b"");
}
let idx = super::command::subcommand_argv_index();
let command_name: &[u8] = bun::argv().get(idx).map(|z| z.as_bytes()).unwrap_or(b"");
pretty_error!(
"<r><red>Uh-oh<r>. <b><yellow>bun {0}<r> is a subcommand reserved for future use by Bun.\n\nIf you were trying to run a package.json script called {0}, use <b><magenta>bun run {0}<r>.\n",
bstr::BStr::new(command_name)
Expand All @@ -785,6 +778,47 @@
(0..a.len()).map(|i| a.get(i).unwrap()).collect()
}

/// See [`SUBCOMMAND_ARGV_INDEX`](super::SUBCOMMAND_ARGV_INDEX).
#[inline]
pub(crate) fn subcommand_argv_index() -> usize {
super::SUBCOMMAND_ARGV_INDEX.load(core::sync::atomic::Ordering::Relaxed)
}

/// Apply a `--cwd <dir>` / `--cwd=<dir>` that preceded the subcommand
/// keyword, for handlers that don't route through `arguments::parse` /
/// `CommandLineArguments::parse`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[cold]
pub(crate) fn apply_leading_cwd() {
fn chdir(dir: &[u8]) {
let dir_z = bun_core::ZBox::from_bytes(dir);
if let bun_sys::Result::Err(err) = bun_sys::chdir(&dir_z) {
Output::err(
err,
"Could not change directory to \"{}\"\n",
format_args!("{}", bstr::BStr::new(dir)),
);
Global::exit(1);
}
}
let argv = bun::argv();
let end = subcommand_argv_index().min(argv.len());
let mut i = 1;
while i < end {
let a = argv.get(i).map(|z| z.as_bytes()).unwrap_or(b"");
if a == b"--cwd" {
if let Some(dir) = argv.get(i + 1).filter(|_| i + 1 < end) {
chdir(dir.as_bytes());
}
return;
}
if let Some(dir) = a.strip_prefix(b"--cwd=") {
chdir(dir);

Check warning on line 815 in src/runtime/cli/mod.rs

View check run for this annotation

Claude / Claude Code Review

apply_leading_cwd() applies first --cwd; every other parser applies last

`apply_leading_cwd()` returns after the **first** `--cwd` match, so `bun --cwd a --cwd b init -y` chdirs to `a/`; every other `--cwd` consumer (`arguments::parse`, `CommandLineArguments::parse`) uses bun_clap's last-wins semantics and would chdir to `b/`. Track the last-seen `dir` in a local and chdir once after the loop instead of `return`ing on match — that keeps init/bunx/create consistent with run/test/install on repeated `--cwd`.
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
return;
}
i += 1;
}
}
Comment thread
claude[bot] marked this conversation as resolved.

pub use bun_options_types::command_tag::Tag;
pub use bun_options_types::command_tag::{LOADS_CONFIG, USES_GLOBAL_OPTIONS};
pub use bun_options_types::context::{Context, ContextData, HotReload, TestOptions};
Expand Down Expand Up @@ -930,18 +964,32 @@
return Tag::RunAsNodeCommand;
}

let mut idx: usize = 1;
let Some(mut first_arg_name) = iter.next() else {
return Tag::AutoCommand;
};
while !first_arg_name.is_empty()
&& first_arg_name[0] == b'-'
&& !(first_arg_name.len() > 1 && first_arg_name[1] == b'e')
{
// Step past the value of the required-value `BASE_PARAMS_` flags.
// `-c, --config` is `Values::OneOptional`: clap never consumes a
// separate token for it, so it's intentionally not listed.
Comment thread
robobun marked this conversation as resolved.
if matches!(first_arg_name, b"--cwd" | b"--env-file") {
if iter.next().is_none() {
return Tag::AutoCommand;
}
idx += 1;
}
match iter.next() {
Some(n) => first_arg_name = n,
Some(n) => {
idx += 1;
first_arg_name = n;
}
None => return Tag::AutoCommand,
}
}
SUBCOMMAND_ARGV_INDEX.store(idx, core::sync::atomic::Ordering::Relaxed);

type RootCommandMatcher = strings::ExactSizeMatcher<12>;
let x = RootCommandMatcher::r#match(first_arg_name);
Expand Down Expand Up @@ -1487,8 +1535,10 @@
#[inline(never)]
fn exec_init() -> CmdResult {
// InitCommand parses its own argv (no Context).
apply_leading_cwd();
let argv = argv_zslice();
super::init_command::InitCommand::exec(&argv[2.min(argv.len())..])
let start = (subcommand_argv_index() + 1).min(argv.len());
super::init_command::InitCommand::exec(&argv[start..])
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.
}

#[cold]
Expand All @@ -1498,7 +1548,7 @@
// exec handles both the non-tty path (dump the embedded completion
// script to stdout) and the tty install path (bunx symlink, fpath/XDG
// dir search, profile patching).
for a in bun::argv().iter().skip(2) {
for a in bun::argv().iter().skip(subcommand_argv_index() + 1) {
if matches!(a, b"--help" | b"-h") {
tag_print_help(Tag::InstallCompletionsCommand, true);
Global::exit(0);
Expand All @@ -1518,6 +1568,7 @@
#[cold]
#[inline(never)]
fn exec_bunx(log: &mut bun_ast::Log) -> CmdResult {
apply_leading_cwd();
let ctx = init(Tag::BunxCommand, log)?;
let start_idx = if IS_BUNX_EXE.load(core::sync::atomic::Ordering::Relaxed) {
0
Expand Down Expand Up @@ -1768,10 +1819,12 @@
}

// Create command wraps bunx
apply_leading_cwd();
let ctx = init(Tag::CreateCommand, log)?;
let args = argv_zslice();
let cmd_idx = subcommand_argv_index();

if args.len() <= 2 {
if args.len() <= cmd_idx + 1 {
tag_print_help(Tag::CreateCommand, false);
Global::exit(1);
}
Expand All @@ -1782,7 +1835,7 @@
let mut dash_dash_bun = false;
let mut print_help = false;

if args.len() > 2 {
{
let remainder = &args[1..];
let mut remainder_i: usize = 0;
while remainder_i < remainder.len() && positional_i < positionals.len() {
Expand All @@ -1800,6 +1853,8 @@
dash_dash_bun = true;
} else if slice == b"--help" || slice == b"-h" {
print_help = true;
} else if slice == b"--cwd" || slice == b"--env-file" {
remainder_i += 1;
}
}
}
Expand Down Expand Up @@ -1918,30 +1973,16 @@
// Parse arguments manually since the standard flow doesn't work for standalone commands
let cli = CommandLineArguments::parse(PmSubcommand::Info)?;
let json_output = cli.json_output;
// `positionals[0]` is the `info` keyword itself.
let positionals = match cli.positionals {
[b"info", rest @ ..] => rest,
rest => rest,
};
let package_name: &[u8] = positionals.first().copied().unwrap_or(b"");
let property_path: Option<&[u8]> = positionals.get(1).copied();
let ctx = init(Tag::InfoCommand, log)?;
let (pm, _) = PackageManager::init(ctx, cli, Subcommand::Info)?;

// Handle arguments correctly for standalone info command
let mut package_name: &[u8] = b"";
let mut property_path: Option<&[u8]> = None;

// Find non-flag arguments starting from argv[2] (after "bun info").
let mut found_package = false;
let argv = bun::argv();
for arg in argv.iter().skip(2) {
// Skip flags
if !arg.is_empty() && arg[0] == b'-' {
continue;
}
if !found_package {
package_name = arg;
found_package = true;
} else {
property_path = Some(arg);
break;
}
}

super::pm_view_command::view(pm, package_name, property_path, json_output)
}

Expand Down
11 changes: 6 additions & 5 deletions src/runtime/cli/package_manager_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,16 @@ Learn more about these at <magenta>https://bun.com/docs/cli/pm<r>.\n";
}

pub(crate) fn exec(ctx: Command::Context) -> crate::Result<()> {
// `bun_core::argv()` includes argv[0]; skip it and collect into a
// borrowed-slice Vec so `&[&[u8]]` callers (TrustCommand/UntrustedCommand,
// `left_has_any_in_right`) keep their shape.
let args_vec: Vec<&'static [u8]> = bun_core::argv().into_iter().skip(1).collect();
// `bun_core::argv()` includes argv[0]; skip to the subcommand keyword and
// collect into a borrowed-slice Vec so `&[&[u8]]` callers
// (TrustCommand/UntrustedCommand, `left_has_any_in_right`) keep their shape.
Comment thread
robobun marked this conversation as resolved.
Outdated
let cmd_idx = Command::subcommand_argv_index();
let args_vec: Vec<&'static [u8]> = bun_core::argv().into_iter().skip(cmd_idx).collect();
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
let args: &[&[u8]] = &args_vec;

// Check if we're being invoked directly as "bun whoami" instead of "bun pm whoami"
let is_direct_whoami = bun_core::argv()
.get(1)
.get(cmd_idx)
.is_some_and(|arg| strings::eql_comptime(arg.as_bytes(), b"whoami"));

let cli = CommandLineArguments::parse(Subcommand::Pm)?;
Expand Down
7 changes: 4 additions & 3 deletions src/runtime/cli/upgrade_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,13 +509,14 @@ impl UpgradeCommand {
#[cold]
pub(crate) fn exec(ctx: Command::Context) -> crate::Result<()> {
let args = bun_core::argv();
if args.len() > 2 {
for arg in args.iter().skip(2) {
let start = Command::subcommand_argv_index() + 1;
if args.len() > start {
for arg in args.iter().skip(start) {
if !strings::contains(arg, b"--") {
bun_core::pretty_error!(
"<r><red>error<r><d>:<r> This command updates Bun itself, and does not take package names.\n<blue>note<r><d>:<r> Use `bun update"
);
for arg_err in args.iter().skip(2) {
for arg_err in args.iter().skip(start) {
bun_core::pretty_error!(" {}", bstr::BStr::new(arg_err));
}
bun_core::pretty_errorln!("` instead.");
Expand Down
Loading
Loading