Jak możemy uruchomić polecenie zapisane w zmiennej?


35
$ ls -l /tmp/test/my\ dir/
total 0

Zastanawiałem się, dlaczego następujące sposoby uruchomienia powyższego polecenia kończą się niepowodzeniem?

$ 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


Odpowiedzi:


54

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 $abcjest podzielony i lsotrzymuje dwa argumenty "/tmp/test/myoraz 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 $abcjest brane jako argument -c, więc Bash po prostu działa lsw bieżącym katalogu. Pozostałe słowa są argumenty bash i są używane do wypełnienia $0, $1itp

$ 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 dashdomyślne /bin/shw 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ż evalwprowadza 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


2
robiąc to, możesz omijać ewaluację cmd="ls -l $(printf "%q" "$filename")". nie jest ładna, ale jeśli użytkownik nie chce używać eval, pomaga. Jest to także bardzo przydatne do wysyłania polecenia choć podobnych rzeczy, takich jak ssh foohost "ls -l $(printf "%q" "$filename")", lub w rozprza tego pytania: ssh foohost "$cmd".
Patrick,

Nie ma to bezpośredniego związku, ale czy na stałe zapisałeś katalog? W takim przypadku możesz spojrzeć na alias. Coś w stylu: $ alias abc='ls -l "/tmp/test/my dir"'
Hopping Bunny

4

Najbezpieczniejszym sposobem na uruchomienie (nietrywialnego) polecenia jest eval. Następnie możesz napisać polecenie tak, jak w wierszu poleceń, i jest ono wykonywane dokładnie tak, jakbyś właśnie je wprowadził. Ale musisz zacytować wszystko.

Prosty przypadek:

abc='ls -l "/tmp/test/my dir"'
eval "$abc"

nie tak prosty przypadek:

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"

3

Drugi znak cudzysłowu łamie polecenie.

Kiedy biegnę:

abc="ls -l '/home/wattana/Desktop'"
$abc

Dał mi błąd.

Ale kiedy biegnę

abc="ls -l /home/wattana/Desktop"
$abc

W ogóle nie ma błędu

W tej chwili nie ma sposobu, aby to naprawić (dla mnie), ale można uniknąć błędu, nie mając spacji w nazwie katalogu.

Ta odpowiedź mówi, że można użyć polecenia eval, aby to naprawić, ale to nie działa dla mnie :(


1
Tak, to działa tak długo, jak długo nie ma potrzeby na przykład nazywanie plików z osadzonymi spacjami (lub takimi, które zawierają znaki globalne).
ilkkachu

0

Jeśli to nie zadziała '', powinieneś użyć ``:

abc=`ls -l /tmp/test/my\ dir/`

Zaktualizuj lepszy sposób:

abc=$(ls -l /tmp/test/my\ dir/)

Przechowuje wynik polecenia w zmiennej. OP (dziwnie) chce zapisać samo polecenie w zmiennej. Och, i naprawdę powinieneś zacząć używać $( command... )zamiast backticks.
roaima

Dziękuję bardzo za wyjaśnienia i wskazówki!
balon

0

Co powiesz na jedno-liniowy python3?

bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)"
total 8
drwxr-xr-x    2 root     root          4096 Mar  4  2019 .
drwxr-xr-x    1 root     root          4096 Nov 27 16:42 ..
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.