Zostało to omówione w wielu pytaniach dotyczących unix.SE, postaram się zebrać wszystkie problemy, które mogę tutaj wymyślić. Referencje na końcu.
Dlaczego zawodzi
Powodem, dla którego napotykasz te problemy, jest dzielenie słów i fakt, że cudzysłowy rozwinięte ze zmiennych nie działają jak cudzysłowy, ale są zwykłymi znakami.
Przypadki przedstawione w pytaniu:
$ abc='ls -l "/tmp/test/my dir"'
Tutaj $abc
jest podzielony i ls
otrzymuje dwa argumenty "/tmp/test/my
oraz dir"
(z cudzysłowami na początku pierwszego i na końcu drugiego):
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
Tutaj rozwinięcie jest cytowane, więc jest przechowywane jako pojedyncze słowo. Powłoka próbuje znaleźć program o nazwie ls -l "/tmp/test/my dir"
, w tym spacje i cudzysłowy.
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
A tutaj, tylko pierwsze słowo lub $abc
jest brane jako argument -c
, więc Bash po prostu działa ls
w bieżącym katalogu. Pozostałe słowa są argumenty bash i są używane do wypełnienia $0
, $1
itp
$ bash -c $abc
'my dir'
Za pomocą bash -c "$abc"
i eval "$abc"
istnieje dodatkowy etap przetwarzania powłoki, który sprawia, że cytaty działają, ale powoduje również, że wszystkie rozszerzenia powłoki są przetwarzane ponownie , więc istnieje ryzyko przypadkowego uruchomienia rozszerzenia poleceń z danych dostarczonych przez użytkownika, chyba że jesteś bardzo ostrożnie z cytowaniem.
Lepsze sposoby na zrobienie tego
Dwa lepsze sposoby przechowywania polecenia to: a) zamiast tego użyj funkcji, b) użyj zmiennej tablicowej (lub parametrów pozycyjnych).
Korzystanie z funkcji:
Po prostu zadeklaruj funkcję z poleceniem w środku i uruchom funkcję tak, jakby była poleceniem. Rozszerzenia poleceń w funkcji są przetwarzane tylko wtedy, gdy polecenie jest uruchomione, a nie kiedy jest zdefiniowane, i nie trzeba cytować poszczególnych poleceń.
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Za pomocą tablicy:
Tablice umożliwiają tworzenie zmiennych składających się z wielu słów, w których pojedyncze słowa zawierają białe znaki. Tutaj poszczególne słowa są przechowywane jako odrębne elementy tablicy, a "${array[@]}"
rozwinięcie rozwija każdy element jako osobne słowa powłoki:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
Składnia jest nieco okropna, ale tablice pozwalają również budować wiersz poleceń kawałek po kawałku. Na przykład:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
lub utrzymuj stałe części wiersza poleceń i użyj wypełnienia tablicy tylko jego częścią, opcjami lub nazwami plików:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
Wadą tablic jest to, że nie są one standardową funkcją, więc zwykłe powłoki POSIX (takie jak dash
domyślne /bin/sh
w Debian / Ubuntu) nie obsługują ich (ale patrz poniżej). Bash, ksh i zsh jednak, więc prawdopodobnie twój system ma jakąś powłokę, która obsługuje tablice.
Za pomocą "$@"
W powłokach bez obsługi nazwanych tablic można nadal używać parametrów pozycyjnych (pseudo-tablica "$@"
) do przechowywania argumentów polecenia.
Poniżej powinny znajdować się przenośne bity skryptu, które odpowiadają ekwiwalentowi bitów kodu w poprzedniej sekcji. Tablica zostaje zastąpiona "$@"
listą parametrów pozycyjnych. Ustawianie "$@"
odbywa się za pomocą set
, a podwójne cudzysłowy "$@"
są ważne (powodują, że elementy listy są indywidualnie cytowane).
Po pierwsze, po prostu zapisz polecenie z argumentami "$@"
i uruchom je:
set -- ls -l "/tmp/test/my dir"
"$@"
Warunkowe ustawienie części opcji wiersza polecenia dla polecenia:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
Używanie tylko "$@"
dla opcji i operandów:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(Oczywiście "$@"
jest zwykle wypełnione argumentami do samego skryptu, więc musisz je gdzieś zapisać przed ponownym wybieraniem "$@"
).
Bądź ostrożny z eval
!
Ponieważ eval
wprowadza dodatkowy poziom przetwarzania ofert i ekspansji, musisz być ostrożny przy wprowadzaniu danych przez użytkownika. Na przykład działa to tak długo, jak użytkownik nie wpisuje żadnych pojedynczych cudzysłowów:
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
Ale jeśli podadzą dane wejściowe '$(uname)'.txt
, skrypt z przyjemnością uruchomi podstawienie polecenia.
Wersja z tablicami jest na to odporna, ponieważ słowa są przechowywane osobno przez cały czas, nie ma cytatu ani innego przetwarzania zawartości filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
Referencje