C# Rodar com dotnet run

Na aula anterior, você rodou o primeiro programa com dotnet run ola.cs. Esse comando é a ferramenta que você vai usar em todo o Nível 1 e no Nível 2, então vale conhecê-lo bem. Nesta aula você descobre o que ele faz por dentro, por que a primeira vez demora e a segunda não, como passar informações para o programa, o que é o código de saída e como transformar um arquivo .cs num script que roda sozinho.

Você vai aprender
  • O que são os aplicativos de arquivo único (file-based apps), a novidade do .NET 10.
  • O que o dotnet run ola.cs faz por dentro e onde ele guarda o programa compilado.
  • Por que a primeira vez demora, e por que os avisos do compilador só aparecem quando ele compila de novo.
  • Como passar argumentos ao programa, e para que serve o --.
  • Os códigos de saída 0, 1 e 134.
  • As diretivas #:package e #:property, no topo do arquivo.
  • Como rodar um .cs como script, com #!/usr/bin/env dotnet.
  • Quando é hora de transformar o arquivo num projeto.

As saídas são da máquina da Ana, com o .NET SDK 10.0.112, na pasta ~/cs. Os projetos com dotnet new console têm aula própria: Projetos (dotnet new console).


A ideia: a padaria que guarda o bolo

Imagine uma padaria que recebe a sua receita, assa o bolo e entrega. Se amanhã você aparecer com a mesma receita, a padaria não assa de novo: ela já guardou um bolo igual na geladeira e entrega na hora. Se você riscar uma linha da receita, ela percebe e assa um bolo novo.

O dotnet run ola.cs é essa padaria. Na primeira vez, ele compila o arquivo e guarda o programa pronto numa pasta escondida. Nas outras vezes, ele confere se o ola.cs mudou: se não mudou, roda o programa guardado; se mudou, compila de novo.


1. O comando

$ dotnet run ola.cs
Olá, mundo!
dotnet
O comando do .NET SDK.
run
"Rodar": compila (se precisar) e roda.
ola.cs
O arquivo com o programa. Ele tem de estar na pasta atual (ou você escreve o caminho: dotnet run ~/cs/ola.cs).

Até o .NET 9, o dotnet run só sabia rodar projetos: uma pasta com um arquivo .csproj que descreve o programa (aula Projetos). O .NET 10 aprendeu a rodar um arquivo .cs sozinho, sem projeto. A Microsoft chama isso de aplicativo baseado em arquivo (file-based app). É o jeito mais simples de começar, e é o que o guia usa nos Níveis 1 e 2.

Três jeitos de escrever

$ dotnet run ola.cs                             « o jeito do guia
Olá, mundo!
$ dotnet ola.cs                                 « atalho: sem o run
Olá, mundo!
$ dotnet run --file ola.cs                      « explícito: "isto é um arquivo"
Olá, mundo!
FormaQuando usar
dotnet run ola.csSempre, no guia. Deixa claro que você está rodando.
dotnet ola.csAtalho para quem já está acostumado. Faz o mesmo.
dotnet run --file ola.csQuando a pasta também tem um projeto e você quer deixar claro que é o arquivo (veja Quando dá errado).

2. O que acontece por dentro

Quando você roda dotnet run ola.cs, o SDK faz isto:

  1. Monta um projeto invisível para o arquivo, com as configurações padrão do .NET 10 (parecidas com as de um projeto dotnet new console).
  2. Confere se precisa compilar. Se o programa já foi compilado e o ola.cs não mudou, pula para o passo 4.
  3. Compila com o Roslyn. Se houver erro, mostra a mensagem e para aqui.
  4. Roda o programa compilado, que escreve na tela.

Onde fica o programa compilado

Na pasta da Ana, nada muda: só o ola.cs continua lá. O programa compilado vai para uma pasta escondida. O dotnet build, que compila sem rodar, mostra o endereço:

$ dotnet build ola.cs
  Determinando os projetos a serem restaurados...
  Todos os projetos estão atualizados para restauração.
  ola -> /home/ana/.local/share/dotnet/runfile/ola-d3e0b993ea942ae2aa43a2c132f9667a87cb5c1410a99c454aae33ed39b33823/bin/debug/ola.dll

Cada arquivo que você roda ganha uma pasta própria em ~/.local/share/dotnet/runfile, com o nome do arquivo e um código comprido, calculado a partir do caminho do arquivo (por isso dois ola.cs em pastas diferentes não se misturam):

$ ls ~/.local/share/dotnet/runfile | head -3
antigo-dd3e9aa2d82fb4bbb24b83889955c931d21639ff05141ce927617435495868b8
args-538ef144dfff177e98d916b3ae5074306e8a7492f32d9b712d144636630addb0
aspas2-50be6b2113d0c1ce0f72ed21813b15054498c67a37acb8b811ee496d04c3fef8

Dentro de cada uma está o ola.dll e os arquivos que a aula .NET: SDK, Runtime e CLR abriu um por um. Você nunca precisa mexer nessa pasta.

A primeira vez e as outras

O comando time, na frente de outro comando, mede quanto tempo ele levou. Veja um arquivo novo, tempo.cs, rodado duas vezes seguidas:

$ time dotnet run tempo.cs
Testando o tempo

real	0m3,800s                                « 1.ª vez: compilou (quase 4 segundos)
user	0m0,405s
sys	0m0,060s
$ time dotnet run tempo.cs
Testando o tempo

real	0m0,281s                                « 2.ª vez: só rodou (menos de 0,3 s)
user	0m0,223s
sys	0m0,060s

A linha real é o tempo de relógio. Na primeira vez, o SDK precisou acordar o compilador e compilar: quase 4 segundos. Na segunda, o arquivo não tinha mudado, e ele só rodou o programa guardado. Depois que você muda o arquivo, ele compila de novo, mas bem mais rápido, porque o compilador fica de prontidão em segundo plano por um tempo:

$ echo 'Console.WriteLine("Tchau!");' >> ola.cs   « acrescenta uma linha no fim do ola.cs
$ time dotnet run ola.cs
Olá, mundo!
Tchau!                                          « mudou o arquivo: compilou de novo

real	0m0,516s
user	0m0,316s
sys	0m0,073s

O editor do guia não guarda nada: ele compila o código de novo toda vez que você clica em Rodar.


3. Os avisos só aparecem quando compila

Além dos erros, que impedem o programa de rodar, o compilador dá avisos (warning): "isto funciona, mas parece engano". Por exemplo, uma variável criada e nunca usada (variáveis são da aula Variáveis):

int idade;
Console.WriteLine("Programa com aviso");
$ dotnet run aviso.cs
/home/ana/cs/aviso.cs(1,5): warning CS0168: A variável "idade" está declarada, mas nunca é usada
Programa com aviso                              « com o aviso, o programa rodou
$ dotnet run aviso.cs
Programa com aviso                              « da segunda vez, o aviso sumiu!

O aviso sumiu na segunda vez porque o arquivo não mudou e o dotnet run não compilou de novo; quem dá o aviso é o compilador. O problema continua lá. Para obrigar o dotnet run a compilar de novo, e ver os avisos, use --no-cache ("sem o guardado"):

$ dotnet run --no-cache aviso.cs
/home/ana/cs/aviso.cs(1,5): warning CS0168: A variável "idade" está declarada, mas nunca é usada
Programa com aviso

Leve os avisos a sério desde já: quase sempre eles apontam um engano de verdade. A aula Avisos do Compilador mostra os mais comuns.

Missão: rode e leia o aviso CS0168. Depois apague a linha da variável que nunca é usada e troque o texto para Programa sem aviso. Rode de novo: o aviso tem de sumir.

int idade;
Console.WriteLine("Programa com aviso");
Console.WriteLine("Programa sem aviso");

4. Passar argumentos ao programa

Você pode mandar informações para o programa direto no comando, depois do nome do arquivo. Elas se chamam argumentos e chegam ao programa numa lista chamada args. Este programa conta quantos argumentos recebeu e mostra cada um (o foreach é da aula foreach, e o args tem aula própria, Argumentos da Linha de Comando):

