Skip to content

Commit c93a25c

Browse files
committed
fix(argv): preserve literal values across executor adapter and typed paths
Observed RED at 94de053: 33 real-child contract tests, 21 failures, 0 errors and 0 skips; full suite 2027 tests. Snapshot submitted tokens, reject null elements without disclosing values, preserve empty/padded/newline arguments and validate blank command identifiers. Align two legacy filtering assertions with OC-ARGV-001 without removing tests. Ordered schema-aware options and remaining C01 closure are still pending.
1 parent 94de053 commit c93a25c

6 files changed

Lines changed: 135 additions & 223 deletions

File tree

‎src/main/java/io/github/easy4j/opencli/core/OpenCliAdapterChannel.java‎

Lines changed: 22 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,27 @@
22

33
import io.github.easy4j.opencli.util.OpenCliStrings;
44
import java.util.ArrayList;
5+
import java.util.Arrays;
56
import java.util.List;
67
import java.util.Objects;
78
import lombok.extern.slf4j.Slf4j;
89

910
/**
10-
* 针对单个 OpenCLI adapter 的轻量通道:自动在 argv 前插入 adapter id。
11-
*/
12-
@Slf4j/**
13-
14-
* Lightweight channel for a single OpenCLI adapter: automatically prepends the adapter id
15-
* to every argv invocation.
16-
11+
* Lightweight channel for one OpenCLI adapter. Values are literal argv tokens;
12+
* only the separately supplied adapter identifier is normalized.
1713
*
18-
1914
* @author <a href="https://github.com/loong10k">Loong Wan</a>
20-
2115
* @since 3.0.0
22-
2316
*/
24-
17+
@Slf4j
2518
public final class OpenCliAdapterChannel {
2619

2720
private final OpenCliExecutor executor;
2821
private final String adapterId;
2922

3023
/**
31-
* @param executor 共享执行器,不得为 null
32-
* @param adapterId 文档中的 adapter 名(如 {@code twitter}),不得为空白
24+
* @param executor shared executor
25+
* @param adapterId nonblank adapter identifier
3326
*/
3427
public OpenCliAdapterChannel(OpenCliExecutor executor, String adapterId) {
3528
this.executor = Objects.requireNonNull(executor, "executor");
@@ -39,76 +32,43 @@ public OpenCliAdapterChannel(OpenCliExecutor executor, String adapterId) {
3932
}
4033
}
4134

42-
/**
43-
* @return 当前通道绑定的 adapter id
44-
*/
35+
/** @return this channel's adapter identifier */
4536
public String getAdapterId() {
4637
return adapterId;
4738
}
4839

4940
/**
50-
* 调用 {@code opencli <adapter> <subcommandAndArgs...>}。
41+
* Invoke an adapter with a snapshot of the supplied literal arguments.
42+
* An empty list invokes its root; null elements are rejected.
5143
*
52-
* @param subcommandAndArgs 子命令及后续参数;不得为 null,可为空(仅调 adapter 根命令时)
53-
* @return 成功时的 {@link OpenCliResult}
44+
* @param subcommandAndArgs subcommand and subsequent values
45+
* @return execution result
5446
*/
5547
public OpenCliResult invoke(List<String> subcommandAndArgs) {
56-
Objects.requireNonNull(subcommandAndArgs, "subcommandAndArgs");
57-
List<String> tokens = new ArrayList<>();
48+
List<String> rest = OpenCliArgSupport.snapshotValues(subcommandAndArgs, "subcommandAndArgs");
49+
List<String> tokens = new ArrayList<>(rest.size() + 1);
5850
tokens.add(adapterId);
59-
for (String s : subcommandAndArgs) {
60-
if (OpenCliStrings.isNotBlank(s)) {
61-
tokens.add(s.trim());
62-
}
63-
}
64-
log.debug("OpenCLI adapter invoke adapterId={} subcommandSummary={}", adapterId, summarizeSubcommand(tokens));
51+
tokens.addAll(rest);
52+
// Do not put prompt text or positional values in default diagnostic logs.
53+
log.debug("OpenCLI adapter invoke argvSize={}", tokens.size());
6554
return executor.invoke(tokens);
6655
}
6756

6857
/**
69-
* 通过 {@link OpenCliAdapterCommandRequest} 发起调用(推荐测试与 SDK 侧结构化入口)。
70-
*
71-
* @param request 结构化子命令请求,不得为 null
72-
* @return 执行结果
58+
* @param request structured command request
59+
* @return execution result
7360
*/
7461
public OpenCliResult invoke(OpenCliAdapterCommandRequest request) {
7562
Objects.requireNonNull(request, "request");
7663
return invoke(request.toSubcommandAndArgs());
7764
}
7865

7966
/**
80-
* {@link #invoke(List)} 的可变参数形式。
81-
*
82-
* @param subcommandAndArgs 子命令及 flag
83-
* @return 执行结果
67+
* @param subcommandAndArgs subcommand and literal values
68+
* @return execution result
8469
*/
8570
public OpenCliResult invoke(String... subcommandAndArgs) {
86-
List<String> list = new ArrayList<>();
87-
if (Objects.nonNull(subcommandAndArgs)) {
88-
for (String s : subcommandAndArgs) {
89-
if (OpenCliStrings.isNotBlank(s)) {
90-
list.add(s.trim());
91-
}
92-
}
93-
}
94-
return invoke(list);
95-
}
96-
97-
private static String summarizeSubcommand(List<String> tokens) {
98-
if (tokens.size() <= 1) {
99-
return "(root)";
100-
}
101-
int limit = Math.min(tokens.size(), 4);
102-
StringBuilder sb = new StringBuilder();
103-
for (int i = 1; i < limit; i++) {
104-
if (i > 1) {
105-
sb.append(' ');
106-
}
107-
sb.append(tokens.get(i));
108-
}
109-
if (tokens.size() > limit) {
110-
sb.append(" ...");
111-
}
112-
return sb.toString();
71+
Objects.requireNonNull(subcommandAndArgs, "subcommandAndArgs");
72+
return invoke(Arrays.asList(subcommandAndArgs));
11373
}
11474
}

