bash: bezpieczne użycie proceduralne funkcji find w select dla bezpiecznych spacji

12

Biorąc pod uwagę te nazwy plików:

$ ls -1
file
file name
otherfile

bash sam doskonale sobie radzi z osadzonymi białymi znakami:

$ for file in *; do echo "$file"; done
file
file name
otherfile
$ select file in *; do echo "$file"; done
1) file
2) file name
3) otherfile
#?

Czasami jednak nie chcę pracować z każdym plikiem, a nawet ściśle w tym $PWD, co tam findjest. Który obsługuje również białe znaki nominalnie:

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name

Próbuję wymyślić bezpieczną dla whispace wersję tego skryptu, który weźmie dane wyjściowe findi zaprezentuje je w select:

$ select file in $(find -type f -name file); do echo $file; break; done
1) ./file
2) ./directory/file

Jednak wybucha to spacjami w nazwach plików:

$ select file in $(find -type f -name file\*); do echo $file; break; done
1) ./file        3) name          5) ./directory/file
2) ./file        4) ./directory/file  6) name

Zwykle poradziłbym sobie z tym, bawiąc się IFS. Jednak:

$ IFS=$'\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
$ IFS='\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'

Jakie jest na to rozwiązanie?

DopeGhoti
źródło
1
Jeśli tylko za pomocą findswojej zdolności do dopasować dany nazwę pliku, możesz po prostu użyć select file in **/file*(po ustaleniu shopt -s globstar) w bash4 lub nowszy.
chepner

Odpowiedzi:

14

Jeśli potrzebujesz tylko obsługiwać spacje i tabulatory (nie osadzone znaki nowej linii), możesz użyć mapfile(lub jego synonimu readarray), aby odczytać tablicę, np.

$ ls -1
file
other file
somefile

następnie

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file

Jeśli zrobić potrzebę nowej linii uchwytów, a bashwersja zapewnia zerowy rozdzielany mapfile1 , a następnie można modyfikować, że do IFS= mapfile -t -d '' files < <(find . -type f -print0). W przeciwnym razie złóż równoważną tablicę z finddanych wyjściowych rozdzielonych znakiem null za pomocą readpętli:

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines

1-d opcji dodano mapfilew bashwersji 4.4 IIRC

steeldriver
źródło
2
+1 za inny czasownik, którego wcześniej nie używałem
roaima,
Rzeczywiście, mapfilejest także dla mnie nowy. Sława.
DopeGhoti
while IFS= readWersja działa z powrotem w v3 bash (co jest ważne dla tych z nas, używając MacOS).
Gordon Davisson
3
+1 za find -print0wariant; narzekam za umieszczenie go po znanej niepoprawnej wersji i opisanie go tylko do użytku, jeśli wiadomo , że trzeba obsługiwać nowe wiersze. Jeśli ktoś poradzi sobie z nieoczekiwanym w miejscach, w których jest to oczekiwane, nigdy nie poradzi sobie z nieoczekiwanym.
Charles Duffy,
8

Ta odpowiedź zawiera rozwiązania dla każdego rodzaju plików. Z nowymi liniami lub spacjami.
Istnieją rozwiązania dla ostatniego basha, a także starożytnego basha, a nawet starych powłok posix.

Do testów użyto drzewa wymienionego poniżej w tej odpowiedzi [1] .

Wybierz

selectPraca z tablicą jest łatwa :

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done

Lub z parametrami pozycyjnymi:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

Tak więc jedynym prawdziwym problemem jest umieszczenie „listy plików” (poprawnie rozdzielonej) w tablicy lub w parametrach pozycyjnych. Czytaj dalej.

grzmotnąć

Nie widzę problemu, który zgłaszasz za pomocą bash. Bash może wyszukiwać w danym katalogu:

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Lub, jeśli lubisz pętlę:

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Zauważ, że powyższa składnia będzie działać poprawnie z dowolną (rozsądną) powłoką (przynajmniej csh).

Jedynym ograniczeniem powyższej składni jest zejście do innych katalogów.
Ale bash może to zrobić:

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Aby wybrać tylko niektóre pliki (takie jak te, które kończą się plikiem), po prostu zamień *:

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>

krzepki

Kiedy w tytule umieścisz słowo „ bezpieczny dla przestrzeni”, założę , że to, co miałeś na myśli, było „ solidne ”.

Najprostszym sposobem na solidne podejście do spacji (lub znaków nowej linii) jest odrzucenie przetwarzania danych wejściowych zawierających spacje (lub znaki nowej linii). Bardzo prostym sposobem na wykonanie tego w powłoce jest wyjście z błędem, jeśli dowolna nazwa pliku rozwija się spacją. Można to zrobić na kilka sposobów, ale najbardziej kompaktowy (i posiks) (ale ograniczony do jednej zawartości katalogu, w tym nazw suddirectories i unikania plików kropkowych):

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.

Jeśli zastosowane rozwiązanie jest solidne w którymkolwiek z tych elementów, usuń test.

W bash podkatalogi mogą być testowane jednocześnie z ** wyjaśnionym powyżej.

