Converti glob in `find`


11

Ho avuto ancora e ancora questo problema: ho un glob, che corrisponde esattamente ai file corretti, ma causa Command line too long. Ogni volta che l'ho convertito in una combinazione di finde grepche funziona per la situazione particolare, ma che non è equivalente al 100%.

Per esempio:

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

Esiste uno strumento per convertire globs in findespressioni di cui non sono a conoscenza? Oppure esiste un'opzione per findabbinare il glob senza abbinare lo stesso glob in un sottodir (ad esempio foo/*.jpgnon è consentito abbinare bar/foo/*.jpg)?


Espandi la parentesi graffa e dovresti essere in grado di usare le espressioni risultanti con -patho -ipath. find . -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg'dovrebbe funzionare - tranne che corrisponderà /fooz/blah/bar/quuxA/pic1234d.jpg. Sarà un problema?
Muru,

Sì, sarà un problema. Deve essere equivalente al 100%.
Ole Tange,

Il problema è che non abbiamo idea di quale sia esattamente la differenza. Il tuo modello è abbastanza ok.
Peter - Ripristina Monica il

Ho aggiunto il tuo post di estensione come risposta alla domanda. Spero non sia così male.
Peter - Ripristina Monica il

Non puoi farlo echo <glob> | cat, supponendo che io conosca bash, l'eco è
integrato

Risposte:


15

Se il problema è che si ottiene un errore topic-list-is-too-long, utilizzare un loop o una shell integrata. Mentre command glob-that-matches-too-muchpuò fuoriuscire, for f in glob-that-matches-too-muchno, quindi puoi semplicemente fare:

for f in foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg
do
    something "$f"
done

Il ciclo potrebbe essere estremamente lento, ma dovrebbe funzionare.

O:

printf "%s\0" foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg |
  xargs -r0 something

( printfessendo incorporato nella maggior parte delle shell, quanto sopra funziona attorno alla limitazione della execve()chiamata di sistema)

$ cat /usr/share/**/* > /dev/null
zsh: argument list too long: cat
$ printf "%s\n" /usr/share/**/* | wc -l
165606

Funziona anche con bash. Non sono sicuro di dove sia documentato.


Sia Vim glob2regpat()che Python fnmatch.translate()possono convertire globs in regex, ma entrambi usano anche .*per la *corrispondenza /.


Se questo è vero, sostituirlo somethingcon echodovrebbe farlo.
Ole Tange,

1
@OleTange Ecco perché ho suggerito printf: sarà più veloce che chiamare echomigliaia di volte e offre maggiore flessibilità.
muru,

4
C'è un limite agli argomenti che possono essere passati exec, che si applica a comandi esterni come cat; ma quel limite non si applica ai comandi integrati della shell come printf.
Stephen Kitt,

1
@OleTange La linea non è troppo lunga perché printfè un builtin, e le shell presumibilmente usano lo stesso metodo per fornirgli argomenti che usano per enumerare gli argomenti for. catnon è incorporato.
Muru,

1
Tecnicamente ci sono shell come mkshdove printfnon è incorporato e shell come ksh93dove catè (o può essere) incorporato. Vedi anche zargsin zshal lavoro intorno ad esso, senza dover ricorrere a xargs.
Stéphane Chazelas,

9

