Czy zmienne powinny być cytowane podczas wykonywania?

18

Ogólna zasada w skryptach powłoki polega na tym, że zmienne powinny być zawsze cytowane, chyba że istnieje ważny powód, aby tego nie robić. Aby uzyskać więcej informacji, niż prawdopodobnie chcesz wiedzieć, spójrz na to świetne pytanie i odpowiedzi: Implikacje bezpieczeństwa związane z zapominaniem o cytowaniu zmiennej w powłokach bash / POSIX .

Zastanów się jednak nad funkcją:

run_this(){
    $@
}

Powinien $@być tam cytowany czy nie? Bawiłem się nim przez chwilę i nie mogłem znaleźć żadnego przypadku, w którym brak cytatów spowodowałby problem. Z drugiej strony użycie cudzysłowów powoduje, że łamie się ono podczas przekazywania polecenia zawierającego spacje jako zmienną cytowaną:

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

Uruchomienie powyższego skryptu zwraca:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

Mogę to obejść, jeśli użyję run_that $commzamiast tego run_that "$comm", ale ponieważ funkcja run_this(niecytowana) działa z oboma, wydaje się, że jest to bezpieczniejszy zakład.

Czy w konkretnym przypadku użycia $@w funkcji, której zadaniem jest wykonanie $@polecenia, należy $@zacytować? Wyjaśnij, dlaczego nie należy go cytować, i podaj przykład danych, które mogą go złamać.

terdon
źródło
6
run_thatZachowanie jest zdecydowanie tym, czego bym się spodziewał (a jeśli na ścieżce do polecenia będzie miejsce?). Jeśli chciałbyś innego zachowania, z pewnością wycofałbyś je z serwisu połączeń, na którym wiesz, jakie są dane? Spodziewałbym się wywołać tę funkcję jako run_that ls -l, która działa tak samo w obu wersjach. Czy jest przypadek, który sprawił, że spodziewałeś się inaczej?
Michael Homer
@MichaelHomer Chyba moja edycja tutaj spowodowała: unix.stackexchange.com/a/250985/70524
muru
@MichaelHomer z jakiegoś powodu (prawdopodobnie dlatego, że wciąż nie mam drugiej filiżanki kawy) Nie rozważałem spacji w argumentach lub ścieżce polecenia, ale tylko w samym poleceniu (opcje). Jak to często bywa, z perspektywy czasu wydaje się to bardzo oczywiste.
terdon
Istnieje powód, dla którego powłoki nadal obsługują funkcje zamiast po prostu upychać polecenia do tablicy i wykonywać je za pomocą ${mycmd[@]}.
chepner

Odpowiedzi:

20

Problem polega na tym, jak polecenie jest przekazywane do funkcji:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@"powinien być używany w ogólnym przypadku, gdy twoja run_thisfunkcja jest poprzedzona normalnie napisanym poleceniem. run_thisprowadzi do cytowania piekła:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

Nie jestem pewien, jak mam przekazać nazwę pliku ze spacjami run_this.

muru
źródło
1
To rzeczywiście była twoja zmiana, która skłoniła to. Z jakiegoś powodu po prostu nie przyszło mi do głowy, aby przetestować nazwę pliku ze spacjami. Nie mam pojęcia, czemu nie, ale proszę bardzo. Oczywiście masz całkowitą rację, nie widzę żadnego sposobu, aby to zrobić poprawnie w run_thisobu przypadkach.
terdon
Cytowanie w @terdon stało się tak dużym nawykiem, że przypuszczałem, że $@przypadkowo nie pozostawiłeś cytatu. Powinienem zostawić przykład. : D
muru
2
Nie, to naprawdę jest taki nawyk, że przetestowałem go (błędnie) i doszedłem do wniosku, że „huh, może ten nie potrzebuje cytatów”. Procedura powszechnie znana jako pierdnięcie mózgu.
terdon
1
Nie możesz przekazać nazwy pliku ze spacjami do run_this. Jest to w zasadzie ten sam problem, na jaki napotykasz wypychanie złożonych poleceń w łańcuchy, jak omówiono w Bash FAQ 050 .
Etan Reisner,
9

To albo:

interpret_this_shell_code() {
  eval "$1"
}

Lub:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

lub:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

Ale:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

To nie ma większego sensu.

Jeśli chcesz wykonać ls -lpolecenie (nie lspolecenie z argumentami lsi -ljako argumenty), wykonaj następujące czynności:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

