Utilizzo di un elenco generato di nomi di file come elenco di argomenti - con spazi


16

Sto cercando di invocare una sceneggiatura con un elenco di nomi di file raccolti da find. Niente di speciale, solo qualcosa del genere:

$ myscript `find . -name something.txt`

Il problema è che alcuni dei nomi dei percorsi contengono spazi, quindi vengono divisi in due nomi non validi durante l'espansione degli argomenti. Normalmente circonderei i nomi con le virgolette, ma qui sono inseriti dall'espansione del backquote. Ho provato a filtrare l'output di finde circondare ogni nome di file con le virgolette, ma quando bash li vede, è troppo tardi per rimuoverli e vengono trattati come parte del nome del file:

$ myscript `find . -name something.txt | sed 's/.*/"&"/'`
No such file or directory: '"./somedir/something.txt"'

Sì, sono le regole su come viene elaborata la riga di comando, ma come posso aggirarla?

Questo è imbarazzante ma non riesco a trovare l'approccio giusto. xargs -0 -n 10000Alla fine ho capito come farlo con ... ma è un brutto hack che voglio ancora chiedere: come posso citare i risultati dell'espansione del backquote o ottenere lo stesso effetto in un altro modo?

Edit: Ero confuso circa il fatto che xargs fa raccogliere tutti gli argomenti in un unico elenco di argomenti, se non è detto altrimenti o limiti del sistema potrebbe essere superato. Grazie a tutti per avermi chiarito! Altri, tienilo a mente mentre leggi la risposta accettata perché non è indicata molto direttamente.

Ho accettato la risposta, ma la mia domanda rimane: non esiste un modo per proteggere gli spazi $(...)nell'espansione del backtick (o )? (Notare che la soluzione accettata è una risposta non bash).


Immagino che dovresti cambiare ciò che la shell usa come separatori di nomi di file (ad esempio, giocando con il valore di IFS, un modo possibile è IFS=", newline, "). Ma è necessario eseguire lo script su tutti i nomi di file? In caso contrario, considerare l'utilizzo di find stesso per eseguire lo script per ciascun file.
njsg

Cambiare l'IFS è un'ottima idea, non ci avevo pensato! Non pratico per l'utilizzo da riga di comando, ma comunque. :-) E sì, l'obiettivo è passare tutti gli argomenti alla stessa invocazione della mia sceneggiatura.
alexis,

Risposte:


12

Puoi fare quanto segue usando alcune implementazioni di finde in xargsquesto modo.

$ find . -type f -print0 | xargs -r0 ./myscript

o, di norma, solo find:

$ find . -type f -exec ./myscript {} +

Esempio

Supponiamo di avere la seguente directory di esempio.

$ tree
.
|-- dir1
|   `-- a\ file1.txt
|-- dir2
|   `-- a\ file2.txt
|-- dir3
|   `-- a\ file3.txt
`-- myscript

3 directories, 4 files

Ora diciamo che ho questo per ./myscript.

#!/bin/bash

for i in "$@"; do
    echo "file: $i"
done

Ora quando eseguo il seguente comando.

$ find . -type f -print0 | xargs -r0 ./myscript 
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

O quando uso il secondo modulo in questo modo:

$ find . -type f -exec ./myscript {} +
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Dettagli

trova + xargs

I suddetti 2 metodi, sebbene diversi, sono sostanzialmente gli stessi. Il primo è prendere l'output di find, dividerlo usando NULLs ( \0) tramite l' -print0interruttore per trovare. È xargs -0progettato specificamente per accettare input divisi mediante NULL. Che la sintassi non standard è stato introdotto da GNU finde xargs, ma si trova anche al giorno d'oggi in pochi altri, come la maggior parte dei sistemi BSD recenti. L' -ropzione è necessaria per evitare di chiamare myscriptse findnon trova nulla con GNU findma non con BSD.

NOTA: questo intero approccio dipende dal fatto che non passerai mai una stringa eccessivamente lunga. In tal caso, verrà avviata una seconda invocazione di ./myscriptcon il resto dei risultati successivi da find.

trova con +