find(per i predicati -name/ -pathstandard) usa modelli jolly proprio come globs (nota che {a,b}non è un operatore glob; dopo l'espansione, ottieni due globs). La differenza principale è la gestione di barre (e file di punti e directory non trattati in modo speciale find). *nei globs non si estenderà su più directory. */*/*farà elencare fino a 2 livelli di directory. L'aggiunta di a -path './*/*/*'corrisponderà a tutti i file con almeno 3 livelli di profondità e non smetterà finddi elencare i contenuti di qualsiasi directory a qualsiasi profondità.

Per quel particolare

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

un paio di globs, è facile da tradurre, stai cercando directory a profondità 3, quindi puoi usare:

find . -mindepth 3 -maxdepth 3 \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

(o -depth 3con alcune findimplementazioni). O POSIXly:

find . -path './*/*/*' -prune \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

Il che garantirebbe che quelli *e ?non potrebbero abbinare i /personaggi.

( findcontrariamente a globs leggerebbe il contenuto di directory diverse da foo*barquelle presenti nella directory corrente¹, e non ordinerebbe l'elenco dei file. Ma se lasciamo da parte il problema che ciò che corrisponde [A-Z]o il comportamento di */ ?rispetto ai caratteri non validi è non specificato, otterrai lo stesso elenco di file).

Ma in ogni caso, come ha dimostrato @muru , non è necessario ricorrere findse si tratta solo di dividere l'elenco di file in più esecuzioni per aggirare il limite della execve()chiamata di sistema. Alcune shell come zsh(con zargs) o ksh93(con command -x) hanno persino il supporto integrato per questo.

Con zsh(i cui globs hanno anche l'equivalente -type fe la maggior parte degli altri findpredicati), ad esempio:

autoload zargs # if not already in ~/.zshrc
zargs ./foo*bar/quux[A-Z](|.bak)/pic[0-9][0-9][0-9][0-9]?.jpg(.) -- cmd

( (|.bak)è un operatore glob contrario {,.bak}, il (.)qualificatore glob è l'equivalente di find's -type f, aggiungilo oNper saltare l'ordinamento come con find, Dper includere file dot (non si applica a questo glob))


¹ Per findeseguire la scansione dell'albero delle directory come farebbero globs, avresti bisogno di qualcosa del tipo:

find . ! -name . \( \
  \( -path './*/*' -o -name 'foo*bar' -o -prune \) \
  -path './*/*/*' -prune -name 'pic[0-9][0-9][0-9][0-9]?.jpg' -exec cmd {} + -o \
  \( ! -path './*/*' -o -name 'quux[A-Z]' -o -name 'quux[A-Z].bak' -o -prune \) \)

Cioè potare tutte le directory al livello 1 tranne foo*barquelle, e tutte al livello 2 tranne le quux[A-Z]o quux[A-Z].bak, quindi selezionare pic...quelle al livello 3 (e potare tutte le directory a quel livello).


3

Puoi scrivere una regex per trovare la corrispondenza con le tue esigenze:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'

Esiste uno strumento che esegue questa conversione per evitare errori umani?
Ole Tange,

No, ma gli unici cambiamenti che ho fatto dovesse sfuggire ., aggiungere il match opzionale .bake il cambiamento *a [^/]*di non corrispondere i percorsi come / foo / foo / bar, ecc
sebasth

Ma anche la tua conversione è sbagliata. ? non è cambiato in [^ /]. Questo è esattamente il tipo di errore umano che voglio evitare.
Ole Tange,

1
Penso che con egrep, puoi accorciare [0-9][0-9][0-9][0-9]?a[0-9]{3,4}
wjandrea il


0

Generalizzando la nota sull'altra mia risposta , come risposta più diretta alla tua domanda, potresti usare questo shscript POSIX per convertire il glob in findun'espressione:

#! /bin/sh -
glob=${1#./}
shift
n=$#
p='./*'

while true; do
  case $glob in
    (*/*)
      set -- "$@" \( ! -path "$p" -o -path "$p/*" -o -name "${glob%%/*}" -o -prune \)
      glob=${glob#*/} p=$p/*;;
    (*)
      set -- "$@" -path "$p" -prune -name "$glob"
      while [ "$n" -gt 0 ]; do
        set -- "$@" "$1"
        shift
        n=$((n - 1))
      done
      break;;
  esac
done
find . "$@"

Da utilizzare con unsh glob standard (quindi non i due globs del tuo esempio che utilizza l' espansione del controvento ):

glob2find './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' \
  -type f -exec cmd {} +

(che non ignora dot-file o dot-dirs tranne .e ..e non ordina l'elenco dei file).

Quello funziona solo con globs relativi alla directory corrente, senza .o ..componenti. Con un po 'di sforzo, potresti estenderlo a qualsiasi glob, più che a glob ... Questo potrebbe anche essere ottimizzato in modo che glob2find 'dir/*'non cerchi dirlo stesso di come sarebbe uno schema.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.