Fare script BASH `per` gestire i nomi di file con spazi (o soluzione alternativa)


12

Mentre uso BASH da diversi anni, la mia esperienza con gli script BASH è relativamente limitata.

Il mio codice è come di seguito. Dovrebbe prendere l'intera struttura di directory dall'interno della directory corrente e replicarla in $OUTDIR.

for DIR in `find . -type d -printf "\"%P\"\040"`
do
  echo mkdir -p \"${OUTPATH}${DIR}\"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done

Il problema è che ecco un esempio della mia struttura di file:

$ ls
Expect The Impossible-Stellar Kart
Five Iron Frenzy - Cheeses...
Five Score and Seven Years Ago-Relient K
Hello-After Edmund
I Will Go-Starfield
Learning to Breathe-Switchfoot
MMHMM-Relient K

Nota gli spazi: -S E foraccetta i parametri parola per parola, quindi l'output del mio script è simile al seguente:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Learning"
Created Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot"
Created Breathe-Switchfoot

Ma ne ho bisogno per prendere interi nomi di file (una riga alla volta) dall'output di find. Ho anche provato a findmettere virgolette doppie attorno a ciascun nome file. Ma questo non aiuta.

for DIR in `find . -type d -printf "\"%P\"\040"`

E output con questa linea modificata:

