¿Cómo escapar de los metacaracteres del shell automáticamente con el comando "buscar"?

¿Cómo escapar de los metacaracteres del shell automáticamente con el comando "buscar"?

Tengo un montón de archivos XML en un árbol de directorios que me gustaría mover a las carpetas correspondientes con el mismo nombre dentro de ese mismo árbol de directorios.

Aquí hay una estructura de muestra (en shell):

touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"

Entonces mi enfoque aquí es:

find . -name "*.xml" -exec sh -c '
  DST=$(
    find . -type d -name "$(basename "{}" .xml)" -print -quit
  )
  [ -d "$DST" ] && mv -v "{}" "$DST/"' ';'

lo que da el siguiente resultado:

‘./( bar ).xml’ -> ‘./bar/( bar )/( bar ).xml’
mv: ‘./bar/( bar )/( bar ).xml’ and ‘./bar/( bar )/( bar ).xml’ are the same file
‘./bar.xml’ -> ‘./bar/bar.xml’
‘./foo.xml’ -> ‘./foo/foo.xml’

Pero el archivo entre corchetes ( [ foo ].xml) no se ha movido como si hubiera sido ignorado.

Revisé y basename(p. ej. basename "[ foo ].xml" ".xml") convertí el archivo correctamente, sin embargo, findtengo problemas con los corchetes. Por ejemplo:

find . -name '[ foo ].xml'

no encontrará el archivo correctamente. Sin embargo, al escapar de los corchetes ( '\[ foo \].xml'), funciona bien, pero no resuelve el problema, porque es parte del script y no sé qué archivos tienen esos caracteres especiales (¿shell?). Probado tanto con BSD como con GNU find.

¿Existe alguna forma universal de escapar de los nombres de archivos cuando se usa con findel parámetro ' -name, para poder corregir mi comando para admitir archivos con metacaracteres?

Respuesta1

Es mucho más fácil con zshglobos aquí:

for f (**/*.xml(.)) (mv -v -- $f **/$f:r:t(/[1]))

O si desea incluir archivos xml ocultos y buscar dentro de directorios ocultos, como findlo haría:

for f (**/*.xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))

Pero tenga en cuenta que los archivos llamados .xml, ..xmlo ...xmlse convertirían en un problema, por lo que es posible que desee excluirlos:

setopt extendedglob
for f (**/(^(|.|..)).xml(.D)) (mv -v -- $f **/$f:r:t(D/[1]))

Con las herramientas GNU, otro enfoque para evitar tener que escanear todo el árbol de directorios para cada archivo sería escanearlo una vez y buscar todos los directorios y xmlarchivos, registrar dónde están y hacer el movimiento al final:

(export LC_ALL=C
find . -mindepth 1 -name '*.xml' ! -name .xml ! \
  -name ..xml ! -name ...xml -type f -printf 'F/%P\0' -o \
  -type d -printf 'D/%P\0' | awk -v RS='\0' -F / '
  {
    if ($1 == "F") {
      root = $NF
      sub(/\.xml$/, "", root)
      F[root] = substr($0, 3)
    } else D[$NF] = substr($0, 3)
  }
  END {
    for (f in F)
      if (f in D) 
        printf "%s\0%s\0", F[f], D[f]
  }' | xargs -r0n2 mv -v --
)

