blob: 343e1f2a2896ec03a7c72ae833594391b7ff92fd [file] [log] [blame]
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace Google.ProtocolBuffers.ProtoGen
{
/// <summary>
/// Preprocesses any input files with an extension of '.proto' by running protoc.exe. If arguments
/// are supplied with '--' prefix they are provided to protoc.exe, otherwise they are assumed to
/// be used for ProtoGen.exe which is run on the resulting output proto buffer. If the option
/// --descriptor_set_out= is specified the proto buffer file is kept, otherwise it will be removed
/// after code generation.
/// </summary>
public class ProgramPreprocess
{
private const string ProtocExecutable = "protoc.exe";
private const string ProtocDirectoryArg = "--protoc_dir=";
private static int Main(string[] args)
{
try
{
return Environment.ExitCode = Run(args);
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return Environment.ExitCode = 2;
}
}
public static int Run(params string[] args)
{
bool deleteFile = false;
string tempFile = null;
int result;
bool doHelp = args.Length == 0;
try
{
List<string> protocArgs = new List<string>();
List<string> protoGenArgs = new List<string>();
string protocFile = GuessProtocFile(args);
foreach (string arg in args)
{
doHelp |= StringComparer.OrdinalIgnoreCase.Equals(arg, "/?");
doHelp |= StringComparer.OrdinalIgnoreCase.Equals(arg, "/help");
doHelp |= StringComparer.OrdinalIgnoreCase.Equals(arg, "-?");
doHelp |= StringComparer.OrdinalIgnoreCase.Equals(arg, "-help");
if (arg.StartsWith("--descriptor_set_out="))
{
tempFile = arg.Substring("--descriptor_set_out=".Length);
protoGenArgs.Add(tempFile);
}
}
if (doHelp)
{
Console.WriteLine();
Console.WriteLine("PROTOC.exe: Use any of the following options that begin with '--':");
Console.WriteLine();
try
{
RunProtoc(protocFile, "--help");
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
}
Console.WriteLine();
Console.WriteLine();
Console.WriteLine(
"PROTOGEN.exe: The following options are used to specify defaults for code generation.");
Console.WriteLine();
Program.Main(new string[0]);
Console.WriteLine();
Console.WriteLine("The following option enables PROTOGEN.exe to find PROTOC.exe");
Console.WriteLine("{0}<directory containing protoc.exe>", ProtocDirectoryArg);
return 0;
}
string pathRoot = Environment.CurrentDirectory;
foreach(string arg in args)
{
if (arg.StartsWith("--proto_path=", StringComparison.InvariantCultureIgnoreCase))
{
pathRoot = arg.Substring(13);
}
}
foreach (string arg in args)
{
if (arg.StartsWith(ProtocDirectoryArg))
{
// Handled earlier
continue;
}
if (arg.StartsWith("--"))
{
protocArgs.Add(arg);
}
else if ((File.Exists(arg) || File.Exists(Path.Combine(pathRoot, arg))) &&
StringComparer.OrdinalIgnoreCase.Equals(".proto", Path.GetExtension(arg)))
{
if (tempFile == null)
{
deleteFile = true;
tempFile = Path.GetTempFileName();
protocArgs.Add(String.Format("--descriptor_set_out={0}", tempFile));
protoGenArgs.Add(tempFile);
}
string patharg = arg;
if (!File.Exists(patharg))
{
patharg = Path.Combine(pathRoot, arg);
}
protocArgs.Add(patharg);
}
else
{
protoGenArgs.Add(arg);
}
}
if (tempFile != null)
{
result = RunProtoc(protocFile, protocArgs.ToArray());
if (result != 0)
{
return result;
}
}
result = Program.Main(protoGenArgs.ToArray());
}
finally
{
if (deleteFile && tempFile != null && File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
return result;
}
/// <summary>
/// Tries to work out where protoc is based on command line arguments, the current
/// directory, the directory containing protogen, and the path.
/// </summary>
/// <returns>The path to protoc.exe, or null if it can't be found.</returns>
private static string GuessProtocFile(params string[] args)
{
// Why oh why is this not in System.IO.Path or Environment...?
List<string> searchPath = new List<string>();
foreach (string arg in args)
{
if (arg.StartsWith("--protoc_dir="))
{
searchPath.Add(arg.Substring(ProtocDirectoryArg.Length));
}
}
searchPath.Add(Environment.CurrentDirectory);
searchPath.Add(AppDomain.CurrentDomain.BaseDirectory);
searchPath.AddRange((Environment.GetEnvironmentVariable("PATH") ?? String.Empty).Split(Path.PathSeparator));
foreach (string path in searchPath)
{
string exeFile = Path.Combine(path, ProtocExecutable);
if (File.Exists(exeFile))
{
return exeFile;
}
}
return null;
}
private static int RunProtoc(string exeFile, params string[] args)
{
if (exeFile == null)
{
throw new FileNotFoundException(
"Unable to locate " + ProtocExecutable +
" make sure it is in the PATH, cwd, or exe dir, or use --protoc_dir=...");
}
ProcessStartInfo psi = new ProcessStartInfo(exeFile);
psi.Arguments = EscapeArguments(args);
psi.RedirectStandardError = true;
psi.RedirectStandardInput = false;
psi.RedirectStandardOutput = true;
psi.ErrorDialog = false;
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
psi.WorkingDirectory = Environment.CurrentDirectory;
Process process = Process.Start(psi);
if (process == null)
{
return 1;
}
process.WaitForExit();
string tmp = process.StandardOutput.ReadToEnd();
if (tmp.Trim().Length > 0)
{
Console.Out.WriteLine(tmp);
}
tmp = process.StandardError.ReadToEnd();
if (tmp.Trim().Length > 0)
{
// Replace protoc output with something more amenable to Visual Studio.
var regexMsvs = new Regex(@"(.*)\((\d+)\).* column=(\d+)\s*:\s*(.*)");
tmp = regexMsvs.Replace(tmp, "$1($2,$3): error CS9999: $4");
var regexGcc = new Regex(@"(.*):(\d+):(\d+):\s*(.*)");
tmp = regexGcc.Replace(tmp, "$1($2,$3): error CS9999: $4");
Console.Error.WriteLine(tmp);
}
return process.ExitCode;
}
/// <summary>
/// Quotes all arguments that contain whitespace, or begin with a quote and returns a single
/// argument string for use with Process.Start().
/// </summary>
/// <remarks>http://csharptest.net/?p=529</remarks>
/// <param name="args">A list of strings for arguments, may not contain null, '\0', '\r', or '\n'</param>
/// <returns>The combined list of escaped/quoted strings</returns>
/// <exception cref="System.ArgumentNullException">Raised when one of the arguments is null</exception>
/// <exception cref="System.ArgumentOutOfRangeException">Raised if an argument contains '\0', '\r', or '\n'</exception>
public static string EscapeArguments(params string[] args)
{
StringBuilder arguments = new StringBuilder();
Regex invalidChar = new Regex("[\x00\x0a\x0d]");// these can not be escaped
Regex needsQuotes = new Regex(@"\s|""");// contains whitespace or two quote characters
Regex escapeQuote = new Regex(@"(\\*)(""|$)");// one or more '\' followed with a quote or end of string
for (int carg = 0; args != null && carg < args.Length; carg++)
{
if (args[carg] == null)
{
throw new ArgumentNullException("args[" + carg + "]");
}
if (invalidChar.IsMatch(args[carg]))
{
throw new ArgumentOutOfRangeException("args[" + carg + "]");
}
if (args[carg] == String.Empty)
{
arguments.Append("\"\"");
}
else if (!needsQuotes.IsMatch(args[carg])) { arguments.Append(args[carg]); }
else
{
arguments.Append('"');
arguments.Append(escapeQuote.Replace(args[carg],
m =>
m.Groups[1].Value + m.Groups[1].Value +
(m.Groups[2].Value == "\"" ? "\\\"" : "")
));
arguments.Append('"');
}
if (carg + 1 < args.Length)
{
arguments.Append(' ');
}
}
return arguments.ToString();
}
}
}