C# Pastas e Caminhos

Nas aulas Arquivos e Ler e Escrever, todos os arquivos ficavam na mesma pasta do programa. Programas de verdade guardam relatórios em subpastas, procuram fotos na pasta Downloads e organizam arquivos. Nesta aula você vai usar a classe Path ("caminho") para montar e desmontar endereços de arquivos, e a classe Directory ("diretório", outro nome para pasta) para criar, listar, mover e apagar pastas. No fim, vai escrever um programa que arruma a pasta de downloads da Ana.

Você vai aprender
  • Montar caminhos com Path.Combine e desmontá-los com GetFileName, GetExtension e GetDirectoryName.
  • A diferença entre caminho relativo e absoluto, e o que é a pasta atual.
  • Encontrar a pasta pessoal com Environment.GetFolderPath.
  • Criar pastas com Directory.CreateDirectory e listar com GetFiles e GetDirectories, com filtro e subpastas.
  • Ver tamanho, nome e extensão com FileInfo e DirectoryInfo.
  • Mover e apagar pastas, e por que Directory.Delete reclama de pasta cheia.

Os programas desta aula rodam com dotnet run na pasta ~/cs da Ana. A pasta pessoal dela, /home/ana, tem as pastas de sempre do Ubuntu em português: Documentos, Downloads, Imagens, Música…

$ ls ~/Downloads ~/Documentos
/home/ana/Documentos:
acesso.log
alunos.csv
notas.txt
projetos
relatorio.odt

/home/ana/Downloads:
foto.jpg
fotos-viagem.zip
manual.pdf
musica.mp3
ubuntu-26.04-desktop-amd64.iso

A ideia: o endereço e o prédio

Um caminho é um endereço: "Rua Documentos, prédio relatorios, apartamento outubro.txt". Escrever um endereço num papel não constrói prédio nenhum. A classe Path trabalha só com o papel: junta, separa e confere pedaços de texto, sem tocar no disco. A classe Directory trabalha com os prédios: ela cria, lista e apaga pastas de verdade.

ClasseMexe no disco?Exemplos
PathNão: só textoPath.Combine, Path.GetExtension
DirectorySim: pastasDirectory.CreateDirectory, Directory.GetFiles
FileSim: arquivosFile.ReadAllText, File.Move (aula Arquivos)

No Linux e no macOS, as partes de um caminho são separadas por /. No Windows, por \. Se você monta os caminhos com Path, o mesmo programa funciona nos três.


1. Montar e desmontar caminhos: Path

string caminho = Path.Combine("Documentos", "relatorios", "outubro.txt");
Console.WriteLine(caminho);
Console.WriteLine(Path.GetFileName(caminho));
Console.WriteLine(Path.GetFileNameWithoutExtension(caminho));
Console.WriteLine(Path.GetExtension(caminho));
Console.WriteLine(Path.GetDirectoryName(caminho));
Console.WriteLine(Path.ChangeExtension(caminho, ".csv"));
Console.WriteLine(Path.GetFullPath(caminho));
Console.WriteLine(Path.IsPathRooted(caminho));
Console.WriteLine(Path.DirectorySeparatorChar);
$ dotnet run path.cs
Documentos/relatorios/outubro.txt
outubro.txt
outubro
.txt
Documentos/relatorios
Documentos/relatorios/outubro.csv
/home/ana/cs/Documentos/relatorios/outubro.txt
False
/
Path.Combine("Documentos", "relatorios", "outubro.txt")
"Combinar": junta as partes com o separador do sistema. No Linux, Documentos/relatorios/outubro.txt; no Windows, Documentos\relatorios\outubro.txt. Aceita quantas partes você quiser.
Path.GetFileName
O último pedaço, o nome do arquivo: outubro.txt.
Path.GetFileNameWithoutExtension
O nome sem a extensão: outubro.
Path.GetExtension
A extensão, com o ponto: .txt. Sem extensão, devolve "".
Path.GetDirectoryName
Tudo menos o último pedaço: a pasta onde o arquivo está.
Path.ChangeExtension(caminho, ".csv")
O mesmo caminho com outra extensão. Só muda o texto: o arquivo no disco continua com o nome antigo (para renomear de verdade, File.Move).
Path.GetFullPath
O caminho absoluto, a partir da raiz /, completando com a pasta atual (seção 2).
Path.IsPathRooted
"Começa na raiz?" False: este caminho é relativo.
Path.DirectorySeparatorChar
O separador do sistema, como char: / no Linux.

