mirror of
https://github.com/actions/runner.git
synced 2026-01-22 20:44:30 +08:00
761 lines
27 KiB
C#
761 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text.RegularExpressions;
|
|
using GitHub.Runner.Common;
|
|
|
|
namespace GitHub.Runner.Worker.Dap.StepCommands
|
|
{
|
|
/// <summary>
|
|
/// Output format for step command responses.
|
|
/// </summary>
|
|
public enum OutputFormat
|
|
{
|
|
/// <summary>Human-readable text output (default)</summary>
|
|
Text,
|
|
/// <summary>JSON output for programmatic use</summary>
|
|
Json
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interface for parsing step commands from REPL strings.
|
|
/// </summary>
|
|
[ServiceLocator(Default = typeof(StepCommandParser))]
|
|
public interface IStepCommandParser : IRunnerService
|
|
{
|
|
/// <summary>
|
|
/// Parses a command string into a structured StepCommand.
|
|
/// </summary>
|
|
/// <param name="input">The input string (e.g., "steps list --verbose")</param>
|
|
/// <returns>Parsed StepCommand</returns>
|
|
/// <exception cref="StepCommandException">If parsing fails</exception>
|
|
StepCommand Parse(string input);
|
|
|
|
/// <summary>
|
|
/// Checks if the input is a step command.
|
|
/// </summary>
|
|
/// <param name="input">The input string to check</param>
|
|
/// <returns>True if this is a step command (starts with "steps")</returns>
|
|
bool IsStepCommand(string input);
|
|
}
|
|
|
|
#region Command Classes
|
|
|
|
/// <summary>
|
|
/// Base class for all step commands.
|
|
/// </summary>
|
|
public abstract class StepCommand
|
|
{
|
|
/// <summary>
|
|
/// Output format for the command response.
|
|
/// </summary>
|
|
public OutputFormat Output { get; set; } = OutputFormat.Text;
|
|
}
|
|
|
|
/// <summary>
|
|
/// steps list [--verbose]
|
|
/// </summary>
|
|
public class ListCommand : StepCommand
|
|
{
|
|
public bool Verbose { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// steps add run "script" [options]
|
|
/// </summary>
|
|
public class AddRunCommand : StepCommand
|
|
{
|
|
public string Script { get; set; }
|
|
public string Name { get; set; }
|
|
public string Shell { get; set; }
|
|
public string WorkingDirectory { get; set; }
|
|
public Dictionary<string, string> Env { get; set; }
|
|
public string Condition { get; set; }
|
|
public bool ContinueOnError { get; set; }
|
|
public int? Timeout { get; set; }
|
|
public StepPosition Position { get; set; } = StepPosition.Last();
|
|
}
|
|
|
|
/// <summary>
|
|
/// steps add uses "action@ref" [options]
|
|
/// </summary>
|
|
public class AddUsesCommand : StepCommand
|
|
{
|
|
public string Action { get; set; }
|
|
public string Name { get; set; }
|
|
public Dictionary<string, string> With { get; set; }
|
|
public Dictionary<string, string> Env { get; set; }
|
|
public string Condition { get; set; }
|
|
public bool ContinueOnError { get; set; }
|
|
public int? Timeout { get; set; }
|
|
public StepPosition Position { get; set; } = StepPosition.Last();
|
|
}
|
|
|
|
/// <summary>
|
|
/// steps edit <index> [modifications]
|
|
/// </summary>
|
|
public class EditCommand : StepCommand
|
|
{
|
|
public int Index { get; set; }
|
|
public string Name { get; set; }
|
|
public string Script { get; set; }
|
|
public string Action { get; set; }
|
|
public string Shell { get; set; }
|
|
public string WorkingDirectory { get; set; }
|
|
public string Condition { get; set; }
|
|
public Dictionary<string, string> With { get; set; }
|
|
public Dictionary<string, string> Env { get; set; }
|
|
public List<string> RemoveWith { get; set; }
|
|
public List<string> RemoveEnv { get; set; }
|
|
public bool? ContinueOnError { get; set; }
|
|
public int? Timeout { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// steps remove <index>
|
|
/// </summary>
|
|
public class RemoveCommand : StepCommand
|
|
{
|
|
public int Index { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// steps move <from> [position options]
|
|
/// </summary>
|
|
public class MoveCommand : StepCommand
|
|
{
|
|
public int FromIndex { get; set; }
|
|
public StepPosition Position { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// steps export [--changes-only] [--with-comments]
|
|
/// </summary>
|
|
public class ExportCommand : StepCommand
|
|
{
|
|
public bool ChangesOnly { get; set; }
|
|
public bool WithComments { get; set; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Position Types
|
|
|
|
/// <summary>
|
|
/// Types of position specifications for inserting/moving steps.
|
|
/// </summary>
|
|
public enum PositionType
|
|
{
|
|
/// <summary>Insert at specific index (1-based)</summary>
|
|
At,
|
|
/// <summary>Insert after specific index (1-based)</summary>
|
|
After,
|
|
/// <summary>Insert before specific index (1-based)</summary>
|
|
Before,
|
|
/// <summary>Insert at first pending position</summary>
|
|
First,
|
|
/// <summary>Insert at end (default)</summary>
|
|
Last
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a position for inserting or moving steps.
|
|
/// </summary>
|
|
public class StepPosition
|
|
{
|
|
public PositionType Type { get; set; }
|
|
public int? Index { get; set; }
|
|
|
|
public static StepPosition At(int index) => new StepPosition { Type = PositionType.At, Index = index };
|
|
public static StepPosition After(int index) => new StepPosition { Type = PositionType.After, Index = index };
|
|
public static StepPosition Before(int index) => new StepPosition { Type = PositionType.Before, Index = index };
|
|
public static StepPosition First() => new StepPosition { Type = PositionType.First };
|
|
public static StepPosition Last() => new StepPosition { Type = PositionType.Last };
|
|
|
|
public override string ToString()
|
|
{
|
|
return Type switch
|
|
{
|
|
PositionType.At => $"at {Index}",
|
|
PositionType.After => $"after {Index}",
|
|
PositionType.Before => $"before {Index}",
|
|
PositionType.First => "first",
|
|
PositionType.Last => "last",
|
|
_ => "unknown"
|
|
};
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Parser implementation for step commands.
|
|
/// </summary>
|
|
public sealed class StepCommandParser : RunnerService, IStepCommandParser
|
|
{
|
|
// Regex to match quoted strings (handles escaped quotes)
|
|
private static readonly Regex QuotedStringRegex = new Regex(
|
|
@"""(?:[^""\\]|\\.)*""|'(?:[^'\\]|\\.)*'",
|
|
RegexOptions.Compiled);
|
|
|
|
public bool IsStepCommand(string input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(input))
|
|
return false;
|
|
|
|
var trimmed = input.Trim();
|
|
|
|
// Command format: steps ...
|
|
if (trimmed.StartsWith("steps ", StringComparison.OrdinalIgnoreCase) ||
|
|
trimmed.Equals("steps", StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
public StepCommand Parse(string input)
|
|
{
|
|
var trimmed = input?.Trim() ?? "";
|
|
return ParseReplCommand(trimmed);
|
|
}
|
|
|
|
#region REPL Parsing
|
|
|
|
private StepCommand ParseReplCommand(string input)
|
|
{
|
|
// Tokenize the input, respecting quoted strings
|
|
var tokens = Tokenize(input);
|
|
|
|
if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Invalid command format. Expected: steps <command> [args...]");
|
|
}
|
|
|
|
var subCommand = tokens[1].ToLower();
|
|
|
|
return subCommand switch
|
|
{
|
|
"list" => ParseReplListCommand(tokens),
|
|
"add" => ParseReplAddCommand(tokens),
|
|
"edit" => ParseReplEditCommand(tokens),
|
|
"remove" => ParseReplRemoveCommand(tokens),
|
|
"move" => ParseReplMoveCommand(tokens),
|
|
"export" => ParseReplExportCommand(tokens),
|
|
_ => throw new StepCommandException(StepCommandErrors.InvalidCommand, $"Unknown sub-command: {subCommand}")
|
|
};
|
|
}
|
|
|
|
private List<string> Tokenize(string input)
|
|
{
|
|
var tokens = new List<string>();
|
|
var remaining = input;
|
|
|
|
while (!string.IsNullOrEmpty(remaining))
|
|
{
|
|
remaining = remaining.TrimStart();
|
|
if (string.IsNullOrEmpty(remaining))
|
|
break;
|
|
|
|
// Check for quoted string
|
|
var match = QuotedStringRegex.Match(remaining);
|
|
if (match.Success && match.Index == 0)
|
|
{
|
|
// Extract the quoted content (without quotes)
|
|
var quoted = match.Value;
|
|
var content = quoted.Substring(1, quoted.Length - 2);
|
|
// Unescape
|
|
content = content.Replace("\\\"", "\"").Replace("\\'", "'").Replace("\\\\", "\\");
|
|
tokens.Add(content);
|
|
remaining = remaining.Substring(match.Length);
|
|
}
|
|
else
|
|
{
|
|
// Non-quoted token
|
|
var spaceIndex = remaining.IndexOfAny(new[] { ' ', '\t' });
|
|
if (spaceIndex == -1)
|
|
{
|
|
tokens.Add(remaining);
|
|
break;
|
|
}
|
|
tokens.Add(remaining.Substring(0, spaceIndex));
|
|
remaining = remaining.Substring(spaceIndex);
|
|
}
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
private ListCommand ParseReplListCommand(List<string> tokens)
|
|
{
|
|
// Extract --output flag before processing other options
|
|
var outputFormat = ExtractOutputFlag(tokens);
|
|
|
|
var cmd = new ListCommand { Output = outputFormat };
|
|
|
|
for (int i = 2; i < tokens.Count; i++)
|
|
{
|
|
var token = tokens[i].ToLower();
|
|
if (token == "--verbose" || token == "-v")
|
|
{
|
|
cmd.Verbose = true;
|
|
}
|
|
else
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
|
$"Unknown option for list: {tokens[i]}");
|
|
}
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private StepCommand ParseReplAddCommand(List<string> tokens)
|
|
{
|
|
// Extract --output flag before processing other options
|
|
var outputFormat = ExtractOutputFlag(tokens);
|
|
|
|
if (tokens.Count < 3)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Usage: steps add <run|uses> <script|action> [options]");
|
|
}
|
|
|
|
var type = tokens[2].ToLower();
|
|
|
|
StepCommand cmd;
|
|
if (type == "run")
|
|
{
|
|
cmd = ParseReplAddRunCommand(tokens);
|
|
}
|
|
else if (type == "uses")
|
|
{
|
|
cmd = ParseReplAddUsesCommand(tokens);
|
|
}
|
|
else
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.InvalidType,
|
|
$"Invalid step type: '{type}'. Must be 'run' or 'uses'.");
|
|
}
|
|
|
|
cmd.Output = outputFormat;
|
|
return cmd;
|
|
}
|
|
|
|
private AddRunCommand ParseReplAddRunCommand(List<string> tokens)
|
|
{
|
|
// steps add run "script" [options]
|
|
if (tokens.Count < 4)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Usage: steps add run \"<script>\" [--name \"...\"] [--shell <shell>] [--at|--after|--before <n>]");
|
|
}
|
|
|
|
var cmd = new AddRunCommand
|
|
{
|
|
Script = tokens[3],
|
|
Env = new Dictionary<string, string>()
|
|
};
|
|
|
|
// Parse options
|
|
for (int i = 4; i < tokens.Count; i++)
|
|
{
|
|
var opt = tokens[i].ToLower();
|
|
|
|
switch (opt)
|
|
{
|
|
case "--name":
|
|
cmd.Name = GetNextArg(tokens, ref i, "--name");
|
|
break;
|
|
case "--shell":
|
|
cmd.Shell = GetNextArg(tokens, ref i, "--shell");
|
|
break;
|
|
case "--working-directory":
|
|
cmd.WorkingDirectory = GetNextArg(tokens, ref i, "--working-directory");
|
|
break;
|
|
case "--if":
|
|
cmd.Condition = GetNextArg(tokens, ref i, "--if");
|
|
break;
|
|
case "--env":
|
|
ParseEnvArg(tokens, ref i, cmd.Env);
|
|
break;
|
|
case "--continue-on-error":
|
|
cmd.ContinueOnError = true;
|
|
break;
|
|
case "--timeout":
|
|
cmd.Timeout = GetNextArgInt(tokens, ref i, "--timeout");
|
|
break;
|
|
case "--at":
|
|
cmd.Position = StepPosition.At(GetNextArgInt(tokens, ref i, "--at"));
|
|
break;
|
|
case "--after":
|
|
cmd.Position = StepPosition.After(GetNextArgInt(tokens, ref i, "--after"));
|
|
break;
|
|
case "--before":
|
|
cmd.Position = StepPosition.Before(GetNextArgInt(tokens, ref i, "--before"));
|
|
break;
|
|
case "--first":
|
|
cmd.Position = StepPosition.First();
|
|
break;
|
|
case "--last":
|
|
cmd.Position = StepPosition.Last();
|
|
break;
|
|
default:
|
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
|
$"Unknown option: {tokens[i]}");
|
|
}
|
|
}
|
|
|
|
if (cmd.Env.Count == 0)
|
|
cmd.Env = null;
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private AddUsesCommand ParseReplAddUsesCommand(List<string> tokens)
|
|
{
|
|
// steps add uses "action@ref" [options]
|
|
if (tokens.Count < 4)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Usage: steps add uses <action@ref> [--name \"...\"] [--with key=value] [--at|--after|--before <n>]");
|
|
}
|
|
|
|
var cmd = new AddUsesCommand
|
|
{
|
|
Action = tokens[3],
|
|
With = new Dictionary<string, string>(),
|
|
Env = new Dictionary<string, string>()
|
|
};
|
|
|
|
// Parse options
|
|
for (int i = 4; i < tokens.Count; i++)
|
|
{
|
|
var opt = tokens[i].ToLower();
|
|
|
|
switch (opt)
|
|
{
|
|
case "--name":
|
|
cmd.Name = GetNextArg(tokens, ref i, "--name");
|
|
break;
|
|
case "--with":
|
|
ParseKeyValueArg(tokens, ref i, cmd.With);
|
|
break;
|
|
case "--if":
|
|
cmd.Condition = GetNextArg(tokens, ref i, "--if");
|
|
break;
|
|
case "--env":
|
|
ParseEnvArg(tokens, ref i, cmd.Env);
|
|
break;
|
|
case "--continue-on-error":
|
|
cmd.ContinueOnError = true;
|
|
break;
|
|
case "--timeout":
|
|
cmd.Timeout = GetNextArgInt(tokens, ref i, "--timeout");
|
|
break;
|
|
case "--at":
|
|
cmd.Position = StepPosition.At(GetNextArgInt(tokens, ref i, "--at"));
|
|
break;
|
|
case "--after":
|
|
cmd.Position = StepPosition.After(GetNextArgInt(tokens, ref i, "--after"));
|
|
break;
|
|
case "--before":
|
|
cmd.Position = StepPosition.Before(GetNextArgInt(tokens, ref i, "--before"));
|
|
break;
|
|
case "--first":
|
|
cmd.Position = StepPosition.First();
|
|
break;
|
|
case "--last":
|
|
cmd.Position = StepPosition.Last();
|
|
break;
|
|
default:
|
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
|
$"Unknown option: {tokens[i]}");
|
|
}
|
|
}
|
|
|
|
if (cmd.With.Count == 0)
|
|
cmd.With = null;
|
|
if (cmd.Env.Count == 0)
|
|
cmd.Env = null;
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private EditCommand ParseReplEditCommand(List<string> tokens)
|
|
{
|
|
// Extract --output flag before processing other options
|
|
var outputFormat = ExtractOutputFlag(tokens);
|
|
|
|
// steps edit <index> [modifications]
|
|
if (tokens.Count < 3)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Usage: steps edit <index> [--name \"...\"] [--script \"...\"] [--if \"...\"]");
|
|
}
|
|
|
|
if (!int.TryParse(tokens[2], out var index))
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
$"Invalid index: {tokens[2]}. Must be a number.");
|
|
}
|
|
|
|
var cmd = new EditCommand
|
|
{
|
|
Index = index,
|
|
Output = outputFormat,
|
|
With = new Dictionary<string, string>(),
|
|
Env = new Dictionary<string, string>(),
|
|
RemoveWith = new List<string>(),
|
|
RemoveEnv = new List<string>()
|
|
};
|
|
|
|
// Parse options
|
|
for (int i = 3; i < tokens.Count; i++)
|
|
{
|
|
var opt = tokens[i].ToLower();
|
|
|
|
switch (opt)
|
|
{
|
|
case "--name":
|
|
cmd.Name = GetNextArg(tokens, ref i, "--name");
|
|
break;
|
|
case "--script":
|
|
cmd.Script = GetNextArg(tokens, ref i, "--script");
|
|
break;
|
|
case "--action":
|
|
cmd.Action = GetNextArg(tokens, ref i, "--action");
|
|
break;
|
|
case "--shell":
|
|
cmd.Shell = GetNextArg(tokens, ref i, "--shell");
|
|
break;
|
|
case "--working-directory":
|
|
cmd.WorkingDirectory = GetNextArg(tokens, ref i, "--working-directory");
|
|
break;
|
|
case "--if":
|
|
cmd.Condition = GetNextArg(tokens, ref i, "--if");
|
|
break;
|
|
case "--with":
|
|
ParseKeyValueArg(tokens, ref i, cmd.With);
|
|
break;
|
|
case "--env":
|
|
ParseEnvArg(tokens, ref i, cmd.Env);
|
|
break;
|
|
case "--remove-with":
|
|
cmd.RemoveWith.Add(GetNextArg(tokens, ref i, "--remove-with"));
|
|
break;
|
|
case "--remove-env":
|
|
cmd.RemoveEnv.Add(GetNextArg(tokens, ref i, "--remove-env"));
|
|
break;
|
|
case "--continue-on-error":
|
|
cmd.ContinueOnError = true;
|
|
break;
|
|
case "--no-continue-on-error":
|
|
cmd.ContinueOnError = false;
|
|
break;
|
|
case "--timeout":
|
|
cmd.Timeout = GetNextArgInt(tokens, ref i, "--timeout");
|
|
break;
|
|
default:
|
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
|
$"Unknown option: {tokens[i]}");
|
|
}
|
|
}
|
|
|
|
// Clean up empty collections
|
|
if (cmd.With.Count == 0)
|
|
cmd.With = null;
|
|
if (cmd.Env.Count == 0)
|
|
cmd.Env = null;
|
|
if (cmd.RemoveWith.Count == 0)
|
|
cmd.RemoveWith = null;
|
|
if (cmd.RemoveEnv.Count == 0)
|
|
cmd.RemoveEnv = null;
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private RemoveCommand ParseReplRemoveCommand(List<string> tokens)
|
|
{
|
|
// Extract --output flag before processing other options
|
|
var outputFormat = ExtractOutputFlag(tokens);
|
|
|
|
// steps remove <index>
|
|
if (tokens.Count < 3)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Usage: steps remove <index>");
|
|
}
|
|
|
|
if (!int.TryParse(tokens[2], out var index))
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
$"Invalid index: {tokens[2]}. Must be a number.");
|
|
}
|
|
|
|
return new RemoveCommand { Index = index, Output = outputFormat };
|
|
}
|
|
|
|
private MoveCommand ParseReplMoveCommand(List<string> tokens)
|
|
{
|
|
// Extract --output flag before processing other options
|
|
var outputFormat = ExtractOutputFlag(tokens);
|
|
|
|
// steps move <from> --to|--after|--before <index>|--first|--last
|
|
if (tokens.Count < 3)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Usage: steps move <from> --to|--after|--before <index>|--first|--last");
|
|
}
|
|
|
|
if (!int.TryParse(tokens[2], out var fromIndex))
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
$"Invalid from index: {tokens[2]}. Must be a number.");
|
|
}
|
|
|
|
var cmd = new MoveCommand { FromIndex = fromIndex, Output = outputFormat };
|
|
|
|
// Parse position
|
|
for (int i = 3; i < tokens.Count; i++)
|
|
{
|
|
var opt = tokens[i].ToLower();
|
|
|
|
switch (opt)
|
|
{
|
|
case "--to":
|
|
case "--at":
|
|
cmd.Position = StepPosition.At(GetNextArgInt(tokens, ref i, opt));
|
|
break;
|
|
case "--after":
|
|
cmd.Position = StepPosition.After(GetNextArgInt(tokens, ref i, "--after"));
|
|
break;
|
|
case "--before":
|
|
cmd.Position = StepPosition.Before(GetNextArgInt(tokens, ref i, "--before"));
|
|
break;
|
|
case "--first":
|
|
cmd.Position = StepPosition.First();
|
|
break;
|
|
case "--last":
|
|
cmd.Position = StepPosition.Last();
|
|
break;
|
|
default:
|
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
|
$"Unknown option: {tokens[i]}");
|
|
}
|
|
}
|
|
|
|
if (cmd.Position == null)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
"Move command requires a position (--to, --after, --before, --first, or --last)");
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private ExportCommand ParseReplExportCommand(List<string> tokens)
|
|
{
|
|
// Extract --output flag before processing other options
|
|
var outputFormat = ExtractOutputFlag(tokens);
|
|
|
|
var cmd = new ExportCommand { Output = outputFormat };
|
|
|
|
for (int i = 2; i < tokens.Count; i++)
|
|
{
|
|
var opt = tokens[i].ToLower();
|
|
|
|
switch (opt)
|
|
{
|
|
case "--changes-only":
|
|
cmd.ChangesOnly = true;
|
|
break;
|
|
case "--with-comments":
|
|
cmd.WithComments = true;
|
|
break;
|
|
default:
|
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
|
$"Unknown option: {tokens[i]}");
|
|
}
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
#region Argument Helpers
|
|
|
|
/// <summary>
|
|
/// Extracts and removes the --output flag from tokens, returning the output format.
|
|
/// Supports: --output json, --output text, -o json, -o text, --output=json, --output=text
|
|
/// </summary>
|
|
private OutputFormat ExtractOutputFlag(List<string> tokens)
|
|
{
|
|
for (int i = 0; i < tokens.Count; i++)
|
|
{
|
|
var token = tokens[i].ToLower();
|
|
|
|
if (token == "--output" || token == "-o")
|
|
{
|
|
if (i + 1 < tokens.Count)
|
|
{
|
|
var format = tokens[i + 1].ToLower();
|
|
tokens.RemoveAt(i); // Remove flag
|
|
tokens.RemoveAt(i); // Remove value (now at same index)
|
|
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
|
|
}
|
|
}
|
|
else if (token.StartsWith("--output="))
|
|
{
|
|
var format = token.Substring("--output=".Length);
|
|
tokens.RemoveAt(i);
|
|
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
|
|
}
|
|
}
|
|
return OutputFormat.Text;
|
|
}
|
|
|
|
private string GetNextArg(List<string> tokens, ref int index, string optName)
|
|
{
|
|
if (index + 1 >= tokens.Count)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
$"Option {optName} requires a value");
|
|
}
|
|
return tokens[++index];
|
|
}
|
|
|
|
private int GetNextArgInt(List<string> tokens, ref int index, string optName)
|
|
{
|
|
var value = GetNextArg(tokens, ref index, optName);
|
|
if (!int.TryParse(value, out var result))
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
$"Option {optName} requires an integer value, got: {value}");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private void ParseEnvArg(List<string> tokens, ref int index, Dictionary<string, string> env)
|
|
{
|
|
ParseKeyValueArg(tokens, ref index, env);
|
|
}
|
|
|
|
private void ParseKeyValueArg(List<string> tokens, ref int index, Dictionary<string, string> dict)
|
|
{
|
|
var value = GetNextArg(tokens, ref index, "key=value");
|
|
var eqIndex = value.IndexOf('=');
|
|
if (eqIndex <= 0)
|
|
{
|
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
|
$"Expected key=value format, got: {value}");
|
|
}
|
|
var key = value.Substring(0, eqIndex);
|
|
var val = value.Substring(eqIndex + 1);
|
|
dict[key] = val;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
}
|
|
}
|