diff --git a/src/runtime/shell/builtin/touch.rs b/src/runtime/shell/builtin/touch.rs index 95ae1398deee..36ef0c89ef08 100644 --- a/src/runtime/shell/builtin/touch.rs +++ b/src/runtime/shell/builtin/touch.rs @@ -349,17 +349,20 @@ pub struct Opts {} impl FlagParser for Opts { fn parse_long(&mut self, flag: &[u8]) -> Option { - match flag { - b"--no-create" => Some(ParseFlagResult::Unsupported(unsupported_flag( - b"--no-create", - ))), + // `--option=VALUE` spells the same option as `--option VALUE`. + let (name, value) = match bun_core::strings::split_once_char(flag, b'=') { + Some((name, value)) => (name, Some(value)), + None => (flag, None), + }; + match name { + b"--no-create" if value.is_none() => Some(ParseFlagResult::Unsupported( + unsupported_flag(b"--no-create"), + )), b"--date" => Some(ParseFlagResult::Unsupported(unsupported_flag(b"--date"))), b"--reference" => Some(ParseFlagResult::Unsupported(unsupported_flag( b"--reference=FILE", ))), - b"--time" => Some(ParseFlagResult::Unsupported(unsupported_flag( - b"--reference=FILE", - ))), + b"--time" => Some(ParseFlagResult::Unsupported(unsupported_flag(b"--time"))), _ => None, } } diff --git a/test/js/bun/shell/commands/touch.test.ts b/test/js/bun/shell/commands/touch.test.ts new file mode 100644 index 000000000000..c8eebab58f9a --- /dev/null +++ b/test/js/bun/shell/commands/touch.test.ts @@ -0,0 +1,54 @@ +import { $ } from "bun"; +import { describe, expect } from "bun:test"; +import { createTestBuilder } from "../test_builder"; +const TestBuilder = createTestBuilder(import.meta.path); + +$.nothrow(); + +describe.concurrent("bunshell touch", () => { + describe("unsupported options are reported under the name they were given", () => { + // option as typed -> how the error names it (--reference's usage suffix predates this table) + const unsupported = { + "--no-create": "--no-create", + "--date": "--date", + "--reference": "--reference=FILE", + "--time": "--time", + // --date, --reference and --time take a value, which GNU touch also accepts as `--option=VALUE`. + "--date=@0": "--date", + "--date=": "--date", + "--reference=other": "--reference=FILE", + "--time=atime": "--time", + "-a": "-a", + "-c": "-c", + "-d": "-d", + "-h": "-h", + "-m": "-m", + "-r": "-r", + "-t": "-t", + }; + + for (const [option, reported] of Object.entries(unsupported)) { + TestBuilder.command`touch ${option} file` + .ensureTempDir() + .quiet() + .stdout("") + .stderr(`touch: unsupported option, please open a GitHub issue -- ${reported}\n`) + .exitCode(1) + .doesNotExist("file") + .runAsTest(option); + } + }); + + describe("options that take no value do not accept one", () => { + // `--no-create` is a plain flag, so `--no-create=VALUE` is not a spelling of it. Which bytes the + // illegal option message quotes is up to the shared flag parser, so only the classification is pinned. + TestBuilder.command`touch --no-create=1 file` + .ensureTempDir() + .quiet() + .stdout("") + .stderr(stderr => expect(stderr).toStartWith("touch: illegal option -- ")) + .exitCode(1) + .doesNotExist("file") + .runAsTest("--no-create=1"); + }); +});