Você está na página 1de 21

Script com C # 28 de junho de 2010 | por Dino Esposito | Arquivado em

Este artigo foi publicado originalmente em VSJ, que agora faz parte do Fusion Developer. essencial para os desenvolvedores e administradores de ser capaz de automatizar tarefas dentro do shell do sistema operacional. No incio, havia apenas arquivos de lote do MS-DOS, com sua sintaxe enigmtica e compacto, eram ideais para die-hard, programadores purosangue, mas no particularmente adequado para o resto de ns. Depois disso, a Microsoft lanou o Windows Script Host (WSH), meio ambiente, um runtime baseado COM capaz de processar arquivos VBScript e JScript. WSH suporta uma boa variedade de objetos de automao COM, e fornece o seu prprio modelo de objeto para manipular o registro, o shell do Windows, eo sistema de arquivos. No a ferramenta perfeita, mas, especialmente se voc pode criar objetos personalizados de automao COM, ele pode ser adaptado para trabalhar na maioria das situaes comuns. WSH existe com diferentes capacidades em todos os sistemas operacionais Windows, desde o Windows 98. Assim que sobre. NET? Um ambiente de scripting uma caracterstica do sistema operacional, enquanto que o quadro. NET uma estrutura externa que inclui uma biblioteca de classe, bem como uma mquina virtual. No entanto, usando alguns dos recursos. NET Framework, voc pode implementar uma srie roteiro puramente gerenciados usando C # (ou Visual. NET Basic) como linguagem de programao. Neste artigo, eu vou construir um pequeno executvel capaz de processar um trecho de C # para um assembly compilado. A ferramenta completa o definido pelo usurio o cdigo-fonte com namespace adequado e declaraes de classe, acrescenta um ponto de entrada, e chamala de volta usando reflexo. Como resultado, voc pode escrever um trecho de cdigo C #, salve-o em um arquivo e clique duas vezes para execut-lo. A grande vantagem sobre WSH que todo o NET Framework. Est disponvel para seus scripts, juntamente com o poder de uma linguagem de primeira classe, como C #. Comece simples Este exemplo surgiu quando, um dia, eu queria testar o comportamento de uma determinada classe. Eu tinha algumas opes para testar a classe rapidamente: eu poderia usar uma ferramenta de teste como NUnit, eu poderia escrever um aplicativo de console, uma pgina Web, ou, por uma questo de mencionar todas as minhas opes, um aplicativo do Windows Forms exagero. Honestamente, hoje eu iria usar NUnit para a tarefa, mas desta forma aconteceu para trs nos primeiros dias da NET e eu nem tenho certeza NUnit estava disponvel, em seguida, ou, pelo menos, eu no sabia disso.. Dito isto, voc sabe, cada nuvem tem um forro de prata eo compilador lote estou apresentando aqui ainda uma ferramenta interessante, com algumas boas aplicaes. Basicamente, para testar a classe que eu no queria criar um projeto e despertar o gigante adormecido (leia-se, o Visual Studio. NET). No seria timo, pensei, se pudesse escrever cdigo C # com a mesma facilidade com que um script WSH? Poucos minutos depois, percebi que o. NET Framework expe o compilador C # como uma classe. Portanto, eu deduzi, escrever um # shell acolhimento C para trechos de cdigo no deve ser to difcil. Suponha que voc, como eu, so to distrado que voc precisa de um programa para lembrlo de datas importantes todas as manhs, o computador iniciado. Este cdigo faz o trabalho muito bem:

DateTime deadline = new DateTime(2005, 12, 31); DateTime today = DateTime.Now; TimeSpan left = deadline today; string BaseText = {0} day(s) left to meet deadline.; MessageBox.Show(String.Format( BaseText, left.Days));

A dificuldade que o cdigo acima no ir compilar. Ele representa corretamente e implementa a lgica do applet, mas no est escrito de uma forma que o compilador C # pode compreender e transformar em um arquivo executvel. Para ser compilado, o trecho de cdigo deve ser preenchido da seguinte forma: classe CSharpScript { static void Main () { / / C # trecho vai aqui } } O nome da classe arbitrrio e, opcionalmente, pode ser completado com um espao de nomes arbitrria. O programa C # Script descrito neste artigo faz exatamente isso. preciso um arquivo que contm um trecho de C #, completa o cdigo para que ele possa ser compilado com sucesso, compila-lo, carrega o conjunto resultante, e chama seu ponto de entrada. Em seguida, atribua uma extenso tpica para estes trechos de cdigo C # (por exemplo,. Csx) e registrar o programa Script C # para lidar com eles quando esto abertas atravs do shell ou uma janela do console. O sonho de um duplo clique um trecho de C #, e tlo executado, torna-se realidade no passado. Vamos ver como isso acontece. Compilar dinamicamente Vrios componentes da plataforma. NET faz uso intensivo de compilao dinmica. O exemplo mais ilustre , provavelmente, a infra-estrutura ASP.NET. Quando voc chama um aspx ou. Recursos. Asmx pela primeira vez, o ASP.NET runtime cria e compila uma classe on the fly para representar o recurso. A infra-estrutura de servio Web tambm utiliza o cdigo gerado dinamicamente para serializar tipos de dados para XML. A classe XmlSerializer responsvel por esta tarefa e trabalha criando primeiro um conjunto temporrio que contm cdigo otimizado para serializao. A compilao dinmica possvel porque alguns compiladores. NET expor um modelo de objeto que permite. NET cham-los de forma programtica. A C #, Visual Basic. NET, e JScript. NET compiladores so includos na lista. importante notar que a compilao dinmica no significa que voc acaba chamando o executvel do compilador e configur-lo atravs de argumentos de linha de comando. Em vez disso, significa que voc trabalha com um Framework. NET que implementa a interface ICodeCompiler e incorpora a capacidade de falar com a ferramenta compilador real. Em ltima anlise, o cdigo C # compilado pelo csc.exe. NET, mas como um programador que trabalha com propriedades e mtodos de uma classe compilador. Para compilar o cdigo C # programaticamente, voc comea a importar o System.CodeDom.Compiler e Microsoft.CSharp namespaces. Ambos so definidas dentro do conjunto do sistema, assim voc no precisa referenciar qualquer outra coisa. A classe principal CSharpCodeProvider. Ele implementa a interface ICodeCompiler e fornece um