Su enfoque tiene una serie de problemas si desea permitir cualquier nombre de archivo arbitrario:

  • incrustar {}en el código shell essiempreequivocado. ¿Qué pasa si hay un archivo llamado, $(rm -rf "$HOME").xmlpor ejemplo? La forma correcta es pasarlos {}como argumento al script de shell en línea ( -exec sh -c 'use as "$1"...' sh {} \;).
  • Con GNU find(implícito aquí ya que está usando -quit), *.xmlsolo coincidiría con archivos que constan de una secuencia de caracteres válidos seguidos de .xml, por lo que excluye los nombres de archivos que contienen caracteres no válidos en la configuración regional actual (por ejemplo, nombres de archivos en el juego de caracteres incorrecto). La solución para esto es fijar la configuración regional donde Ccada byte sea un carácter válido (aunque eso significa que los mensajes de error se mostrarán en inglés).
  • Si alguno de esos xmlarchivos es de tipo directorio o enlace simbólico, eso causaría problemas (afectaría el escaneo de directorios o rompería los enlaces simbólicos cuando se moviera). Es posible que desee agregar un -type fpara mover solo archivos normales.
  • Tiras de sustitución de comandos ( $(...))todocaracteres de nueva línea finales. Eso causaría problemas con un archivo llamado, foo␤.xmlpor ejemplo. Solucionar esto es posible, pero es una molestia: base=$(basename "$1" .xml; echo .); base=${base%??}. Al menos puedes reemplazar basenamecon los ${var#pattern}operadores. Y evite la sustitución de comandos si es posible.
  • su problema con los nombres de archivos que contienen caracteres comodín ( ?, y barra invertida; no son especiales del shell, sino de la coincidencia de patrones ( ) realizada por [la cual resulta ser muy similar a la coincidencia de patrones del shell). Tendrías que escapar de ellos con una barra invertida.*fnmatch()find
  • el problema con .xml, ..xmlmencionado ...xmlanteriormente.

Entonces, si abordamos todo lo anterior, terminamos con algo como:

LC_ALL=C find . -type f -name '*.xml' ! -name .xml ! -name ..xml \
  ! -name ...xml -exec sh -c '
  for file do
    base=${file##*/}
    base=${base%.xml}
    escaped_base=$(printf "%s\n" "$base" |
      sed "s/[[*?\\\\]/\\\\&/g"; echo .)
    escaped_base=${escaped_base%??}
    find . -name "$escaped_base" -type d -exec mv -v "$file" {\} \; -quit
  done' sh {} +

Uf...

Ahora bien, no es todo. Con -exec ... {} +, ejecutamos la menor cantidad shposible. Si tenemos suerte, ejecutaremos solo uno, pero si no, después de la primera shinvocación, habremos movido una cantidad de xmlarchivos y luego findcontinuaremos buscando más, y es muy posible que encontremos los archivos que tenemos. movidos en la primera ronda nuevamente (y lo más probable es que intentemos moverlos donde están).

Aparte de eso, es básicamente el mismo enfoque que los de zsh. Algunas otras diferencias notables:

  • con zshuno, la lista de archivos está ordenada (por nombre de directorio y nombre de archivo), por lo que el directorio de destino es más o menos consistente y predecible. Con find, se basa en el orden sin formato de los archivos en los directorios.
  • con zsh, recibirá un mensaje de error si no se encuentra ningún directorio coincidente al que mover el archivo, no con el findenfoque anterior.
  • Con find, recibirá mensajes de error si no se pueden atravesar algunos directorios, no con ese zsh.

Una última nota de advertencia. Si la razón por la que obtiene algunos archivos con nombres poco fiables es porque un adversario puede escribir en el árbol de directorios, entonces tenga en cuenta que ninguna de las soluciones anteriores es segura si el adversario puede cambiar el nombre de los archivos bajo ese comando.

Por ejemplo, si está utilizando LXDE, el atacante podría crear un archivo malicioso foo/lxde-rc.xml, crear una lxde-rccarpeta, detectar cuándo está ejecutando su comando y reemplazarlo lxde-rccon un enlace simbólico a su ~/.config/openbox/durante la ventana de carrera (que puede hacerse tan grande como sea necesario). de muchas maneras) entre findencontrar eso lxde-rcy mvhacer rename("foo/lxde-rc.xml", "lxde-rc/lxde-rc.xml")( footambién podría cambiarse a ese enlace simbólico que le haga moverse lxde-rc.xmla otra parte).

Probablemente sea imposible solucionar esto utilizando utilidades estándar o incluso GNU; necesitaría escribirlo en un lenguaje de programación adecuado, realizar un recorrido seguro del directorio y utilizar renameat()llamadas al sistema.

Todas las soluciones anteriores también fallarán si el árbol de directorios es lo suficientemente profundo como para alcanzar el límite de longitud de las rutas dadas a la rename()llamada al sistema realizada por (lo que provocará un error con ). Una solución que utilice también solucionaría el problema.mvrename()ENAMETOOLONGrenameat()

Respuesta2

Cuando usa un script en línea con find ... -exec sh -c ..., debe pasar findel resultado al shell a través del parámetro posicional, entonces no tiene que usarlo {}en todas partes de su script en línea.

Si tiene basho zsh, puede pasar basenamela salida a través de printf '%q':

find . -name "*.xml" -exec bash -c '
  for f do
    BASENAME="$(printf "%q" "$(basename -- "$f" .xml)")"
    DST=$(find . -type d -name "$BASENAME" -print -quit)
    [ -d "$DST" ] && mv -v -- "$f" "$DST/"
  done
' bash {} +

Con bash, puede usar printf -v BASENAME, y este enfoque no funcionará correctamente si el nombre del archivo contiene caracteres de control o caracteres que no sean ASCII.

Si desea que funcione correctamente, debe escribir una función de shell para escapar solo [, *y ?barra invertida.

Respuesta3

Las buenas noticias:

find . -name '[ foo ].xml'

no es interpretado por el shell, se pasa de esta manera al programa de búsqueda. Sin embargo, Find interpreta el argumento -namecomo un globpatrón y esto debe tenerse en cuenta.

Si te gusta llamar find -exec \;o mejor find -exec +, no hay ningún caparazón involucrado.

Si desea procesar la findsalida del shell, le recomiendo simplemente deshabilitar la inclusión de nombres de archivos en el shell llamando set -fantes del código en cuestión y activarlo nuevamente llamando set +fmás tarde.

Respuesta4

La siguiente es una canalización relativamente sencilla y compatible con POSIX. Explora la jerarquía dos veces, primero en busca de directorios y luego en busca de archivos normales *.xml. Una línea en blanco entre escaneos indica AWK de la transición.

El componente AWK asigna nombres base a directorios de destino (si hay varios directorios con el mismo nombre base, solo se recuerda el primer recorrido). Para cada archivo *.xml, imprime una línea delimitada por tabulaciones con dos campos: 1) la ruta del archivo y 2) su directorio de destino correspondiente.

{
    find . -type d
    echo
    find . -type f -name \*.xml
} |
awk -F/ '
    !NF { ++i; next }
    !i && !($NF".xml" in d) { d[$NF".xml"] = $0 }
    i { print $0 "\t" d[$NF] }
' |
while IFS='     ' read -r f d; do
    mv -- "$f" "$d"
done

El valor asignado a IFS justo antes de la lectura es un carácter de tabulación literal, no un espacio.

Aquí hay una transcripción que utiliza el esqueleto touch/mkdir de la pregunta original:

$ touch foo.xml bar.xml "[ foo ].xml" "( bar ).xml"
$ mkdir -p foo bar "foo/[ foo ]" "bar/( bar )"
$ find .
.
./foo
./foo/[ foo ]
./bar.xml
./foo.xml
./bar
./bar/( bar )
./[ foo ].xml
./( bar ).xml
$ ../mv-xml.sh
$ find .
.
./foo
./foo/[ foo ]
./foo/[ foo ]/[ foo ].xml
./foo/foo.xml
./bar
./bar/( bar )
./bar/( bar )/( bar ).xml
./bar/bar.xml

información relacionada