Console.WriteLine($"Recebi {args.Length} argumentos");
foreach (var a in args) Console.WriteLine(a);
$ dotnet run args.cs Ana 29
Recebi 2 argumentos
Ana
29
$ dotnet run args.cs -- "Ana Souza" 29          « com aspas, "Ana Souza" é um argumento só
Recebi 2 argumentos
Ana Souza
29
Ana 29
Dois argumentos, separados por espaço.
"Ana Souza"
Entre aspas, o espaço não separa: é um argumento só. As aspas são do terminal e não chegam ao programa.
--
Os dois tracinhos dizem ao dotnet: "daqui para a frente, nada é para você, é tudo para o programa". Sem ele, funciona na maioria das vezes, mas um argumento que começa com - pode ser "roubado" pelo dotnet (veja Quando dá errado). Use sempre que tiver dúvida.

A caixa abaixo passa os argumentos Ana 29 para o programa, como se você tivesse digitado dotnet run args.cs Ana 29:

Experimente: rode e compare com a saída da Ana. Depois acrescente uma linha no fim, Console.WriteLine("Fim da lista");, e rode de novo.

Console.WriteLine($"Recebi {args.Length} argumentos");
foreach (var a in args) Console.WriteLine(a);

5. O código de saída passa adiante

O código de saída (aula Olá, Mundo) do dotnet run é o do seu programa. Assim, um script que roda o seu programa sabe se ele deu certo. Há três casos para conhecer:

$ cat saida3.cs
Console.WriteLine("Saindo com 3");
return 3;
$ dotnet run saida3.cs; echo "código: $?"
Saindo com 3
código: 3                                       « o número do return
$ dotnet run semponto.cs; echo "código: $?"
/home/ana/cs/semponto.cs(1,33): error CS1002: ; esperado

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
código: 1                                       « a compilação falhou: o programa nem rodou
$ cat excecao.cs
int[] notas = { 8, 9 };
Console.WriteLine(notas[2]);
$ dotnet run excecao.cs; echo "código: $?"
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<Main>$(String[] args) in /home/ana/cs/excecao.cs:line 2
código: 134                                     « o programa rodou e "quebrou"
CódigoQuer dizer
0O programa rodou até o fim, sem problemas
1A compilação falhou (erro CSxxxx), ou o dotnet não achou o arquivo
o número do returnO programa escolheu o código (return 3;)
134O programa compilou, rodou e parou no meio com uma exceção não tratada

O último caso é diferente dos outros: o compilador não viu problema nenhum, mas, ao rodar, o programa tentou pegar a terceira nota (notas[2]) de uma lista que só tem duas. A mensagem sai em inglês (Unhandled exception = "exceção não tratada"; Index was outside the bounds of the array = "o índice estava fora dos limites do array") e diz a linha: excecao.cs:line 2. O 134 é o código que o Linux dá a um programa que foi abortado. As exceções têm uma seção inteira no Nível 2, a partir de Exceções.

