Skip to content

Commit 0183cec

Browse files
committed
[O] 优化CLI,加入--output参数(自适应目录或文件,也支持标准输出)
1 parent 15dc46e commit 0183cec

2 files changed

Lines changed: 115 additions & 30 deletions

File tree

Program.cs

Lines changed: 111 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ private static int Main(string[] args)
1515
var root = BuildRootCommand();
1616
try
1717
{
18-
return root.Parse(args).Invoke();
18+
var parseResult = root.Parse(args);
19+
var invocation = new InvocationConfiguration
20+
{
21+
EnableDefaultExceptionHandler = false
22+
};
23+
return parseResult.Invoke(invocation);
1924
}
2025
catch (ConversionException ex)
2126
{
@@ -43,6 +48,17 @@ private static Command BuildRootCommand()
4348
HelpName = "N[,N...]"
4449
};
4550

51+
var outputOption = new Option<string?>("--output", "-o")
52+
{
53+
Description =
54+
"输出位置:\n" +
55+
"· 省略:写入输入文件同目录,文件名按默认规则(maidata.txt、lv_N.ma2 等)。\n" +
56+
"· 目录:写入该目录,文件名同上按默认规则。\n" +
57+
"· 文件:仅当本次转换只会生成一个输出文件时可用;扩展名须为 .txt(输出 maidata)或 .ma2(输出 MA2)。\n" +
58+
\"-\":仅当本次转换只会生成一个输出文件时可用;将输出内容写到stdout。",
59+
HelpName = "path"
60+
};
61+
4662
var inputArgument = new Argument<string>("path")
4763
{
4864
Description = "可以输入以下几种情况:\n" +
@@ -54,19 +70,49 @@ private static Command BuildRootCommand()
5470
};
5571

5672
root.Options.Add(levelsOption);
73+
root.Options.Add(outputOption);
5774
root.Arguments.Add(inputArgument);
5875

5976
root.SetAction(parseResult =>
6077
{
6178
var inputPath = parseResult.GetValue(inputArgument)
6279
?? throw new InvalidOperationException("缺少参数 path。");
6380
var levelsRaw = parseResult.GetValue(levelsOption);
81+
_outputSpec = OutputSpec.Parse(parseResult.GetValue(outputOption));
6482
RunConvert(inputPath, levelsRaw);
6583
});
6684

6785
return root;
6886
}
6987

88+
/// <summary>由 CLI 在每次 <c>SetAction</c> 入口赋值;转换逻辑只读此字段。</summary>
89+
private static OutputSpec _outputSpec;
90+
91+
private enum OutputSinkKind { Default, Stdout, Directory, File }
92+
93+
private readonly record struct OutputSpec(OutputSinkKind Kind, string? FsPath)
94+
{
95+
internal static OutputSpec Parse(string? raw)
96+
{
97+
if (string.IsNullOrWhiteSpace(raw))
98+
return new OutputSpec(OutputSinkKind.Default, null);
99+
var t = raw.Trim();
100+
if (t == "-")
101+
return new OutputSpec(OutputSinkKind.Stdout, null);
102+
var full = Path.GetFullPath(t);
103+
if (Directory.Exists(full))
104+
return new OutputSpec(OutputSinkKind.Directory, full);
105+
if (File.Exists(full))
106+
return new OutputSpec(OutputSinkKind.File, full);
107+
if (!string.IsNullOrEmpty(Path.GetExtension(full)))
108+
return new OutputSpec(OutputSinkKind.File, full);
109+
return new OutputSpec(OutputSinkKind.Directory, full);
110+
}
111+
112+
internal string ResolveOutputDir(string defaultDir) =>
113+
Kind == OutputSinkKind.Directory ? FsPath! : defaultDir;
114+
}
115+
70116
private static void RunConvert(string inputPath, string? levelsRaw)
71117
{
72118
var fullPath = Path.GetFullPath(inputPath.Trim());
@@ -139,9 +185,22 @@ private static void RunConvertTxtFile(string inputPath, string? levelsRaw)
139185
var text = File.ReadAllText(inputPath, Encoding.UTF8);
140186

141187
if (LooksLikeMaidata(text))
142-
ConvertMaidata(text, inputDir, levelFilter, inputPath);
188+
{
189+
var maidata = new Maidata(text);
190+
var ids = maidata.Levels.Keys.OrderBy(k => k).ToList();
191+
if (ids.Count == 0) throw new ArgumentException("maidata 中未找到任何 &inote_* 谱面。");
192+
var selected = levelFilter == null ? ids : ids.Where(id => levelFilter.Contains(id)).ToList();
193+
if (selected.Count == 0) throw new ArgumentException("-l / --levels 指定的难度在文件中均不存在。");
194+
ValidateOutputForMa2Targets(selected.Count);
195+
196+
ConvertMaidata(maidata, selected, inputDir, inputPath);
197+
}
143198
else
144-
ConvertPlainSimai(text, inputDir, levelFilter, inputPath);
199+
{
200+
if (levelFilter != null) throw new ArgumentException("纯 simai 单谱(非 maidata)不能使用 -l / --levels。");
201+
ValidateOutputForMa2Targets(1);
202+
ConvertPlainSimai(text, inputDir, inputPath);
203+
}
145204
}
146205

