using System.Collections.Generic; using System.Linq; using System.Text; namespace Semmle.Autobuild { /// /// Utility to construct a build command. /// class CommandBuilder { enum EscapeMode { Process, Cmd }; readonly StringBuilder arguments; bool firstCommand; string executable; readonly EscapeMode escapingMode; readonly string workingDirectory; readonly IDictionary environment; /// /// Initializes a new instance of the class. /// /// The working directory (null for current directory). /// Additional environment variables. public CommandBuilder(IBuildActions actions, string workingDirectory = null, IDictionary environment = null) { arguments = new StringBuilder(); if (actions.IsWindows()) { executable = "cmd.exe"; arguments.Append("/C"); escapingMode = EscapeMode.Cmd; } else { escapingMode = EscapeMode.Process; } firstCommand = true; this.workingDirectory = workingDirectory; this.environment = environment; } void OdasaIndex(string odasa) { RunCommand(odasa, "index --auto"); } public CommandBuilder CallBatFile(string batFile, string argumentsOpt = null) { NextCommand(); arguments.Append(" CALL"); QuoteArgument(batFile); Argument(argumentsOpt); return this; } /// /// Perform odasa index on a given command or BAT file. /// /// The odasa executable. /// The command to run. /// Additional arguments. /// this for chaining calls. public CommandBuilder IndexCommand(string odasa, string command, string argumentsOpt = null) { OdasaIndex(odasa); QuoteArgument(command); Argument(argumentsOpt); return this; } static readonly char[] specialChars = { ' ', '\t', '\n', '\v', '\"' }; static readonly char[] cmdMetacharacter = { '(', ')', '%', '!', '^', '\"', '<', '>', '&', '|' }; /// /// Appends the given argument to the command line. /// /// The argument to append. /// Whether to always quote the argument. /// Whether to escape for cmd.exe /// /// /// This implementation is copied from /// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ /// void ArgvQuote(string argument, bool force) { bool cmd = escapingMode == EscapeMode.Cmd; if (!force && !string.IsNullOrEmpty(argument) && argument.IndexOfAny(specialChars) == -1) { arguments.Append(argument); } else { if (cmd) arguments.Append('^'); arguments.Append('\"'); for (int it = 0; ; ++it) { var numBackslashes = 0; while (it != argument.Length && argument[it] == '\\') { ++it; ++numBackslashes; } if (it == argument.Length) { arguments.Append('\\', numBackslashes * 2); break; } else if (argument[it] == '\"') { arguments.Append('\\', numBackslashes * 2 + 1); if (cmd) arguments.Append('^'); arguments.Append(arguments[it]); } else { arguments.Append('\\', numBackslashes); if (cmd && cmdMetacharacter.Any(c => c == argument[it])) arguments.Append('^'); arguments.Append(argument[it]); } } if (cmd) arguments.Append('^'); arguments.Append('\"'); } } public CommandBuilder QuoteArgument(string argumentsOpt) { if (argumentsOpt != null) { NextArgument(); ArgvQuote(argumentsOpt, false); } return this; } void NextArgument() { if (arguments.Length > 0) arguments.Append(' '); } public CommandBuilder Argument(string argumentsOpt) { if (argumentsOpt != null) { NextArgument(); arguments.Append(argumentsOpt); } return this; } void NextCommand() { if (firstCommand) firstCommand = false; else arguments.Append(" &&"); } public CommandBuilder RunCommand(string exe, string argumentsOpt = null) { var (exe0, arg0) = escapingMode == EscapeMode.Process && exe.EndsWith(".exe", System.StringComparison.Ordinal) ? ("mono", exe) // Linux : (exe, null); NextCommand(); if (executable == null) { executable = exe0; } else { QuoteArgument(exe0); } Argument(arg0); Argument(argumentsOpt); return this; } /// /// Returns a build script that contains just this command. /// public BuildScript Script => BuildScript.Create(executable, arguments.ToString(), workingDirectory, environment); } }