Ale jeśli (bardziej prawdopodobne), jest to lspolecenie z argumentami lsi -ljako argument, uruchomiłbyś:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Teraz, jeśli jest to coś więcej niż proste polecenie, które chcesz wykonać, jeśli chcesz wykonać przypisania zmiennych, przekierowania, potoki ..., tylko interpret_this_shell_code:

interpret_this_shell_code 'ls -l 2> /dev/null'

choć oczywiście zawsze możesz zrobić:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'
Stéphane Chazelas
źródło
5

Patrząc z bash / ksh / zsh punktu widzenia $*i $@są szczególnym przypadkiem ogólnego rozszerzalności tablicy. Rozszerzenia tablic nie są jak zwykłe rozszerzenia zmiennych:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

Dzięki rozszerzeniom $*/ ${a[*]}macierz łączy tablicę z pierwszą wartością IFS- która jest domyślnie spacją - w jeden gigantyczny ciąg. Jeśli go nie zacytujesz, zostanie podzielony jak zwykły ciąg znaków.

W przypadku rozszerzeń $@/ ${a[@]}zachowanie zależy od tego, czy cytowane jest rozszerzenie $@/ ${a[@]}:

  1. jeśli jest cytowany ( "$@"lub "${a[@]}"), otrzymasz odpowiednik "$1" "$2" "$3" #... lub"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. jeśli nie jest cytowany ( $@lub ${a[@]}), otrzymujesz odpowiednik $1 $2 $3 #... lub${a[1]} ${a[2]} ${a[3]} # ...

W przypadku poleceń owijania zdecydowanie potrzebujesz cytowanych @ rozszerzeń (1.).


Więcej dobrych informacji na temat tablic bash (i bash-like): https://lukeshu.com/blog/bash-arrays.html

PSkocik
źródło
1
Właśnie zdałem sobie sprawę, że mam na myśli link zaczynający się od Luke'a, mając na sobie maskę Vadera. Siła jest silna z tym postem.
PSkocik,
4

Od kiedy nie $@podwoiłeś cudzysłowu , pozostawiłeś wszystkie problemy z globowaniem w linku, który podałeś swojej funkcji.

Jak możesz uruchomić polecenie o nazwie *? Nie możesz tego zrobić za pomocą run_this:

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

I widzisz, nawet gdy wystąpił błąd, run_thatdał ci bardziej znaczący komunikat.

Jedynym sposobem na rozwinięcie $@pojedynczych słów jest podwójne cytowanie. Jeśli chcesz uruchomić je jako polecenie, musisz przekazać polecenie i jego parametry jako osobne słowa. To, co zrobiłeś po stronie dzwoniącego, a nie w ramach funkcji.

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

jest lepszym wyborem. Lub jeśli twoje powłoki obsługują tablice:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

Nawet jeśli powłoka w ogóle nie obsługuje tablicy, nadal możesz z nią grać, używając"$@" .

Cuonglm
źródło
3

Wykonywanie zmiennych w bashjest podatną na awarie techniką. Po prostu niemożliwe jest napisanie run_thisfunkcji, która poprawnie obsługuje wszystkie przypadki brzegowe, takie jak:

  • rurociągi (np. ls | grep filename)
  • przekierowania wejścia / wyjścia (np. ls > /dev/null)
  • instrukcje powłoki takie jak if whileitp.

Jeśli wszystko, co chcesz zrobić, to uniknąć powtarzania kodu, lepiej skorzystaj z funkcji. Na przykład zamiast:

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

Powinieneś pisać

command() {
    ls -l
}
...
command

Jeśli polecenia są dostępne tylko w czasie wykonywania, powinieneś użyć eval, który jest specjalnie zaprojektowany do obsługi wszystkich dziwactw, które spowodują run_thisniepowodzenie:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

Należy pamiętać, że evaljest znany na kwestie bezpieczeństwa, ale jeśli przechodzą zmienne z niezaufanych źródeł do run_this, będziesz musiał stawić czoła wykonanie dowolnego kodu tak dobrze.

Dmitrij Grigoriew
źródło
1

Wybór nalezy do ciebie. Jeśli nie podasz $@żadnej z jej wartości, poddaj ją dodatkowej rozbudowie i interpretacji. Jeśli ją zacytujesz, wszystkie przekazane argumenty są odtwarzane dosłownie w jej rozwinięciu. Nigdy nie będziesz w stanie w niezawodny sposób obsługiwać tokenów składni powłoki, takich jak &>|i etc, bez samodzielnego analizowania argumentów - więc masz bardziej rozsądny wybór przekazania funkcji:

  1. Dokładnie słowa użyte w wykonaniu pojedynczego prostego polecenia za pomocą "$@".