mtodo auto-explicativo como CreateCompiler. CSharpCodeProvider prov = new CSharpCodeProvider (); ICodeCompiler compilador = prov.CreateCompiler (); Neste ponto, voc tem uma instncia em memria do objeto que representa o compilador C # (csc.exe). O prximo passo configurar o compilador. Voc pode definir parmetros do compilador por meio de uma instncia da classe CompilerParameters. CompilerParameters cp = new CompilerParameters (); cp.GenerateExecutable = true; cp.GenerateInMemory = true; A propriedade GenerateExecutable indica se voc quer uma DLL ou um arquivo executvel. GenerateInMemory permite indicar o meio de armazenamento para o conjunto resultante. Voc pode cri-lo na memria ou salvar em um arquivo particular. Observe que um arquivo temporrio criado, mesmo que defina GenerateInMemory a verdade. Qualquer assembler que sero referenciados durante a compilao deve ser adicionado coleo ReferencedAssemblies. cp.ReferencedAssemblies.Add ("system.dll"); cp.ReferencedAssemblies.Add ("system.xml.dll"); cp.ReferencedAssemblies.Add ("system.data.dll"); cp.ReferencedAssemblies.Add ("system.windows.forms.dll"); Observe que voc deve indicar a extenso. Dll tambm. A interface ICodeCompiler fornece vrios mtodos de compilao. Dois em particular merecem destaque: CompileAssemblyFromFile e CompileAssemblyFromSource. Como os nomes sugerem, o excompila um arquivo fonte C # existente. Este ltimo, ao contrrio, compila a partir de uma seqncia de memria. Neste caso, voc deve ler o trecho de cdigo do arquivo de origem, complet-lo com classe e informaes do ponto de entrada e, finalmente compilar a partir da string. CompilerResults cr; cr = compiler.CompileAssemblyFromSource (cp, finalSource); O cdigo fonte final construdo usando um string builder para envolver o trecho de cdigo com classe e declaraes de namespace. Aqui est o procedimento que faz o trabalho: int GenerateWithMain (StringBuilder sb, String csSource, String nameSpace) { / / Completa o cdigo fonte para torn-lo compilable sb.AppendFormat ("\ r \ nnamespace {0} \ r \ n", nameSpace); sb.Append ("{\ r \ n"); sb.Append ("class CSharpScript {\ r \ n"); sb.Append ("static void Main () \ r \ n {"); sb.Append (csSource); sb.Append ("} \ r \ n} \ r \ n} \ r \ n"); / / Retorna o nmero de linhas de texto so adicionados antes / / Cdigo CSX real encontrado

return 5; } O cdigo gerado dinamicamente inclui um namespace baseado no nome do arquivo de origem. Se o script vive em um arquivo chamado test.csx, o namespace correspondente ser test_csx. E sobre os namespaces para importar? A aplicao Script C # adiciona um monte de namespaces padro usando clusulas fora da declarao da classe. A lista de namespaces importados padro arbitrria. Neste cdigo de exemplo eu consegui incluir System.Data, System.Xml, System.IO, e System.Windows.Forms. O cdigo na listagem acima termina com um valor de retorno bastante badalado. Por que 5, voc pode estar se perguntando? Tem a ver com a manipulao de erro. Manipular erros do compilador A classe CompilerResults preenchido com as informaes obtidas a partir do compilador. Ele faz referncia de valor do compilador nativo do retorno, todos os arquivos temporrios gerados, as mensagens de sada, e os erros. Erros em particular, so devolvidos embalados em uma propriedade de coleo chamada erros. O executvel Script C # pega essa informao e prepara uma mensagem de erro personalizada. Aqui est um exemplo: if (cr.Errors.HasErrors) { StringBuilder sbErr; sbErr = new StringBuilder ( "Compilando arquivo:"); sbErr.AppendFormat ( "\" {0} \ "", CSFile); sbErr.Append ("\ n \ n"); foreach (CompilerError errar em cr.Errors) { sbErr.AppendFormat ("{0} no linha {1} coluna {2} ", err.ErrorText, err.Line, err.Column); sbErr.Append ("\ n"); } MessageBox.Show (sbErr.ToString () "C # Script - Erro"); voltar; } Cada erro representado com um objeto CompilerError. Ela apresenta informao til tal como a linha e da coluna, onde o erro ocorreu e a mensagem descrevendo o erro. Observe que a propriedade da linha refere-se linha do arquivo final - o que est sendo compilado para. Esta informao no precisa, pois o usurio no sabe nada sobre a estrutura interna do script. Para ser realmente til, o nmero da linha deve referir-se a linha no arquivo CSX fonte. Voc deve subtrair algumas linhas a partir do valor retornado pela propriedade linha. Em particular, voc deve subtrair uma linha para cada um usando clusula adicionado e um para cada linha de texto usada para completar a fonte. A 5 retornado pelo procedimento GenerateWithMain indica que cinco novas linhas foi inserida antes da fonte de C # trecho compilar encontrado no cdigo final. Eu inclu uma caixa de mensagem que sinaliza um erro de sintaxe (ver Figura 1). O nmero da linha coincide com a linha no arquivo trecho CSX perfeitamente.

