Eseguire il comando su tutti i file in una directory


290

Qualcuno potrebbe fornire il codice per fare quanto segue: Supponiamo che ci sia una directory di file, che devono essere eseguiti attraverso un programma. Il programma emette i risultati su standard out. Ho bisogno di uno script che andrà in una directory, eseguirà il comando su ciascun file e concatigherà l'output in un grande file di output.

Ad esempio, per eseguire il comando su 1 file:

$ cmd [option] [filename] > results.out

3
Vorrei aggiungere alla domanda. Può essere fatto usando xargs? ad es. ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
Ozair Kafray,

2
Può, ma probabilmente non vuoi usarels per guidare xargs. Se cmdè stato scritto con competenza, forse puoi semplicemente farlo cmd <wildcard>.
tripla il

Risposte:


425

Il seguente codice bash passerà $ file per comandare dove $ file rappresenterà ogni file in / dir

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

Esempio

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt

23
Se non esiste alcun file /dir/, il ciclo viene comunque eseguito una volta con un valore di '*' per $file, che potrebbe essere indesiderabile. Per evitare ciò, abilitare nullglob per la durata del ciclo. Aggiungi questa riga prima del loop shopt -s nullglobe questa riga dopo il loop shopt -u nullglob #revert nullglob back to it's normal default state.
Stew-au

43
+1, e mi è costato tutta la mia intera collezione di sfondi. tutti dopo di me usano le doppie virgolette. "$ file"
Behrooz,

Se il file di output è lo stesso all'interno del ciclo, è molto più efficiente reindirizzare al di fuori del ciclo done >results.out(e probabilmente quindi è possibile sovrascrivere anziché aggiungere, come ho ipotizzato qui).
tripla il

Come si ottengono i singoli file dei risultati che sono personalizzati nei loro file di input?
Timothy Swan,

1
fai attenzione usando questo comando per enormi quantità di file in dir. Usa invece find -exec.
Kolisko,

181

Cosa ne pensi di questo:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1l'argomento impedisce a find di scendere ricorsivamente in qualsiasi sottodirectory. (Se si desidera che tali directory nidificate vengano elaborate, è possibile ometterle.)
  • -type -f specifica che verranno elaborati solo i file semplici.
  • -exec cmd option {}gli dice di funzionare cmdcon il specificato optionper ogni file trovato, con il nome del file sostituito{}
  • \; indica la fine del comando.
  • Infine, cmdviene reindirizzato l'output di tutte le singole esecuzioni results.out

Tuttavia, se ti interessa l'ordine in cui i file vengono elaborati, è meglio scrivere un ciclo. Penso che findelabori i file in ordine di inode (anche se potrei sbagliarmi al riguardo), che potrebbe non essere quello che vuoi.


1
Questo è il modo corretto di elaborare i file. L'uso di un ciclo for è soggetto a errori per molte ragioni. Inoltre, l'ordinamento può essere eseguito utilizzando altri comandi come state sort, che ovviamente dipende dai criteri di ordinamento.
tuxdna,

1
se volessi eseguire due comandi come li collegherei dopo l' -execopzione? Devo avvolgerli tra virgolette singole o qualcosa del genere?
Frei,

findè sempre l'opzione migliore perché puoi filtrare per modello di nome file con opzione -namee puoi farlo in un solo comando.
João Pimentel Ferreira,

3
@frei la risposta alla tua domanda è qui: stackoverflow.com/a/6043896/1243247 ma in pratica aggiungi solo -execopzioni:find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira il

2
come puoi fare riferimento al nome del file come opzione?
Toskan,

54

Lo sto facendo sul mio Raspberry Pi dalla riga di comando eseguendo:

for i in *;do omxplayer "$i";done

7

Le risposte accettate / votate con successo sono ottime, ma mancano di alcuni dettagli chiacchieroni. Questo post tratta i casi su come gestire meglio quando l'espansione del nome percorso della shell (glob) fallisce, quando i nomi dei file contengono nuovi simboli incorporati di linee / trattini e spostando l'output del comando dalla direzione for-loop quando si scrivono i risultati in un file.

Quando si esegue l'espansione shell glob usando *v'è la possibilità per l'espansione di sicuro se sono presenti file presenti nella directory e un glob stringa non-espanso viene passato al comando da eseguire, che potrebbe avere risultati indesiderati. La bashshell fornisce un'opzione shell estesa per questo utilizzo nullglob. Quindi il ciclo diventa sostanzialmente come segue all'interno della directory che contiene i tuoi file

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

Ciò consente di uscire in sicurezza dal ciclo for quando l'espressione ./*non restituisce alcun file (se la directory è vuota)

o in modo conforme a POSIX ( nullglobè bashspecifico)

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

Questo ti permette di entrare nel ciclo quando l'espressione fallisce per una volta e la condizione [ -f "$file" ]controlla se la stringa non espansa ./*è un nome file valido in quella directory, che non lo sarebbe. Quindi, in caso di errore di questa condizione, l'utilizzo continueriprendiamo al forciclo che non verrà eseguito successivamente.

Si noti inoltre l'utilizzo di --poco prima di passare l'argomento nome file. Ciò è necessario perché, come notato in precedenza, i nomi dei file della shell possono contenere trattini in qualsiasi punto del nome file. Alcuni dei comandi della shell lo interpretano e li trattano come un'opzione di comando quando il nome non è quotato correttamente ed esegue il comando pensando che sia fornito il flag.

In questo caso --segnala la fine delle opzioni della riga di comando, il che significa che il comando non deve analizzare nessuna stringa oltre questo punto come flag di comando ma solo come nomi di file.


