Jak mogę uciec od spacji na liście pętli bash?

121

Mam skrypt powłoki bash, który przechodzi przez wszystkie katalogi podrzędne (ale nie pliki) określonego katalogu. Problem polega na tym, że niektóre nazwy katalogów zawierają spacje.

Oto zawartość mojego katalogu testowego:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Oraz kod, który przechodzi przez katalogi:

for f in `find test/* -type d`; do
  echo $f
done

Oto wynik:

test / Baltimore
test / Cherry
Wzgórze
test / Edison 
test / nowy
York
Miasto
test / Philadelphia

Cherry Hill i Nowy Jork są traktowane jako 2 lub 3 osobne wpisy.

Próbowałem zacytować nazwy plików, na przykład:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

ale bez skutku.

Musi być na to prosty sposób.


Odpowiedzi poniżej są świetne. Ale żeby to jeszcze bardziej skomplikować - nie zawsze chcę używać katalogów wymienionych w moim katalogu testowym. Czasami chcę zamiast tego przekazać nazwy katalogów jako parametry wiersza polecenia.

Przyjąłem sugestię Charlesa dotyczącą ustawienia IFS i wymyśliłem, co następuje:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

i działa to dobrze, chyba że w argumentach wiersza poleceń są spacje (nawet jeśli te argumenty są cytowane). Na przykład wywołanie skryptu w ten sposób: test.sh "Cherry Hill" "New York City"daje następujący wynik:

wiśnia
Wzgórze
Nowy
York
Miasto
MCS
źródło
re: edit, list="$@"całkowicie odrzuca listę pierwotnej wartości, zwijając ją do łańcucha. Proszę postępować zgodnie z praktykami zawartymi w mojej odpowiedzi dokładnie tak, jak podano - nigdzie nie zachęca się do takiego przydziału; jeśli chcesz przekazać listę argumentów wiersza poleceń do programu, powinieneś zebrać je w tablicy i bezpośrednio rozwinąć tę tablicę.
Charles Duffy

Odpowiedzi:

105

Po pierwsze, nie rób tego w ten sposób. Najlepszym podejściem jest find -execprawidłowe użycie :

# this is safe
find test -type d -exec echo '{}' +

Innym bezpiecznym podejściem jest użycie listy zakończonej znakiem NUL, chociaż wymaga to wsparcia wyszukiwania -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

Możesz również wypełnić tablicę z funkcji find i przekazać ją później:

# 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

Jeśli twoje znalezisko nie obsługuje -print0, twój wynik jest niebezpieczny - poniższe nie będą zachowywać się zgodnie z oczekiwaniami, jeśli istnieją pliki zawierające znaki nowej linii w nazwie (co, tak, jest legalne):

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Jeśli nie mamy zamiaru używać jednego z powyższych, trzecim podejściem (mniej wydajnym pod względem czasu i wykorzystania pamięci, ponieważ odczytuje całe wyjście podprocesu przed wykonaniem podziału na słowa) jest użycie IFSzmiennej, która nie nie zawierają znaku spacji. Wyłącz globbing ( set -f), aby zapobiec ciągi znaków zawierające takie jak glob [], *lub ?przed rozwinięciem:

# 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
)