Nenhuma dessas linhas conferiu se Documentos/relatorios/outubro.txt existe. E ele não existe: a classe Path só trabalha com o texto.

Missão: a Ana converteu a foto da praia para PNG. Acrescente uma linha que mostre o caminho da foto com a extensão .png, usando Path.ChangeExtension. A última linha da saída deve ser Documentos/fotos/praia.png.

string foto = Path.Combine("Documentos", "fotos", "praia.jpg");
Console.WriteLine(foto);
Console.WriteLine(Path.GetFileName(foto));
Console.WriteLine(Path.GetDirectoryName(foto));
string foto = Path.Combine("Documentos", "fotos", "praia.jpg");
Console.WriteLine(foto);
Console.WriteLine(Path.GetFileName(foto));
Console.WriteLine(Path.GetDirectoryName(foto));
Console.WriteLine(Path.ChangeExtension(foto, ".png"));

2. Relativo × absoluto, e a pasta atual

Um caminho absoluto começa na raiz, /, e aponta sempre para o mesmo lugar: /home/ana/Documentos/notas.txt. Um caminho relativo, como notas.txt ou ../Documentos/notas.txt, é completado com a pasta atual: a pasta em que o terminal estava quando o programa começou (o pwd do terminal). O .. quer dizer "a pasta de cima".

Console.WriteLine(Path.GetFullPath("notas.txt"));
Console.WriteLine(Path.GetFullPath("../Documentos/notas.txt"));
$ dotnet run abs.cs
/home/ana/cs/notas.txt
/home/ana/Documentos/notas.txt
$ cd ..                         « agora a pasta atual é /home/ana
$ dotnet run cs/abs.cs
/home/ana/notas.txt
/home/Documentos/notas.txt

O mesmo programa, rodado de outra pasta, procura os arquivos em outro lugar. É a causa número um de FileNotFoundException "misteriosa": o arquivo está lá, mas o programa foi iniciado em outra pasta.

A pasta pessoal e outras pastas do sistema

Para um caminho que funcione de qualquer pasta, comece pela pasta pessoal do usuário. Quem diz qual é ela é o Environment ("ambiente"):

string casa = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
Console.WriteLine($"Pasta pessoal: {casa}");
Console.WriteLine($"Pasta atual: {Directory.GetCurrentDirectory()}");
Console.WriteLine($"Temporária: {Path.GetTempPath()}");

string notas = Path.Combine(casa, "Documentos", "notas.txt");
Console.WriteLine($"{notas} existe? {File.Exists(notas)}");
$ dotnet run pastas.cs
Pasta pessoal: /home/ana
Pasta atual: /home/ana/cs
Temporária: /tmp/
/home/ana/Documentos/notas.txt existe? True
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
"A pasta do perfil do usuário": /home/ana no Linux, /Users/ana no macOS e C:\Users\ana no Windows. SpecialFolder é um enum com dezenas de pastas especiais.
Directory.GetCurrentDirectory()
A pasta atual. (Environment.CurrentDirectory é o mesmo.)
Path.GetTempPath()
A pasta de arquivos temporários, que o sistema pode limpar: /tmp/ no Linux.
Por que "Documentos", e não "Documents"?

Os nomes das pastas pessoais dependem do idioma do sistema: no Ubuntu em português é Documentos, em inglês é Documents. O SpecialFolder.MyDocuments tenta descobrir pela configuração do sistema, e o resultado muda de máquina para máquina: na máquina da Ana, /home/ana/Documentos; no editor do guia, um texto vazio. O UserProfile é o único que existe em todo lugar. Para dados do seu programa, uma escolha comum é uma pasta própria dentro dele, como Path.Combine(casa, ".lojinha").


3. Criar pastas e listar o conteúdo: Directory