‎src/main/java/io/github/easy4j/opencli/core/OpenCliAdapterCommandRequest.java‎

Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,18 @@
1313
import lombok.Singular;
1414

1515
/**
16-
* 结构化 adapter 子命令请求:由子命令名、positional 参数与命名 options 构建 argv,
17-
* 供 {@link OpenCliAdapterChannel#invoke(OpenCliAdapterCommandRequest)} 及覆盖测试使用。
18-
* <p>
19-
* 禁止在测试中手工拼接 {@code List.of("sub", "--flag", "value")};应通过 builder 建模参数。
20-
* </p>
21-
*/
22-
@Getter
23-
@Builder/**
24-
25-
* Structured adapter subcommand request: builds argv from a subcommand name,
26-
* positional arguments, and named options for use with
27-
* {@link OpenCliAdapterChannel#invoke(OpenCliAdapterCommandRequest)}.
28-
*
29-
* <p>Avoid manually assembling {@code List.of("sub", "--flag", "value")} in tests;
30-
* use the builder to model parameters instead.</p>
31-
16+
* Structured adapter request. Positional and valued-option contents are literal;
17+
* command and option identifiers are validated separately.
18+
* The legacy options map represents a single-valued subset: Boolean values
19+
* retain their historical presence-only flag semantics.
3220
*
33-
3421
* @author <a href="https://github.com/loong10k">Loong Wan</a>
35-
3622
* @since 3.0.0
37-
3823
*/
39-
24+
@Getter
25+
@Builder
4026
public final class OpenCliAdapterCommandRequest {
4127

42-
/** 子命令名(不含 adapter id)。 */
4328
private final String subcommand;
4429

4530
@Getter(AccessLevel.NONE)
@@ -50,19 +35,15 @@ public final class OpenCliAdapterCommandRequest {
5035
@Builder.Default
5136
private final Map<String, Object> options = Collections.emptyMap();
5237

53-
/**
54-
* @return positional 参数副本
55-
*/
38+
/** @return an immutable copy of positional values */
5639
public List<String> getPositionals() {
5740
if (Objects.isNull(positionals)) {
5841
return Collections.emptyList();
5942
}
6043
return Collections.unmodifiableList(new ArrayList<>(positionals));
6144
}
6245

63-
/**
64-
* @return 命名选项副本
65-
*/
46+
/** @return an immutable copy of named options */
6647
public Map<String, Object> getOptions() {
6748
if (Objects.isNull(options)) {
6849
return Collections.emptyMap();
@@ -71,20 +52,17 @@ public Map<String, Object> getOptions() {
7152
}
7253

7354
/**
74-
* 将本请求转换为 {@link OpenCliAdapterChannel#invoke(List)} 所需的 token 列表。
75-
*
76-
* @return 以 subcommand 开头、随后 positional、再 options 的 argv 片段
55+
* @return subcommand, then unchanged positional values and named options
7756
*/
7857
public List<String> toSubcommandAndArgs() {
7958
Objects.requireNonNull(subcommand, "subcommand");
59+
if (OpenCliStrings.isBlank(subcommand)) {
60+
throw new IllegalArgumentException("subcommand must not be blank");
61+
}
8062
List<String> tokens = new ArrayList<>();
8163
tokens.add(subcommand.trim());
82-
if (Objects.nonNull(positionals)) {
83-
for (String p : positionals) {
84-
if (OpenCliStrings.isNotBlank(p)) {
85-
tokens.add(p.trim());
86-
}
87-
}
64+
if (positionals != null) {
65+
tokens.addAll(OpenCliArgSupport.snapshotValues(positionals, "positionals"));
8866
}
8967
if (Objects.nonNull(options)) {
9068
for (Map.Entry<String, Object> entry : options.entrySet()) {
@@ -106,16 +84,14 @@ private static void appendOption(List<String> target, String name, Object value)
10684
return;
10785
}
10886
target.add(flag);
109-
target.add(String.valueOf(value).trim());
87+
target.add(String.valueOf(value));
11088
}
11189

11290
/**
113-
* 从 manifest 风格的 options map 创建请求(测试资源反序列化辅助)。
114-
*
115-
* @param subcommand 子命令
116-
* @param positionals positional 列表,可为 null
117-
* @param options 选项 map,可为 null
118-
* @return 请求实例
91+
* @param subcommand command identifier
92+
* @param positionals optional positional values
93+
* @param options optional legacy single-value options
94+
* @return a structured request
11995
*/
12096
public static OpenCliAdapterCommandRequest of(
12197
String subcommand,

‎src/main/java/io/github/easy4j/opencli/core/OpenCliArgSupport.java‎

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,61 @@
11
package io.github.easy4j.opencli.core;
22

3-
import io.github.easy4j.opencli.util.OpenCliStrings;
43
import java.util.ArrayList;
4+
import java.util.Collections;
55
import java.util.List;
66
import java.util.Objects;
77

88
/**
99
* CLI argument assembly utilities: merges business segments with pass-through
10-
* {@code additionalRawArgs}.
10+
* {@code additionalRawArgs} without changing argument values.
1111
*
1212
* @author <a href="https://github.com/loong10k">Loong Wan</a>
1313
* @since 3.0.0
14-
*/public final class OpenCliArgSupport {
14+
*/
15+
public final class OpenCliArgSupport {
1516

1617
private OpenCliArgSupport() {
1718
}
1819

1920
/**
20-
* 将前缀片段与可选附加片段合并为连续 argv(过滤 null/空白)。
21+
* Capture values before execution or queuing. Validate indices without exposing other values.
22+
*/
23+
static List<String> snapshotValues(List<String> values, String field) {
24+
Objects.requireNonNull(values, field);
25+
List<String> copy = new ArrayList<>(values);
26+
for (int i = 0; i < copy.size(); i++) {
27+
if (copy.get(i) == null) {
28+
throw new IllegalArgumentException(field + "[" + i + "] must not be null");
29+
}
30+
}
31+
return Collections.unmodifiableList(copy);
32+
}
33+
34+
/**
35+
* Merge optional segments into a new argv list. Null segments are absent;
36+
* null elements are invalid, while empty and whitespace-only values are preserved.
2137
*
22-
* @param prefix 子命令与已建模参数,可为 null
23-
* @param additionalRawArgs 额外原生参数,可为 null
24-
* @return 新列表副本
38+
* @param prefix command and modeled arguments, or null
39+
* @param additionalRawArgs extra literal arguments, or null
40+
* @return a new list, without modifying either source
2541
*/
2642
public static List<String> merge(List<String> prefix, List<String> additionalRawArgs) {
2743
List<String> out = new ArrayList<>();
28-
if (Objects.nonNull(prefix)) {
29-
for (String s : prefix) {
30-
if (OpenCliStrings.isNotBlank(s)) {
31-
out.add(s.trim());
32-
}
33-
}
44+
if (prefix != null) {
45+
out.addAll(snapshotValues(prefix, "prefix"));
3446
}
35-
if (Objects.nonNull(additionalRawArgs)) {
36-
for (String s : additionalRawArgs) {
37-
if (OpenCliStrings.isNotBlank(s)) {
38-
out.add(s.trim());
39-
}
40-
}
47+
if (additionalRawArgs != null) {
48+
out.addAll(snapshotValues(additionalRawArgs, "additionalRawArgs"));
4149
}
4250
return out;
4351
}
4452

4553
/**
46-
* 追加 {@code --name=value}(value 含空格时由调用方决定是否使用
47-
* {@link OpenCliExecutor#appendQuotedKeyValue(CommandLine, String, String)};
48-
* 此处仅做简单拼接)。
54+
* Append one literal {@code --name=value} token.
4955
*
50-
* @param target 目标列表,不得为 null
51-
* @param name 完整名称(含 {@code --},不含 {@code =})
52-
* @param value 非空值
56+
* @param target destination list
57+
* @param name full option name, including {@code --}
58+
* @param value non-null value, which may be empty
5359
*/
5460
public static void addOptionEquals(List<String> target, String name, String value) {
5561
Objects.requireNonNull(target, "target");
@@ -63,11 +69,11 @@ public static void addOptionEquals(List<String> target, String name, String valu
6369
}
6470

6571
/**
66-
* 追加 {@code --flag value} 双 token 形式。
72+
* Append a {@code --flag value} pair without altering the value.
6773
*
68-
* @param target 目标列表
69-
* @param flag 如 {@code --limit}
70-
* @param value 非空值
74+
* @param target destination list
75+
* @param flag option name
76+
* @param value non-null value, which may be empty
7177
*/
7278
public static void addOptionPair(List<String> target, String flag, String value) {
7379
Objects.requireNonNull(target, "target");
@@ -78,11 +84,11 @@ public static void addOptionPair(List<String> target, String flag, String value)
7884
}
7985

8086
/**
81-
* 当 {@code value} 非 null 时追加 {@code --flag value}。
87+
* Append a pair when the optional value is non-null.
8288
*
83-
* @param target 目标 argv 列表
84-
* @param flag 选项名
85-
* @param value 可为 null
89+
* @param target destination list
90+
* @param flag option name
91+
* @param value optional value
8692
*/
8793
public static void addOptionPairIfPresent(List<String> target, String flag, Object value) {
8894
if (Objects.nonNull(value)) {
@@ -91,11 +97,11 @@ public static void addOptionPairIfPresent(List<String> target, String flag, Obje
9197
}
9298

9399
/**
94-
* 当 {@code enabled} 为 {@code true} 时追加 boolean flag(无值)。
100+
* Append a presence-only flag when enabled. This helper is not a valued boolean option.
95101
*
96-
* @param target 目标 argv 列表
97-
* @param flag 如 {@code --follow}
98-
* @param enabled 开关,null/false 时不追加
102+
* @param target destination list
103+
* @param flag option name
104+
* @param enabled true to append; null/false to omit
99105
*/
100106
public static void addFlagIfTrue(List<String> target, String flag, Boolean enabled) {
101107
if (Boolean.TRUE.equals(enabled)) {

0 commit comments

Comments
 (0)