La doppia citazione dei nomi dei file risolve correttamente i casi in cui i nomi contengono caratteri glob o spazi bianchi. Ma i nomi di file * nix possono contenere anche nuove righe. Quindi limitiamo i nomi di file con l'unico carattere che non può far parte di un nome file valido: il byte null ( \0). Poiché bashutilizza internamente Cstringhe di stile in cui vengono utilizzati i byte null per indicare la fine della stringa, è il candidato giusto per questo.

Quindi usando l' printfopzione della shell per delimitare i file con questo byte NULL usando l' -dopzione del readcomando, possiamo fare di seguito

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

Il nullglobe il printfsono racchiusi, il (..)che significa che sono fondamentalmente eseguiti in una sotto-shell (shell figlio), perché per evitare l' nullglobopzione di riflettere sulla shell padre, una volta che il comando termina. L' -d ''opzione di readcomando non è conforme a POSIX, quindi per farlo è necessaria una bashshell. Usando il findcomando questo può essere fatto come

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

Per le findimplementazioni che non supportano -print0(diverse dalle implementazioni GNU e FreeBSD), questo può essere emulato usandoprintf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

Un'altra soluzione importante è spostare la reindirizzamento fuori dal for-loop per ridurre un numero elevato di I / O di file. Se utilizzata all'interno del ciclo, la shell deve eseguire due volte le chiamate di sistema per ogni iterazione del ciclo for, una volta per aprire e una volta per chiudere il descrittore di file associato al file. Questo diventerà un collo di bottiglia sulle tue prestazioni per eseguire iterazioni di grandi dimensioni. Il suggerimento consigliato sarebbe di spostarlo al di fuori del ciclo.

Estendendo il codice sopra con queste correzioni, potresti farlo

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

che fondamentalmente metterà il contenuto del tuo comando per ogni iterazione del tuo input di file su stdout e quando il ciclo termina, apri il file di destinazione una volta per scrivere il contenuto dello stdout e salvarlo. La findversione equivalente della stessa sarebbe

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out

1
+1 per verificare l'esistenza del file. Se si cerca in una directory non esistente, $ file contiene la stringa regex "/ invald_dir / *" non un nome file valido.
cdalxndr,

3

Un modo rapido e sporco che a volte fa il lavoro è:

find directory/ | xargs  Command 

Ad esempio, per trovare il numero di righe in tutti i file nella directory corrente, puoi fare:

find . | xargs wc -l

8
@Hubert Perché hai newline nei tuoi nomi di file ?!
musicin3d

2
non è una questione di "perché", è una questione di correttezza - i nomi dei file non devono includere caratteri stampabili, non devono nemmeno essere sequenze UTF-8 valide. Inoltre, ciò che è una newline dipende molto dalla codifica, una codifica ♀ è la nuova riga di un'altra. Vedi
tabella

2
cmon, davvero? questo funziona il 99,9% delle volte, e ha detto "veloce e sporco"
Edoardo

Non sono un fan degli script Bash "veloci e sporchi" (AKA "spezzati"). Prima o poi finisce in cose come il famoso "Spostato ~/.local/share/steam. Ha funzionato a vapore. Ha eliminato tutto sul sistema di proprietà dell'utente." riportare un errore.
riduzione dell'attività

Anche questo non funzionerà con i file che hanno spazi nel nome.
Shamas S - Ripristina Monica il

2

Avevo bisogno di copiare tutti i file .md da una directory in un'altra, quindi ecco cosa ho fatto.

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

Che è piuttosto difficile da leggere, quindi lasciamolo scomporre.

primo cd nella directory con i tuoi file,

for i in **/*.md; per ogni file nel tuo modello

mkdir -p ../docs/"$i"crea quella directory in una cartella documenti al di fuori della cartella contenente i tuoi file. Che crea una cartella aggiuntiva con lo stesso nome di quel file.

rm -r ../docs/"$i" rimuovere la cartella aggiuntiva creata come risultato di mkdir -p

cp "$i" "../docs/$i" Copia il file attuale

echo "$i -> ../docs/$i" Eco quello che hai fatto

; done Vivi felici e contenti


Nota: per **funzionare, è globstarnecessario impostare l' opzione shell:shopt -s globstar
Hubert Kario

2

Puoi usare xarg

ls | xargs -L 1 -d '\n' your-desired-command

-L 1 fa passare 1 oggetto alla volta

-d '\n'rendere l'output di lsè diviso in base alla nuova riga.


1

Basato sull'approccio di @Jim Lewis:

Ecco una soluzione rapida che utilizza finde ordina anche i file in base alla data di modifica:

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

Per l'ordinamento vedere:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time


questo non funzionerà se i file hanno una nuova riga nei loro nomi
Hubert Kario,

1
@HubertKario Si consiglia di leggere di più su -print0per il finde -0per il xargsquale utilizzare carattere null al posto di qualsiasi spazio bianco (includendo i fine riga).
tuxdna,

sì, usare -print0è qualcosa che aiuta, ma l'intera pipeline deve usare qualcosa del genere, e sortnon lo è
Hubert Kario

1

penso che la soluzione semplice sia:

sh /dir/* > ./result.txt

2
Hai capito correttamente la domanda? Questo tenterà semplicemente di eseguire ciascun file nella directory attraverso la shell, come se fosse uno script.
rdas,

1

Profondità massima

Ho scoperto che funziona bene con la risposta di Jim Lewis, basta aggiungere un po 'come questo:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

Ordinamento

Se si desidera eseguire l'ordinamento, modificarlo in questo modo:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

Solo per un esempio, questo verrà eseguito con il seguente ordine:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

Profondità illimitata

Se si desidera eseguire a profondità illimitata in base a determinate condizioni, è possibile utilizzare questo:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

quindi metti sopra ogni file nelle directory secondarie in questo modo:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

e da qualche parte nel corpo del file principale:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
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.