string pasta = Path.Combine("relatorios", "2026", "outubro");
DirectoryInfo d = Directory.CreateDirectory(pasta);
Console.WriteLine($"Criei {d.FullName}");

File.WriteAllText(Path.Combine(pasta, "vendas.txt"), "Vendas: 1520\n");
File.WriteAllText(Path.Combine(pasta, "gastos.txt"), "Gastos: 830\n");
Directory.CreateDirectory(pasta);

foreach (string f in Directory.GetFiles("relatorios", "*.txt", SearchOption.AllDirectories).Order())
{
    Console.WriteLine(f);
}
foreach (string p in Directory.GetDirectories("relatorios"))
{
    Console.WriteLine($"pasta: {p}");
}
$ dotnet run criar.cs
Criei /home/ana/cs/relatorios/2026/outubro
relatorios/2026/outubro/gastos.txt
relatorios/2026/outubro/vendas.txt
pasta: relatorios/2026
Directory.CreateDirectory(pasta)
Cria a pasta e todas as que faltarem no caminho (relatorios, 2026 e outubro), como o mkdir -p do terminal. Se ela já existe, não faz nada e não reclama: por isso a segunda chamada, mais abaixo, passou sem erro. Devolve um DirectoryInfo (seção 5), aqui usado para mostrar o caminho completo.
Directory.GetFiles("relatorios", "*.txt", SearchOption.AllDirectories)
Os caminhos dos arquivos da pasta, num string[]. O segundo argumento é um filtro: * quer dizer "qualquer coisa", então *.txt é "tudo que termina em .txt". O terceiro diz para procurar também dentro das subpastas (o padrão é SearchOption.TopDirectoryOnly, só a própria pasta).
.Order()
Põe os caminhos em ordem alfabética (aula Ordenar). A seção 4 explica por quê.
Directory.GetDirectories("relatorios")
As subpastas, só do primeiro nível: relatorios/2026, e não outubro, que está dentro dela.

Os caminhos que voltam são do mesmo tipo que você passou: com "relatorios" (relativo), eles voltam relativos. Para só o nome, use Path.GetFileName em cada um.


4. A ordem da lista não é garantida

O GetFiles devolve os arquivos na ordem em que o sistema de arquivos os guarda, que não é a alfabética e pode mudar de um computador para outro:

string casa = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
foreach (string a in Directory.GetFiles(Path.Combine(casa, "Downloads")))
{
    Console.WriteLine(Path.GetFileName(a));
}
$ dotnet run ordem.cs
fotos-viagem.zip
manual.pdf
ubuntu-26.04-desktop-amd64.iso
musica.mp3
foto.jpg

O ls do terminal ordena antes de mostrar; o GetFiles, não. Quando a ordem importa (para mostrar ao usuário, para comparar duas listas), ordene: Directory.GetFiles(…).Order(), ou Array.Sort(lista) (aula A Classe Array).


5. Detalhes do arquivo: FileInfo e DirectoryInfo

As classes File e Directory respondem uma pergunta por vez, com o caminho em cada chamada. As classes FileInfo ("informação do arquivo") e DirectoryInfo são objetos que representam um arquivo ou uma pasta, com propriedades prontas: nome, extensão, tamanho, data.

string casa = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
DirectoryInfo docs = new DirectoryInfo(Path.Combine(casa, "Documentos"));
Console.WriteLine($"{docs.Name} existe? {docs.Exists}");

foreach (FileInfo f in docs.GetFiles().OrderBy(f => f.Name))
{
    Console.WriteLine($"{f.Name,-15} {f.Extension,-5} {f.Length,6} bytes");
}
foreach (DirectoryInfo sub in docs.GetDirectories())
{
    Console.WriteLine($"[{sub.Name}]");
}

