Eu pensei que investigaria o comportamento um pouco mais, então aqui está outra resposta.
Internamente, rm
usa FTS para recursão em hierarquias de arquivos. fts_open
usa uma matriz de caminhos como um parâmetro e cria uma estrutura de árvore para cada caminho. Isso permite que o programador explore vários locais, como se eles fossem parte de uma hierarquia unificada.
Aqui está um programa de teste que você pode usar para jogar com o FTS.
#include <stdio.h>
#include <stdlib.h>
#include <fts.h>
int main(int argc, char* argv[])
{
if(argc < 2) return EXIT_FAILURE;
char* const* arr = argv + 1;
FTS* hier = fts_open(arr, FTS_NOSTAT | FTS_PHYSICAL, NULL);
FTSENT* ent;
while((ent = fts_read(hier))) {
printf("%s info=%d (D=%d DP=%d F=%d SL=%d)\n",
ent->fts_accpath, ent->fts_info,
ent->fts_info == FTS_D, ent->fts_info == FTS_DP,
ent->fts_info == FTS_F || ent->fts_info == FTS_NSOK,
ent->fts_info == FTS_SL);
}
fts_close(hier);
return EXIT_SUCCESS;
}
Vamos supor que criamos a seguinte estrutura de diretório:
$ mkdir dir
$ touch dir/file
$ ln -s dir sym
Agora, vamos considerar seu primeiro caso e ver como o FTS lidera a exploração.
$ gcc fts.c
$ ./a.out sym
sym info=12 (D=0 DP=0 F=0 SL=1)
Como você pode ver, neste caso, sym
é visto como um arquivo. Um link simbólico, para ser mais exato. Com essas informações, rm
o trataria como um arquivo e chamaria unlinkat(AT_FDCWD, "sym", 0)
. O último parâmetro (0) faz com que unlinkat
se comporte como unlink
. Em outras palavras: simplesmente exclui um arquivo. Como resultado, seu link desaparece.
Agora, vamos dar uma olhada no que acontece com sym/
.
$ ./a.out sym/
sym/ info=1 (D=1 DP=0 F=0 SL=0)
file info=11 (D=0 DP=0 F=1 SL=0)
sym/ info=6 (D=0 DP=1 F=0 SL=0)
Nesse caso, sym
foi tratado como seu diretório de destino. Primeiro, iteramos para sym
e, em seguida, sym/file
, em seguida, sym
novamente. Este último é devido a como o FTS funciona: primeiro, ele itera sobre o conteúdo e retorna ao nó raiz. Isso é realmente muito conveniente para rm
. Na primeira passagem ( D
), ele pode apagar arquivos e, no segundo ( DP
), remover os diretórios vazios.
Como você pode ver, neste caso, o FTS reporta sym/
como sendo um diretório em ambos os casos. Isso é porque nós demos ao caminho uma barra final, o que força o kernel a interpretá-lo como um diretório. No caso de um link, isso significa que ele vai segui-lo, não importa o quê. Em termos mais técnicos, as especificações dizem:
A pathname that contains at least one non-slash character and that ends with one or more trailing slashes shall be resolved as if a single dot character ( '.' ) were appended to the pathname.
Como o FTS reporta sym/
como um diretório, rm
se comporta como se estivesse excluindo um diretório vazio. Consequentemente, chama unlinkat(AT_FDCWD, "sym/", AT_REMOVEDIR)
. Isso faz com que unlinkat
se comporte como rmdir
.
No entanto, ao resolver o caminho sym/
, a chamada do sistema unlinkat
perceberá que, de fato, não está recebendo um diretório. Por conseguinte, irá reportar ENOTDIR
, o que desencadeia:
$ rm: cannot remove ‘sym/’: Not a directory
E, na verdade, se você remover o -f
de suas chamadas ... Isso é exatamente o que você verá. Agora, se isso é ou não um bug ou um recurso ... não faço ideia.