Figura 1: Quando um erro de compilao capturado, uma mensagem com uma descrio completa e linha e coluna de informaes aparece Figura 1: Quando um erro de compilao capturado, uma mensagem com uma descrio completa e linha e coluna de informaes aparece Chame o ponto de entrada Se o compilador tiver sucesso, o assembly compilado automaticamente carregado na memria e voltou atravs da propriedade CompiledAssembly da classe CompilerResults. O prximo passo envolve a criao de uma instncia da classe na assemblia e invocando o ponto de entrada. Pelo projeto, o conjunto contm apenas uma classe com um nome codificado - CSharpScript - e um mtodo esttico Main (). Usando a reflexo, voc pode obter informaes sobre o mtodo de ponto de entrada e invoc-lo com ou sem parmetros. Este cdigo demonstra como chamar um mtodo Main parmetros:
Assembly a = cr.CompiledAssembly; try { object o = a.CreateInstance( CSharpScript); MethodInfo mi = a.EntryPoint; mi.Invoke(o, null); } catch(Exception ex) { MessageBox.Show(ex.Message); }

Qualquer erro que ocorre dentro do mtodo est preso e uma caixa de mensagem exibida. Observe que o bloco try / catch pega apenas os erros de reflexo para que a mensagem final no especifica o erro que ocorreu. Para obter informaes mais especficas, voc deve usar um bloco try / catch diretamente em seu cdigo CSX. Alternativamente, voc pode adicionar um bloco try / catch em sua GenerateWithMain () mtodo. Integr-lo com o shell At agora, eu constru um pequeno e compacto executvel - c # script.exe - que leva o nome do arquivo na linha de comando que contm um trecho de cdigo C #. O programa completa o cdigo-fonte para torn-lo compilar e carrega o conjunto resultante. Porque o objetivo inicial foi proporcionar um. NET substituto para o ambiente WSH, voc precisa dar mais um passo para integrar a ferramenta com o shell do Windows. Quando voc clicar duas vezes em um arquivo de dentro da casca, Explorer procura de um aplicativo que est registrado para abrir arquivos desse tipo. Se essa aplicao existe, ele chamado passando o nome do arquivo na linha de comando. Esse esquema fornecido para uma variedade de arquivos, incluindo VBS e JS, mas no os arquivos CSX. Registrando uma associao entre um executvel e um tipo de arquivo requer ajustes no registro. D uma olhada em um arquivo REG que liga c # script.exe a arquivos CSX e tambm registra um cone padro para eles: Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT \. Csx]

@ = "Csxfile" [HKEY_CLASSES_ROOT \ csxfile] @ = "C # Script" [HKEY_CLASSES_ROOT \ csxfile \ DefaultIcon] @ = "Youricon.ico, 0" [HKEY_CLASSES_ROOT \ csxfile \ Shell] [HKEY_CLASSES_ROOT \ csxfile \ Shell \ Edit] [HKEY_CLASSES_ROOT \ csxfile \ Shell \ Editar \ Command] @ = "Notepad.exe '% 1'" [HKEY_CLASSES_ROOT \ csxfile \ Shell \ Open] [HKEY_CLASSES_ROOT \ csxfile \ Shell \ Open \ Command] @ = "\" C: \ \ C # Pro \ \ C # Script \ \ c # script.exe \ "\"% 1 \ "% *" O arquivo REG um script de registro que cria novas chaves e entradas conforme especificado. Em particular, ele cria um n chamado. Csx em HKEY_CLASSES_ROOT (HKCR). O n notifica o sistema que uma nova extenso de arquivo agora existe. O valor padro da chave definido como csxfile, que simplesmente uma referncia a outro n que contm informaes reais para lidar com o tipo de arquivo. Um novo n csxfile , ento, sempre criados sob HKCR. Esse n contm o cone padro para embelezar arquivos CSX no shell e os comandos para executar quando um arquivo aberto CSX ou editado. Isto o que o n csxfile no registro do sistema se parece (veja a Figura 2). Figura 2: Toda a informao que permite que o shell para lidar com arquivos CSX armazenado no \ n csxfile HKCR do registro Figura 2: Toda a informao que permite que o shell para lidar com arquivos CSX armazenado no \ n csxfile HKCR do registro Escusado ser dizer que o cone e os caminhos de comando na listagem acima so totalmente arbitrria. Certifique-se de modificar o arquivo REG para refletir o caminho real, onde voc instalar a sua cpia do c # script.exe. Fazer outras melhorias O que eu descrevi at agora uma verso bsica do aplicativo Script C #. Quando comecei a us-lo, uma srie de melhorias veio mente. At agora, as referncias de script C # somente um nmero fixo de assemblias e tambm importa um conjunto esttico de namespaces. Isto significa que o que voc pode realmente fazer com ele limitado em pelo menos duas maneiras. Primeiro, voc no pode usar qualquer assemblies personalizados ou qualquer sistema de montagem que no referenciado automaticamente. Em segundo lugar, voc no pode resolver qualquer NET funcionalidade. Quadro que requer um namespace que no est includo na lista, ou pelo menos que tem as assemblias contendo vinculados. Por exemplo, voc no pode usar o objeto StringBuilder porque requer a namespace System.Text. No entanto, a classe StringBuilder definido no conjunto mscorlib, que est ligado aplicao. Assim, se voc usou o nome totalmente qualificado da classe - System.Text.StringBuilder - ele iria trabalhar. Da mesma forma, voc no pode usar as funes de rede, a menos que descobrir uma maneira de importar o namespace System.Net eo conjunto relacionado.