long total = 0;
foreach (string a in Directory.EnumerateFiles(docs.FullName, "*", SearchOption.AllDirectories))
{
    total += new FileInfo(a).Length;
}
Console.WriteLine($"Total: {total} bytes");
$ dotnet run docs.cs
Documentos existe? True
acesso.log      .log     389 bytes
alunos.csv      .csv     132 bytes
notas.txt       .txt      69 bytes
relatorio.odt   .odt    8394 bytes
[projetos]
Total: 9065 bytes
new DirectoryInfo(caminho)
Um objeto que representa a pasta. Criá-lo não cria a pasta: docs.Exists diz se ela existe (e docs.Create() cria).
docs.GetFiles()
Devolve um FileInfo[], e não um string[]: cada item já sabe o seu Name, Extension, Length (tamanho em bytes), FullName (caminho completo) e LastWriteTime (data da última mudança).
.OrderBy(f => f.Name)
Ordena pelo nome (aulas Lambdas e Ordenar).
{f.Name,-15}
Alinha o texto em 15 colunas, à esquerda; {f.Length,6} alinha o número em 6, à direita (aula Formatar Números).
Directory.EnumerateFiles(…)
Igual ao GetFiles, mas entrega um caminho por vez, como o File.ReadLines da aula Arquivos. Para pastas com milhares de arquivos, começa a responder antes e gasta menos memória.

O total inclui os arquivos de dentro de projetos, por causa do SearchOption.AllDirectories: 389 + 132 + 69 + 8394 são 8984, e os 81 bytes restantes estão na subpasta.


6. Mover e apagar pastas

Directory.CreateDirectory(Path.Combine("rascunhos", "contos"));
File.WriteAllText(Path.Combine("rascunhos", "ideias.txt"), "um robô jardineiro\n");

Directory.Move("rascunhos", "textos");
Console.WriteLine(Directory.Exists("rascunhos"));
Console.WriteLine(File.Exists(Path.Combine("textos", "ideias.txt")));

Directory.Delete(Path.Combine("textos", "contos"));
Console.WriteLine(Directory.Exists(Path.Combine("textos", "contos")));

Directory.Delete("textos", recursive: true);
Console.WriteLine(Directory.Exists("textos"));
$ dotnet run mover.cs
False
True
False
False
Directory.Move("rascunhos", "textos")
Move ou renomeia a pasta, com tudo o que tem dentro. Depois dele, rascunhos não existe mais, e o ideias.txt está em textos.
Directory.Delete(caminho)
Apaga uma pasta vazia. A contos estava vazia, então foi.
Directory.Delete("textos", recursive: true)
"Recursivo": apaga a pasta e tudo o que estiver dentro, subpastas incluídas, sem lixeira. É o rm -r do terminal. Confira duas vezes o caminho antes de rodar.

Formas de fazer: qual usar

Você quer…Use
Juntar pedaços de um caminhoPath.Combine(a, b, c), nunca a + "/" + b
Só o nome, a extensão ou a pastaPath.GetFileName, GetExtension, GetDirectoryName
Um caminho que funcione de qualquer pastaPath.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), …)
Garantir que uma pasta existeDirectory.CreateDirectory (não reclama se já existir)
Os caminhos dos arquivos de uma pastaDirectory.GetFiles(pasta, filtro), ordenado com .Order()
Muitos arquivos, um por vezDirectory.EnumerateFiles
Nome, tamanho e data de cada arquivonew DirectoryInfo(pasta).GetFiles() ou new FileInfo(caminho)
Apagar uma pasta com conteúdoDirectory.Delete(pasta, recursive: true)

Quando dá errado

IOException: Directory not empty
try
{
    Directory.Delete("relatorios");
}
catch (IOException e)
{
    Console.WriteLine($"{e.GetType().Name}: {e.Message}");
}
$ dotnet run apagar.cs
IOException: Directory not empty : '/home/ana/cs/relatorios'

O que aconteceu: "A pasta não está vazia". Sem o segundo argumento, o Directory.Delete só apaga pastas vazias, para você não perder arquivos sem querer. A relatorios da seção 3 tem a 2026 e dois arquivos dentro.

Como resolver: se é isso mesmo que você quer, Directory.Delete("relatorios", recursive: true). Se não, apague antes os arquivos que você conhece, um por um, com File.Delete.

DirectoryNotFoundException ao listar
string[] fotos = Directory.GetFiles("Fotos");
$ dotnet run fotos.cs
Unhandled exception. System.IO.DirectoryNotFoundException: Could not find a part of the path '/home/ana/cs/Fotos'.
   at System.IO.Enumeration.FileSystemEnumerator`1.CreateDirectoryHandle(String path, Boolean ignoreNotFound)
   at System.IO.Enumeration.FileSystemEnumerator`1.Init()
   …                                                « mais 4 linhas de dentro do .NET
   at Program.<Main>$(String[] args) in /home/ana/cs/fotos.cs:line 1