147206
/// <summary>
@@ -201,14 +260,20 @@ private static void ConvertMa2PathsToMaidata(string outputDir, string title, IRe
201260
.OrderBy(t => t.LevelId)
202261
.Where((_, lv)=> levelFilter == null || levelFilter.Contains(lv))
203262
.ToList();
204-
var outPath = Path.Combine(outputDir, "maidata.txt");
263+
264+
if (assignments.Count == 0) throw new ArgumentException("-l / --levels 过滤后没有可转换的 .ma2 文件。");
265+
ValidateOutputForMaidataTxt();
266+
267+
var baseDir = _outputSpec.ResolveOutputDir(outputDir);
268+
var diskPath = _outputSpec.Kind == OutputSinkKind.File ? _outputSpec.FsPath! : Path.Combine(baseDir, "maidata.txt");
269+
var destNote = _outputSpec.Kind == OutputSinkKind.Stdout ? "(标准输出)" : diskPath;
205270

206271
int clockCount = 4;
207272
var inoteBlocks = new List<(int LevelId, string Inote)>();
208273

209274
foreach (var (fullPath, levelId) in assignments)
210275
{
211-
Console.WriteLine($"Simai → MA2: {fullPath}(lv{levelId}) → {outPath}");
276+
Console.Error.WriteLine($"Simai → MA2: {fullPath}(lv{levelId}) → {destNote}");
212277
var ma2Text = File.ReadAllText(fullPath, Encoding.UTF8);
213278
var (chart, parseAlerts) = new MA2Parser().Parse(ma2Text);
214279
PrintAlerts(parseAlerts);
@@ -224,7 +289,10 @@ private static void ConvertMa2PathsToMaidata(string outputDir, string title, IRe
224289
maidata["clock_count"] = clockCount.ToString();
225290
foreach (var (levelId, inote) in inoteBlocks)
226291
maidata.AddLevel(levelId, new MaidataChart(inote));
227-
File.WriteAllText(outPath, maidata.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
292+
293+
var maidataText = maidata.ToString();
294+
if (_outputSpec.Kind == OutputSinkKind.Stdout) Console.Out.Write(maidataText);
295+
else File.WriteAllText(diskPath, maidataText, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
228296
}
229297

230298
private static HashSet<int> ParseLevelList(string s)
@@ -272,42 +340,56 @@ private static void PrintAlerts(IReadOnlyList<Alert> alerts, string? header = nu
272340
Console.Error.WriteLine(a.ToString());
273341
}
274342

275-
private static void ConvertMaidata(string text, string outputDir, HashSet<int>? levelFilter, string inputPath)
343+
private static void ConvertMaidata(Maidata maidata, IReadOnlyList<int> selected, string inputDir, string inputPath)
276344
{
277-
var maidata = new Maidata(text);
278-
var ids = maidata.Levels.Keys.OrderBy(k => k).ToList();
279-
if (ids.Count == 0)
280-
throw new ArgumentException("maidata 中未找到任何 &inote_* 谱面。");
281-
282-
var selected = levelFilter == null
283-
? ids
284-
: ids.Where(id => levelFilter.Contains(id)).ToList();
285-
286-
if (selected.Count == 0)
287-
throw new ArgumentException("-l / --levels 指定的难度在文件中均不存在。");
288-
345+
var baseDir = _outputSpec.ResolveOutputDir(inputDir);
289346
foreach (var id in selected)
290347
{
291-
var outPath = Path.Combine(outputDir, $"lv_{id}.ma2");
292-
Console.WriteLine($"Simai → MA2: {inputPath}(lv${id}) → {outPath}");
348+
var outPath = _outputSpec.Kind == OutputSinkKind.File ? _outputSpec.FsPath! : Path.Combine(baseDir, $"lv_{id}.ma2");
349+
var destNote = _outputSpec.Kind == OutputSinkKind.Stdout ? "(标准输出)" : outPath;
350+
Console.Error.WriteLine($"Simai → MA2: {inputPath}(lv{id}) → {destNote}");
293351
var chartInfo = maidata.Levels[id];
294352
var bigTouch = id is 2 or 3;
295353
var isUtage = IsUtageFromLevelString(chartInfo.Level);
296354
var ma2 = SimaiToMa2(chartInfo.Inote, maidata.ClockCount, bigTouch, isUtage);
297-
File.WriteAllText(outPath, ma2, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
355+
if (_outputSpec.Kind == OutputSinkKind.Stdout) Console.Out.Write(ma2);
356+
else File.WriteAllText(outPath, ma2, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
298357
}
299358
}
300359

301-
private static void ConvertPlainSimai(string text, string outputDir, HashSet<int>? levelFilter, string inputPath)
360+
private static void ConvertPlainSimai(string text, string inputDir, string inputPath)
302361
{
303-
if (levelFilter != null)
304-
throw new ArgumentException("纯 simai 单谱(非 maidata)不能使用 -l / --levels。");
305-
306362
const int outputLevel = 0;
307-
var outPath = Path.Combine(outputDir, $"lv_{outputLevel}.ma2");
308-
Console.WriteLine($"Simai → MA2: {inputPath}(lv${outputLevel}) → {outPath}");
363+
var baseDir = _outputSpec.ResolveOutputDir(inputDir);
364+
var outPath = _outputSpec.Kind == OutputSinkKind.File ? _outputSpec.FsPath! : Path.Combine(baseDir, $"lv_{outputLevel}.ma2");
365+
var destNote = _outputSpec.Kind == OutputSinkKind.Stdout ? "(标准输出)" : outPath;
366+
Console.Error.WriteLine($"Simai → MA2: {inputPath}(lv{outputLevel}) → {destNote}");
309367
var ma2 = SimaiToMa2(text);
310-
File.WriteAllText(outPath, ma2, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
368+
if (_outputSpec.Kind == OutputSinkKind.Stdout) Console.Out.Write(ma2);
369+
else File.WriteAllText(outPath, ma2, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
370+
}
371+
372+
private static void ValidateOutputForMa2Targets(int ma2FileCount)
373+
{
374+
if (_outputSpec.Kind == OutputSinkKind.Stdout && ma2FileCount != 1)
375+
throw new ArgumentException($"-o \"-\" 仅适用于恰好输出一个 MA2 文件的情况(当前会输出 {ma2FileCount} 个)。请通过-l指定难度,或改为指定-o为一个目录。");
376+
if (_outputSpec.Kind == OutputSinkKind.File && ma2FileCount != 1)
377+
throw new ArgumentException($"使用 -o 指定输出为文件时,本次必须只生成一个 MA2 文件(当前会生成 {ma2FileCount} 个)。请通过-l指定难度,或改为指定-o为一个目录。");
378+
if (_outputSpec.Kind == OutputSinkKind.File)
379+
ValidateOutputFileExtension(_outputSpec.FsPath!, ".ma2");
380+
}
381+
382+
private static void ValidateOutputForMaidataTxt()
383+
{
384+
if (_outputSpec.Kind == OutputSinkKind.File)
385+
ValidateOutputFileExtension(_outputSpec.FsPath!, ".txt");
386+
}
387+
388+
private static void ValidateOutputFileExtension(string filePath, string requiredExt)
389+
{
390+
var ext = Path.GetExtension(filePath);
391+
if (!string.Equals(ext, requiredExt, StringComparison.OrdinalIgnoreCase))
392+
throw new ArgumentException($"输出文件扩展名须为「{requiredExt}」,当前为「{(string.IsNullOrEmpty(ext) ? "(无)" : ext)}」。");
311393
}
312394

313395
private static string SimaiToMa2(string inote, int clockCount=4, bool bigTouch=false, bool isUtage=false)

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ MuConvert 是一个支持**Simai与MA2互转**的转谱器。
2525
#### 基本用法
2626

2727
```shell
28-
MuConvert.exe <path> [-l|--levels N[,N...]]
28+
MuConvert.exe <path> [-l|--levels N[,N...]] [-o|--output <输出路径或->]
2929
```
3030
3131
- **`path`**:输入路径(必填),可以是 `.txt` / `.ma2` / 目录(见下文)
3232
- **`-l, --levels`**:仅转换指定难度(以 `maidata.txt``&inote_编号` 为准),多个难度用英文逗号分隔;省略则转换全部难度
33+
- **`-o, --output`**:指定输出位置(可选);不传入此参数时,文件将保存到“输入文件所在的目录”。
34+
- 会智能识别你传入的是目录还是文件,做智能的处理,将转谱结果输入到目录下或保存为文件。
35+
- 此外,还可以传入 `-` ,表示输出到stdout。
3336
3437
#### `path` 支持的输入形式与输出规则
3538
通过命令行传入的参数,既可以是文件,也可以是目录。

0 commit comments

Comments
 (0)