substituteCLIArguments method
Substitutes CLI command arguments in a string with their execution results.
This function finds patterns like %{{COMMAND}} or %{COMMAND} and replaces them
with the output of executing the command.
Parameters:
input- The input string containing command placeholders (can be null)
Returns the string with command placeholders replaced by their standard output. Throws VariableException when a command cannot be run or exits non-zero: substituting the empty string or the command's stderr silently changes what gets built and still reports success.
Implementation
Future<String> substituteCLIArguments(String? input) async {
if (input == null) return "";
// Pattern matches %{{COMMAND}} OR %{COMMAND}
final pattern = RegExp(r'\%\{\{([^\}]+)\}\}|\%\{([^\}]+)\}');
// Use replaceAllMapped to handle each match individually and asynchronously
final matches = pattern.allMatches(input).toList();
if (matches.isEmpty) return input;
// Since replaceAllMapped cannot be async, we process matches manually
String result = input;
for (final match in matches.reversed) {
final value = match.group(1) ?? match.group(2) ?? "";
String processResults;
if (value.trim().isEmpty) {
processResults = "";
} else {
final args = value.contains(" ")
? _parseCommandArguments(value)
: <String>[value];
final ProcessResult process;
try {
process = await Process.run(args.first, args.sublist(1));
} on ProcessException catch (e) {
// A missing binary used to substitute an empty string, so
// `build-name: "%{{git describe}}"` on a machine without git
// produced `--build-name=` and a green run.
throw VariableException(
"`%{{$value}}` could not be run: ${e.message}",
);
}
if (process.exitCode != 0) {
// The old behaviour substituted stderr into the value. Since stderr
// contains spaces it split into extra arguments on the flutter
// command line, producing a build nobody asked for that still
// reported success.
final detail = process.stderr.toString().trim();
throw VariableException(
"`%{{$value}}` exited with ${process.exitCode}"
"${detail.isEmpty ? '' : ': $detail'}",
);
}
processResults = process.stdout.toString().trim();
}
// Replace only the current match
result = result.replaceRange(match.start, match.end, processResults);
}
return result;
}