Na koniec, w przypadku parametrów wiersza poleceń, powinieneś używać tablic, jeśli twoja powłoka je obsługuje (tj. Jest to ksh, bash lub zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

utrzyma separację. Zauważ, że cytowanie (i użycie $@zamiast $*) jest ważne. Tablice można również wypełniać na inne sposoby, na przykład wyrażenia glob:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done
Charles Duffy
źródło
1
nie wiedziałem o smaku „+” dla -exec. słodki
Johannes Schaub - litb
1
wygląda na to, że może też, podobnie jak xargs, umieścić argumenty tylko na końcu podanej komendy: / ​​to mi czasem przeszkadzało
Johannes Schaub - litb
Myślę, że -exec [nazwa] {} + jest rozszerzeniem GNU i 4.4-BSD. (Przynajmniej nie pojawia się w Solarisie 8 i nie sądzę, żeby był w AIX 4.3.) Myślę, że reszta z nas może utknąć w rurociągu do xargs ...
Michael Ratanapintha
2
Nigdy wcześniej nie widziałem składni $ '\ n'. Jak to działa? (Myślałem, że IFS = '\ n' lub IFS = "\ n" zadziała, ale też nie.)
MCS,
1
@crosstalk to zdecydowanie Solaris 10, właśnie go użyłem.
Nick
26
find . -type d | while read file; do echo $file; done

Jednak nie działa, jeśli nazwa pliku zawiera znaki nowej linii. Powyższe jest jedynym rozwiązaniem, jakie znam, kiedy faktycznie chcesz mieć nazwę katalogu w zmiennej. Jeśli chcesz po prostu wykonać jakąś komendę, użyj xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '
Johannes Schaub - litb
źródło
Nie ma potrzeby korzystania z xargs, zobacz find -exec ... {} +
Charles Duffy
4
@Charles: w przypadku dużej liczby plików xargs jest znacznie bardziej wydajny: generuje tylko jeden proces. Opcja -exec tworzy nowy proces dla każdego pliku, który może być o rząd wielkości wolniejszy.
Adam Rosenfield
1
Bardziej lubię Xargs. Ci dwaj zasadniczo wydają się robić to samo, podczas gdy xargs ma więcej opcji, takich jak bieganie równolegle
Johannes Schaub - litb
2
Adam, nie, ten '+' będzie agregował tak wiele nazw plików, jak to możliwe, a następnie wykonuje. ale nie będzie miał tak zgrabnych funkcji jak bieganie równolegle :)
Johannes Schaub - litb
2
Zauważ, że jeśli chcesz coś zrobić z nazwami plików, będziesz musiał je zacytować. Np .:find . -type d | while read file; do ls "$file"; done
David Moles
23

Oto proste rozwiązanie, które obsługuje tabulatory i / lub spacje w nazwie pliku. Jeśli masz do czynienia z innymi dziwnymi znakami w nazwie pliku, takimi jak znaki nowej linii, wybierz inną odpowiedź.

Katalog testów

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Kod, aby przejść do katalogów

find test -type d | while read f ; do
  echo "$f"
done

Jeśli nazwa pliku jest "$f"używana jako argument, należy ją wpisać w cudzysłów ( ). Bez cudzysłowów spacje działają jako separator argumentów, a wywoływanemu poleceniu jest podawanych wiele argumentów.

A wynik:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia
cbliard
źródło
dzięki, to zadziałało dla aliasu, który tworzyłem, aby wyświetlić, ile miejsca zajmuje każdy katalog w bieżącym folderze, dławiło się niektórymi katalogami ze spacjami w poprzednim wcieleniu. Działa to w zsh, ale niektóre inne odpowiedzi nie:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid,
2
Jeśli używasz zsh, możesz również zrobić to:alias duc='du -sh *(/)'
cbliard
@cbliard To nadal jest błędne. Spróbuj uruchomić go z nazwą pliku zawierającą, powiedzmy, sekwencję tabulatorów lub wiele spacji; zauważysz, że zmienia to dowolne z nich w pojedynczą spację, ponieważ nie cytujesz w swoim echu. Jest też przypadek nazw plików zawierających znaki nowej linii ...
Charles Duffy,
@CharlesDuffy Próbowałem z sekwencjami tabulatorów i wieloma spacjami. Działa z cytatami. Próbowałem też z nowymi liniami i to w ogóle nie działa. Odpowiednio zaktualizowałem odpowiedź. Dziękuję za zwrócenie uwagi.
cbliard
1
@cbliard Right - dodawanie cudzysłowów do twojego polecenia echo było tym, do czego zmierzałem. Jeśli chodzi o nowe wiersze, możesz to zrobić, używając poleceń find -print0i IFS='' read -r -d '' f.
Charles Duffy,
7

Jest to niezwykle trudne w standardowym Uniksie, a większość rozwiązań zawiera znaki nowej linii lub inne znaki. Jeśli jednak używasz zestawu narzędzi GNU, możesz wykorzystać tę findopcję -print0i użyć xargsz odpowiednią opcją -0(minus-zero). Istnieją dwa znaki, które nie mogą pojawić się w prostej nazwie pliku; to są slash i NUL '\ 0'. Oczywiście ukośnik pojawia się w nazwach ścieżek, więc rozwiązanie GNU polegające na użyciu NUL '\ 0' do oznaczenia końca nazwy jest genialne i niezawodne.

Jonathan Leffler
źródło
4

Dlaczego po prostu nie położyć

IFS='\n'

