1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
//! `--flag=value` after `--` is one literal operand, the way `key=value`
//! already is.
//!
//! `--` means "everything after this is data". The post-`--` grammar had no
//! `--flag=value` production, so `echo -- --json=true` lexed as three tokens —
//! `LongFlag("json")`, `Eq`, `true` — and the glue guard rejected them as
//! adjacent unpasted words:
//!
//! ```text
//! 1:15 [parse]: adjacent words with no space between them are not joined
//! ```
//!
//! That error is right about pre-`--` argv, where kaish deliberately does no
//! token pasting, and wrong here: past `--` there is no flag to glue anything
//! to. `key=value` was already handled (`export -- A=1`, GH #189); this is the
//! same rule for the flag spelling, short and long.
//!
//! The fix also makes the `past_double_dash` guards on the binders' `Named`
//! arms reachable from real source for the first time — before this, no
//! program could produce a post-`--` `Arg::Named` at all.
// Test-fixture code: unwrap/expect on known-good setup is the idiom here.
#![allow(clippy::unwrap_used, clippy::expect_used)]
use kaish_kernel::{Kernel, KernelConfig};
fn kernel() -> Kernel {
Kernel::new(KernelConfig::isolated()).expect("kernel")
}
async fn echoed(script: &str) -> String {
let k = kernel();
let r = k.execute(script).await.expect("kernel execute");
assert_eq!(r.code, 0, "`{script}` exited {}: {:?}", r.code, r.err);
r.text_out().trim_end().to_string()
}
#[tokio::test]
async fn long_flag_value_is_one_operand() {
assert_eq!(echoed("echo -- --flag=value").await, "--flag=value");
}
/// `-x=value` is deliberately NOT part of this: it is refused on both sides
/// of `--` today (`echo -n=1` is the same parse error), so accepting it only
/// after `--` would trade one asymmetry for another. Pinned so the decision is
/// visible if someone changes the short-flag grammar.
#[tokio::test]
async fn short_flag_value_is_still_refused_on_both_sides() {
let k = kernel();
for script in ["echo -n=1", "echo -- -n=1"] {
let err = match k.execute(script).await {
Err(e) => e.to_string(),
Ok(r) => {
assert_ne!(r.code, 0, "`{script}` must not succeed");
r.err.clone()
}
};
assert!(
err.contains("adjacent words with no space between them"),
"`{script}` should still be the parse error, got: {err}"
);
}
}
/// The bare form leaked the same way, and predates this change: past `--`,
/// `--json` landed in `positional`, and the scan that exists for `raw_argv`
/// tools (which keep the `--` marker there) read it as the kernel's flag.
/// `echo -- --json hi` answered in JSON.
#[tokio::test]
async fn post_dash_bare_json_is_an_operand_too() {
assert_eq!(echoed("echo -- --json hi").await, "--json hi");
}
/// ...while a `raw_argv` tool still honors `--json` from its positionals,
/// which is what that scan is for (GH #198). `test` is raw_argv.
#[tokio::test]
async fn raw_argv_tools_still_read_json_from_positionals() {
let k = kernel();
let r = k.execute("test --json -n abc").await.expect("kernel execute");
assert!(
r.text_out().trim_start().starts_with('{'),
"test --json should still render its error as JSON, got {:?}",
r.text_out()
);
}
/// Several in a row, each its own word — the glue guard must not see them as
/// one pasted run.
#[tokio::test]
async fn several_flag_values_stay_separate_operands() {
assert_eq!(echoed("echo -- --a=1 --b=2").await, "--a=1 --b=2");
}
/// The value still expands, as it does for `key=value` and as bash does.
#[tokio::test]
async fn the_value_expands() {
let k = kernel();
let r = k
.execute("V=hello; echo -- --greeting=$V")
.await
.expect("kernel execute");
assert_eq!(r.code, 0, "{r:?}");
assert_eq!(r.text_out().trim_end(), "--greeting=hello");
}
/// The sharp case: past `--`, the kernel's own flag is an operand, so it must
/// be printed rather than switching the output format.
#[tokio::test]
async fn post_dash_json_is_an_operand_not_the_kernel_flag() {
let out = echoed("echo -- --json=true").await;
assert_eq!(out, "--json=true");
assert!(
serde_json::from_str::<serde_json::Value>(&out).is_err(),
"output must not be JSON-rendered: {out:?}"
);
}
/// Before `--`, nothing changes: `--json=true` is still the kernel's flag.
#[tokio::test]
async fn pre_dash_json_still_switches_the_format() {
let out = echoed("echo --json=true hi").await;
assert_eq!(out, "\"hi\\n\"");
}
/// Real pasting is still refused — a fragment glued to the END of the value.
///
/// The first version of this test used an undefined `$V` and accepted any
/// failure, so it passed on the runtime "undefined variable" error while the
/// glue guard was being bypassed entirely. It asserts the guard's own message
/// now, and `V` is defined so nothing else can fail first.
#[tokio::test]
async fn a_fragment_glued_after_the_value_is_still_rejected() {
let k = kernel();
for script in [
"V=hello; echo -- --a=$V--b",
"echo -- --a=1--b=2",
// and the same word before `--`, where the split used to survive as
// far as clap and surface as "unexpected argument '-a'".
"echo --a=1--b=2",
"V=hello; echo A=$V--b",
] {
let err = match k.execute(script).await {
Err(e) => e.to_string(),
Ok(r) => {
assert_ne!(r.code, 0, "`{script}` must not succeed: {:?}", r.text_out());
r.err.clone()
}
};
assert!(
err.contains("adjacent words with no space between them"),
"`{script}` must fail with the glue error, got: {err}"
);
}
}
/// The guard compares a zero source gap, so a space is all it takes to be
/// two words — no legitimate spelling is caught.
#[tokio::test]
async fn spaced_flag_values_are_not_glued() {
assert_eq!(echoed("echo -- --a=1 --b=2").await, "--a=1 --b=2");
let k = kernel();
let r = k.execute("export A=1; echo $A").await.expect("kernel execute");
assert_eq!(r.text_out().trim_end(), "1");
}