Missão: rode e veja a exceção e o código de saída 134. A lista tem duas notas, nas posições 0 e 1 (em C#, a contagem começa do zero). Troque o 2 por 1 para mostrar a última nota, 9, e rode de novo: o código de saída tem de virar 0.

int[] notas = { 8, 9 };
Console.WriteLine(notas[2]);
int[] notas = { 8, 9 };
Console.WriteLine(notas[1]);

6. As diretivas #: no topo do arquivo .NET 10

Um projeto guarda as suas configurações no arquivo .csproj. Um arquivo solto não tem .csproj, então o .NET 10 criou outro lugar para elas: linhas especiais no topo do .cs, que começam com #: e se chamam diretivas. As duas mais úteis:

DiretivaO que fazNo projeto, seria
#:package Nome@versãoUsa um pacote do NuGet, a "loja" de bibliotecas do .NET<PackageReference> (aula Pacotes NuGet)
#:property Nome=valorMuda uma configuração do build, como a versão do C#uma linha no <PropertyGroup> (aula O Arquivo .csproj)

Um exemplo com #:package: a biblioteca Humanizer transforma números, datas e nomes em texto "de gente". A primeira linha do arquivo pede a versão 2.14.1 dela:

#:package Humanizer@2.14.1
using Humanizer;

Console.WriteLine(TimeSpan.FromMinutes(90).Humanize(2));
Console.WriteLine("notas_da_ana".Humanize());
$ time dotnet run pacote.cs
1 hora, 30 minutos                              « 90 minutos, em português
notas da ana                                    « o nome com _ virou frase

real	0m11,170s                               « 1.ª vez: baixou o pacote da internet
user	0m6,069s
sys	0m0,360s
$ dotnet run pacote.cs
1 hora, 30 minutos                              « da segunda vez, já está no computador
notas da ana

Na primeira vez, o dotnet baixou o pacote do site nuget.org e guardou em ~/.nuget/packages (por isso os 11 segundos); depois, ele usa a cópia guardada. O using Humanizer; é da aula Namespaces e using. A diretiva #:property aparece na aula Versões, para escolher a versão do C#. As diretivas precisam ficar no topo, antes de qualquer código. No editor do guia, que roda no navegador, a diretiva #:package não baixa nada, e o mesmo código dá o erro CS0246 (O nome do tipo ou do namespace "Humanizer" não pode ser encontrado): os exemplos com pacotes são para o terminal.


7. Rodar como um script

No Linux e no macOS, um arquivo de texto vira um comando quando tem duas coisas: a permissão de execução e uma primeira linha especial, o shebang, que diz qual programa deve lê-lo. O .NET 10 aceita o shebang num .cs:

#!/usr/bin/env dotnet
Console.WriteLine("Rodei como um script!");
$ chmod +x script.cs                            « dá permissão de execução
$ ls -l script.cs
-rwxrwxr-x 1 ana ana 66 out 17 12:42 script.cs  « o x quer dizer "executável"
$ ./script.cs                                   « roda direto, sem escrever dotnet run
Rodei como um script!
#!
O shebang (o nome vem de hash + bang, os nomes em inglês do # e do !). Tem de ser os dois primeiros caracteres do arquivo.
/usr/bin/env dotnet
"Procure o programa dotnet e entregue este arquivo a ele." O env procura o dotnet onde quer que ele esteja instalado.
chmod +x
Dá ao arquivo a permissão de execução (x). O chmod é explicado no Guia Bash.
./script.cs
Roda o arquivo desta pasta. Por baixo, é o mesmo dotnet run script.cs, com a mesma pasta escondida e a mesma rapidez da segunda vez.

Para o compilador, a linha do shebang não é código: ele simplesmente a ignora. No Windows, que não usa shebang, o mesmo arquivo continua rodando com dotnet run script.cs.


8. Quando o arquivo cresce: vire projeto

O arquivo solto é ótimo para aprender e para programas pequenos. Quando o programa precisar de vários arquivos .cs, de testes ou de configurações mais elaboradas, é hora de um projeto. O .NET 10 converte o arquivo para você:

$ cd ~/conv
$ dotnet project convert ola.cs                 « cria um projeto a partir do arquivo
$ ls -R
.:
ola                                             « a pasta do projeto, nova
ola.cs                                          « o arquivo original continua aqui

./ola:
ola.cs                                          « uma cópia do código
ola.csproj                                      « o arquivo do projeto

O comando não escreveu nada na tela (silêncio quer dizer sucesso) e criou a pasta ola com um ola.csproj, onde ficam as configurações que antes eram invisíveis (e as diretivas #:, se houvesse). A partir daí, você entra na pasta e usa dotnet run sem o nome do arquivo, como na aula Projetos (dotnet new console).

Arquivo solto ou projeto?

SituaçãoUse
Aprender, testar uma ideia, um programa de um arquivoArquivo solto: dotnet run ola.cs
Um script para automatizar uma tarefa no LinuxArquivo solto com shebang
Um programa com vários arquivos .csProjeto: dotnet new console (ou dotnet project convert)
Testes automáticos, bibliotecas, soluções com vários projetosProjeto (aulas do grupo Ferramentas)

9. Limpar a pasta escondida

Cada arquivo que você roda deixa uma pasta em ~/.local/share/dotnet/runfile. Elas são pequenas, mas, depois de meses de estudo, viram centenas. O comando dotnet clean file-based-apps apaga as que não são usadas há 30 dias ou mais. Com --dry-run ("ensaio"), ele só diz o que faria:

$ dotnet clean file-based-apps --dry-run
Procurando pastas para remover em: /home/ana/.local/share/dotnet/runfile
Pastas que seriam removidas: 0                  « tudo da Ana foi usado hoje

Para apagar de verdade, rode sem o --dry-run. Para mudar o prazo, use --days: --days 7 apaga as que estão paradas há uma semana. Não tem perigo: se você rodar o arquivo de novo, o dotnet compila de novo.


Quando dá errado

Não foi possível localizar um projeto para executar.
$ dotnet run ole.cs
Não foi possível localizar um projeto para executar. Verifique se existe um projeto em /home/ana/cs ou passe o caminho para o projeto usando --project.
$ echo $?
1

O que significa: o arquivo ole.cs não existe (a Ana errou o nome: é ola.cs). Quando o arquivo não existe, o dotnet run acha que ole.cs é um argumento e procura um projeto na pasta para rodar. Não acha, e reclama do projeto. A mensagem engana: o problema é o nome do arquivo. O mesmo acontece se você estiver na pasta errada, ou escrever o nome sem o .cs (dotnet run ola).

Como consertar: ls e confira o nome exato. A tecla Tab completa o nome do arquivo e evita o erro.

Na pasta de um projeto: error CS8802: Apenas uma unidade de compilação pode ter instruções de nível superior.
$ ls
obj
ola.cs
Program.cs
Teste.csproj
$ dotnet run ola.cs
/home/ana/projetos/Teste/Program.cs(2,1): error CS8802: Apenas uma unidade de compilação pode ter instruções de nível superior. [/home/ana/projetos/Teste/Teste.csproj]

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que significa: a Ana copiou o ola.cs para dentro da pasta de um projeto (a que tem o Teste.csproj). Quando a pasta tem um projeto, o dotnet run roda o projeto, e não o arquivo: o ola.cs virou um argumento. Pior: o projeto compila todos os .cs da pasta juntos, e agora há dois arquivos com instruções de nível superior (o Program.cs e o ola.cs). Só um pode ter ("apenas uma unidade de compilação", isto é, um arquivo).

Como consertar: não misture arquivos soltos com projetos. Guarde os arquivos soltos numa pasta só deles (como a ~/cs da Ana) e os projetos em outra (~/projetos).

Um argumento com - foi "roubado" pelo dotnet
$ dotnet run args.cs --help | head -4
Description:
  Comando Run do .NET

Uso:
$ dotnet run args.cs -- --help
Recebi 1 argumentos
--help

O que significa: o --help era para o programa, mas o dotnet run achou que era para ele e mostrou a própria ajuda. O programa nem rodou.

Como consertar: ponha -- antes dos argumentos do programa: tudo o que vem depois dele vai direto para o args.

./script.cs: Permissão negada
$ ./semx.cs
bash: ./semx.cs: Permissão negada
$ echo $?
126

O que significa: o arquivo tem o shebang, mas não tem a permissão de execução. O código 126 é o do terminal para "achei, mas não posso executar".

Como consertar: chmod +x semx.cs. Ou rode do jeito normal, dotnet run semx.cs, que não precisa de permissão.

Script vindo do Windows: env: ‘dotnet\r’: Arquivo ou diretório inexistente
$ ./crlf.cs
env: ‘dotnet\r’: Arquivo ou diretório inexistente
env: usa -[v]S para passar opções em linhas shebang
$ echo $?
127

O que significa: o arquivo foi salvo no Windows, onde cada linha termina com dois caracteres invisíveis (\r\n), e não só com \n, como no Linux. O \r grudou no fim do shebang, e o env procurou um programa chamado dotnet\r, que não existe. (A segunda linha é só uma dica do env que não tem a ver com o problema.)

Como consertar: converta o fim das linhas: no VS Code, clique em CRLF no canto de baixo da janela e escolha LF; no terminal, sed -i 's/\r$//' crlf.cs. O dotnet run crlf.cs funciona mesmo com \r\n; só o shebang se confunde.


Referência rápida

ComandoO que faz
dotnet run ola.csCompila (se precisar) e roda o arquivo
dotnet ola.csO mesmo, mais curto
dotnet run --file ola.csO mesmo, dizendo explicitamente que é um arquivo
dotnet run ola.cs -- a b cPassa a, b e c para o args
dotnet run --no-cache ola.csCompila de novo mesmo sem mudança (e mostra os avisos)
dotnet run ola.cs -c ReleaseCompila na configuração de entrega, otimizada
dotnet build ola.csSó compila, e diz onde ficou o .dll
dotnet project convert ola.csCria um projeto a partir do arquivo
dotnet clean file-based-appsApaga as pastas escondidas paradas há 30 dias
#:package Nome@versãoUsa um pacote NuGet (no topo do arquivo)
#:property Nome=valorMuda uma configuração do build (no topo do arquivo)
#!/usr/bin/env dotnet + chmod +xRoda o arquivo como script: ./ola.cs

Exercícios

1. Compilar de novo

Complete o comando que obriga o dotnet run a compilar de novo, para ver os avisos:

dotnet run aviso.cs

2. Argumento com traço

Complete o comando para o programa receber o argumento -v, sem o dotnet roubá-lo:

dotnet run args.cs -v

3. O shebang

Complete a primeira linha de um script em C#:

#!/usr/bin/env

4. Você rodou um programa com aviso duas vezes, sem mudar nada. Na segunda vez, o aviso não apareceu. Por quê?

5. O dotnet run terminou com o código de saída 134. O que aconteceu?

6. Onde o dotnet run ola.cs guarda o programa compilado?

7. Numa pasta com um .csproj, o dotnet run ola.cs deu o erro CS8802. Por quê?

Missão final: o programa que cumprimenta

Missão: a caixa roda o programa como dotnet run ola.cs Ana. Faça o programa cumprimentar quem veio no primeiro argumento, args[0] (a contagem começa do zero): troque a linha por Console.WriteLine($"Olá, {args[0]}!"); e rode. A saída deve ser Olá, Ana!.

Console.WriteLine("Olá, mundo!");
Console.WriteLine($"Olá, {args[0]}!");

No seu terminal, rode o mesmo arquivo com dotnet run ola.cs Bia e com dotnet run ola.cs, sem argumento. O segundo dá uma exceção, com o código 134: sem argumento, não existe args[0]. A aula Argumentos da Linha de Comando ensina a conferir antes.


Resumo

  • O .NET 10 roda um arquivo .cs sozinho, sem projeto: dotnet run ola.cs (ou dotnet ola.cs).
  • Ele compila na primeira vez (uns segundos) e guarda o programa em ~/.local/share/dotnet/runfile. Se o arquivo não mudou, só roda (menos de meio segundo).
  • Os avisos só aparecem quando ele compila; para ver de novo, --no-cache.
  • Os argumentos vêm depois do arquivo e chegam no args; use -- antes deles para o dotnet não roubar nenhum.
  • Códigos de saída: 0 (deu certo), 1 (não compilou), o do return, 134 (exceção não tratada).
  • No topo do arquivo, #:package usa pacotes NuGet e #:property muda configurações. Com #!/usr/bin/env dotnet e chmod +x, o arquivo vira script.
  • Não misture arquivos soltos com pastas de projeto (CS8802). Quando o programa crescer, dotnet project convert.
  • Próximo passo: Projetos (dotnet new console).