Paul Selles
Computers and cats
Running Command Line Executables in C#
June 4, 2014
Posted by on The Problem
A co-worker wanted to build a tool in C# which involves running a command line executable. Since an API is not available to them, they ask for a quick way to launch a command line executable with the option of parsing through the standard output.
The Solution
My solution for them that will allow them to:
- Launch a command line executable with arguments
- Have the option to retrieve Standard Output
- Have the option to retrieve Standard Error
- Have the option to throw exception on Standard Error
- Forward any exceptions (ex: executable does not exist)
The solution is a single static function that uses the System.Diagnostic.Process class to perform the execution [1]. The executable name and the arguments are fed into the Process.StartInfo property as a System.Diagnostic.ProcessStartInfo object [2][3]. We want to set the Process.StartInfo.UseShellExecute to false since we want the option to retrieve both standard output and standard error [4]. Standard output and standard error can be redirected with Process.StartInfo.RedirectStandardOutput and Process.StartInfo.RedirectStandardOutput respectively, which we will turn on or off with flags[5][6]. Finally, in the event that we are reading the Process.StandardOutput or Process.StandardOutput, we need to invoke the method Process.WaitForExit [6][7][8].
The Implementation
public static class CommandLine { /// <summary> /// Run console command in C# /// </summary> /// /// /// /// /// /// public static string Execute(string executable, string arguments, bool standardOutput = false, bool standardError = false, bool throwOnError = false) { // This will be out return string string standardOutputString = string.Empty; string standardErrorString = string.Empty; // Use process Process process; try { // Setup our process with the executable and it's arguments process = new Process(); process.StartInfo = new ProcessStartInfo(executable, arguments); // To get IO streams set use shell to false process.StartInfo.UseShellExecute = false; // If we want to return the output then redirect standard output if (standardOutput) process.StartInfo.RedirectStandardOutput = true; // If we std error or to throw on error then redirect error if (standardError || throwOnError) process.StartInfo.RedirectStandardError = true; // Run the process process.Start(); // Get the standard error if (standardError || throwOnError) standardErrorString = process.StandardError.ReadToEnd(); // If we want to throw on error and there is an error if (throwOnError && !string.IsNullOrEmpty(standardErrorString)) throw new Exception( string.Format("Error in ConsoleCommand while executing {0} with arguments {1}.", executable, arguments, Environment.NewLine, standardErrorString)); // If we want to return the output then get it if (standardOutput) standardOutputString = process.StandardOutput.ReadToEnd(); // If we want standard error then append it to our output string if (standardError) standardOutputString += standardErrorString; // Wait for the process to finish process.WaitForExit(); } catch (Exception e) { // Encapsulate and throw throw new Exception( string.Format("Error in ConsoleCommand while executing {0} with arguments {1}.", executable, arguments), e); } // Return the output string return standardOutputString; } }
The Example
As an example here are the unit tests all centered around git.exe:
[TestClass] public class CommandLineTests { private const string GIT_PATH = @"C:\Program Files (x86)\Git\bin\git.exe"; private const string GIT_VALID = "git version 1.9.2.msysgit.0"; private const string GIT_USAGE = "usage: git [--version] [--help] [-C ] [-c name=value]"; private const string GIT_ERROR = "git: 'kitten' is not a git command. See 'git --help'."; private const string GIT_ERRONIOUS_ARG = "kitten"; private const string GIT_VALID_ARG = "version"; [TestMethod] public void GitNoAgurments() { /// Act: Call command with no argument string value = CommandLine.Execute(GIT_PATH, ""); /// Assert: Output and error off expect empty Assert.AreEqual(string.Empty, value); } [TestMethod] public void GitValidArgument() { /// Arrange: Our argument string argument = GIT_VALID_ARG; /// Act: Call command with argument string value = CommandLine.Execute(GIT_PATH, argument); /// Assert: Output and error off expect empty Assert.AreEqual(string.Empty, value); } [TestMethod] public void GitErroniousArgument() { /// Arrange: Our argument string argument = GIT_ERRONIOUS_ARG; /// Act: Call command with argument string value = CommandLine.Execute(GIT_PATH, argument); /// Assert: Output and error off expect empty Assert.AreEqual(string.Empty, value); } [TestMethod] public void GitNoAgurmentsWithStandardOutputOnly() { /// Act: Call command with no argument standard output on string value = CommandLine.Execute(GIT_PATH, "", true); /// Assert: Output expected StringAssert.StartsWith(value, GIT_USAGE); } [TestMethod] public void GitValidArgumentWithStandardOutputOnly() { /// Arrange: Our argument string argument = GIT_VALID_ARG; /// Act: Call command with argument standard output on string value = CommandLine.Execute(GIT_PATH, argument, true); /// Assert: Error expect output expected to be empty StringAssert.StartsWith(value, GIT_VALID); } [TestMethod] public void GitErroniousArgumentWithStandardOutputOnly() { /// Arrange: Our argument string argument = GIT_ERRONIOUS_ARG; /// Act: Call command with argument standard output on string value = CommandLine.Execute(GIT_PATH, argument, true); /// Assert: Error expect output expected to be empty Assert.AreEqual(string.Empty, value); } [TestMethod] public void GitNoAgurmentsWithStandardErrorOnly() { /// Act: Call command no with argument standard error on string value = CommandLine.Execute(GIT_PATH, "", standardError: true); /// Assert: No error expect empty Assert.AreEqual(string.Empty, value); } [TestMethod] public void GitValidArgumentWithStandardErrorOnly() { /// Arrange: Our argument string argument = GIT_VALID_ARG; /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH, argument, standardError: true); /// Assert: Expect error expect not empty Assert.AreEqual(string.Empty, value); } [TestMethod] public void GitErroniousArgumentWithStandardErrorOnly() { /// Arrange: Our argument string argument = GIT_ERRONIOUS_ARG; /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH, argument, standardError: true); /// Assert: Expect error expect not empty StringAssert.StartsWith(value, GIT_ERROR); } [TestMethod] public void GitNoAgurmentsWithStandardOutputAndStandardError() { /// Act: Call command no with argument standard error on string value = CommandLine.Execute(GIT_PATH, "", true, true); /// Assert: No error expect empty StringAssert.StartsWith(value, GIT_USAGE); } [TestMethod] public void GitValidArgumentWithStandardOutputAndStandardError() { /// Arrange: Our argument string argument = GIT_VALID_ARG; /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH, argument, true, true); /// Assert: Expect error expect not empty StringAssert.StartsWith(value, GIT_VALID); } [TestMethod] public void GitErroniousArgumentWithStandardOutputAndStandardError() { /// Arrange: Our argument string argument = GIT_ERRONIOUS_ARG; /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH, argument, true, true); /// Assert: Expect error expect not empty StringAssert.StartsWith(value, GIT_ERROR); } [TestMethod] public void GitNoAgurmentsWithThrowOnError() { /// Act: Call command no with argument standard error on string value = CommandLine.Execute(GIT_PATH, "", throwOnError: true); /// Assert: No error expect empty Assert.AreEqual(string.Empty, value); } [TestMethod] public void GitValidArgumentWithThrowOnError() { /// Arrange: Our argument string argument = GIT_VALID_ARG; /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH, argument, throwOnError: true); /// Assert: No error expect empty Assert.AreEqual(string.Empty, value); } [TestMethod] [ExpectedException(typeof(Exception))] public void GitErroniousArgumentWithThrowOnError() { /// Arrange: Our argument string argument = GIT_ERRONIOUS_ARG; /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH, argument, throwOnError: true); } [TestMethod] public void GitNoAgurmentsWithStandardOutputAndStandardErrorAndThrowOnError() { /// Act: Call command no with argument standard error on string value = CommandLine.Execute(GIT_PATH, "", true, true, true); /// Assert: No error expect empty StringAssert.StartsWith(value, GIT_USAGE); } [TestMethod] public void GitValidArgumentWithStandardOutputAndStandardErrorAndThrowOnError() { /// Arrange: Our argument string argument = GIT_VALID_ARG; /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH, argument, true, true, true); /// Assert: No error expect empty StringAssert.StartsWith(value, GIT_VALID); } [TestMethod] [ExpectedException(typeof(Exception))] public void GitErroniousArgumentWithStandardOutputAndStandardErrorAndThrowOnError() { /// Arrange: Our argument string argument = GIT_ERRONIOUS_ARG; /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH, argument, true, true, true); } [TestMethod] [ExpectedException(typeof(Exception))] public void ExecutableNameExeption() { /// Act: Call command with argument standard error on string value = CommandLine.Execute(GIT_PATH + "a", ""); } }
Enjoy
References
[1] Process Class. MSDN Libarary.
[2] Process.StartInfo Property. MSDN Libarary.
[3] ProcessStartInfo Class. MSDN Libarary.
[4] ProcessStartInfo.UseShellExecute Property. MSDN Libarary.
[5] ProcessStartInfo.RedirectStandardOutput Property. MSDN Libarary.
[6] ProcessStartInfo.RedirectStandardError Property. MSDN Libarary.
[7] Process.StandardOutput Property. MSDN Libarary.
[8] Process.StandardError Property. MSDN Libarary.
[9] Process.WaitForExit Method. MSDN Libarary.
Paul
XAML Formatting: Programmatically Tabify XAML and XML Files in C#
April 2, 2014
Posted by on With many developers working together, formatting styles can sometimes be an issue. This is most noticeable when working with XAML files, and the biggest culprit for formatting issues is poor tabbing and inconsistent tab characters. Luckily there is an easy way to standardize XAML formatting through that implantation of a custom check-in policy that can programmatically fix all XAML files prior to being checked in.
Using XmlWriter in conjunction with the XmlWriterSettings class we can easily customize our output [1][2]. The XmlWriterSettings properties give us a lot of control as to how we want the XML or XAML to look and our settings are documented and are worth looking over. I am also using XmlReader and XmlReaderSettings to convert the text into an XmlDocument, were the XmlReaderSettings class is used to ensure that we ignore any potentially invalid XML characters in our XAML [3][4][5][6]. I encountered one tricky bit were the XmlReader will break on decimal and hex character reference, this was solved by doing a text replace on all “&” with “&” pre-XmlReader and converting back post-XmlWriter.
public static class Tabify { // Tabify XML document public static void Xml(string filename) { DoTabify(filename, false); } // Tabify Xaml document public static void Xaml(string filename) { DoTabify(filename, true); } // Tabify private static void DoTabify(string filename, bool xaml=false) { // XmlDocument container XmlDocument xmlDocument = new XmlDocument(); // We want to make sure that decimal and hex character references are not lost string xmlString = File.ReadAllText(filename); xmlString = xmlString.Replace("&", "&"); // Xml Reader settings XmlReaderSettings xmlReadSettings = new XmlReaderSettings() { CheckCharacters = false, // We have some invalid characters we want to ignore }; // Use XML reader to load content to XmlDocument container using (XmlReader xmReader = XmlReader.Create(new StringReader(xmlString), xmlReadSettings)) { xmReader.MoveToContent(); xmlDocument.Load(xmReader); } // Customize how our XML will look, we want tabs, UTF8 encoding and new line on attributes XmlWriterSettings xmlWriterSettings = new XmlWriterSettings() { Indent = true, // Indent elements IndentChars = "\t", // Indent with tabs CheckCharacters = false, // Ignore invalid characters NewLineChars = Environment.NewLine, // Set newline character NewLineHandling = NewLineHandling.None, // Normalize line breaks Encoding = new UTF8Encoding() // UTF8 encoding }; // We do not want the xml declaration for xaml files if (xaml) xmlWriterSettings.OmitXmlDeclaration = true; // For XAML this must be false!!!! StringBuilder xmlStringBuilder = new StringBuilder(); // Write xml to file using saved settings using (XmlWriter xmlWriter = XmlWriter.Create(xmlStringBuilder, xmlWriterSettings)) { xmlWriter.Flush(); xmlDocument.WriteContentTo(xmlWriter); } // Restore decimal and hex character references xmlString = xmlStringBuilder.ToString().Replace("&", "&"); File.WriteAllText(filename, xmlString); } }
Paul
References
[1] XmlWriter Class. MSDN Library.
[2] XmlWriterSettings Class. MSDN Library.
[3] XmlReader ClassXmlReader Class. MSDN Library.
[4] XmlReaderSettings Class. MSDN Library.
[5] XmlDocument Class. MSDN Library.
[6] Parsing Xml with Invalid Characters in C#. Paul Selles
TFS API: TFS User Email Address Lookup and Reverse Lookup
March 24, 2014
Posted by on Occasionally I need to develop a tool that requires sending and receiving emails to and from developers. To do this I will need to lookup email addresses based on a TFS user’s display or account name and vice versa. Lucky for us this is painfully easy to do using IIdentityManagementService.ReadIdentity and the TeamFoundationIdentity class [1][2]:
private static readonly string TeamProjectUriString = "http://tfs.yourtfsurl.com"; // Team Project Collection getter private static TfsTeamProjectCollection _tfsTeamProjectCollection; public static TfsTeamProjectCollection TfsTeamProjectCollection { get { return _tfsTeamProjectCollection ?? (_tfsTeamProjectCollection = new TfsTeamProjectCollection(new Uri(TeamProjectUriString))); } } // Identity Management Service getter private static IIdentityManagementService _identityManagementService; public static IIdentityManagementService IdentityManagementService { get { return _identityManagementService ?? (_identityManagementService = TfsTeamProjectCollection.GetService<IIdentityManagementService>()); } } // Get Email Address from TFS Account or Display Name public static string GetEmailAddress(string userName) { TeamFoundationIdentity teamFoundationIdentity = IdentityManagementService.ReadIdentity( IdentitySearchFactor.AccountName | IdentitySearchFactor.DisplayName, userName, MembershipQuery.None, ReadIdentityOptions.None); return teamFoundationIdentity.GetAttribute("Mail", null); } // Get TFS Display Name from and Email Address public static string GetDisplayName(string emailAddress) { TeamFoundationIdentity teamFoundationIdentity = IdentityManagementService.ReadIdentity( IdentitySearchFactor.MailAddress, emailAddress, MembershipQuery.None, ReadIdentityOptions.None); return teamFoundationIdentity.DisplayName; }
References
[1] IIdentityManagementService.ReadIdentity Method. MSDN
[2] TeamFoundationIdentity Class. MSDN
VS2013: Opening Build In Browser
March 6, 2014
Posted by on Task Parallelism: Passing Values into a Tasks
February 12, 2014
Posted by on One road block that I stubble upon when starting to work with Task Parallelism is passing values into my Tasks. Here is a quick guide to help get you head around this issue.
We can easily create a single task that takes in non-changing values within a function and run it getting expected results:
int i = 0; int j = 1; int k = 2; Task task = Task.Factory.StartNew(() => { Console.WriteLine(string.Format("i='{1}'{0}j='{2}'{0}k='{3}'{0}{0}", Environment.NewLine, i, j, k)); }); task.Wait(); Console.ReadLine();
Results:
i=’0′
j=’1′
k=’2′
Once the the values start changing then the Tasks will run against the latest values rather than the original value when the Task was created. The values are not fixed within the scope of the Task itself but rather in the scope of the parent function, so the variables do not preserve the value from when the Task was created:
int i = 0; int j = 1; int k = 2; Task task = Task.Factory.StartNew(() => { Console.WriteLine(string.Format("i='{1}'{0}j='{2}'{0}k='{3}'{0}{0}", Environment.NewLine, i, j, k)); }); i = -1; j = -1; k = -1; task.Wait(); Console.ReadLine();
Results:
i=’2′
j=’2′
k=’2′i=’2′
j=’2′
k=’2′i=’2′
j=’2′
k=’2′i=’2′
j=’2′
k=’2′i=’2′
j=’2′
k=’2′i=’2′
j=’2′
k=’2′i=’2′
j=’2′
k=’2′i=’2′
j=’2′
k=’2′
What happened here is that all the loops completed before any of the tasks had a chance to run (all the loops incrementing incremented to 2 before failing the for condition).
We can get around the problem a couple of ways, first we can reassign the values to an unchanging variable that shares the same scope as our Task. This will ensure that when ours Tasks are created the values that they use will be assigned uniquely for that instance. If we choose this route then our code will look like this:
List<Task> tasks = new List<Task>(); for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { for (int k = 0; k < 2; k++) { int I = i; int J = j; int K = k; tasks.Add(Task.Factory.StartNew(() => { Console.WriteLine(string.Format("i='{1}'{0}j='{2}'{0}k='{3}'{0}{0}", Environment.NewLine, I, K, J)); })); } } }
Results:
i=’0′
j=’0′
k=’0′i=’0′
j=’1′
k=’0′i=’0′
j=’1′
k=’1′i=’1′
j=’0′
k=’0′i=’1′
j=’1′
k=’0′i=’1′
j=’0′
k=’1′i=’0′
j=’0′
k=’1′i=’1′
j=’1′
k=’1′
Though the Tasks may not have completed in order, all the tasks are present and all the values have been accounted for.
Finally, my favourite solution, passing all the variables as an Anonymous Type to the Task [1]. Within the Task the object will have to be casted to a dynamic type to access all the Anonymous Type’s members [2]. The reason why I like this solution is that it lends itself well to working with more complicated data types without having to reassign a large number of variables locally. For our above example our solution will look like this:
List<Task> tasks = new List<Task>(); for (int i = 0; i < 2; i++) { for (int j = 0; j < 2; j++) { for (int k = 0; k < 2; k++) { tasks.Add(Task.Factory.StartNew((Object obj) => { var data = (dynamic)obj; Console.WriteLine(string.Format("i='{1}'{0}j='{2}'{0}k='{3}'{0}{0}", Environment.NewLine, data.i, data.k, data.j)); }, new { i = i, j = j, k = k })); } } } Task.WaitAll(tasks.ToArray()); Console.ReadLine();
And the results will be similar to those above.
Paul
References
[1] Anonymous Types (C# Programming Guide). MSDN Library.
[2] Using Type dynamic (C# Programming Guide). MSDN Library.
Recent Comments