przed dowództwem? Spowoduje to zmianę separatora pól z <Spacja> <Tab> <Nowa linia> na <Nowa linia>

oshunluvr
źródło
4
find . -print0|while read -d $'\0' file; do echo "$file"; done
Freakus
źródło
1
-d $'\0'jest dokładnie tym samym, co -d ''- ponieważ bash używa ciągów zakończonych znakiem NUL, pierwszym znakiem pustego łańcucha jest NUL iz tego samego powodu wartości NUL w ogóle nie mogą być reprezentowane wewnątrz łańcuchów C.
Charles Duffy,
4

używam

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

Czy to nie wystarczy?
Pomysł zaczerpnięty z http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

murpel
źródło
świetna wskazówka: jest to bardzo pomocne w przypadku opcji do skryptu wiersza poleceń (OS X AppleScript), w którym spacje dzielą argument na wiele parametrów, w których tylko jeden jest przeznaczony
czas
Nie, to nie wystarczy. Jest nieefektywny (ze względu na niepotrzebne użycie $(echo ...)), nie obsługuje poprawnie nazw plików z wyrażeniami glob, nie obsługuje poprawnie nazw plików, które zawierają $'\b'lub znaki $ '\ n', a ponadto konwertuje wiele ciągów białych znaków na pojedyncze białe znaki na strona wyjściowa z powodu nieprawidłowego cytowania.
Charles Duffy,
4

Nie przechowuj list jako ciągów; przechowuj je jako tablice, aby uniknąć całego zamieszania w separatorach. Oto przykładowy skrypt, który będzie działał na wszystkich podkatalogach testu lub na liście podanej w jego wierszu poleceń:

#!/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

Teraz wypróbujmy to w katalogu testowym z wrzuconą krzywą lub dwiema:

$ 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
Gordon Davisson
źródło
1
Patrząc wstecz na to - tam rzeczywiście było rozwiązanie z POSIX sh: Można ponownie wykorzystać "$@"tablicę, dołączając do niego z set -- "$@" "$f".
Charles Duffy
4

Możesz tymczasowo użyć IFS (wewnętrznego separatora pól), używając:

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

niesamowite
źródło
Proszę podać wyjaśnienie.
Steve K
IFS określił, czym jest symbol separatora, wtedy nazwa pliku ze spacjami nie zostanie obcięta.
amazingthere
$ IFS = $ OLD_IFS na końcu powinno wyglądać następująco: IFS = $ OLD_IFS
Michel Donais
3

ps jeśli chodzi tylko o spację na wejściu, to niektóre podwójne cudzysłowy działały dla mnie gładko ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
hardbutnot
źródło
2

Aby dodać do tego, co powiedział Jonathan : użyj -print0opcji findw połączeniu z xargsnastępującymi:

find test/* -type d -print0 | xargs -0 command

Spowoduje to wykonanie polecenia commandz odpowiednimi argumentami; katalogi ze spacjami będą odpowiednio cytowane (tj. zostaną przekazane jako jeden argument).

Adam Rosenfield
źródło
1
#!/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

Powyższy kod konwertuje pliki .mov do .avi. Pliki .mov znajdują się w różnych folderach, a nazwy folderów mają spacje . Mój powyższy skrypt sam skonwertuje pliki .mov do pliku .avi w tym samym folderze. Nie wiem, czy to wam pomoże, ludzie.

Walizka:

[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!

Twoje zdrowie!

Sony George
źródło
echo "$name" | ...nie działa, jeśli namejest -n, a sposób, w jaki zachowuje się z nazwami z sekwencjami ucieczki odwrotnym ukośnikiem, zależy od twojej implementacji - POSIX sprawia, że ​​zachowanie echow tym przypadku jest jawnie niezdefiniowane (podczas gdy rozszerzony XSI POSIX sprawia, że ​​rozwinięcie sekwencji ucieczki odwrotnym ukośnikiem jest standardowe zachowanie , a systemy GNU - w tym bash - bez POSIXLY_CORRECT=1przerywania standardu POSIX poprzez implementację -e(podczas gdy specyfikacja wymaga echo -edrukowania -ena wyjściu). printf '%s\n' "$name" | ...jest bezpieczniejsze.
Charles Duffy
1

Musiał też mieć do czynienia ze spacjami w nazwach ścieżek. W końcu użyłem rekurencji i for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}
Gilles 'SO- przestań być zły'
źródło
1
Nie używaj functionsłowa kluczowego - sprawia, że ​​twój kod jest niekompatybilny z POSIX sh, ale nie ma innego użytecznego celu. Możesz po prostu zdefiniować funkcję za pomocą recursedir() {, dodając dwa pareny i usuwając słowo kluczowe function, a będzie to zgodne ze wszystkimi powłokami zgodnymi z POSIX.
Charles Duffy,
1

Przekonwertuj listę plików na tablicę Bash. Wykorzystuje podejście Matta McClure'a do zwracania tablicy z funkcji Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html Wynikiem jest sposób aby przekonwertować dowolne wejście wieloliniowe na tablicę 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

To podejście wydaje się działać nawet wtedy, gdy obecne są złe znaki i jest ogólnym sposobem konwersji dowolnego wejścia na tablicę Bash. Wadą jest to, że jeśli dane wejściowe są długie, możesz przekroczyć limity rozmiaru wiersza poleceń Basha lub zużywać duże ilości pamięci.

Podejścia, w których pętla, która ostatecznie działa na liście, również ma tę listę, ma tę wadę, że odczyt stdin nie jest łatwy (na przykład proszenie użytkownika o wprowadzenie danych), a pętla jest nowym procesem, więc możesz się zastanawiać, dlaczego zmienne ustawione wewnątrz pętli nie są dostępne po zakończeniu pętli.

Nie podoba mi się też ustawianie IFS, może to zepsuć inny kod.

Steve Zobell
źródło
Jeśli używasz IFS='' read, w tym samym wierszu, ustawienie IFS jest obecne tylko dla polecenia odczytu i nie powoduje zmiany znaczenia. Nie ma powodu, by nie lubić ustawiania IFS w ten sposób.
Charles Duffy,
1

Cóż, widzę zbyt wiele skomplikowanych odpowiedzi. Nie chcę przekazywać danych wyjściowych narzędzia find ani pisać pętli, ponieważ find ma do tego opcję "exec".

Mój problem polegał na tym, że chciałem przenieść wszystkie pliki z rozszerzeniem dbf do bieżącego folderu, a niektóre z nich zawierały spacje.

Tak sobie z tym poradziłem:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Wydaje mi się to bardzo proste

Tebe
źródło
0

właśnie się dowiedziałem, że istnieją pewne podobieństwa między moim pytaniem a twoim. Na razie, jeśli chcesz przekazywać argumenty do poleceń

test.sh "Cherry Hill" "New York City"

wydrukować je w kolejności

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

zauważ, że $ @ jest otoczony podwójnymi cudzysłowami, kilka uwag tutaj

Jeffrey04
źródło
0

Potrzebowałem tej samej koncepcji, aby skompresować sekwencyjnie kilka katalogów lub plików z określonego folderu. Rozwiązałem użycie awk do przeanalizowania listy z ls i uniknięcia problemu spacji w nazwie.

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!!!"

co myślisz?

Hìr0
źródło
Myślę, że to nie zadziała poprawnie, jeśli nazwy plików zawierają znaki nowej linii. Może powinieneś spróbować.
user000001
0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done
Johan Kasselman
źródło
-3

U mnie to działa i jest prawie „czyste”:

for f in "$(find ./test -type d)" ; do
  echo "$f"
done
AndrzejP
źródło
4
Ale to jest gorsze. Podwójne cudzysłowy wokół find powodują, że wszystkie nazwy ścieżek są łączone w jeden ciąg. Zmień echo na ls, aby zobaczyć problem.
NVRAM,
-4

Właśnie miałem prosty problem z wariantem ... Konwertuj pliki o typie .flv do .mp3 (ziewanie).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

rekurencyjnie znajdź wszystkie pliki flash użytkownika Macintosha i zamień je na audio (kopiuj, bez transkodowania) ... to tak jak powyżej, zauważ, że odczyt zamiast tylko „dla pliku w ” zostanie usunięty.

mark washeim
źródło
2
readPo injeszcze jeden wyraz w liście jesteś iteracji skończona. To, co opublikowałeś, jest nieco zepsutą wersją tego, co miał pytający, co nie działa. Być może zamierzałeś zamieścić coś innego, ale prawdopodobnie i tak jest to objęte innymi odpowiedziami.
SO- Gilles 'SO- przestań być zły'