Uma possvel soluo para todos os namespaces necessrios seriam necessrios usando a insero de clusulas na fonte, ou talvez atravs de uma sintaxe de costume como a abaixo. <% @ Import Namespace = "..."%> Isto mais fcil dizer do que fazer dado # Script C no analisa a fonte CSX. Nesta verso limitada a lavagem do trecho de cdigo em um construtor de corda onde outros elementos de sintaxe so adicionados para fazer a compilao de cdigo final. Alm disso, mesmo se voc tiver C # Script detectar e lidar com qualquer linha que comea com o uso, que ser de nenhuma ajuda para referenciar assemblias extra. Para fazer melhorias em relao a este ponto-chave, voc pode definir sua prpria sintaxe e adicionar um parser ou associar um arquivo que acompanha o trecho. Minha soluo baseada em arquivos XML adicionais que voc pode usar para criar um "projeto" em todo o trecho de cdigo. C # Script atribui um significado especial para qualquer arquivo XML encontrado na mesma pasta que o trecho cujo nome comea com o nome do arquivo CSX. Por exemplo, test.csx.xml o arquivo de projeto para test.csx. Esta lista mostra os contedos tpicos do arquivo de projeto: <CSharpScriptCompiler> <reference> <Nome> Utils.dll </ Name> </ Reference> <Import> <Nome> Expoware </ Name> </ Import> <Import> <Nome> System.Text </ Name> </ Import> </ CSharpScriptCompiler> O esquema XML se encaixa perfeitamente em um DataSet com duas tabelas: Referncia e Import. Ambas as tabelas tm uma nica coluna chamada nome. O cdigo abaixo um trecho do C # Script que demonstra como as informaes do projeto so recuperados e geridos: corda namespace = Path.GetFileName (CSFile); namespace = nameSpace.Replace ("_" "."); CSharpCodeProvider prov = new CSharpCodeProvider (); ICodeCompiler ICC = prov.CreateCompiler (); CompilerParameters cp = novos CompilerParameters (); cp.GenerateExecutable = true; cp.GenerateInMemory = true; / / Tente ler a partir do arquivo de configurao hasConfig bool = false;

DataSet configdata = new DataSet (); corda configFile = String.Format ("{0} xml.", CSFile); if ((File.Exists configFile)) { hasConfig = true; configData.ReadXml (configFile); } / / Adiciona referncias padro cp.ReferencedAssemblies.Add ( "System.dll"); cp.ReferencedAssemblies.Add ( "System.xml.dll"); cp.ReferencedAssemblies.Add ( "System.data.dll"); cp.ReferencedAssemblies.Add ( "System.windows.forms.dll"); / / Adicione referncias definidas pelo usurio if (hasConfig) { DataTable refs = configData.Tables ["referncia"]; foreach (DataRow linha refs.Rows) { cp.ReferencedAssemblies.Add ( row ["Nome"] ToString ()).; } } int usingLines = 7; / / Vamos adicionar 7 linhas StringBuilder sb = new StringBuilder (""); sb.Append ("using System; \ r \ n"); sb.Append ("usando System.Windows.Forms; \ r \ n "); sb.Append ("usando System.IO; \ r \ n"); sb.Append ("using System.Xml; \ r \ n"); sb.Append ("using System.Data; \ r \ n"); sb.Append ("usando System.Data.SqlClient; \ r \ n "); sb.Append ("usando System.Data.SqlTypes; \ r \ n "); / / Adicionar Imports definidos pelo usurio if (hasConfig) { Importaes DataTable =

configData.Tables ["Import"]; foreach (DataRow linha imports.Rows) { sb.AppendFormat ( "Usando {0}; \ r \ n", row ["Nome"] ToString ()).; usingLines + +; } } / / Obter o cdigo-fonte do Arquivo / / CSX StreamReader sr = new StreamReader (CSFile); fonte string = sr.ReadToEnd (); sr.Close (); / / Obter o cdigo fonte final de int linhas superiores GenerateWithMain = ( sb, fonte, namespace); corda finalSourceCode = sb.ToString (); / / Pronto para compilar agora Se voc usar uma montagem personalizada, ento voc deve coloc-lo na mesma pasta onde voc instalar o utilitrio de script C #. Apoiar mtodos personalizados Neste ponto, voc pode fazer referncia a qualquer reunio e fazer chamadas em qualquer namespace. No entanto, voc no pode criar mtodos ainda e so forados a escrever o trecho como um nico pedao de cdigo. H duas maneiras de eliminar essa lacuna. Um deles para adicionar capacidades de anlise de C # Script, extrair qualquer cdigo que no est includo em um mtodo ou funo e lave-o em principal ponto de entrada do C # Script. Outra abordagem, que mais simples de cdigo, mas igualmente eficaz, faz algumas suposies razoveis sobre a estrutura do arquivo CSX. Em particular, se as funes personalizados esto a ser utilizados, em seguida, o cdigo principal deve ser acondicionada em uma funo muito. Esta funo deve ser chamada main (). Minsculas importante porque Main () tem um significado especial o compilador C #. A funo main () no o ponto de partida real para o cdigo, que apenas um nome que voc lembra da sua funo lgica. Mesmo que o nome arbitrria, uma vez que voc faa a sua escolha voc deve ficar com ela e codific-lo na fonte Script C #. Ao concluir o trecho de cdigo para compilao, C # Script procura por um void main () substring. Se for encontrado, C # Script assume que voc deseja definir suas classes e mtodos. Como consequncia disto, o cdigo segue um esquema diferente: namespace test_csx {

