$ ls -l /tmp/test/my\ dir/ total 0
Zastanawiałem się, dlaczego poniższe sposoby uruchamiania powyższego polecenia kończą się niepowodzeniem lub sukcesem?
$ 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
Komentarze
- mywiki.wooledge.org/BashFAQ / 050
- Implikacje dla bezpieczeństwa zapomnienia o cytowaniu zmiennej w powłokach bash / POSIX – ale co jeśli…?
Odpowiedź
Zostało to omówione w wielu pytaniach na unix.SE, spróbuję zebrać wszystkie Problemy, które mogę tu wymyślić. Na końcu referencje.
Dlaczego to się nie udaje
Powodem, dla którego napotykasz te problemy, jest podział na słowa i fakt, że cudzysłowy rozwinięte ze zmiennych nie działają jak cudzysłowy, ale są zwykłymi znakami.
przypadki przedstawione w pytaniu:
Przypisanie tutaj przypisuje pojedynczy ciąg ls -l "/tmp/test/my dir"
do abc
:
$ abc="ls -l "/tmp/test/my dir""
Poniżej $abc
jest podzielony na białe znaki, a ls
pobiera trzy argumenty -l
, "/tmp/test/my
i dir"
(z cudzysłowem z przodu drugiego i drugim z tyłu trzeciego). Opcja działa, ale ścieżka jest niepoprawnie przetwarzana:
$ $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 aby znaleźć program nazwany dosłownie ls -l "/tmp/test/my dir"
, zawierający spacje i cudzysłowy.
$ "$abc" bash: ls -l "/tmp/test/my dir": No such file or directory
A tutaj jest dzielone i tylko pierwsze słowo wynikowe jest traktowane jako argument funkcji -c
, więc Bash uruchamia po prostu ls
w bieżącym katalogu. Pozostałe słowa są argumentami dla basha i są używane do wypełniania $0
, $1
, itd.
$ bash -c $abc "my dir"
W przypadku bash -c "$abc"
i eval "$abc"
jest dodatkowy krok przetwarzania powłoki, który sprawia, że cudzysłowy działają, ale powoduje również ponowne przetworzenie wszystkich rozszerzeń powłoki , więc istnieje ryzyko przypadkowego wykonania np. zamiany polecenia z danych podanych przez użytkownika, chyba że są bardzo ostrożni pełne cytowanie.
Lepsze sposoby na zrobienie tego
Dwa lepsze sposoby przechowywania poleceń to a) użycie funkcji zamiast niej, b) użycie zmiennej tablicowej ( lub parametry pozycyjne).
Użycie funkcji:
Po prostu zadeklaruj funkcję z poleceniem w środku i uruchom funkcję tak, jakby była poleceniem. Rozszerzenia w poleceniach w ramach funkcji są przetwarzane tylko wtedy, gdy polecenie jest uruchomione, a nie gdy jest zdefiniowane, i nie trzeba cytować poszczególnych poleceń.
# define it myls() { ls -l "/tmp/test/my dir" } # run it myls
Korzystanie z tablicy:
Tablice umożliwiają tworzenie zmiennych wielowyrazowych, w których poszczególne słowa zawierają kolor biały przestrzeń. Tutaj poszczególne słowa są przechowywane jako odrębne elementy tablicy, a rozwinięcie "${array[@]}"
rozszerza każdy element jako oddzielne słowa powłoki:
# define the array mycmd=(ls -l "/tmp/test/my dir") # run the command "${mycmd[@]}"
Składnia jest trochę okropna, ale tablice pozwalają także 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 zachowaj stałą część wiersza poleceń i użyj tablicy wypełnij tylko jej część, na przykład opcje lub nazwy 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ślna /bin/sh
w Debianie / Ubuntu) nie obsługuj ich (ale zobacz poniżej). Jednak Bash, ksh i zsh to robią, więc jest prawdopodobne, że Twój system ma jakąś powłokę obsługującą tablice.
Używanie "$@"
W powłokach bez obsługi nazwanych tablic nadal można używać parametrów pozycyjnych (pseudo-tablica "$@"
) do przechowywania argumentów polecenia.
Poniżej powinny znajdować się przenośne bity skryptu, które stanowią odpowiednik bitów kodu z poprzedniej sekcji. Tablica jest zastępowana przez "$@"
, lista parametrów pozycyjnych. Ustawienie "$@"
jest wykonywane za pomocą set
i podwójnych cudzysłowów około "$@"
są ważne (powodują one, że elementy listy są cytowane indywidualnie).
Po pierwsze, po prostu zapisz polecenie z argumentami w "$@"
i uruchom je:
set -- ls -l "/tmp/test/my dir" "$@"
Warunkowe ustawienie części opcji wiersza poleceń dla polecenia:
set -- ls if [ "$want_detail" = 1 ]; then set -- "$@" -l fi set -- "$@" "$targetdir" "$@"
Tylko użycie "$@"
dla opcji i argumentów :
set -- -x -v set -- "$@" file1 "file name with whitespace" set -- "$@" /somedir transmutate "$@"
(Oczywiście "$@"
jest zwykle wypełnione argumentami skryptu, więc „ Muszę je gdzieś zapisać przed ponownym użyciem "$@"
.)
Uważaj na eval
!
Ponieważ eval
wprowadza dodatkowy poziom przetwarzania cytatów i rozwinięć, należy zachować ostrożność przy wprowadzaniu danych przez użytkownika. Na przykład działa to tak długo, jak długo użytkownik nie „t wpisuje żadnych pojedynczych cudzysłowów:
read -r filename cmd="ls -l "$filename"" eval "$cmd";
Ale jeśli podają dane wejściowe "$(uname)".txt
, twój skrypt szczęśliwie uruchamia podstawianie poleceń.
Wersja z tablicami jest odporna na th w ponieważ słowa są przechowywane oddzielnie przez cały czas, nie ma cytowania ani innego przetwarzania dla treści filename
.
read -r filename cmd=(ls -ld -- "$filename") "${cmd[@]}"
Odnośniki
- Podział słów w BashGuide
- BashFAQ / 050 lub ” I „m próbuję wprowadzić polecenie w zmiennej, ale złożone przypadki zawsze zawodzą! ”
- Pytanie Dlaczego mój skrypt powłoki dławi się białymi znakami lub innymi znakami specjalnymi? , w którym omówiono szereg problemów związanych z cytowaniem i odstępami, w tym zapisywaniem poleceń.
Komentarze
- możesz obejść cytowanie eval, wykonując
cmd="ls -l $(printf "%q" "$filename")"
. nie jest ładny, ale jeśli użytkownik jest martwy przy używaniueval
, to pomaga. ' jest również bardzo przydatne do wysyłania polecenia, chociaż podobne rzeczy, takie jakssh foohost "ls -l $(printf "%q" "$filename")"
, lub w duchu tego pytania:ssh foohost "$cmd"
. - Brak bezpośredniego związku, ale czy zakodowałeś katalog na stałe? W takim przypadku możesz spojrzeć na alias. Na przykład:
$ alias abc='ls -l "/tmp/test/my dir"'
Odpowiedź
Najbezpieczniejszy sposób uruchom (nietrywialne) polecenie eval
. Następnie możesz napisać polecenie tak, jak w linii poleceń, a zostanie ono wykonane dokładnie tak, jakbyś je właśnie wprowadził. Ale musisz wszystko zacytować.
Prosty przypadek:
abc="ls -l "/tmp/test/my dir"" eval "$abc"
Nie taki prosty przypadek:
# command: awk "! a[$0]++ { print "foo: " $0; }" inputfile abc="awk "\""! a[$0]++ { print "foo: " $0; }"\"" inputfile" eval "$abc"
Komentarze
- Cześć @HaukeLaging, co to jest \ ' '?
- @jumping_monkey Nie możesz mieć
'
w'
-cytowanym ciągu . W związku z tym musisz (1) zakończyć / przerwać cytowanie, (2) uciec przed następnym'
, aby uzyskać dosłowne, (3) wpisać dosłowny cytat i (4) w przypadek przerwy wznowić cytowany ciąg:(1)'(2)\(3)'(4)'
- Cześć @HaukeLaging, interesujące, a dlaczego nie tylko
abc='awk \'! a[$0]++ { print "foo: " $0; }\' inputfile'
? - @jumping_monkey Nie mówiąc już o tym, że właśnie wyjaśniłem wam, dlaczego to nie działa: czy nie ' t testowanie kodu przed wysłaniem nie ma sensu?
Odpowiedź
Drugi cudzysłów przerywa polecenie.
Kiedy uruchamiam:
abc="ls -l "/home/wattana/Desktop"" $abc
Wystąpił błąd.
Ale kiedy uruchamiam
abc="ls -l /home/wattana/Desktop" $abc
W ogóle nie ma błędu
Nie ma sposobu, aby to naprawić (dla mnie), ale możesz uniknąć błędu, nie mając miejsca w nazwie katalogu.
Ta odpowiedź mówi, że polecenie eval może być użyte, aby to naprawić, ale nie działa dla mnie 🙁
Komentarze
- Tak, to działa, o ile ' nie ma potrzeby np. nazwy plików z osadzonymi spacjami (lub zawierające znaki glob).
Odpowiedź
Jeśli to nie działa z ""
, następnie użyj ``
:
abc=`ls -l /tmp/test/my\ dir/`
Aktualizuj lepiej sposób:
abc=$(ls -l /tmp/test/my\ dir/)
Komentarze
- To zapisuje wynik polecenia w zmiennej. OP chce (dziwnie) zapisać samo polecenie w zmiennej. Aha, i naprawdę powinieneś zacząć używać
$( command... )
zamiast grawisów. - Dziękuję bardzo za wyjaśnienia i wskazówki!
Odpowiedź
A co powiesz na jedną linijkę 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 ..
Komentarze
- Pytanie dotyczy BASH, a nie Pythona.
Odpowiedź
Kolejna prosta sztuczka do uruchomienia dowolnego (trywialnego / nietrywialnego) polecenia przechowywanego w abc
zmienną jest:
$ history -s $abc
i push UpArrow
lub Ctrl-p
, aby wprowadzić go do wiersza poleceń. W przeciwieństwie do innych metod, w ten sposób możesz ją edytować przed wykonaniem w razie potrzeby.
To polecenie doda zawartość zmiennej jako nowy wpis do historii Bash i możesz ją przywołać przez UpArrow
.