O que aconteceu: a pasta Fotos não existe dentro de ~/cs. (As fotos da Ana estão em ~/Imagens.) O caminho completo da mensagem mostra onde o .NET procurou.

Como resolver: confira com Directory.Exists antes de listar, ou monte o caminho a partir da pasta pessoal: Path.Combine(casa, "Imagens").

O Path.Combine jogou a pasta fora
Console.WriteLine(Path.Combine("/home/ana", "/Documentos", "notas.txt"));
Console.WriteLine(Path.Join("/home/ana", "/Documentos", "notas.txt"));
$ dotnet run combine.cs
/Documentos/notas.txt
/home/ana/Documentos/notas.txt

O que aconteceu: a segunda parte, "/Documentos", começa com /, então é um caminho absoluto. Quando uma das partes é absoluta, o Path.Combine descarta tudo o que veio antes e começa dali. Nenhum erro: o programa simplesmente procura no lugar errado.

Como resolver: escreva as partes sem a barra no começo: Path.Combine("/home/ana", "Documentos", "notas.txt"). Se as partes vêm de fora (digitadas pelo usuário, lidas de um arquivo), use Path.Join, que só junta e nunca descarta.

Caminhos montados com +
Console.WriteLine("Documentos" + "notas.txt");
File.WriteAllText("Documentos\\notas.txt", "oi");
$ dotnet run somar.cs
Documentosnotas.txt
$ ls
Documentos\notas.txt
somar.cs

O que aconteceu: na primeira linha, faltou a barra entre a pasta e o arquivo. Na segunda, a Ana copiou um caminho do Windows, com \ (escrita \\ dentro da string, aula Escapes). No Linux, \ não separa pastas: é uma letra comum. O programa criou na pasta cs um arquivo chamado, literalmente, Documentos\notas.txt.

Como resolver: deixe o Path.Combine("Documentos", "notas.txt") escolher o separador. Para apagar o arquivo estranho no terminal, use aspas: rm 'Documentos\notas.txt'.


Referência rápida

CódigoO que faz
Path.Combine(a, b, …) / Path.Join(a, b, …)Junta as partes com o separador (o Join nunca descarta partes)
Path.GetFileName(c) / GetFileNameWithoutExtension(c)Nome, com e sem extensão
Path.GetExtension(c)Extensão com o ponto (.txt)
Path.GetDirectoryName(c)A pasta do caminho
Path.ChangeExtension(c, ".csv")O texto do caminho com outra extensão
Path.GetFullPath(c)Caminho absoluto
Path.GetTempPath()Pasta temporária
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)Pasta pessoal (/home/ana)
Directory.GetCurrentDirectory()Pasta atual
Directory.Exists(c)A pasta existe?
Directory.CreateDirectory(c)Cria a pasta e as que faltarem
Directory.GetFiles(c, "*.txt", SearchOption.AllDirectories)Arquivos (filtro e subpastas opcionais), sem ordem garantida
Directory.GetDirectories(c)Subpastas do primeiro nível
Directory.EnumerateFiles(c)Arquivos, um por vez
Directory.Move(a, b)Move ou renomeia a pasta
Directory.Delete(c) / Directory.Delete(c, recursive: true)Apaga a pasta vazia / com tudo dentro
new FileInfo(c): Name, Extension, Length, LastWriteTimeDetalhes de um arquivo
new DirectoryInfo(c): GetFiles(), GetDirectories()Detalhes e conteúdo de uma pasta

Exercícios

1. Juntar as partes

Complete para montar o caminho Imagens/ferias.jpg com o separador certo do sistema:

string foto = Path.("Imagens", "ferias.jpg");

2. A extensão

O que Path.GetExtension("musica.mp3") devolve?

3. Apagar com tudo

Complete para apagar a pasta antigos mesmo que ela tenha arquivos dentro:

Directory.Delete("antigos", recursive: );