Istnieje kilka sposobów dołączania plików kropek, rozwiązaniem Posix jest:

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

odnaleźć

Jeśli z jakiegoś powodu należy użyć find, zamień separator na NUL (0x00).

bash 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>

bash 2.05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"

POSIXLY

Aby stworzyć prawidłowe rozwiązanie POSIX, w którym find nie ma separatora NUL i nie ma -d(ani -a) do odczytu, potrzebujemy zupełnie innego podejścia.

Musimy użyć kompleksu -execz find z wywołaniem powłoki:

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

Lub, jeśli potrzebna jest opcja select (select jest częścią bash, a nie sh):

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>

[1] To drzewo (\ 012 to nowe linie):

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                   ├── file
                   ├── file name
                   └── file with a \012newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a\012newline
                └── zz last file

Można go zbudować za pomocą tych dwóch poleceń:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}
Izaak
źródło
6

Nie można ustawić zmiennej przed konstrukcją zapętloną, ale można ustawić ją przed warunkiem. Oto segment ze strony podręcznika:

Środowisko dla dowolnej prostej komendy lub funkcji można tymczasowo rozszerzyć, poprzedzając je przypisaniami parametrów, jak opisano powyżej w PARAMETRACH.

(Pętla nie jest prostym poleceniem .)

Oto często używany konstrukt pokazujący scenariusze awarii i sukcesu:

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

Niestety nie widzę sposobu na osadzenie zmiany IFSw selectkonstrukcie, gdy ma to wpływ na przetwarzanie powiązanego $(...). Jednak nic nie stoi na przeszkodzie, IFSaby ustawić ją poza pętlą:

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

i widzę, że ten konstrukt działa z select:

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done

Podczas pisania kodu obronną, polecam, że klauzula albo być uruchamiane w podpowłoce, albo IFSi SHELLOPTSzapisane i przywrócone wokół bloku:

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob
roaima
źródło
5
Zakładanie, że IFS=$'\n'jest bezpieczny, jest bezpodstawne. Nazwy plików mogą doskonale zawierać literały nowej linii.
Charles Duffy,
4
Szczerze waham się przed przyjęciem takich twierdzeń na temat możliwego zestawu danych według wartości nominalnej, nawet jeśli są obecne. Najgorszym zdarzeniem utraty danych, na które byłem obecny, był przypadek, w którym skrypt konserwacyjny odpowiedzialny za czyszczenie starych kopii zapasowych próbował usunąć plik utworzony przez skrypt Pythona przy użyciu modułu C ze złym dereferencją wskaźnika, który zrzucił losowe śmieci - w tym nazwę wieloznaczną oddzieloną spacjami.
Charles Duffy,
2
Ludzie budujący skrypt powłoki wykonujący czyszczenie tych plików nie zawracali sobie głowy cytowaniem, ponieważ nazwy „nie mogły” nie pasować [0-9a-f]{24}. TB kopii zapasowych danych służących do obsługi fakturowania klientów zostało utraconych.
Charles Duffy,
4
Całkowicie zgadzam się z @CharlesDuffy. Brak obsługi przypadków skrajnych jest w porządku tylko wtedy, gdy pracujesz interaktywnie i widzisz, co robisz. selectsama konstrukcja jest przeznaczona dla rozwiązań skryptowych , dlatego zawsze powinna być zaprojektowana do obsługi przypadków skrajnych.
Wildcard
2
@ilkkachu, oczywiście - nigdy nie zadzwonisz selectze powłoki, w której wpisujesz polecenia do uruchomienia, ale tylko w skrypcie, w którym odpowiadasz na monit dostarczony przez ten skrypt i gdzie ten skrypt jest wykonywanie predefiniowanej logiki (zbudowanej bez wiedzy o obsługiwanych nazwach plików) na podstawie tych danych wejściowych.
Charles Duffy
4

Mogę być poza moją jurysdykcją tutaj, ale może możesz zacząć od czegoś takiego, przynajmniej nie ma żadnych problemów z białymi znakami:

find -maxdepth 1 -type f -printf '%f\000' | {
    while read -d $'\000'; do
            echo "$REPLY"
            echo
    done
}

Aby uniknąć potencjalnych fałszywych założeń, jak zauważono w komentarzach, należy pamiętać, że powyższy kod jest równoważny z:

   find -maxdepth 1 -type f -printf '%f\0' | {
        while read -d ''; do
                echo "$REPLY"
                echo
        done
    }
flerb
źródło
read -djest sprytnym rozwiązaniem; dzięki za to.
DopeGhoti
2
read -d $'\000'jest dokładnie identyczny z read -d '', ale dla wprowadzających w błąd ludzi o możliwościach basha (niepoprawnie sugerując, że jest w stanie reprezentować dosłowne wartości NUL w łańcuchach). Uruchom s1=$'foo\000bar'; s2='foo', a następnie spróbuj znaleźć sposób na rozróżnienie tych dwóch wartości. (Przyszła wersja może się znormalizować z zachowaniem zastępowania poleceń, czyniąc przechowywaną wartość równoważną foobar, ale dzisiaj tak nie jest).
Charles Duffy,