...lub...

  1. Kolejna rozszerzona i zinterpretowana wersja twoich argumentów, które są następnie stosowane razem jako proste polecenie z $@.

Żaden sposób nie jest zły, jeśli jest celowy i jeśli skutki tego, co wybierzesz, są dobrze zrozumiane. Oba sposoby mają zalety jeden nad drugim, chociaż zalety drugiego rzadko są szczególnie przydatne. Nadal...

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... to nie ma sensu , tylko rzadko mogą być znacznie użytku . A w bashpowłoce, ponieważ bashdomyślnie nie przykleja definicji środowiska do swojego środowiska, nawet jeśli wspomniana definicja jest dołączona do wiersza polecenia specjalnego wbudowanego lub funkcji, globalna wartość parametru $IFSnie jest zmieniana, a deklaracja jest lokalna tylko na run_this()połączenie.

Podobnie:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

... globbing jest również konfigurowalny. Cytaty służą celowi - nie są na darmo. Bez nich rozszerzenie powłoki podlega dodatkowej interpretacji - interpretacji konfigurowalnej . Kiedyś - z kilkoma bardzo starymi powłokami - $IFSbył globalnie stosowany do wszystkich danych wejściowych, a nie tylko rozszerzeń. W rzeczywistości wspomniane powłoki zachowywały się bardzo podobnie run_this(), ponieważ łamały wszystkie słowa wejściowe o wartości $IFS. Tak więc, jeśli szukasz tego bardzo starego zachowania powłoki, powinieneś użyć run_this().

Nie szukam tego i jestem w tej chwili dość mocno zmuszony, aby wymyślić przydatny przykład. Generalnie wolę, aby polecenia, które uruchamia moja powłoka, były tymi, które wpisuję. I tak, biorąc pod uwagę wybór, prawie zawsze run_that(). Oprócz tego...

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

Cokolwiek można cytować. Polecenia będą działać w cudzysłowie Działa, ponieważ do czasu uruchomienia polecenia wszystkie słowa wejściowe przeszły już usuwanie cudzysłowów - co jest ostatnim etapem procesu interpretacji danych wejściowych powłoki. Tak więc różnica między 'ls'i lsmoże mieć znaczenie tylko podczas interpretacji powłoki - i dlatego cytowanie lszapewnia, że ​​żaden z nazwanych aliasówls nie zostanie zastąpiony moim cytowanym lssłowem polecenia. Poza tym jedyne, na co wpływają cytaty, to delimitacja słów (czyli jak i dlaczego działa cytowanie zmiennych / białych znaków) oraz interpretacja metaznaków i słów zastrzeżonych.

Więc:

'for' f in ...
 do   :
 done

bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

Nigdy nie będziesz w stanie tego zrobić za pomocą ani, run_this()ani run_that().

Ale nazwy funkcji, $PATHpolecenia „d” lub wbudowane funkcje wykonają dokładnie cytowane lub niecytowane, i właśnie tak działa run_this()i run_that()przede wszystkim działa. Z $<>|&(){}żadną z nich nie będziesz w stanie nic zrobić . Krótko mówiąc eval, jest.

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')

eval
printf
"%s\n"
"$@"

Ale bez niego jesteś ograniczony limitami prostego polecenia na podstawie używanych cytatów (nawet jeśli nie, ponieważ $@zachowuje się jak cytat na początku procesu, gdy polecenie jest analizowane dla metaznaków) . To samo ograniczenie dotyczy przypisań i przekierowań wiersza poleceń, które są ograniczone do wiersza polecenia funkcji. Ale to nie jest wielka sprawa:

(run_that(){ "$@";}; echo hey | run_that cat)

hey

Mogłem mieć tak łatwo <przekierowane wejście lub >wyjście, jak otworzyłem potok.

W każdym bądź razie, nie ma tu dobrego ani złego sposobu - każdy sposób ma swoje zastosowanie. Po prostu powinieneś napisać to tak, jak zamierzasz go używać i powinieneś wiedzieć, co masz zamiar zrobić. Pomijając cytaty mogą mieć cel - w przeciwnym razie nie byłoby być cytaty w ogóle - ale jeśli pominąć ich istotnych powodów nie do celu, jesteś po prostu pisząc zły kod. Rób, co masz na myśli; W każdym razie staram się.

mikeserv
źródło