$ ls -l /tmp/test/my dir/
total 0
Zajímalo by mě, proč následující způsoby spuštění výše uvedeného příkazu selžou nebo uspějí?
$ abc='ls -l "/tmp/test/my dir"'
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
$ bash -c $abc
'my dir'
$ bash -c "$abc"
total 0
$ eval $abc
total 0
$ eval "$abc"
total 0
Přijatá odpověď:
Toto bylo probráno v řadě otázek na unix.SE, pokusím se zde shromáždit všechny problémy, na které mohu přijít. Reference na konci.
Proč se to nedaří
Důvodem, proč čelíte těmto problémům, je dělení slov a skutečnost, že uvozovky rozšířené z proměnných nefungují jako uvozovky, ale jsou to jen obyčejné znaky.
Případy uvedené v otázce:
Přiřazení zde přiřadí jeden řetězec ls -l "/tmp/test/my dir"
na abc
:
$ abc='ls -l "/tmp/test/my dir"'
Níže $abc
je rozdělena na mezery a ls
získá tři argumenty -l
, "/tmp/test/my
a dir"
(s citátem na přední straně druhého a dalším na zadní straně třetího). Tato možnost funguje, ale cesta je nesprávně zpracována:
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
Zde je rozšíření citováno, takže je zachováno jako jediné slovo. Shell se snaží najít program doslova nazvaný ls -l "/tmp/test/my dir"
včetně mezer a uvozovek.
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
A tady, $abc
je rozděleno a pouze první výsledné slovo je bráno jako argument pro -c
, takže Bash pouze spustí ls
v aktuálním adresáři. Ostatní slova jsou argumenty pro bash a používají se k vyplnění $0
, $1
, atd.
$ bash -c $abc
'my dir'
Pomocí bash -c "$abc"
a eval "$abc"
, existuje další krok zpracování shellu, díky kterému budou uvozovky fungovat, ale také způsobí, že všechna rozšíření shellu budou znovu zpracována , takže existuje riziko náhodného spuštění např. náhrada příkazu z dat poskytnutých uživatelem, pokud nejste velmi opatrní při citování.
Lepší způsoby, jak to udělat
Dva lepší způsoby uložení příkazu jsou a) místo toho použít funkci, b) použít proměnnou pole (nebo poziční parametry).
Použití funkce:
Jednoduše deklarujte funkci s příkazem uvnitř a spusťte funkci, jako by to byl příkaz. Rozšíření v příkazech v rámci funkce se zpracují pouze při spuštění příkazu, nikoli při jeho definování, a jednotlivé příkazy nemusíte citovat.
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Použití pole:
Pole umožňují vytvářet víceslovné proměnné, kde jednotlivá slova obsahují mezery. Zde jsou jednotlivá slova uložena jako odlišné prvky pole a "${array[@]}"
Expanze rozbalí každý prvek jako samostatná slova shellu:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
Syntaxe je trochu příšerná, ale pole vám také umožňují sestavit příkazový řádek po částech. Například:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
nebo ponechte části příkazového řádku konstantní a použijte pole vyplňte pouze jeho část, jako jsou možnosti nebo názvy souborů:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
Nevýhodou polí je, že nejsou standardní funkcí, takže obyčejné shelly POSIX (jako dash
, výchozí /bin/sh
v Debianu/Ubuntu) je nepodporují (ale viz níže). Bash, ksh a zsh však ano, takže je pravděpodobné, že váš systém má nějaké prostředí, které podporuje pole.
Pomocí "[email protected]"
V shellech bez podpory pojmenovaných polí lze stále používat poziční parametry (pseudopole "[email protected]"
) k uložení argumentů příkazu.
Následují bity přenosného skriptu, které dělají ekvivalent bitů kódu v předchozí části. Pole je nahrazeno textem "[email protected]"
, seznam polohových parametrů. Nastavení "[email protected]"
se provádí pomocí set
a dvojité uvozovky kolem "[email protected]"
jsou důležité (způsobí, že prvky seznamu budou jednotlivě citovány).
Nejprve jednoduše uložíte příkaz s argumenty do "[email protected]"
a jeho spuštění:
set -- ls -l "/tmp/test/my dir"
"[email protected]"
Podmíněné nastavení částí voleb příkazového řádku pro příkaz:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "[email protected]" -l
fi
set -- "[email protected]" "$targetdir"
"[email protected]"
Pouze pomocí "[email protected]"
pro možnosti a operandy:
set -- -x -v
set -- "[email protected]" file1 "file name with whitespace"
set -- "[email protected]" /somedir
transmutate "[email protected]"
(Samozřejmě "[email protected]"
je obvykle vyplněno argumenty samotného skriptu, takže je budete muset někam uložit, než znovu použijete "[email protected]"
.)
Pomocí eval
(zde buďte opatrní!)
eval
vezme řetězec a spustí jej jako příkaz, stejně jako kdyby byl zadán na příkazovém řádku shellu. To zahrnuje veškeré zpracování nabídek a rozšíření, což je užitečné i nebezpečné.
V jednoduchém případě umožňuje dělat přesně to, co chceme:
cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"
S eval
, jsou uvozovky zpracovány, takže ls
nakonec vidí pouze dva argumenty -l
a /tmp/test/my dir
, jak chceme. eval
je také dostatečně chytrý na to, aby spojil všechny argumenty, které získá, takže eval $cmd
může v některých případech také fungovat, ale např. všechny běhy mezer by se změnily na jednotlivé mezery. Stále je lepší uvádět proměnnou tam, protože to zajistí, že nebude upravena na eval
.
Nicméně je nebezpečné zahrnout uživatelský vstup do příkazového řetězce do eval
. Zdá se, že například toto funguje:
read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";
Pokud však uživatel zadá vstup, který obsahuje jednoduché uvozovky, může se z uvozovek vymanit a spustit libovolné příkazy! Např. se vstupem '$(whatever)'.txt
, váš skript šťastně spustí substituci příkazu. Že to mohlo být rm -rf
(nebo hůře).
Problém je v tom, že hodnota $filename
byl vložen do příkazového řádku, který eval
běží. Byl rozšířen před eval
, která viděla např. příkaz ls -l ''$(whatever)'.txt'
. Abyste byli v bezpečí, museli byste vstup předem zpracovat.
Pokud to uděláme jinak, ponecháme název souboru v proměnné a necháme eval
příkaz rozbalte, je to opět bezpečnější:
read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";
Všimněte si, že vnější uvozovky jsou nyní jednoduché, takže k rozšíření uvnitř nedochází. Proto eval
vidí příkaz ls -l "$filename"
a sám bezpečně rozšíří název souboru.
Ale to se příliš neliší od pouhého uložení příkazu do funkce nebo pole. U funkcí nebo polí takový problém neexistuje, protože slova jsou po celou dobu udržována odděleně a obsah filename
neobsahuje žádné citace ani jiné zpracování. .
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
V podstatě jediný důvod, proč používat eval
je taková, kde proměnlivá část zahrnuje prvky syntaxe shellu, které nelze vložit prostřednictvím proměnných (potrubí, přesměrování atd.). Ani potom se ujistěte, že nevkládáte vstup od uživatele do eval
příkaz!
Odkazy
- Rozdělení slov v BashGuide
- BashFAQ/050 nebo „Pokouším se zadat příkaz do proměnné, ale složité případy vždy selžou!“
- Otázka Proč se můj skript shellu dusí mezerami nebo jinými speciálními znaky?, která pojednává o řadě problémů souvisejících s citacemi a mezerami, včetně ukládání příkazů.