Creating directory structure...
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"""
Created ""
mkdir -p "/myfiles/multimedia/samjmusicmp3test/"Learning"
Created "Learning
mkdir -p "/myfiles/multimedia/samjmusicmp3test/to"
Created to
mkdir -p "/myfiles/multimedia/samjmusicmp3test/Breathe-Switchfoot""
Created Breathe-Switchfoot"

Ora, ho bisogno di un modo per poter scorrere in questo modo, perché desidero anche eseguire un comando più complicato che coinvolge gstreamerogni file in una struttura simile seguente. Come dovrei farlo?

Modifica: ho bisogno di una struttura di codice che mi permetta di eseguire più righe di codice per ogni directory / file / loop. Scusa se non ero chiaro.

Soluzione: inizialmente ho provato:

find . -type d | while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done

Questo ha funzionato bene per la maggior parte. Tuttavia, in seguito ho scoperto che, poiché la pipe risulta in esecuzione nel ciclo while in una subshell, tutte le variabili impostate nel ciclo non sono state successivamente disponibili, il che ha reso piuttosto difficile l'implementazione di un contatore di errori. La mia soluzione finale (da questa risposta su SO ):

while read DIR
do
  mkdir -p "${OUTPATH}${DIR}"
  echo Created $DIR
done < <(find . -type d)

Questo in seguito mi ha permesso di incrementare condizionalmente le variabili all'interno del ciclo che sarebbero rimaste disponibili più avanti nello script.


Why_would_you_ever_need_a_space_in_a_file_name?
Kevin Panko,

Vero, non la mia preferenza. Tuttavia, per rimuovere gli spazi, devi prima gestire i file con spazi;)
Samuel Jaeschke

1
In realtà, i nomi dei file dovrebbero consentire spazi. Consentirei caratteri tutt'altro che /non stampabili. Ma tutto è permesso tranne /e \0quindi è necessario consentirli.
Kevin Panko,

Risposte:


11

È necessario reindirizzare findin un whileciclo.

find ... | while read -r dir
do
    something with "$dir"
done

Inoltre, non sarà necessario utilizzare -printfin questo caso.

Puoi fare questa prova contro i file con nuove righe nei loro nomi, se lo desideri, usando un delimitatore nullbyte (che è l'unico carattere che non può apparire in un percorso file * nix):

find ... -print0 | while read -d '' -r dir
do
    something with "$dir"
done

Troverai anche l'utilizzo al $()posto dei backtick per essere più versatile e più facile. Possono essere nidificati molto più facilmente e la quotazione può essere eseguita molto più facilmente. Questo esempio inventato illustrerà questi punti:

echo "$(echo "$(echo "hello")")"

Prova a farlo con i backtick.


2
Inoltre, piuttosto che "$dir", è preferibile usare "${dir}": è facile dire la differenza tra $ {dir} name e $ {dirname}, ma $ dirname potrebbe essere interpretato in entrambi i modi.
James Polley,

La cosa importante qui è che readlegge un'intera riga ${dir}, quindi l'IFS non ha importanza.
James Polley,

1
Grazie per aver trovato il refuso $ / ". Le parentesi graffe non sono necessarie se non c'è nulla che segue il nome della variabile.
Pausa fino a ulteriore comunicazione.

4
Questo gestirà i nomi dei percorsi con spazi (U + 0020), ma non riuscirà comunque a gestire correttamente i nomi dei percorsi con feed di linea (U + 000A). Preferisco find … -print0 | xargs -0 …perché il delimitatore che utilizza corrisponde esattamente all'unico carattere non consentito nei nomi dei marchi POSIX: NUL (U + 0000).
Chris Johnsen,

2
Perfetto! Proprio quello che stavo cercando. Non mi era mai venuto in mente che potresti essere in grado di convogliare while. @Chris Johnsen: Vero, ma anche i programmi di ripping di musica non tendono a mettere le linefeed nei loro nomi di file. E se lo fanno, voglio sapere (cioè: qualcosa va storto) e liberarmene immediatamente ...
Samuel Jaeschke,

8

Vedi questa risposta che ho scritto qualche giorno fa per un esempio di uno script che gestisce i nomi dei file con spazi.

C'è un modo leggermente più contorto (ma più conciso) per ottenere ciò che stai cercando di fare:

find . -type d -print0 | xargs -0 -I {} mkdir -p ../theredir/{}

-print0dice a find di separare gli argomenti con un null; da -0 a xargs indica che si aspettano argomenti separati da null. Ciò significa che gestisce bene gli spazi.

-I {}dice a xargs di sostituire la stringa {}con il nome file. Questo implica anche che dovrebbe essere usato un solo nome di file per riga di comando (normalmente xargs ne riempirà quanti ne rientrano nella riga)

Il resto dovrebbe essere ovvio.


Il suggerimento di Dennis Williamson è, tuttavia (a parte gli errori di battitura) molto più leggibile, e quindi preferibile in quasi tutti i modi.
James Polley,

Funziona, per mkdir, ma scusate avrei dovuto essere più chiaro: vorrei eseguire una serie di comandi per ogni file. Vedete, per la mia routine simile in seguito desidero generare un nome file di output basato sul nome file di input (che comporta l'eliminazione dell'estensione .ogg e l'aggiunta di .mp3) e quindi utilizzare queste variabili multiple nella mia pipeline quando invoco gst-launch.
Samuel Jaeschke,

5

Il problema che stai riscontrando è che l'istruzione for sta rispondendo alla ricerca come argomenti separati. Il delimitatore di spazio. Devi usare la variabile IFS di bash per non dividere lo spazio.

Ecco un link che spiega come farlo.

La variabile interna IFS

Un modo per aggirare questo problema è cambiare la variabile IFS (Internal Field Separator) interna di Bash in modo che divida i campi con qualcosa di diverso dallo spazio bianco predefinito (spazio, tab, newline), in questo caso, una virgola.

#!/bin/bash
IFS=$';'

for I in `find -type d -printf \"%P\"\;`
do
   echo "== $I =="
done

Imposta la tua ricerca per generare il delimitatore di campo dopo% P e imposta l'IFS in modo appropriato. Ho scelto il punto e virgola poiché è molto improbabile trovarlo nei nomi dei file.

L'altra alternativa è quella di chiamare mkdir dalla ricerca direttamente tramite -execè possibile saltare del tutto il ciclo for. Questo se non hai bisogno di eseguire ulteriori analisi.


Cosa succede se il nome file contiene l'IFS? Quindi devi sceglierne uno diverso. Ma poi, cosa succede se ...
Pausa fino a nuovo avviso.

3
Puoi scegliere /tra POSIX e :i filesystem DOS. Esistono caratteri illegali per diversi filesystem che puoi scegliere per l'IFS. Qualcosa di più complicato e stai meglio usando il perl.
Darren Hall,

2
Il problema con l'utilizzo di / è che è il delimitatore di directory e findrestituisce nomi di file con percorsi che includono una barra. Prova a cambiare il punto e virgola nel tuo script in una barra e l'eco stamperà la directory e il nome del file su righe separate.
In pausa fino a ulteriore avviso.

Anche questo sembra abbastanza utile. Sono andato con la pipa per l' whileopzione, ma anche questo sembra abbastanza praticabile. Sì, nella mia struttura simile in seguito ho dovuto eseguire ulteriori analisi. (Il nome file di input sarebbe .ogg, che verrebbe passato come filesrcnella pipeline gst, ma verrebbe generato un finale equivalente in .mp3 basato sulla directory di output e anche passato alla pipeline come filesink, e ovviamente questo deve essere fatto per ogni file, insieme ad alcuni echoall'utente.)
Samuel Jaeschke

4

Se il corpo del tuo ciclo è più di un singolo comando, è possibile usare xargs per guidare uno script di shell:

export OUTPATH=/some/where/else/
find . -type d -print0 | xargs -0 bash -c 'for DIR in "$@"; do
  printf "mkdir -p %q\\n" "${OUTPATH}${DIR}"        # Using echo for debug; working script will simply execute mkdir
  echo Created $DIR
done' -

Assicurati di includere il trattino finale (o qualche altra 'parola') se la shell è della varietà Bourne / POSIX (è usata per impostare $ 0 nello script della shell). Inoltre, è necessario fare attenzione con le virgolette, poiché lo script della shell viene scritto all'interno di una stringa tra virgolette anziché direttamente al prompt.


Un altro concetto interessante. Grazie - Sono sicuro che troverò un uso più tardi :)
Samuel Jaeschke

1

nella tua domanda aggiornata hai

mkdir -p \"${OUTPATH}${DIR}\"

questo dovrebbe essere

mkdir -p "${OUTPATH}${DIR}"

Grazie. Fisso. Stava anche leggendo FILENAME invece di DIR - copia-incolla: P
Samuel Jaeschke

1
find . -type d -exec mkdir -p "{}\040" ';' -exec echo "Created {}\040" ';'

0

o per rendere il tutto molto meno complicato:

% rsync -av --include='*/' --exclude='*' SRC DST

questo replica la struttura di directory di SRC in ora legale.


No, ho bisogno di una struttura iterativa come questa, che mi consenta di eseguire più righe di codice per ciascun file. "Ora, ho bisogno di un modo per poter scorrere in questo modo, perché desidero anche eseguire un comando più complicato che coinvolga gstreamer su ciascun file in una struttura simile seguente." Scusa se non ero chiaro.
Samuel Jaeschke,

il comando che ho dato risolve il problema che hai chiesto, non importa se questa è solo una parte di una 'pipeline' più grande dalla tua parte. per qualcun altro che ha il problema, come descritto nella domanda, l'approccio rsync funzionerà. così, non c'è bisogno di essere dispiaciuto per il potenziale unclearity :)
Akira

Sì. No, voglio dire che in seguito utilizzerei una struttura simile while... do... doneper eseguire un'elaborazione simile da find, che richiederebbe l'esecuzione di più righe di codice su ciascun file (modifica stringa, eco, gst-launch, ecc. ) e rsyncnon raggiungerebbe questo obiettivo. Ecco perché ho specificato che dovevo essere in grado di eseguire un insieme più complicato di comandi all'interno di una struttura simile. Il mio script utilizza questa struttura a ciclo due volte, quindi per la domanda ho pubblicato quello con meno rozzo nel mezzo.
Samuel Jaeschke,

0

Se hai GNU Parallel http: // www.gnu.org/software/parallel/ installato puoi farlo:

find . -type d | parallel echo making {} ";" mkdir -p /tmp/outdir/{} ";" echo made {}

Guarda il video introduttivo di GNU Parallel per saperne di più: http://www.youtube.com/watch?v=OpaiGYxkSuQ

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.