Recently I try to compile and run C# code stored somewhere else. My goal is to import a .txt file, compile it and run it. I followed this article on Simeon's blog about compiling and running C# code within the program, and everything work well.
Then I try making something a bit more complex by importing the C# code from my computer, so I created a .txt file with the following lines that is store for instance at "C:\program.txt" :
(the text file)
using System;
namespace Test
{
public class DynaCore
{
static public int Main(string str)
{
Console.WriteLine("Cool it work !");
return str.Length;
}
}
}
I do some coding based on the same article and that is my code :
(the C# program)
using System;
using System.Collections.Generic;
using System.Text;
using System.CodeDom.Compiler;
using System.IO;
using Microsoft.CSharp;
using System.Reflection;
namespace DynaCode
{
class Program
{
static void Main(string[] args)
{
string[] lines = System.IO.File.ReadAllLines(#"C:\program.txt");
string bigLine = string.Empty;
foreach(string s in lines)
{
bigLine += s;
}
string[] finalLine = new string[1] { bigLine };
CompileAndRun(finalLine);
Console.ReadKey();
}
static void CompileAndRun(string[] code)
{
CompilerParameters CompilerParams = new CompilerParameters();
string outputDirectory = Directory.GetCurrentDirectory();
CompilerParams.GenerateInMemory = true;
CompilerParams.TreatWarningsAsErrors = false;
CompilerParams.GenerateExecutable = false;
CompilerParams.CompilerOptions = "/optimize";
string[] references = { "System.dll" };
CompilerParams.ReferencedAssemblies.AddRange(references);
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults compile = provider.CompileAssemblyFromSource(CompilerParams, code);
if (compile.Errors.HasErrors)
{
string text = "Compile error: ";
foreach (CompilerError ce in compile.Errors)
{
text += "rn" + ce.ToString();
}
throw new Exception(text);
}
Module module = compile.CompiledAssembly.GetModules()[0];
Type mt = null;
MethodInfo methInfo = null;
if (module != null)
{
mt = module.GetType("Test.DynaCore");
}
if (mt != null)
{
methInfo = mt.GetMethod("Main");
}
if (methInfo != null)
{
Console.WriteLine(methInfo.Invoke(null, new object[] { "here in dyna code. Yes it work !!" }));
}
}
}
}
This work well, and I got the following output as expected :
Cool it work !
33
Note that I put all the code of the .txt file in one big line that I do myseft, because as Simeon said :
CompileAssemblyFromSource consumes is a single string for each block (file) worth of C# code, not for each line.
Even now this sentence still a bit obscure for me.
( I tried CompileAndRun(new string[1] { lines.ToString() }); before but there was an error when compiling the .txt file, that's why I do the big line myself. )
And here is my problem : I ask myself : "What if I add a comment in my .txt file ?", so I edit it and that how it look : (the text file)
using System;
namespace Test
{
//This is a super simple test
public class DynaCore
{
static public int Main(string str)
{
Console.WriteLine("Cool it work !");
return str.Length;
}
}
}
And of course I got an error (CS1513) because I convert the .txt file in one big string, so everything after the // is ignored. So how can I use comment using // inside my .txt file and got the program work ?
I also try CompileAndRun(lines);, but after launching the program it crash when compiling the .txt file because of the exception.
I do some search about it and I didn't find anythings about comment. I guess there is somethings wrong about passing only one big line in the CompileAndRun method, but passing several lines don't work as I say upper.
(Another note : Comment using /* insert comment */ works.)
Each element given to CompileAssemblyFromSource is supposed to be a file, not a single line of code. So read the whole file into a single string and give it to the method and it'll work just fine.
static void Main(string[] args)
{
var code = System.IO.File.ReadAllText(#"C:\program.txt");
CompileAndRun(code);
Console.ReadKey();
}
static void CompileAndRun(string code)
{
CompilerParameters CompilerParams = new CompilerParameters();
string outputDirectory = Directory.GetCurrentDirectory();
CompilerParams.GenerateInMemory = true;
CompilerParams.TreatWarningsAsErrors = false;
CompilerParams.GenerateExecutable = false;
CompilerParams.CompilerOptions = "/optimize";
string[] references = { "System.dll" };
CompilerParams.ReferencedAssemblies.AddRange(references);
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults compile = provider.CompileAssemblyFromSource(CompilerParams, code);
// ...
}
I just found Unity's script template for C# scripts. To get the script name you write #SCRIPTNAME# so it looks like this:
using UnityEngine;
using System.Collections;
public class #SCRIPTNAME# : MonoBehaviour
{
void Start ()
{
}
void Update ()
{
}
}
Then it would create the script with the right name, but is there something like #FOLDERNAME# so that I can put it in the right namespace directly when creating the script?
There is no built-in template variables like #FOLDERNAME#.
According to this post, there are only 3 magic variables.
"#NAME#"
"#SCRIPTNAME#"
"#SCRIPTNAME_LOWER#"
But you can always hook into the creation process of a script and append the namespace yourself using AssetModificationProcessor.
Here is an example that adds some custom data to the created script.
//Assets/Editor/KeywordReplace.cs
using UnityEngine;
using UnityEditor;
using System.Collections;
public class KeywordReplace : UnityEditor.AssetModificationProcessor
{
public static void OnWillCreateAsset ( string path )
{
path = path.Replace( ".meta", "" );
int index = path.LastIndexOf( "." );
string file = path.Substring( index );
if ( file != ".cs" && file != ".js" && file != ".boo" ) return;
index = Application.dataPath.LastIndexOf( "Assets" );
path = Application.dataPath.Substring( 0, index ) + path;
file = System.IO.File.ReadAllText( path );
file = file.Replace( "#CREATIONDATE#", System.DateTime.Now + "" );
file = file.Replace( "#PROJECTNAME#", PlayerSettings.productName );
file = file.Replace( "#SMARTDEVELOPERS#", PlayerSettings.companyName );
System.IO.File.WriteAllText( path, file );
AssetDatabase.Refresh();
}
}
I know it's and old question but in newer versions of Unity you can define a root namespace to be used in the project.
You can define the namespace in Edit > Project Settings > Editor > Root Namespace
Doing this will add the defined namespace on newly created scripts.
Using zwcloud's answer and some other resources I was able to generate a namespace on my script files:
First step, navigate:
Unity's default templates can be found under your Unity installation's directory in Editor\Data\Resources\ScriptTemplates for Windows and /Contents/Resources/ScriptTemplates for OSX.
And open the file 81-C# Script-NewBehaviourScript.cs.txt
And make the following change:
namespace #NAMESPACE# {
At the top and
}
At the bottom. Indent the rest so that the whitespace is as desired. Don't save this just yet. If you wish, you can make other changes to the template, such as removing the default comments, making Update() and Start() private, or even removing them entirely.
Again, do not save this file yet or Unity will throw an error on the next step. If you saved, just hit ctrl-Z to undo and then resave, then ctrl-Y to re-apply the changes.
Now create a new script inside an Editor folder inside your Unity Assets directory and call it AddNameSpace. Replace the contents with this:
using UnityEngine;
using UnityEditor;
public class AddNameSpace : UnityEditor.AssetModificationProcessor {
public static void OnWillCreateAsset(string path) {
path = path.Replace(".meta", "");
int index = path.LastIndexOf(".");
if(index < 0) return;
string file = path.Substring(index);
if(file != ".cs" && file != ".js" && file != ".boo") return;
index = Application.dataPath.LastIndexOf("Assets");
path = Application.dataPath.Substring(0, index) + path;
file = System.IO.File.ReadAllText(path);
string lastPart = path.Substring(path.IndexOf("Assets"));
string _namespace = lastPart.Substring(0, lastPart.LastIndexOf('/'));
_namespace = _namespace.Replace('/', '.');
file = file.Replace("#NAMESPACE#", _namespace);
System.IO.File.WriteAllText(path, file);
AssetDatabase.Refresh();
}
}
Save this script as well as saving the changes to 81-C# Script-NewBehaviourScript.cs.txt
And you're done! You can test it by creating a new C# script inside any series of folders inside Assets and it will generate the new namespace definition we created.
I'm well aware that this question has been answered by the awesome people (zwcloud, Darco18 and Alexey).
However, since my namespace organization follows the folder structure in the project, I've put together in a jiffy some minor modification to the code, and I'm sharing it here in case someone needs it and has the same organizational methodology which I'm following.
Please keep in mind that you need to set the root namespace in your project settings in the C# project generation section.
EDIT: I've adjusted the code a bit work with placement in the root folders of Scripts, Editor, etc..
public class NamespaceResolver : UnityEditor.AssetModificationProcessor
{
public static void OnWillCreateAsset(string metaFilePath)
{
var fileName = Path.GetFileNameWithoutExtension(metaFilePath);
if (!fileName.EndsWith(".cs"))
return;
var actualFile = $"{Path.GetDirectoryName(metaFilePath)}\\{fileName}";
var segmentedPath = $"{Path.GetDirectoryName(metaFilePath)}".Split(new[] { '\\' }, StringSplitOptions.None);
var generatedNamespace = "";
var finalNamespace = "";
// In case of placing the class at the root of a folder such as (Editor, Scripts, etc...)
if (segmentedPath.Length <= 2)
finalNamespace = EditorSettings.projectGenerationRootNamespace;
else
{
// Skipping the Assets folder and a single subfolder (i.e. Scripts, Editor, Plugins, etc...)
for (var i = 2; i < segmentedPath.Length; i++)
{
generatedNamespace +=
i == segmentedPath.Length - 1
? segmentedPath[i]
: segmentedPath[i] + "."; // Don't add '.' at the end of the namespace
}
finalNamespace = EditorSettings.projectGenerationRootNamespace + "." + generatedNamespace;
}
var content = File.ReadAllText(actualFile);
var newContent = content.Replace("#NAMESPACE#", finalNamespace);
if (content != newContent)
{
File.WriteAllText(actualFile, newContent);
AssetDatabase.Refresh();
}
}
}
OK, so this question was already answered by two wonderful people, zwcloud and Draco18s, and their solution works, I'm just showing another version of the same code that, I hope, will be a little more clear in terms of what exactly happening.
Quick notes:
Yes, in this method we are getting not the actual file path,
but the path of its meta file as a parameter
No, you can not use AssetModificationProcessor without UnityEditor prefix, it is deprecated
OnWillCreateAsset method is not shown via Ctrl+Shift+M, 'override' typing or base class metadata
_
using UnityEditor;
using System.IO;
public class ScriptTemplateKeywordReplacer : UnityEditor.AssetModificationProcessor
{
//If there would be more than one keyword to replace, add a Dictionary
public static void OnWillCreateAsset(string metaFilePath)
{
string fileName = Path.GetFileNameWithoutExtension(metaFilePath);
if (!fileName.EndsWith(".cs"))
return;
string actualFilePath = $"{Path.GetDirectoryName(metaFilePath)}{Path.DirectorySeparatorChar}{fileName}";
string content = File.ReadAllText(actualFilePath);
string newcontent = content.Replace("#PROJECTNAME#", PlayerSettings.productName);
if (content != newcontent)
{
File.WriteAllText(actualFilePath, newcontent);
AssetDatabase.Refresh();
}
}
}
And this is the contents of my file c:\Program Files\Unity\Editor\Data\Resources\ScriptTemplates\81-C# Script-NewBehaviourScript.cs.txt
using UnityEngine;
namespace #PROJECTNAME#
{
public class #SCRIPTNAME# : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
}
Here is my solution, in my case Unity added the root namespace right before OnWillCreateAsset(), so I had to replace the root namespace in the script with the one I want.
Here is the code
public class AddNameSpace : UnityEditor.AssetModificationProcessor
{
public static void OnWillCreateAsset(string path)
{
var rootNamespace =
string.IsNullOrWhiteSpace(EditorSettings.projectGenerationRootNamespace) ?
string.Empty : $"{EditorSettings.projectGenerationRootNamespace}";
path = path.Replace(".meta", "");
int index = path.LastIndexOf(".");
if (index < 0)
return;
string file = path.Substring(index);
if (file != ".cs" && file != ".js" && file != ".boo")
return;
string formattedNamespace = GetNamespace(path, rootNamespace);
file = System.IO.File.ReadAllText(path);
if (file.Contains(formattedNamespace))
return;
file = file.Replace(rootNamespace, formattedNamespace);
System.IO.File.WriteAllText(path, file);
AssetDatabase.Refresh();
}
private static string GetNamespace(string filePath, string rootNamespace)
{
filePath = filePath.Replace("Assets/Scripts/", "/")
.Replace('/', '.')
.Replace(' '.ToString(), string.Empty);
var splitPath = filePath.Split('.');
string pathWithoutFileName = string.Empty;
for (int i = 1; i < splitPath.Length - 2; i++)
{
if (i == splitPath.Length - 3)
{
pathWithoutFileName += splitPath[i];
}
else
{
pathWithoutFileName += splitPath[i] + '.';
}
}
return $"{rootNamespace}.{pathWithoutFileName}";
}
}
I have TeamCity running for a C# project. The Unit tests are written using MSTest and they include an external JSON file. They are loaded in because they're large and I don't want to have to escape them in C#.
I import them like this:
[TestInitialize]
public void Setup()
{
using (StreamReader r = new StreamReader(#".\currency2.json"))
{
_json = r.ReadToEnd();
}
...
They run fine locally. I have 'Copy always set' but when the tests are ran using Teamcity I get an error saying that it can't find them in a temp folder. They are copied over to the build server but they're not in this temp folder.
Could not find file 'E:\TeamCity\buildAgent\temp\buildTmp\SYSTEM_SERVER 2016-07-18 15_28_19\Out\currency2.json'
I have **\bin\release\*test*.dll setup as my Test File Names in the test build step.
Any help appreciated.
I had a similar problem.
I changed the properties of the test file to this
Build Action = Content
Copy to Output Directory = Copy always
Teamcity will copy the file to the build folder, but it does not seem to maintain the same structure you'd expect.
So I created a file lookup loop. That will step down the expected folder until it finds the text file in question.
var pathLookups = new string[]
{
"2ndFolder\\3rdFolder\\test.json", // folder that normally workes
"3rdFolder\\test.json",
"test.json"
};
foreach (var pathLookup in pathLookups)
{
try
{
jsonFileCollection = JsonFileLoader<TestJsonType>.LoadJson(pathLooksup);
if (jsonFileCollection!= null)
{
break;
}
}
catch (Exception)
{
Console.WriteLine("Attempted to load test json from path:" + pathLooksup);
}
}
It's not the cleanest solution, but it will get the job done. You could refactor this to look a little nicer.
You might pass the full pass by argument to your program (and value defined in TeamCity).
Something like this (this is a pseudo-code example only) :
string[] programArgs;
static void Main(string[] args)
{
programArgs = args
}
[TestInitialize]
public void Setup()
{
using (StreamReader r = new StreamReader(programArgs[1]))
{
_json = r.ReadToEnd();
}
...
}
I have a Test.cs file in C:\ This test file reads from an input file and writes the same to an output file.
Test.cs
public class Test
{
public static int Main(string[] args)
{
var reader = new StreamReader("in.txt");
string input = reader.ReadLine();
var writer = new StreamWriter("out.txt");
writer.WriteLine(input);
return 0;
}
}
Here it should be noted that the code only uses the filename and not the full file path, which means the file is expected to be in the directory where the program is running. And I have created the in.txt in C:\
Now, there is a c# code called Runner.cs in a solution in C:\Project\Runner.cs, that dynamically compiles the Test.cs code and runs it using reflection. Now, when the Test.cs runs, it expects the in.txt file to be in C:\Project\bin\Debug\in.txt , but it is actually present in C:\in.txt
So, my question is, is there a way to make the code to get the file from C:\in.txt and not from the bin directory without changing the path of the file in the Test.cs code file.
Edit: It is my bad that I forgot to mention why I am in need of this requirement.
The Test.cs file comes from over the wire. And I felt it will not be a good choice to edit this file and set the file path accordingly. I want to compile it and run it as it is.
I hope I am clear. If not, please feel free to ask for more information.
If it is as simple as you show in your code switching the CurrentDirectory works for this example:
var mainMembers = new CSharpCodeProvider()
.CreateCompiler()
.CompileAssemblyFromSource(
new CompilerParameters { GenerateInMemory = true }
, #"
using System;
using System.IO;
public class M {
public static int Main() {
Console.WriteLine(""CurDir = ""+ Environment.CurrentDirectory);
var reader = new StreamReader(""in.txt"");
string input = reader.ReadLine();
var writer = new StreamWriter(""out.txt"");
writer.WriteLine(input);
return 0;
}
}")
.CompiledAssembly
.GetType("M")
.GetMember("Main");
// inspect
Environment.CurrentDirectory.Dump("current");
// keep
var oldcd = Environment.CurrentDirectory;
// switch
Environment.CurrentDirectory = "c:\\temp";
// invoke external code
((MethodInfo) mainMembers[0]).Invoke(null,null);
// restore
Environment.CurrentDirectory = oldcd;
In a multi threaded scenario this becomes unreliable.
How can I open mp3 file with RealPlayer while the default is MediaPlayer
I know Process and ProcessStartInfo method, but I would like to know how to "open program with..."
Can you help me, plz?
Okay, so thought I'd make this possible for you before I clock off for the night. I have thrown together a working console application which loads (known) installed programs from the registry's App Path key. The solution is far from perfect, won't be the safest, fastest, or most reliable solution, and it certainly shouldn't be seen amongst any production code, but it is more than enough to aid you, hopefully, in developing what it is you need:
So, here is the code, minus the namespace...
using System;
using System.IO;
using Microsoft.Win32;
using System.Diagnostics;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
if (args.Length >= 0 && !string.IsNullOrEmpty(args[0]) && File.Exists(args[0]))
{
var programs = new InstalledPrograms();
var programKey = "RealPlay.exe".ToLowerInvariant();
if (programs.ContainsKey(programKey))
{
var programPath = programs[programKey];
if (!string.IsNullOrEmpty(programPath) && File.Exists(programPath))
{
var process = new Process();
process.StartInfo = new ProcessStartInfo(programPath);
process.StartInfo.Arguments = args[0];
if (process.Start())
{
Console.WriteLine("That was easy!");
}
else
{
Console.WriteLine("Hell's bells and buckets of blood, we seem to have hit a snag!");
}
}
}
}
else
{
Console.WriteLine("Specify a file as an argument, silly!");
}
Console.WriteLine();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
class InstalledPrograms : Dictionary<string, string>
{
static string PathKeyName = "Path";
static string RegistryKeyToAppPaths = #"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
public InstalledPrograms()
{
Refresh();
}
public void Refresh()
{
Clear();
using (var registryKey = Registry.LocalMachine.OpenSubKey(RegistryKeyToAppPaths))
{
var executableFullPath = string.Empty;
foreach (var registrySubKeyName in registryKey.GetSubKeyNames())
{
using (var registrySubKey = registryKey.OpenSubKey(registrySubKeyName))
{
executableFullPath = registrySubKey.GetValue(string.Empty) as string;
Add(registrySubKeyName.ToLowerInvariant(), executableFullPath);
}
}
}
}
}
}
Though we check for file existence, and other minor but necessary checks are made, you would still need to tighten this up further when plugged into the environment of your own code, including, among other things, exception handling for, but not limited to, registry access issues.