public class CSharpScript () { public static void Main () { CSharpScript app = novo CSharpScript (); } pblico CSharpScript () { main (); { / / C # trecho que vai aqui / / Contm main () } } Para terminar o artigo, vamos escrever um cdigo de exemplo para demonstrar como C # Script realmente funciona. Uma das primeiras coisas que fiz quando aprendi sobre Amazon.com era escrever um pequeno aplicativo para fazer download de informaes sobre livros. Eu arranjei uma classe que faz uma chamada para o servio Web e serve informaes de volta para o chamador usando um DataTable. O script completo : TextBox txt; void main () { corda isbn = "0735619034"; Form f = new Form (); f.Text = "Amazon Livro Finder"; txt = new TextBox (); txt.Text = ISBN; Boto btn = new Button (); btn.Left = 130; btn.Text = "Localizar"; btn.Click + = new EventHandler (OnFind); f.Controls.Add (btn); f.Controls.Add (txt); f.ShowDialog (); } vazio OnFind (object sender, EventArgs e) { AmazonBooksInfo info = novo AmazonBooksInfo (); Detalhes DataTable = info.FindBook (txt.Text);

ShowData (detalhes, txt.Text); } vazio ShowData (DataTable dt, chave string) { StringBuilder sb = new StringBuilder (""); foreach (DataRow linha dt.Rows) { sb.AppendFormat ("{0} ({1}) {2} Hierarquia de venda \ n ", row ["ProductName"]. ToString (), linha [ "Fabricante"]. ToString () row ["SalesRank"]. ToString ()); } MessageBox.Show (sb.ToString () "Os resultados para \" "+ chave +" \ ""); } Ele cria um formulrio e preenche-lo com uma caixa de texto e um boto. O boto est ligado a um manipulador de eventos que acionado quando o usurio clica no boto. O manipulador usa uma classe personalizada em um assembly personalizado para se conectar a Amazon.com e pegar as informaes solicitadas (ver Figura 3). Figura 3: Os resultados gerados pela execuo do arquivo BookInfo.csx contra um ISBN livro Figura 3: Os resultados gerados pela execuo do arquivo BookInfo.csx contra um ISBN livro Graas s instalaes CodeDOM no quadro. NET, voc pode compilar o cdigo na mosca. E graas a. NET reflexo, voc pode carregar um assembly dinamicamente, instanciar suas classes, e chamar mtodos. Ao combinar esses recursos, eu constru um processador trecho C #. um pequeno executvel (8 KB), que completa o trecho C # armazenados em um arquivo CSX com classe e declaraes de namespace e executa-lo. Qual o problema? Agora voc pode usar C # como uma linguagem para gravar arquivos em lotes e automatizar tarefas. No etapa de compilao explcita necessria e no arquivos de projeto so necessrios. Basta escrever, clicar e ir embora. puro C #, puro. NET.

Artigo original.

Scripting with C#
This article was originally published in VSJ, which is now part of Developer Fusion. Its essential for developers and administrators to be able to automate tasks within the shell of the operating system. In the beginning, there were only MS-DOS batch files with their cryptic and compact syntax; they were ideal for die-hard, pure-blooded programmers, but not particularly suitable for the rest of us. After that, Microsoft introduced the Windows Script Host (WSH) environment, a COM-based runtime capable of processing VBScript and JScript files. WSH supports a good range of COM automation objects, and provides its own object model to manipulate the registry, the Windows shell, and the file system. It is not the perfect tool but, especially if you can create custom COM automation objects, it can be adapted to work in most common situations. WSH exists with different capabilities on all Windows operating systems, from Windows 98 on. So what about .NET? A scripting environment is an OS feature, whereas the .NET Framework is an external framework that includes a class library as well as a virtual machine. However, by using some of the .NET Framework capabilities, you can implement a purely managed script host using C# (or Visual Basic .NET) as the programming language. In this article, Ill build a small executable capable of processing a C# snippet to a compiled assembly. The tool completes the user-defined source code with proper namespace and class declarations, adds an entry point, and calls it back using reflection. As a result, you can write a C# code snippet, save it to a file, and double-click to run it. The big advantage over WSH is that the whole .NET Framework is available to your scripts, along with the power of a first-class language such as C#.

Start simple
This example came about when, one day, I wanted to test the behaviour of a certain class. I had quite a few options for testing the class quickly: I could use a testing tool like NUnit, I could write a console application, a Web page or, for the sake of mentioning all my options, an overkill Windows Forms application. Honestly, today I would use NUnit for the task; but this happened way back in the early days of .NET and Im not even sure NUnit was available then or, at least, I didnt know of it. This said, you know, every cloud has a silver lining and the batch compiler Im presenting here is still an interesting tool with some good applications. Basically, to test the class I didnt want to create a project and wake up the sleeping giant (read, Visual Studio .NET). Wouldnt it be great, I thought, if one could write C# code with the same ease as a WSH script? A few minutes later, I realised that the .NET Framework exposes the C# compiler as a class. Therefore, I deduced, writing a C# shell host for code snippets shouldnt be that hard. Suppose you, like me, are so absent-minded you need a program to remind you of important deadlines each morning as your computer starts up. This code does the job only too well:

DateTime deadline = new DateTime(2005, 12, 31); DateTime today = DateTime.Now; TimeSpan left = deadline today; string BaseText = {0} day(s) left to meet deadline.; MessageBox.Show(String.Format( BaseText, left.Days));

The hitch is that the code above wont compile. It correctly represents and implements the logic of the applet, but it is not written in a form that the C# compiler can understand and transform into an executable. To be compiled, the code snippet must be completed as follows:
class CSharpScript { static void Main() { // C# snippet goes here } }

The name of the class is arbitrary and can optionally be completed with an arbitrary namespace. The C#Script program described in this article does just that. It takes a file that contains a C# snippet, completes the code so that it can be successfully compiled, compiles it, loads the resulting assembly, and invokes its entry point. Next, you assign a typical extension to these C# code snippets (for example, .csx) and register the C#Script program to handle them when theyre opened through the shell or a console window. The dream of double-clicking a C# snippet, and having it run, comes true at last. Lets see how it happens.

Compile dynamically
Various components of the .NET platform make intensive use of dynamic compilation. The most illustrious example is probably the ASP.NET infrastructure. When you invoke an .aspx or .asmx resource for the first time, the ASP.NET runtime creates and compiles a class on the fly to represent the resource. The Web Service infrastructure also uses dynamically generated code to serialize data types to XML. The XmlSerializer class is in charge of this task and works by first creating a temporary assembly that contains optimized code for serialization. Dynamic compilation is possible because some .NET compilers expose an object model that lets .NET applications call them programmatically. The C#, Visual Basic .NET, and JScript .NET compilers are included in the list. It is important to note that dynamic compilation doesnt mean that you end up calling the compilers executable and configuring it through command line arguments. Instead, it means that you work with a .NET Framework class that implements the ICodeCompiler interface and incorporates the ability to talk to the real compiler tool. Ultimately, C# code is compiled by the csc.exe .NET compiler, but as a programmer you work with properties and methods of a compiler class. To compile C# code programmatically, you start by importing the System.CodeDom.Compiler and Microsoft.CSharp namespaces. Both are defined within the System assembly so you dont have to reference anything else. The key class is CSharpCodeProvider. It implements the ICodeCompiler interface and provides a selfexplanatory method such as CreateCompiler.
CSharpCodeProvider prov = new CSharpCodeProvider();

ICodeCompiler compiler = prov.CreateCompiler();

At this point, you hold an in-memory instance of the object that represents the C# compiler (csc.exe). The next step is configuring the compiler. You set compiler parameters through an instance of the CompilerParameters class.
CompilerParameters cp = new CompilerParameters(); cp.GenerateExecutable = true; cp.GenerateInMemory = true;

The GenerateExecutable property indicates whether you want a DLL or an executable. GenerateInMemory lets you indicate the storage medium for the resulting assembly. You can create it in memory or save to a particular file. Notice that a temporary file is created even if you set GenerateInMemory to true. Any assemblies that will be referenced during compilation must be added to the ReferencedAssemblies collection.
cp.ReferencedAssemblies.Add(system.dll); cp.ReferencedAssemblies.Add(system.xml.dll); cp.ReferencedAssemblies.Add(system.data.dll); cp.ReferencedAssemblies.Add(system.windows.forms.dll);

Notice that you must indicate the .dll extension too. The ICodeCompiler interface provides several compile methods. Two in particular are worth mentioning: CompileAssemblyFromFile and CompileAssemblyFromSource. As the names suggest, the former compiles an existing C# source file. The latter, instead, compiles from a memory string. In this case, you must read the code snippet from the source file, complete it with class and entry point information, and finally compile from the string.
CompilerResults cr; cr = compiler.CompileAssemblyFromSource(cp, finalSource);

The final source code is built using a string builder to wrap the code snippet with class and namespace declarations. Heres the procedure that does the job:
int GenerateWithMain(StringBuilder sb, string csSource, string nameSpace) { // Complete the source code to make it compilable sb.AppendFormat(\r\nnamespace {0}\r\n, nameSpace); sb.Append({\r\n); sb.Append(class CSharpScript {\r\n); sb.Append(static void Main() {\r\n); sb.Append(csSource); sb.Append(}\r\n}\r\n}\r\n); // Returns how many lines of text are added before // actual CSX code is found return 5; }

The dynamically generated code includes a namespace based on the name of the source file. If the script lives in a file named test.csx, the corresponding namespace will be test_csx. What about the namespaces to import? The C#Script application adds a bunch of standard namespaces using clauses outside the class declaration. The list of standard imported namespaces is arbitrary. In this sample code I managed to include System.Data, System.Xml, System.IO, and System.Windows.Forms. The code in the listing above ends with a rather funky return value. Why is it 5, you might be wondering? It has to do with error handling.

Handle Compiler Errors


The CompilerResults class is filled with information obtained from the compiler. It references the native compilers return value, any temporary files generated, output

messages, and errors. Errors in particular are returned packed in a collection property named Errors. The C#Script executable grabs this information and prepares a custom error message. Heres an example:
if (cr.Errors.HasErrors) { StringBuilder sbErr; sbErr = new StringBuilder( Compiling file: ); sbErr.AppendFormat( \{0}\, csfile); sbErr.Append(\n\n); foreach(CompilerError err in cr.Errors) { sbErr.AppendFormat({0} at line {1} column {2} , err.ErrorText, err.Line, err.Column); sbErr.Append(\n); } MessageBox.Show(sbErr.ToString(), C#Script Error); return; }

Each error is represented with a CompilerError object. It exposes useful information such as the line and the column where the error occurred and the message describing the error. Note that the Line property refers to the line in the final file the one that it is being compiled to. This information is not precise because the user doesnt know anything about the scripts internal structure. To be really helpful, the line number must refer to the line in the source CSX file. You must subtract some lines from the value returned by the Line property. In particular, you must subtract one line for each using clause you added and one for each line of text used to complete the source. The 5 returned by the GenerateWithMain procedure indicates that five new rows have been inserted before the source C# snippet to compile is found in the final code. Ive included a message box that signals a syntax error (see Figure 1). The line number matches the line in the CSX snippet file perfectly.

Figure 1: When a compile error is caught, a message with a full description and line and column information pops up

Invoke the entry point


If the compiler succeeds, the compiled assembly is automatically loaded in memory and returned through the CompiledAssembly property of the CompilerResults class. The next step entails creating an instance of the class in the assembly and invoking the entry point. By design, the assembly contains just one class with a hard coded name CSharpScript and a static method Main().

Using reflection, you can obtain method information about the entry point and invoke it with or without parameters. This code demonstrates how invoke a parameterless Main method:
Assembly a = cr.CompiledAssembly; try { object o = a.CreateInstance( CSharpScript); MethodInfo mi = a.EntryPoint; mi.Invoke(o, null); } catch(Exception ex) { MessageBox.Show(ex.Message); }

Any error that occurs within the method is trapped and a message box is displayed. Notice that the try/catch block catches only reflection errors so the final message doesnt specify the error that occurred. To get more specific information, you should use a try/catch block directly in your CSX code. Alternatively, you can add a try/catch block in your GenerateWithMain() method.

Integrate it with the shell


So far Ive built a small and compact executable c#script.exe that takes a file name on the command line that contains a C# code snippet. The program completes the source code to make it compile and loads the resulting assembly. Because the original goal was providing a .NET replacement for the WSH environment, youll need to take one more step to integrate the tool with the Windows shell. When you double-click on a file within the shell, Explorer looks for an application that is registered to open files of that type. If such an application exists, it is invoked passing the file name on the command line. This schema is supplied for a variety of files, including VBS and JS files, but not CSX files. Registering an association between an executable and a file type requires tweaking the registry. Take a look at a REG file that binds c#script.exe to CSX files and also registers a default icon for them:
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\.csx] @=csxfile [HKEY_CLASSES_ROOT\csxfile] @=C# Script [HKEY_CLASSES_ROOT\csxfile\DefaultIcon] @=youricon.ico,0 [HKEY_CLASSES_ROOT\csxfile\Shell] [HKEY_CLASSES_ROOT\csxfile\Shell\Edit] [HKEY_CLASSES_ROOT\csxfile\Shell\Edit\ Command] @=notepad.exe %1 [HKEY_CLASSES_ROOT\csxfile\Shell\Open] [HKEY_CLASSES_ROOT\csxfile\Shell\Open\ Command] @=\c:\\C#Pro\\C#Script\\ c#script.exe\ \%1\ %*

The REG file is a registry script that creates new keys and entries as specified. In particular, it creates a node named .csx under HKEY_CLASSES_ROOT (HKCR). The node notifies the system that a new file extension now exists. The default value of the key is set to csxfile, which is simply a reference to another node that contains actual information to handle the file type. A new csxfile node is then always created under HKCR. This node contains the default icon to embellish CSX files in the shell and the commands to execute when a CSX file is opened or edited. This is what the csxfile node in the system registry looks like (see Figure 2).

Figure 2: All the information that enables the shell to handle CSX files is stored in the HKCR\csxfile node of the registry

It goes without saying that the icon and the command paths in the listing above are totally arbitrary. Make sure you modify the REG file to reflect the real path where you install your copy of c#script.exe.

Make further enhancements


What Ive described so far is a basic version of the C#Script application. As I began using it, a number of enhancements sprang to mind. So far, C#Script references only a fixed number of assemblies and also imports a static set of namespaces. This means that what you can really do with it is limited in at least two ways. First, you cant use any custom assemblies or any system assembly that is not automatically referenced. Second, you cant address any .NET Framework functionality that requires a namespace thats not included in the list, or at least that has the containing assemblies linked. For example, you cant use the StringBuilder object because it requires the System.Text namespace. However, the StringBuilder class is defined in the mscorlib assembly, which is linked to the application. Thus if you used the fully qualified name of the class System.Text.StringBuilder it would work. Likewise, you cannot use network functions unless you figure out a way to import the System.Net namespace and the related assembly. A possible workaround for any needed namespaces would be inserting required using clauses in the source or perhaps through a custom syntax like the one below.
<%@ Import Namespace=... %>

This is easier said than done given C#Script doesnt parse the CSX source. In this version it is limited to flushing the code snippet into a string builder where other syntax elements are added to make the final code compile. Furthermore, even if you have

C#Script detect and handle any line that begins with using, that will be of no help for referencing extra assemblies. To make improvements regarding this key point, you can either define your own syntax and add a parser or associate a companion file to the snippet. My solution is based on additional XML files that you can use to create a project around the code snippet. C#Script assigns a special meaning to any XML file found in the same folder as the snippet whose name begins with the name of the CSX file. For example, test.csx.xml is the project file for test.csx. This listing shows the typical contents of the project file:
<CSharpScriptCompiler> <Reference> <Name>utils.dll</Name> </Reference> <Import> <Name>Expoware</Name> </Import> <Import> <Name>System.Text</Name> </Import> </CSharpScriptCompiler>

The XML schema fits perfectly to a DataSet with two tables: Reference and Import. Both tables have a single column called Name. The code below is an excerpt from C#Script that demonstrates how project information is retrieved and managed:
string nameSpace = Path.GetFileName(csfile); nameSpace = nameSpace.Replace(., _); CSharpCodeProvider prov = new CSharpCodeProvider(); ICodeCompiler icc = prov.CreateCompiler(); CompilerParameters cp = new CompilerParameters(); cp.GenerateExecutable = true; cp.GenerateInMemory = true; // Try to read from the config file bool hasConfig = false; DataSet configData = new DataSet(); string configFile = String.Format({0}.xml, csfile); if (File.Exists(configFile)) { hasConfig = true; configData.ReadXml(configFile); } // Add default references cp.ReferencedAssemblies.Add( system.dll); cp.ReferencedAssemblies.Add( system.xml.dll); cp.ReferencedAssemblies.Add( system.data.dll); cp.ReferencedAssemblies.Add( system.windows.forms.dll); // Add user-defined references if (hasConfig) { DataTable refs =

configData.Tables[Reference]; foreach(DataRow row in refs.Rows) { cp.ReferencedAssemblies.Add( row[Name].ToString()); } } int usingLines = 7; // going to add 7 lines StringBuilder sb = new StringBuilder(); sb.Append(using System;\r\n); sb.Append(using System.Windows.Forms;\r\n); sb.Append(using System.IO;\r\n); sb.Append(using System.Xml;\r\n); sb.Append(using System.Data;\r\n); sb.Append(using System.Data.SqlClient;\r\n); sb.Append(using System.Data.SqlTypes;\r\n); // Add user-defined Imports if (hasConfig) { DataTable imports = configData.Tables[Import]; foreach(DataRow row in imports.Rows) { sb.AppendFormat( using {0};\r\n, row[Name].ToString()); usingLines++; } } // Get the source code from the // CSX file StreamReader sr = new StreamReader(csfile); string source = sr.ReadToEnd(); sr.Close(); // Get the final source code int topLines = GenerateWithMain( sb, source, nameSpace); string finalSourceCode = sb.ToString(); // Ready to compile now

If you use a custom assembly, then you must place it in the same folder where you install the c#script utility.

Supporting custom methods


At this point, you can reference any assembly and make calls into any namespace. However, you cant create methods yet and are forced to write your snippet as a single piece of code. There are two ways to eliminate this shortcoming. One is to add parsing capabilities to C#Script, extract any code thats not included in a method or function and flush it in the C#Scripts Main entrypoint. Another approach, which is simpler to

code but equally effective, makes some reasonable assumptions about the structure of the CSX file. In particular, if custom functions are to be used, then the main code must be wrapped in a function too. This function must be named main(). Lowercase is important because Main() has a special meaning the C# compiler. The main() function is not the real entry point to the code; it is just a name that reminds you of its logical role. Even though the name is arbitrary, once you make your choice you must stick with it and hardcode it in the C#Script source. When completing the code snippet for compilation, C#Script looks for a void main() substring. If its found, C#Script assumes that you want to define your classes and methods. As a result, the code completion follows a different scheme:
namespace test_csx { public class CSharpScript() { public static void Main() { CSharpScript app = new CSharpScript(); } public CSharpScript() { main(); { // C# snippet goes here that // contains main() } }

To round off the article, lets write some sample code to demonstrate how C#Script really works. One of the first things I did when learned about Amazon.com was to write a small application to download information about books. I arranged a class that makes a call to the Web service and serves information back to the caller using a DataTable. The full script is:
TextBox txt; void main() { string isbn = 0735619034; Form f = new Form(); f.Text = Amazon Book Finder; txt = new TextBox(); txt.Text = isbn; Button btn = new Button(); btn.Left = 130; btn.Text = Find; btn.Click += new EventHandler(OnFind); f.Controls.Add(btn); f.Controls.Add(txt); f.ShowDialog(); }

void OnFind(object sender, EventArgs e) { AmazonBooksInfo info = new AmazonBooksInfo(); DataTable details = info.FindBook(txt.Text); ShowData(details, txt.Text); } void ShowData(DataTable dt, string key) { StringBuilder sb = new StringBuilder(); foreach(DataRow row in dt.Rows) { sb.AppendFormat({0} ({1}) -{2} sales rank\n, row[ProductName]. ToString(), row[ Manufacturer].ToString(), row[SalesRank]. ToString()); } MessageBox.Show(sb.ToString(), Results for \ + key + \); }

It creates a form and populates it with a textbox and a button. The button is bound to an event handler that fires when a user clicks the button. The handler uses a custom class in a custom assembly to connect to Amazon.com and grab the requested information (see Figure 3).

Figure 3: The results generated by running the BookInfo.csx file against a book ISBN

Thanks to the CodeDOM facilities in the .NET Framework, you can compile code on the fly. And thanks to .NET reflection, you can load an assembly dynamically, instantiate its classes, and call methods. By combining these features, Ive built a C# snippet processor. It is a small (8KB) executable that completes the C# snippet stored in a CSX file with class and namespace declarations and runs it. Whats the deal? You can now use C# as a language for writing batch files and automating tasks. No explicit compilation step is required and no project files are needed. Just write, click, and go. Its pure C#, pure .NET.

Você também pode gostar