How to get command prompt output - c#

I'm writing a c# application for validating the detailed information about no.of lines changes in SVN commit. After providing the below arguments in command prompt, it displays the revision number, author name and last changed date etc...
Argument:
svn info –r {revision no} {Source path}
Eg - svn info -r 113653 "F:\SVN"
I have to achieve the same in C# also. While giving the above arguments in C#, it should read the output(revision number, author name and last changed date) from the command prompt and store it in a string. I have tried the StandardOutput.ReadToEnd() but couldn't meet my requirement. Any detailed explanation will be helpful.

Have you tried just running the command from a command prompt with C# as explained in this question?
string strCmdText = #"/C svn info -r 113653 ""F:\SVN""";
System.Diagnostics.Process.Start("CMD.exe",strCmdText);

You can use the following method to run a command and retrieve the standard output from the console :
public static string StdOut(string args)
{
string cmdOut = "";
ProcessStartInfo startInfo = new ProcessStartInfo("cmd", "/C " + args)
{
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
cmdOut = ExecuteCommand(cmdOut, startInfo);
return cmdOut;
}
It will return the output as a string.
You will also need this method (as it is used in the above):
private static string ExecuteCommand(string cmdOut, ProcessStartInfo startInfo)
{
Process p = Process.Start(startInfo);
p.OutputDataReceived += (x, y) => cmdOut += y.Data;
p.BeginOutputReadLine();
p.BeginErrorReadLine();
p.WaitForExit();
return cmdOut;
}
p.OutputdataReceived is a DataReceivedEventHandler and it will concatenate any std output received onto the cmdOut variable.

Related

How to open and use Git Bash through c# code

I'm trying to include opening Git Bash, pushing and pulling in my c# code. Whilst opening Git Bash with Process.Start() is not the problem, I cannot manage to write commands into Git Bash.
I've tried including commands in ProcessStartInfo.Arguments, as well as redirecting the standard Output. Both has not worked at all. Down below you can see the different code snippets I tried.
private void Output()
{
//Try 1
processStartInfo psi = new ProcessStartInfo();
psi.FileName = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Git\Git Bash.lnk";
psi.UseShellExecute = false;
psi.RedirectStandardOutput = true;
psi.Argument = "git add *";
Process p = Process.Start(psi);
string strOutput = p.StandardOutput.ReadToEnd();
Console.WriteLine(strOutput);
//Try 2
ProcessStartInfo psi = new ProcessStartInfo(#"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Git\Git Bash.lnk");
Process.Start(psi);
psi.Arguments = "git add *";
Process.Start(psi);
//Try 3
var escapedArgs = cmd.Replace("\"", "\\\"");
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = #"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Git\Git Bash.lnk",
Arguments = "cd C:\\Users\\strit\\autocommittest2\\autocommittest2\n",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
}
};
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit();
}
Git Bash opens but nothing is written in the command line.
I know it is old question, still adding answer as few days ago I was also facing same issue.
I think what you are missing is -c parameter. I used below code and it solved this issue. -c tells git-bash to execute whatever follows, it is similar to -cmd parameter in command line.
In below mentioned function -
fileName = path of git-bash.exe.
command = git command which you want to execute.
workingDir = Local path of git repository.
public static void ExecuteGitBashCommand(string fileName, string command, string workingDir)
{
ProcessStartInfo processStartInfo = new ProcessStartInfo(fileName, "-c \" " + command + " \"")
{
WorkingDirectory = workingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true
};
var process = Process.Start(processStartInfo);
process.WaitForExit();
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
var exitCode = process.ExitCode;
process.Close();
}
I hope it solves the issue.
I think you are on the right way. I would try to use the git in the path, but it should be possible to also use the git-bash.exe directly, at my machine it is located here: C:\Program Files\Git\git-bash.exe.
Process gitProcess = new Process();
gitInfo.Arguments = YOUR_GIT_COMMAND; // such as "fetch origin"
gitInfo.WorkingDirectory = YOUR_GIT_REPOSITORY_PATH;
gitInfo.UseShellExecute = false;
gitProcess.StartInfo = gitInfo;
gitProcess.Start();
string stderr_str = gitProcess.StandardError.ReadToEnd(); // pick up STDERR
string stdout_str = gitProcess.StandardOutput.ReadToEnd(); // pick up STDOUT
gitProcess.WaitForExit();
gitProcess.Close();
Like #S.Spieker already told you in it's good answer, no need to use git bash (it makes it harder to achieve and less performant), just call directly the git executable.
You could have a look to the GitExtensions code that is doing that: https://github.com/gitextensions/gitextensions/blob/027eabec3be497f8d780cc68ece268c64a43a7d5/GitExtensionsVSIX/Git/GitCommands.cs#L112
You could also achieve what you want using libgit2sharp (https://github.com/libgit2/libgit2sharp). That could be easier if you want to interpret the results of the command run.

How to let the command window open while executing command prompt scripts from C#?

I am facing a strange issue. I have an application in which a method executes some of command prompt commands but they are not executing in an appropriate way. The code snippet looks like
private void encryptAndWrite(String FileName)
{
string strEntry = FileName.Replace("\\web.config", ""); // Let the user assign to this string, for example like "C:\Users\cnandy\Desktop\Test\Websites\AccountDeduplicationWeb"
Process p = new Process()
{
StartInfo = new ProcessStartInfo("cmd.exe")
{
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
p.Start();
p.StandardInput.WriteLine(#"cd C:\Windows\Microsoft.NET\Framework64\v4.0.30319");
p.StandardInput.WriteLine("aspnet_regiis.exe -pc \"CustomKeys12\" -exp");
p.StandardInput.WriteLine("aspnet_regiis.exe -pa \"CustomKeys12\" \"NT AUTHORITY\\NETWORK SERVICE\"");
p.StandardInput.WriteLine("aspnet_regiis -pef \"connectionStrings\" {0} -prov \"CustomEncryptProvider\"",strEntry);
p.StandardInput.WriteLine("aspnet_regiis -px \"CustomKeys12\" {0} -pri",KeyFileName);
p.StandardInput.WriteLine("exit");
Thread.Sleep(7000);
updateTextBox.ReadOnly = true;
updateTextBox.Text = String.Empty;
updateMessageLabel.Text = String.Empty;
showConfig(FileName);
}
Essentially the last command should overwrite a file which is supplied by
KeyFileName
each time the command executes successfully. But it is not.
If I hard code it like
p.StandardInput.WriteLine("aspnet_regiis -px \"CustomKeys12\" \"C:\\Users\\Chiranjib\\Desktop\\XMLKey\\CustomEncryptKey.xml\" -pri");
it executes successfully.
So is there a way where I can let the command window open so that I can see where the commands are going wrong ? I though tried CreateNoWindow = false
but it does not show the command by command execution.
Thanks in advance.

How to run shell script with C# on OSX?

I'd like to use C# to execute a shell script.
Based on similar questions I came to a solution that looks like this.
System.Diagnostics.Process.Start("/Applications/Utilities/Terminal.app","sunflow/sunflow.sh");
It currently opens Terminal, then opens the shell file with the default application (Xcode in my case). Changing the default application is not an option, since this app will need to be installed for other users.
Ideally the solution will allow for arguments for the shell file.
I can't test with a Mac right now, but the following code works on Linux and should work on a Mac because Mono hews pretty closely to Microsoft's core .NET interfaces:
ProcessStartInfo startInfo = new ProcessStartInfo()
{
FileName = "foo/bar.sh",
Arguments = "arg1 arg2 arg3",
};
Process proc = new Process()
{
StartInfo = startInfo,
};
proc.Start();
A few notes about my environment:
I created a test directory specifically to double-check this code.
I created a file bar.sh in subdirectory foo, with the following code:
#!/bin/sh
for arg in $*
do
echo $arg
done
I wrapped a Main method around the C# code above in Test.cs, and compiled with dmcs Test.cs, and executed with mono Test.exe.
The final output is "arg1 arg2 arg3", with the three tokens separated by newlines
Thanks Adam, it is good starting point for me. However, for some reason when I tried with above code (changed to my needs) I am getting below error
System.ComponentModel.Win32Exception: Exec format error
see below code that gives above error
ProcessStartInfo startInfo = new ProcessStartInfo()
{
FileName = "/Users/devpc/mytest.sh",
Arguments = string.Format("{0} {1} {2} {3} {4}", "testarg1", "testarg2", "testarg3", "testarg3", "testarg4"),
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
Process proc = new Process()
{
StartInfo = startInfo,
};
proc.Start();
while (!proc.StandardOutput.EndOfStream)
{
string result = proc.StandardOutput.ReadLine();
//do something here
}
and spent some time and come up with below and it is working in my case - just in case if anyone encounter this error try below
Working Solution:
var command = "sh";
var scriptFile = "/Users/devpc/mytest.sh";//Path to shell script file
var arguments = string.Format("{0} {1} {2} {3} {4}", "testarg1", "testarg2", "testarg3", "testarg3", "testarg4");
var processInfo = new ProcessStartInfo()
{
FileName = command,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
Process process = Process.Start(processInfo); // Start that process.
while (!process.StandardOutput.EndOfStream)
{
string result = process.StandardOutput.ReadLine();
// do something here
}
process.WaitForExit();

Issue with a Process using '&&'

I have an external app with a number of arguments I need to run programmatically. It's all working fine in a command prompt but when I try to launch the same arguments from a .Net app, it fails. I've now learned it's the use of '&&' that is making the mess at the moment that some of the arguments has.
To simplify my question, say I have something like this working in my command prompt:
"Notepad.exe C:\tmp\tmp.txt && Notepad.exe c:\tmp\tmp2.txt"
This works fine. My attempt to achieve the equivalent looks like this:
string app = "Notepad.exe";
string args = #"C:\tmp\tmp.txt && Notepad.exe c:\tmp\tmp2.txt";
using (Process process = new Process())
{
process.StartInfo = new ProcessStartInfo(app, args)
{
UseShellExecute = false,
CreateNoWindow = false,
};
process.Start();
process.WaitForExit();
}
But this obviously does not work. Is my only option to split the arguments by '&&' and run them one by one or is there a way to make this work?
Try this:
string args = #"C:\tmp\tmp.txt && Notepad.exe c:\tmp\tmp2.txt";
The reason is that \ starts an escape sequence, meaning the text you pass as arguments is really this:
C: mp mp.txt && Notepad.exe c: mp mp2.txt
\t is the escape sequence for a tab.
The only difference between my code and your is the # in front of the string. This tells the compiler to interpret the string literally. If you need to know more, the name for this is "verbatim string".
UPDATE:
You can use this code:
string app = "cmd";
string args = #"/c Notepad.exe C:\tmp\tmp.txt && Notepad.exe c:\tmp\tmp2.txt";
using (Process process = new Process())
{
process.StartInfo = new ProcessStartInfo(app, args)
{
UseShellExecute = false,
CreateNoWindow = true,
};
process.Start();
process.WaitForExit();
}
It will use cmd.exe to execute the compounded command. I changed CreateNoWindow to true to not show the command line window, but only the notepad windows.
Have a method:
private void RunAndWait(string app, params string[] args)
{
foreach (string arg in args)
{
Process proc = new Process
{
StartInfo = new ProcessStartInfo(app, arg)
{
UseShellExecute = false,
CreateNoWindow = true
}
};
proc.Start();
proc.WaitForExit();
}
}
And then the code very is simple:
string app = "Notepad.exe";
string args1 = #"C:\tmp\tmp.txt";
string args2 = #"C:\tmp\tmp2.txt";
RunAndWait(app, args1, args2);
And to generalize for more than two files (args):
string[] args = new string[] {#"C:\tmp\tmp.txt", #"C:\tmp\tmp2.txt", ...}
RunAndWait(app, args);

Run Command Prompt Commands

Is there any way to run command prompt commands from within a C# application? If so how would I do the following:
copy /b Image1.jpg + Archive.rar Image2.jpg
This basically embeds an RAR file within JPG image. I was just wondering if there was a way to do this automatically in C#.
this is all you have to do run shell commands from C#
string strCmdText;
strCmdText= "/C copy /b Image1.jpg + Archive.rar Image2.jpg";
System.Diagnostics.Process.Start("CMD.exe",strCmdText);
EDIT:
This is to hide the cmd window.
System.Diagnostics.Process process = new System.Diagnostics.Process();
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
startInfo.FileName = "cmd.exe";
startInfo.Arguments = "/C copy /b Image1.jpg + Archive.rar Image2.jpg";
process.StartInfo = startInfo;
process.Start();
EDIT 2:
It is important that the argument begins with /C, otherwise it won't work. As #scott-ferguson said: /C carries out the command specified by the string and then terminates.
Tried RameshVel's solution but I could not pass arguments in my console application. If anyone experiences the same problem here is a solution:
using System.Diagnostics;
Process cmd = new Process();
cmd.StartInfo.FileName = "cmd.exe";
cmd.StartInfo.RedirectStandardInput = true;
cmd.StartInfo.RedirectStandardOutput = true;
cmd.StartInfo.CreateNoWindow = true;
cmd.StartInfo.UseShellExecute = false;
cmd.Start();
cmd.StandardInput.WriteLine("echo Oscar");
cmd.StandardInput.Flush();
cmd.StandardInput.Close();
cmd.WaitForExit();
Console.WriteLine(cmd.StandardOutput.ReadToEnd());
var proc1 = new ProcessStartInfo();
string anyCommand;
proc1.UseShellExecute = true;
proc1.WorkingDirectory = #"C:\Windows\System32";
proc1.FileName = #"C:\Windows\System32\cmd.exe";
proc1.Verb = "runas";
proc1.Arguments = "/c "+anyCommand;
proc1.WindowStyle = ProcessWindowStyle.Hidden;
Process.Start(proc1);
None of the above answers helped for some reason, it seems like they sweep errors under the rug and make troubleshooting one's command difficult. So I ended up going with something like this, maybe it will help someone else:
var proc = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = #"C:\Program Files\Microsoft Visual Studio 14.0\Common7\IDE\tf.exe",
Arguments = "checkout AndroidManifest.xml",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
WorkingDirectory = #"C:\MyAndroidApp\"
}
};
proc.Start();
Though technically this doesn't directly answer question posed, it does answer the question of how to do what the original poster wanted to do: combine files. If anything, this is a post to help newbies understand what Instance Hunter and Konstantin are talking about.
This is the method I use to combine files (in this case a jpg and a zip). Note that I create a buffer that gets filled with the content of the zip file (in small chunks rather than in one big read operation), and then the buffer gets written to the back of the jpg file until the end of the zip file is reached:
private void CombineFiles(string jpgFileName, string zipFileName)
{
using (Stream original = new FileStream(jpgFileName, FileMode.Append))
{
using (Stream extra = new FileStream(zipFileName, FileMode.Open, FileAccess.Read))
{
var buffer = new byte[32 * 1024];
int blockSize;
while ((blockSize = extra.Read(buffer, 0, buffer.Length)) > 0)
{
original.Write(buffer, 0, blockSize);
}
}
}
}
if you want to run the command in async mode - and print the results. you can you this class:
public static class ExecuteCmd
{
/// <summary>
/// Executes a shell command synchronously.
/// </summary>
/// <param name="command">string command</param>
/// <returns>string, as output of the command.</returns>
public static void ExecuteCommandSync(object command)
{
try
{
// create the ProcessStartInfo using "cmd" as the program to be run, and "/c " as the parameters.
// Incidentally, /c tells cmd that we want it to execute the command that follows, and then exit.
System.Diagnostics.ProcessStartInfo procStartInfo = new System.Diagnostics.ProcessStartInfo("cmd", "/c " + command);
// The following commands are needed to redirect the standard output.
//This means that it will be redirected to the Process.StandardOutput StreamReader.
procStartInfo.RedirectStandardOutput = true;
procStartInfo.UseShellExecute = false;
// Do not create the black window.
procStartInfo.CreateNoWindow = true;
// Now we create a process, assign its ProcessStartInfo and start it
System.Diagnostics.Process proc = new System.Diagnostics.Process();
proc.StartInfo = procStartInfo;
proc.Start();
// Get the output into a string
string result = proc.StandardOutput.ReadToEnd();
// Display the command output.
Console.WriteLine(result);
}
catch (Exception objException)
{
// Log the exception
Console.WriteLine("ExecuteCommandSync failed" + objException.Message);
}
}
/// <summary>
/// Execute the command Asynchronously.
/// </summary>
/// <param name="command">string command.</param>
public static void ExecuteCommandAsync(string command)
{
try
{
//Asynchronously start the Thread to process the Execute command request.
Thread objThread = new Thread(new ParameterizedThreadStart(ExecuteCommandSync));
//Make the thread as background thread.
objThread.IsBackground = true;
//Set the Priority of the thread.
objThread.Priority = ThreadPriority.AboveNormal;
//Start the thread.
objThread.Start(command);
}
catch (ThreadStartException )
{
// Log the exception
}
catch (ThreadAbortException )
{
// Log the exception
}
catch (Exception )
{
// Log the exception
}
}
}
if you want to keep the cmd window open or want to use it in winform/wpf then use it like this
string strCmdText;
//For Testing
strCmdText= "/K ipconfig";
System.Diagnostics.Process.Start("CMD.exe",strCmdText);
/K
Will keep the cmd window open
Yes, there is (see link in Matt Hamilton's comment), but it would be easier and better to use .NET's IO classes. You can use File.ReadAllBytes to read the files and then File.WriteAllBytes to write the "embedded" version.
with a reference to Microsoft.VisualBasic
Interaction.Shell("copy /b Image1.jpg + Archive.rar Image2.jpg", AppWinStyle.Hide);
This can also be done by P/Invoking the C standard library's system function.
using System.Runtime.InteropServices;
[DllImport("msvcrt.dll")]
public static extern int system(string format);
system("copy Test.txt Test2.txt");
Output:
1 file(s) copied.
Here is little simple and less code version. It will hide the console window too-
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/C copy /b Image1.jpg + Archive.rar Image2.jpg";
process.Start();
I have the following method, which I use to run the command prompt commands from C#
In first parameter pass the command you want to run
public static string RunCommand(string arguments, bool readOutput)
{
var output = string.Empty;
try
{
var startInfo = new ProcessStartInfo
{
Verb = "runas",
FileName = "cmd.exe",
Arguments = "/C "+arguments,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = false
};
var proc = Process.Start(startInfo);
if (readOutput)
{
output = proc.StandardOutput.ReadToEnd();
}
proc.WaitForExit(60000);
return output;
}
catch (Exception)
{
return output;
}
}
You can achieve this by using the following method (as mentioned in other answers):
strCmdText = "'/C some command";
Process.Start("CMD.exe", strCmdText);
When I tried the methods listed above I found that my custom command did not work using the syntax of some of the answers above.
I found out more complex commands need to be encapsulated in quotes to work:
string strCmdText;
strCmdText = "'/C cd " + path + " && composer update && composer install -o'";
Process.Start("CMD.exe", strCmdText);
you can use simply write the code in a .bat format extension ,the code of the batch file :
c:/ copy /b Image1.jpg + Archive.rar Image2.jpg
use this c# code :
Process.Start("file_name.bat")
You can use RunProcessAsTask pacakge and run your process async and easily like this:
var processResults = await ProcessEx.RunAsync("git.exe", "pull");
//get process result
foreach (var output in processResults.StandardOutput)
{
Console.WriteLine("Output line: " + output);
}
This may be a bit of a read so im sorry in advance. And this is my tried and tested way of doing this, there may be a simpler way but this is from me throwing code at a wall and seeing what stuck
If it can be done with a batch file then the maybe over complicated work around is have c# write a .bat file and run it. If you want user input you could place the input into a variable and have c# write it into the file. it will take trial and error with this way because its like controlling a puppet with another puppet.
here is an example, In this case the function is for a push button in windows forum app that clears the print queue.
using System.IO;
using System;
public static void ClearPrintQueue()
{
//this is the path the document or in our case batch file will be placed
string docPath =
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
//this is the path process.start usues
string path1 = docPath + "\\Test.bat";
// these are the batch commands
// remember its "", the comma separates the lines
string[] lines =
{
"#echo off",
"net stop spooler",
"del %systemroot%\\System32\\spool\\Printers\\* /Q",
"net start spooler",
//this deletes the file
"del \"%~f0\"" //do not put a comma on the last line
};
//this writes the string to the file
using (StreamWriter outputFile = new StreamWriter(Path.Combine(docPath, "test.bat")))
{
//This writes the file line by line
foreach (string line in lines)
outputFile.WriteLine(line);
}
System.Diagnostics.Process.Start(path1);
}
IF you want user input then you could try something like this.
This is for setting the computer IP as static but asking the user what the IP, gateway, and dns server is.
you will need this for it to work
public static void SetIPStatic()
{
//These open pop up boxes which ask for user input
string STATIC = Microsoft.VisualBasic.Interaction.InputBox("Whats the static IP?", "", "", 100, 100);
string SUBNET = Microsoft.VisualBasic.Interaction.InputBox("Whats the Subnet?(Press enter for default)", "255.255.255.0", "", 100, 100);
string DEFAULTGATEWAY = Microsoft.VisualBasic.Interaction.InputBox("Whats the Default gateway?", "", "", 100, 100);
string DNS = Microsoft.VisualBasic.Interaction.InputBox("Whats the DNS server IP?(Input required, 8.8.4.4 has already been set as secondary)", "", "", 100, 100);
//this is the path the document or in our case batch file will be placed
string docPath =
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
//this is the path process.start usues
string path1 = docPath + "\\Test.bat";
// these are the batch commands
// remember its "", the comma separates the lines
string[] lines =
{
"SETLOCAL EnableDelayedExpansion",
"SET adapterName=",
"FOR /F \"tokens=* delims=:\" %%a IN ('IPCONFIG ^| FIND /I \"ETHERNET ADAPTER\"') DO (",
"SET adapterName=%%a",
"REM Removes \"Ethernet adapter\" from the front of the adapter name",
"SET adapterName=!adapterName:~17!",
"REM Removes the colon from the end of the adapter name",
"SET adapterName=!adapterName:~0,-1!",
//the variables that were set before are used here
"netsh interface ipv4 set address name=\"!adapterName!\" static " + STATIC + " " + STATIC + " " + DEFAULTGATEWAY,
"netsh interface ipv4 set dns name=\"!adapterName!\" static " + DNS + " primary",
"netsh interface ipv4 add dns name=\"!adapterName!\" 8.8.4.4 index=2",
")",
"ipconfig /flushdns",
"ipconfig /registerdns",
":EOF",
"DEL \"%~f0\"",
""
};
//this writes the string to the file
using (StreamWriter outputFile = new StreamWriter(Path.Combine(docPath, "test.bat")))
{
//This writes the file line by line
foreach (string line in lines)
outputFile.WriteLine(line);
}
System.Diagnostics.Process.Start(path1);
}
Like I said. It may be a little overcomplicated but it never fails unless I write the batch commands wrong.

Categories

Resources