Pensé en investigar el comportamiento un poco más, así que aquí hay otra respuesta.
Internamente, rm
usa FTS para recursivamente en jerarquías de archivos. fts_open
toma una matriz de rutas como parámetro y crea una estructura de árbol para cada ruta. Esto le permite al programador explorar varias ubicaciones sin problemas como si fueran parte de una jerarquía unificada.
Aquí hay un programa de prueba que puedes usar para jugar con FTS tú mismo.
#include
#include
#include
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;
}
Supongamos que hemos creado la siguiente estructura de directorios:
$ mkdir dir
$ touch dir/file
$ ln -s dir sym
Ahora, consideremos su primer caso y veamos cómo FTS lidera la exploración.
$ gcc fts.c
$./a.out sym
sym info=12 (D=0 DP=0 F=0 SL=1)
Como puede ver, en este caso, sym
se ve como un archivo. Un enlace simbólico, para ser más exactos. Con esta información, rm
lo trataría como un archivo y llamaría a unlinkat(AT_FDCWD, "sym", 0)
. El último parámetro (0 )hace que unlinkat
se comporte como unlink
. En otras palabras :simplemente borra un archivo. Como resultado, su enlace desaparece.
Ahora, echemos un vistazo a lo que sucede con 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)
En este caso, sym
fue tratado como su directorio de destino. Primero iteramos a sym
, luego sym/file
y luego sym
nuevamente. Este último se debe a cómo funciona FTS :primero, itera sobre el contenido y luego regresa al nodo raíz. Esto es bastante conveniente para rm
. En la primera pasada (D
), puede borrar archivos, y en la segunda(DP
)eliminar los directorios vacíos.
Como puede ver, en este caso, FTS informa sym/
como un directorio en ambos casos. Esto se debe a que le dimos la ruta con una barra al final, lo que obliga al núcleo a interpretarla como un directorio. En el caso de un enlace, esto significa que lo seguirá sin importar nada. En términos más técnicos, las especificaciones dicen:
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.
Debido a que FTS informa sym/
como un directorio, rm
se comporta como si estuviera eliminando un directorio vacío. En consecuencia, llama unlinkat(AT_FDCWD, "sym/", AT_REMOVEDIR)
.Esto hace que unlinkat
se comporte como rmdir
.
Sin embargo, al resolver la ruta sym/
, la llamada al sistema unlinkat
se dará cuenta de que, de hecho, no se le está dando un directorio. Por lo tanto, informará ENOTDIR
, lo que desencadena:
$ rm: cannot remove ‘sym/’: Not a directory
Y, de hecho, si elimina la marca -f
de sus llamadas... Eso es exactamente lo que verá. Ahora, si esto es un error o una característica... No tengo idea.
Puede usar comm
para eliminar todo lo que sea común a ambas listas:
listr=($(comm -3 <(printf "%s\n" "${list1[@]}" | sort) <(printf "%s\n" "${list2[@]}" | sort) | sort -n))
Esto ordena ambas listas en el orden comm
esperado, las compara, genera solo elementos que son exclusivos de cualquiera de las listas y los ordena nuevamente en orden numérico.
Si ambas listas están ordenadas lexicográficamente(según LC_COLLATE
), puede evitar ordenarlas de nuevo:
listr=($(comm --nocheck-order -3 <(printf "%s\n" "${list1[@]}") <(printf "%s\n" "${list2[@]}")))
Esto también funciona muy bien si los valores que necesita comparar están almacenados en archivos.
#!/bin/zsh
list1=( 1 2 3 4 5 6 7 8 9 10 11 12 )
list2=( 1 2 3 5 7 8 9 11 12 )
listr=("${(@)list1:|list2}")
typeset -p listr
Resumen:
Para listas largas, si las listas ya están ordenadas, comm (alg7 )es el más rápido.
La solución zsh es (con mucho )la más rápida si no se lee ningún archivo, es decir, las listas se dan "en memoria". Sin embargo, esa no es una comparación justa con todas las demás soluciones que tienen que leer valores de archivos. Cambié el código original (que evitó el tiempo de lectura de archivos de prueba )a un código que también incluye el tiempo de lectura de archivos.
Esta es una respuesta de la comunidad, solo debe informar la hora de cada respuesta.
Por favor, HAGA editar y agregar su solución/opción para comparar todo.
sort
yuniq -u
Nota del manual zsh:
${name:|arrayname}
If arrayname is the name (N.B., not contents) of an array variable, then any elements contained in arrayname are removed from the substitution of name.
Como hay varias formas de resolver esto,necesitamos un marco general para probar (con equidad )las respuestas.
Algunas pautas (cámbialas si las encuentras injustas):
Código de prueba:
#!/bin/bash
alg1(){
arr=( "${list1[@]}" )
for i in "${list2[@]}"; do
for j in "${!arr[@]}"; do
if [[ "$i" == "${arr[j]}" ]]; then
unset arr["$j"]
break
fi
done
done
printf '%s ' "${arr[@]}"; echo
}
alg2(){
arr=($({ printf '%s\n' "${list1[@]}" "${list2[@]}"; } | sort | uniq -u))
printf '%s ' "${arr[@]}"; echo
}
alg3(){
a=" $(printf '%s ' ${list1[@]})" # Watch the space at the start!!.
for i in "${list2[@]}"; do
a=${a/ "$i" / };
done
printf '%s\n' "$a"
}
alg4(){ join -v 1 list1.txt list2.txt ; }
alg5(){ #listr=$(
comm -3 <(printf "%s\n" "${list1[@]}" | sort) \
<(printf "%s\n" "${list2[@]}" | sort) |
sort -n
#)
}
alg6(){ zsh -c ' alg6(){
list1=( $(cat list1.txt) )
list2=( $(cat list2.txt) )
listr=("${(@)list1:|list2}")
typeset -p listr
}
TIMEFMT="%E %U %S"
time ( for ((k=0;k<'"$1"';k++)); do alg6; done; )
'
}
#: <<-\_comment_
alg7(){ comm -23 list1.txt list2.txt; }
try(){ for ((k=0;k<$1;k++)); do "$2"; done; }
#list1=( 1 2 3 4 5 6 7 8 9 10 11 12 )
#list2=( 1 2 3 5 7 8 9 11 12 )
#list1=( a a b b b c d d )
#list2=( b b c c c d d e )
size=1000000
list1=( "0" $(seq 1 "$size") )
list2=( "${list1[@]}" ); unset "list2[123]" "list2[234]" "list2[345]"
printf '%s\n' "${list1[@]}" | sort >list1.txt
printf '%s\n' "${list2[@]}" | sort >list2.txt
repeats=${1:-10}; shift
out=${1:-no} ; shift
(($#==0)) && set -- alg{1..7}
TIMEFORMAT='%R %U %S'
for i
do printf '%s ' "$i"
if [[ $out == no ]]; then
[[ $i != alg6 ]] &&
time try "$repeats" "$i" >/dev/null ||
time alg6 "$repeats" >/dev/null
else
[[ $i != alg6 ]] &&
time try "$repeats" "$i" ||
time alg6 "$repeats"
fi
done
$./script
alg1 2.056 0.806 1.237
alg2 3.478 3.126 1.756
alg3 0.999 0.728 0.304
alg4 1.186 0.780 0.434
alg5 5.234 1.926 1.722
alg6 2.71s 1.64s 1.26s
2.758 1.637 1.265
alg7 1.156 0.799 0.422
Los tiempos para alg6 son los informados por zsh y después según lo informado por bash.
Además, el tiempo de ejecución de zsh es realmente menor (0.050 )si se quita la lectura de archivos de la función al exterior.
Probar una lista de solo 500 valores (10 repeticiones )revela que alg1 es muy ineficiente. Eliminarlo de más pruebas:
alg1 4.149 3.471 0.657
alg2 0.145 0.055 0.063
alg3 0.219 0.180 0.009
alg4 0.039 0.015 0.003
alg5 0.149 0.018 0.027
alg6 0.06s 0.02s 0.02s
0.087 0.030 0.018
alg7 0.033 0.008 0.008
Probar valores de 5k (10 repeticiones )revela que alg3 también es ineficiente:
alg2 0.590 0.526 0.187
alg3 12.957 12.888 0.044
alg4 0.098 0.047 0.008
alg5 0.654 0.028 0.036
alg6 0.16s 0.12s 0.04s
0.211 0.118 0.044
alg7 0.038 0.022 0.014
Probando 50k valores (10 repeticiones):
alg2 6.487 5.838 1.611
alg4 0.488 0.469 0.019
alg5 5.073 0.250 0.056
alg6 1.42s 1.20s 0.21s
1.467 1.206 0.216
alg7 0.271 0.247 0.014
Para 500k (10 repeticiones)
alg4 5.471 5.269 0.156
alg6 15.14s 13.33s 1.91s
15.215 13.335 1.926
alg7 2.833 2.655 0.138
Para 1M (10 repeticiones)
alg4 11.127 10.804 0.251
alg7 5.772 5.525 0.230