GNU/Linux >> Znalost Linux >  >> Linux

Jak spustíme příkaz uložený v proměnné?

$ 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.

Související:příkaz „eval“ v bash?

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!

Související:Přidání cesty prostředí do příkazového řádku Visual Studio?

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ů.

Linux
  1. Jak spustit příkaz jako správce systému (root)?

  2. Jak spustit příkaz Vim z shellu?

  3. jak předat proměnnou prostředí sudo su

  1. Jak přiřadit výstup příkazu proměnné shellu?

  2. Jak spustit příkaz uvnitř spuštěného kontejneru Systemd?

  3. Jak spustit příkaz bez vlastností root?

  1. Jak spustit příkaz na spuštěném kontejneru Docker

  2. Jak uložit příkaz do proměnné v shell skriptu?

  3. linux:jak spustit příkaz v daném adresáři