Questo è il modo standard (sebbene sia stato aggiunto relativamente di recente (2005) all'implementazione di GNU di find). La capacità di fare ciò che stiamo facendo xargsè letteralmente integrata find. Quindi findtroverà un elenco di file e quindi passerà a tale elenco tutti gli argomenti che possono rientrare nel comando specificato dopo -exec(si noti che in questo caso {}può durare solo poco prima +), eseguendo i comandi più volte se necessario.

Perché non citare?

Nel primo esempio stiamo prendendo una scorciatoia evitando completamente i problemi con il preventivo, usando NULL per separare gli argomenti. Quando xargsviene fornito questo elenco, viene richiesto di dividere i NULL in modo efficace per proteggere i nostri singoli atomi di comando.

Nel secondo esempio manteniamo i risultati interni finde quindi sa qual è ogni atomo di file e garantirà di gestirli in modo appropriato, evitando così il fastidioso affare di quotarli.

Dimensione massima della riga di comando?

Questa domanda viene di volta in volta, quindi come bonus la sto aggiungendo a questa risposta, principalmente per poterla trovare in futuro. Puoi usare xargsper vedere quale sia il limite dell'ambiente in questo modo:

$ xargs --show-limits
Your environment variables take up 4791 bytes
POSIX upper limit on argument length (this system): 2090313
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2085522
Size of command buffer we are actually using: 131072

1
Grazie, ma devo passare tutti gli argomenti alla stessa invocazione della mia sceneggiatura. Questo è nella descrizione del problema, ma immagino di non aver chiarito che non è casuale.
alexis

@alexis - leggi di nuovo le risposte, stanno passando tutti gli argomenti a una singola chiamata del tuo script.
slm

Sarò dannato! Non sapevo +dell'argomento find(e anche tu lo usi +in prosa, quindi la prima volta ho perso la tua spiegazione). Ma più precisamente, avevo capito male cosa xargsfa di default !!! In trent'anni di utilizzo di Unix non ci avevo mai usato fino ad ora, ma pensavo di conoscere la mia cassetta degli attrezzi ...
alexis,

@alexis - Ho pensato che ti saresti perso quello che stavamo dicendo. Sì xargsè un diavolo di un comando. Devi leggerlo e findle pagine man molte volte per capire cosa possono fare. Maggio degli interruttori sono controproducenti l'uno dell'altro in modo da aggiungere confusione.
slm

@alexis: anche un'altra cosa da aggiungere alla casella degli strumenti, non usare i backquotes / backtick per eseguire i comandi nidificati, utilizzare $(..)invece ora. Gestisce automaticamente l'annidamento di virgolette, ecc. I backtick vengono deprecati.
slm

3
find . -name something.txt -exec myscript {} +

In quanto sopra, findtrova tutti i nomi di file corrispondenti e li fornisce come argomenti a myscript. Funziona con i nomi dei file indipendentemente dagli spazi o da altri caratteri dispari.

Se tutti i nomi dei file si adattano su una riga, myscript viene eseguito una volta. Se l'elenco è troppo lungo per essere gestito dalla shell, find troverà myscript più volte in base alle necessità.

ALTRO: quanti file si adattano a una riga di comando? man finddice che findcostruisce le sue linee di comando "più o meno allo stesso modo in cui xargs costruisce il suo". E man xargsche i limiti dipendono dal sistema e che è possibile determinarli eseguendo xargs --show-limits. ( getconf ARG_MAXè anche una possibilità). Su Linux, il limite è in genere (ma non sempre) di circa 2 milioni di caratteri per riga di comando.


2

Qualche aggiunta alla bella risposta di @ slm.

Il limite sulla dimensione degli argomenti è sulla execve(2)chiamata di sistema (in realtà, è sulla dimensione cumulativa dell'argomento e stringhe e puntatori di ambiente). Se myscriptè scritto in una lingua che la tua shell può interpretare, allora forse non hai bisogno di eseguirla , potresti farla interpretare dalla tua shell senza dover eseguire un altro interprete.

Se esegui lo script come:

(. myscript x y)

È come:

myscript x y

Tranne per il fatto che viene interpretato da un figlio della shell corrente, invece di eseguirla (che alla fine comporta l' esecuzione sh (o qualunque cosa la linea she-bang specifichi se presente) con ancora più argomenti).

Ora, ovviamente, non è possibile utilizzare find -exec {} +con il .comando, poiché .essendo un comando incorporato della shell, deve essere eseguito dalla shell, non da find.

Con zsh, è facile:

IFS=$'\0'
(. myscript $(find ... -print0))

O:

(. myscript ${(ps:\0:)"$(find ... -print0)"}

Anche se con zsh, non avresti bisogno findin primo luogo poiché la maggior parte delle sue funzionalità sono integrate nel zshglobbing.

bashle variabili tuttavia non possono contenere caratteri NUL, quindi devi trovare un altro modo. Un modo potrebbe essere:

files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")

Puoi anche usare il globbing ricorsivo in stile zsh con l' globstaropzione in bash4.0 e successive:

shopt -s globstar failglob dotglob
(. myscript ./**/something.txt)

Si noti che ha **seguito i collegamenti simbolici alle directory fino a quando non è stato corretto in bash4.3. Si noti inoltre che bashnon implementa le zshqualificazioni globbing in modo da non avere tutte le funzionalità di findlì.

Un'altra alternativa sarebbe usare GNU ls:

eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)"
(. myscript "${files[@]}")

I metodi di cui sopra possono essere utilizzati anche se si vuole fare in modo myscriptviene eseguito solo una volta (in mancanza, se la lista degli argomenti è troppo grande). Nelle versioni recenti di Linux, è possibile aumentare e persino eliminare tale limitazione nell'elenco degli argomenti con:

ulimit -s 1048576

(1GiB stack size, un quarto dei quali può essere utilizzato per la lista arg + env).

ulimit -s unlimited

(Senza limiti)


1

Nella maggior parte dei sistemi, esiste un limite alla lunghezza di una riga di comando passata a qualsiasi programma, utilizzando xargso -exec command {} +. Da man find:

-exec command {} +
      This  variant  of the -exec action runs the specified command on
      the selected files, but the command line is built  by  appending
      each  selected file name at the end; the total number of invoca
      tions of the command will  be  much  less  than  the  number  of
      matched  files.   The command line is built in much the same way
      that xargs builds its command lines.  Only one instance of  `{}'
      is  allowed  within the command.  The command is executed in the
      starting directory.

Le invocazioni saranno molto meno, ma non saranno garantite. Quello che dovresti fare è leggere i nomi di file separati NUL nello script da stdin, possibile in base a un argomento da riga di comando -o -. Vorrei fare qualcosa del tipo:

$ find . -name something.txt -print0 | myscript -0 -o -

e implementare gli argomenti dell'opzione di myscriptconseguenza.


Sì, il sistema operativo impone un limite nel numero / dimensione degli argomenti che possono essere passati. Sui moderni sistemi Linux questo è (gigantesco) ( linux.die.net/man/2/execve ) (1/4 della dimensione dello stack, argomenti 0x7FFFFFFF). AFAIK bash stesso non impone alcun limite. Le mie liste sono molto più piccole e il mio problema è stato causato da incomprensioni o ricordi errati di come xargsfunziona. La tua soluzione è davvero la più robusta, ma in questo caso è eccessiva.
alexis,

0

Non esiste un modo per proteggere gli spazi nell'espansione backtick (o $ (...))?

No, non c'è. Perché?

Bash non ha modo di sapere cosa dovrebbe essere protetto e cosa no.

Non ci sono matrici nel file / pipe unix. È solo un flusso di byte. Il comando all'interno di ``o $()genera un flusso, che bash ingoia e tratta come una singola stringa. A quel punto, hai solo due scelte: mettilo tra virgolette, per tenerlo come una stringa o metterlo nudo, in modo che bash lo divida in base al suo comportamento configurato.

Quindi, ciò che devi fare se vuoi un array è definire un formato byte che abbia un array, ed è quello che gli strumenti piacciono xargse findfanno: se li esegui con l' -0argomento, funzionano secondo un formato binario di array che termina gli elementi con il byte null, aggiungendo semantica al flusso di byte altrimenti opaco.

Sfortunatamente, bashnon può essere configurato per dividere le stringhe sul byte null. Grazie a /unix//a/110108/17980 per averci mostrato che zshpossiamo.

xargs

Vuoi che il tuo comando venga eseguito una volta e hai detto che xargs -0 -n 10000risolve il tuo problema. In caso contrario, garantisce che se si dispone di più di 10000 parametri, il comando verrà eseguito più di una volta.

Se si desidera eseguirlo rigorosamente o una volta o fallire, è necessario fornire l' -xargomento e un -nargomento più grandi -sdell'argomento (in realtà: abbastanza grandi da non far rientrare un intero gruppo di argomenti di lunghezza zero più il nome del comando la -sdimensione). ( man xargs , vedi estratto molto più in basso)

Il sistema su cui mi trovo attualmente ha uno stack limitato a circa 8 M, quindi ecco il mio limite:

$ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true
xargs: argument list too long
$ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true
(no output)

bash

Se non si desidera coinvolgere un comando esterno, il ciclo while-read che alimenta un array, come mostrato in /unix//a/110108/17980 , è l'unico modo per bash di dividere le cose in il byte nullo.

L'idea di procurarsi lo script ( . ... "$@" )per evitare il limite delle dimensioni dello stack è interessante (l'ho provato, funziona!), Ma probabilmente non è importante per le situazioni normali.

L'uso di una fd speciale per la pipe di processo è importante se vuoi leggere qualcos'altro da stdin, ma per il resto non ne avrai bisogno.

Quindi, il modo "nativo" più semplice, per le necessità domestiche quotidiane:

files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"

Se ti piace che il tuo albero dei processi sia pulito e piacevole da guardare, questo metodo ti consente di fare exec mynonscript "${files[@]}", che rimuove il processo bash dalla memoria, sostituendolo con il comando chiamato. xargsrimarrà sempre in memoria durante l'esecuzione del comando chiamato, anche se il comando verrà eseguito solo una volta.


Ciò che parla contro il metodo bash nativo è questo:

$ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; }

real    0m2.014s
user    0m2.008s
sys     0m0.172s

$ time {
  args=()
  while IFS= read -rd '' arg; do
    args+=( "$arg" )
  done < <(printf '%s\0' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s

bash non è ottimizzato per la gestione dell'array.


man xargs :

-n max-args

Utilizzare al massimo argomenti max-args per riga di comando. Verranno utilizzati meno argomenti di max-args se la dimensione (vedere l'opzione -s) viene superata, a meno che non venga fornita l'opzione -x, nel qual caso uscirà xargs.

-s max-chars

Utilizzare al massimo i caratteri max-chars per riga di comando, inclusi il comando e gli argomenti iniziali e i null che terminano alle estremità delle stringhe di argomenti. Il valore massimo consentito dipende dal sistema e viene calcolato come limite di lunghezza dell'argomento per exec, meno la dimensione dell'ambiente, meno 2048 byte di headroom. Se questo valore è superiore a 128 KiB, come valore predefinito viene utilizzato 128 KiB; in caso contrario, il valore predefinito è il massimo. 1 KiB è 1024 byte.

-X

Esci se la dimensione (vedi l'opzione -s) viene superata.


Grazie per tutti i problemi, ma la premessa di base ignora il fatto che bash normalmente utilizza un elaborato sistema di elaborazione delle quotazioni. Ma non in espansione backquote. Confrontare la seguente (errori che sia dare, ma mostrare la differenza): ls "what is this"contro ls `echo '"what is this"'` . Qualcuno ha trascurato di implementare l'elaborazione delle quotazioni per il risultato di backquotes.
alexis,

Sono contento che i backquotes non eseguano l'elaborazione delle quotazioni. Il fatto che facciano persino la divisione delle parole ha causato un aspetto abbastanza confuso, grattandosi la testa e difetti di sicurezza nella storia dell'informatica moderna.
clacke,

La domanda è "Non esiste un modo per proteggere gli spazi $(...)nell'espansione di backtick (o )?", Quindi sembra appropriato ignorare l'elaborazione che non viene eseguita in quella situazione.
clacke,

Il formato array elemento con terminazione null è il modo più semplice e quindi più sicuro per esprimere un array. È solo un peccato che bashnon lo supporti nativamente come sembra zsh.
clacke,

In effetti, proprio questa settimana ho usato printf "%s\0"e xargs -0per aggirare una situazione di quotazione in cui uno strumento intermedio avrebbe passato i parametri attraverso una stringa analizzata da una shell. La citazione torna sempre a morderti.
clacke,
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.