Come posso sfuggire allo spazio bianco in un elenco di loop bash?
-
08-07-2019 - |
Domanda
Ho uno script bash shell che scorre in tutte le directory figlio (ma non nei file) di una determinata directory. Il problema è che alcuni dei nomi di directory contengono spazi.
Ecco i contenuti della mia directory di test:
$ls -F test
Baltimore/ Cherry Hill/ Edison/ New York City/ Philadelphia/ cities.txt
E il codice che scorre attraverso le directory:
for f in `find test/* -type d`; do
echo $f
done
Ecco l'output:
test/Baltimore test/Cherry Hill test/Edison test/New York City test/Philadelphia
Cherry Hill e New York City sono considerate come 2 o 3 voci separate.
Ho provato a citare i nomi dei file, in questo modo:
for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
echo $f
done
ma inutilmente.
Deve esserci un modo semplice per farlo.
Le risposte di seguito sono fantastiche. Ma per renderlo più complicato, non sempre voglio usare le directory elencate nella mia directory di test. A volte, invece, desidero passare i nomi delle directory come parametri della riga di comando.
Ho preso il suggerimento di Charles di impostare l'IFS e ho pensato a quanto segue:
dirlist="${@}"
(
[[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
for d in $dirlist; do
echo $d
done
)
e questo funziona bene a meno che non ci siano spazi negli argomenti della riga di comando (anche se questi argomenti sono citati). Ad esempio, chiamando lo script in questo modo: test.sh "Cherry Hill" "New York City"
produce il seguente output:
Cherry Hill New York City
Soluzione
Innanzitutto, non farlo in questo modo. L'approccio migliore è utilizzare find -exec
correttamente:
# this is safe
find test -type d -exec echo '{}' +
L'altro approccio sicuro consiste nell'utilizzare l'elenco con terminazione NUL, sebbene ciò richieda il supporto per la ricerca -print0
:
# this is safe
while IFS= read -r -d '' n; do
printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)
Puoi anche popolare un array da find e passarlo successivamente:
# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want
Se la tua ricerca non supporta IFS
, il tuo risultato non è sicuro - il seguito non si comporterà come desiderato se esistono file contenenti nuove righe nei loro nomi (che, sì, è legale):
# this is unsafe
while IFS= read -r n; do
printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)
Se non si intende utilizzare uno dei precedenti, un terzo approccio (meno efficiente in termini di tempo e utilizzo della memoria, in quanto legge l'intero output del sottoprocesso prima di eseguire la suddivisione in parole) consiste nell'utilizzare un set -f
variabile che non contiene il carattere spazio. Disattiva il globbing ([]
) per impedire l'espansione di stringhe contenenti caratteri glob come *
, ?
o $@
:
# this is unsafe (but less unsafe than it would be without the following precautions)
(
IFS=$'\n' # split only on newlines
set -f # disable globbing
for n in $(find test -mindepth 1 -type d); do
printf '%q\n' "$n"
done
)
Infine, per il caso dei parametri della riga di comando, dovresti usare gli array se la tua shell li supporta (cioè è ksh, bash o zsh):
# this is safe
for d in "$@"; do
printf '%s\n' "$d"
done
manterrà la separazione. Si noti che il preventivo (e l'uso di $*
anziché <=>) è importante. Le matrici possono essere popolate anche in altri modi, come le espressioni glob:
# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
printf '%s\n' "$d"
done
Altri suggerimenti
find . -type d | while read file; do echo $file; done
Tuttavia, non funziona se il nome file contiene nuove righe. Quanto sopra è l'unica soluzione che conosco quando in realtà si desidera avere il nome della directory in una variabile. Se vuoi solo eseguire qualche comando, usa xargs.
find . -type d -print0 | xargs -0 echo 'The directory is: '
Ecco una semplice soluzione che gestisce le schede e / o gli spazi bianchi nel nome del file. Se devi trattare con altri strani personaggi nel nome del file come newline, scegli un'altra risposta.
La directory di prova
ls -F test
Baltimore/ Cherry Hill/ Edison/ New York City/ Philadelphia/ cities.txt
Il codice da inserire nelle directory
find test -type d | while read f ; do
echo "$f"
done
Il nome del file deve essere citato ("$f"
) se utilizzato come argomento. Senza virgolette, gli spazi fungono da separatore di argomenti e più argomenti vengono dati al comando invocato.
E l'output:
test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia
Questo è estremamente complicato in Unix standard, e la maggior parte delle soluzioni si sporca di newline o di qualche altro personaggio. Tuttavia, se si utilizza il set di strumenti GNU, è possibile sfruttare l'opzione find
-print0
e utilizzare xargs
con l'opzione corrispondente -0
(meno-zero). Esistono due caratteri che non possono apparire in un semplice nome file; quelli sono barra e NUL '\ 0'. Ovviamente, la barra appare nei nomi dei percorsi, quindi la soluzione GNU di usare un NUL '\ 0' per segnare la fine del nome è ingegnosa e infallibile.
Perché non semplicemente mettere
IFS='\n'
davanti al comando for? Questo cambia il separatore di campo da & Lt; Spazio gt &; Lt &; Tab GT &; Lt &; Newline gt &; a solo < Newline gt &;
find . -print0|while read -d $'\0' file; do echo "$file"; done
Uso
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
echo $f
done
IFS=$SAVEIFS
Non sarebbe abbastanza?
Idea tratta da http://www.cyberciti.biz/ suggerimenti / movimentazione-nomi di file-con-spazi-in-bash.html
Non archiviare elenchi come stringhe; archiviarli come array per evitare tutta questa confusione delimitatore. Ecco uno script di esempio che funzionerà su tutte le sottodirectory di test o sull'elenco fornito sulla sua riga di comando:
#!/bin/bash
if [ $# -eq 0 ]; then
# if no args supplies, build a list of subdirs of test/
dirlist=() # start with empty list
for f in test/*; do # for each item in test/ ...
if [ -d "$f" ]; then # if it's a subdir...
dirlist=("${dirlist[@]}" "$f") # add it to the list
fi
done
else
# if args were supplied, copy the list of args into dirlist
dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
printf "Directory: %s\n" "$dir"
done
Ora proviamo questo in una directory di test con una curva o due lanciati:
$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City
È possibile utilizzare IFS (separatore di campo interno) nel tempo usando:
OLD_IFS=$IFS # Stores Default IFS
IFS=$'\n' # Set it to line break
for f in `find test/* -type d`; do
echo $f
done
$IFS=$OLD_IFS
ps se riguarda solo lo spazio nell'input, allora alcune doppie virgolette hanno funzionato senza problemi per me ...
read artist;
find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
Per aggiungere a ciò Jonathan detto: utilizzare l'opzione -print0
per find
insieme a xargs
come segue:
find test/* -type d -print0 | xargs -0 command
Questo eseguirà il comando command
con gli argomenti appropriati; le directory con spazi al loro interno verranno opportunamente quotate (cioè verranno passate come un argomento).
#!/bin/bash
dirtys=()
for folder in *
do
if [ -d "$folder" ]; then
dirtys=("${dirtys[@]}" "$folder")
fi
done
for dir in "${dirtys[@]}"
do
for file in "$dir"/\*.mov # <== *.mov
do
#dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'` -- This line will replace each space into '\ '
out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'` # These two line code can be written in one line using multiple sed commands.
out=`echo "$out" | sed 's/[[:space:]]/_/g'`
#echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"
`ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`
done
done
Il codice sopra convertirà i file .mov in .avi. I file .mov sono in diverse cartelle e i nomi delle cartelle hanno anche spazi bianchi . Il mio script sopra convertirà i file .mov in file .avi nella stessa cartella stessa. Non so se ti aiuti i popoli.
Caso:
[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov 0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi 0102_-_Course_Structure.avi
0101 - About this Course.mov 0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!
Cheers!
Doveva occuparsi anche degli spazi bianchi nei nomi dei percorsi. Quello che finalmente ho fatto è stato usare una ricorsione e for item in /path/*
:
function recursedir {
local item
for item in "${1%/}"/*
do
if [ -d "$item" ]
then
recursedir "$item"
else
command
fi
done
}
Converti l'elenco dei file in un array Bash. Questo utilizza l'approccio di Matt McClure per restituire un array da una funzione Bash: http: // notes-matthewlmcclure .blogspot.com / 2009/12 / ritorno-array-da-bash-funzione-v-2.html Il risultato è un modo per convertire qualsiasi input multilinea in un array Bash.
#!/bin/bash
# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"
# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"
for f in "${myArray[@]}"
do
echo "Element: $f"
done
Questo approccio sembra funzionare anche quando sono presenti caratteri errati ed è un modo generale per convertire qualsiasi input in un array Bash. Lo svantaggio è che se l'input è lungo potresti superare i limiti di dimensione della riga di comando di Bash o utilizzare grandi quantità di memoria.
Gli approcci in cui il loop che alla fine sta funzionando nell'elenco ha anche il piping dell'elenco hanno lo svantaggio che leggere stdin non è facile (come chiedere input all'utente) e il loop è un nuovo processo, quindi potresti essere chiedersi perché le variabili impostate all'interno del ciclo non siano disponibili al termine del ciclo.
Non mi piace anche impostare IFS, può rovinare altro codice.
ho appena scoperto che ci sono alcune somiglianze tra la mia question e il tuo. Allo stesso modo, se si desidera passare argomenti nei comandi
test.sh "Cherry Hill" "New York City"
per stamparli in ordine
for SOME_ARG in "$@"
do
echo "$SOME_ARG";
done;
nota che $ @ è racchiuso tra virgolette, alcune note qui
Avevo bisogno dello stesso concetto per comprimere sequenzialmente più directory o file da una determinata cartella. Ho risolto usando awk per analizzare l'elenco da ls ed evitare il problema dello spazio vuoto nel nome.
source="/xxx/xxx"
dest="/yyy/yyy"
n_max=`ls . | wc -l`
echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"
che ne pensi?
find Downloads -type f | while read file; do printf "%q\n" "$file"; done
Bene, vedo troppe risposte complicate. Non voglio passare l'output di find utility o scrivere un loop, perché find ha & Quot; exec & Quot; opzione per questo.
Il mio problema era che volevo spostare tutti i file con estensione dbf nella cartella corrente e alcuni di essi contenevano spazi bianchi.
L'ho affrontato così:
find . -name \*.dbf -print0 -exec mv '{}' . ';'
Sembra molto semplice per me
Per me funziona, ed è praticamente " clean " ;:
for f in "$(find ./test -type d)" ; do
echo "$f"
done
Ho appena avuto un semplice problema di variante ... Converti i file di .flv digitati in .mp3 (sbadiglio).
for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done
trova ricorsivamente tutti i file flash dell'utente Macintosh e trasformali in audio (copia, nessuna transcodifica) ... è come nel caso precedente, notando che la lettura invece del solo 'per file in ' scapperà.