4. O que Directory.CreateDirectory("a/b/c") faz se a pasta a não existe?

5. O programa lê "dados.txt" e funciona quando rodado em ~/cs. Rodado em ~, dá FileNotFoundException. Por quê?

6. Quanto vale Path.Combine("/home/ana", "/tmp", "x.txt")?

Missão final: arrumar os downloads

Missão: o programa cria uma pasta Downloads com cinco arquivos misturados. Complete o laço para guardar cada arquivo numa subpasta com o nome da extensão (Downloads/jpg/foto.jpg, Downloads/pdf/manual.pdf…):

  1. descubra a extensão com Path.GetExtension(arquivo) e tire o ponto com .TrimStart('.');
  2. monte o caminho da subpasta com Path.Combine(origem, extensao) e crie-a com Directory.CreateDirectory;
  3. mova o arquivo para lá com File.Move(arquivo, Path.Combine(destino, Path.GetFileName(arquivo)), overwrite: true).

A saída deve ser:

Arquivos soltos: 0
jpg: 2
mp3: 1
pdf: 2
string origem = "Downloads";
Directory.CreateDirectory(origem);
string[] nomes = ["foto.jpg", "manual.pdf", "praia.jpg", "musica.mp3", "boleto.pdf"];
foreach (string nome in nomes)
{
    File.WriteAllText(Path.Combine(origem, nome), "");
}

foreach (string arquivo in Directory.GetFiles(origem))
{
    // 1. a extensão sem o ponto
    // 2. a subpasta com o nome da extensão
    // 3. mova o arquivo para a subpasta
}

Console.WriteLine($"Arquivos soltos: {Directory.GetFiles(origem).Length}");
foreach (string pasta in Directory.GetDirectories(origem).Order())
{
    Console.WriteLine($"{Path.GetFileName(pasta)}: {Directory.GetFiles(pasta).Length}");
}
string origem = "Downloads";
Directory.CreateDirectory(origem);
string[] nomes = ["foto.jpg", "manual.pdf", "praia.jpg", "musica.mp3", "boleto.pdf"];
foreach (string nome in nomes)
{
    File.WriteAllText(Path.Combine(origem, nome), "");
}

foreach (string arquivo in Directory.GetFiles(origem))
{
    string extensao = Path.GetExtension(arquivo).TrimStart('.');
    string destino = Path.Combine(origem, extensao);
    Directory.CreateDirectory(destino);
    File.Move(arquivo, Path.Combine(destino, Path.GetFileName(arquivo)), overwrite: true);
}

Console.WriteLine($"Arquivos soltos: {Directory.GetFiles(origem).Length}");
foreach (string pasta in Directory.GetDirectories(origem).Order())
{
    Console.WriteLine($"{Path.GetFileName(pasta)}: {Directory.GetFiles(pasta).Length}");
}

Os arquivos são criados vazios ("") só para o exemplo. O GetFiles(origem) do laço é chamado uma vez, antes de o laço começar, então mover arquivos lá dentro não atrapalha a lista. Rodando de novo, o resultado é o mesmo: os arquivos são recriados soltos e o overwrite: true deixa o Move substituir os que já estavam nas subpastas. Se quiser ir além: em vez da extensão, separe por tipo (Imagens para .jpg e .png, Documentos para .pdf) com uma expressão switch.


Resumo

  • Path só mexe no texto do caminho: Combine junta, GetFileName, GetExtension e GetDirectoryName separam. Monte caminhos com ele, nunca com +.
  • Caminhos relativos são completados com a pasta atual, onde o programa foi iniciado. Para um caminho fixo, comece da pasta pessoal: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile).
  • Directory.CreateDirectory cria a pasta e as que faltarem, sem reclamar se já existe.
  • Directory.GetFiles e GetDirectories listam (com filtro *.txt e SearchOption.AllDirectories); a ordem não é garantida, então ordene.
  • FileInfo e DirectoryInfo dão nome, extensão, tamanho e data como propriedades.
  • Directory.Delete só apaga pasta vazia; com recursive: true, apaga tudo, sem lixeira. Uma parte absoluta no Path.Combine descarta as anteriores.