echo kontra <<<, czy bezużyteczne użycie echa w Bash Award?

19

Do tej pory Nagroda za Bezużyteczne Wykorzystaniecat jest bardzo dobrze znana, jest też wzmianka o Bezużytecznym Wykorzystaniuecho (nie dotyczy tego pytania). Zastanawiam się, czy powinna istnieć „ echoNagroda za bezużyteczne wykorzystanie w Bash”: Według niektórych nienaukowych pomiarów orurowanie wydaje się być znacznie wolniejsze niż heredoki i tustringi:

  • Heredocs:

    for reps in 1 2 3
    do
        time for i in {1..1000}
        do
            cat <<'END'
    test string
    END
        done > /dev/null
    done
    
    real    0m1.786s
    user    0m0.212s
    sys     0m0.332s
    
    real    0m1.817s
    user    0m0.232s
    sys     0m0.332s
    
    real    0m1.846s
    user    0m0.256s
    sys     0m0.320s
  • Herestrings

    for reps in 1 2 3
    do
        time for i in {1..1000}
        do
            cat <<< 'test string'
        done > /dev/null
    done
    
    real    0m1.932s
    user    0m0.280s
    sys     0m0.288s
    
    real    0m1.956s
    user    0m0.248s
    sys     0m0.348s
    
    real    0m1.968s
    user    0m0.268s
    sys     0m0.324s
  • Przekierowanie

    for reps in 1 2 3
    do
        time for i in {1..1000}
        do
            echo 'test string' | cat
        done > /dev/null
    done
    
    real    0m3.562s
    user    0m0.416s
    sys     0m0.548s
    
    real    0m3.924s
    user    0m0.384s
    sys     0m0.604s
    
    real    0m3.343s
    user    0m0.400s
    sys     0m0.552s

Ogólnie rzecz biorąc, heredoki i instrukcje mają mniej więcej tę samą prędkość (jest to tylko jeden zestaw danych z kilku testów), podczas gdy przekierowanie jest konsekwentnie o ponad 50% wolniejsze. Czy coś źle zrozumiałem, czy może to być ogólną zasadą dla komend czytających standardowe dane wejściowe w Bash?

l0b0
źródło
4
Z pewnością pozwolę ci na nagrodę za „bezużyteczne wykorzystanie seq” ;-)
Chris Down
2
Pamiętaj, że powinieneś albo porównać cat <<ENDvs. cat <<< "test string"vs. echo "test string" | catlub cat <<'END'vs. cat <<< 'test string'vs., echo 'test string' | cataby powłoka wykonała taką samą liczbę rozszerzeń na łańcuchach.
manatwork
@ChrisDown Derp. Zawsze o tym zapominam. Dzięki!
l0b0,
@manatwork Dzięki, ustalone i zaktualizowane czasy.
l0b0,
Ciekawość, co jest z tymi przedstawicielami? Czy nie była twoja pierwotna intencja zapętlania $ powtórzeń razy $(seq $reps)?
manatwork

Odpowiedzi:

11

Po pierwsze, skoncentrujmy się na wydajności. Przeprowadziłem testy porównawcze dla nieco innego programu na skądinąd w większości bezczynnym procesorze x86_64 z uruchomionym ściśnięciem Debiana.

herestring.bash, używając ciągu znaków, aby przekazać wiersz danych wejściowych:

#! /bin/bash
i=0
while [ $i -lt $1 ]; do
  tr a-z A-Z <<<'hello world'
  i=$((i+1))
done >/dev/null

heredoc.bash, używając heredoc, aby przekazać wiersz danych wejściowych:

#! /bin/bash
i=0
while [ $i -lt $1 ]; do
  tr a-z A-Z <<'EOF'
hello world
EOF
  i=$((i+1))
done >/dev/null

echo.bash, za pomocą echoi potoku do przekazania linii danych wejściowych:

#! /bin/bash
i=0
while [ $i -lt $1 ]; do
  echo 'hello world' | tr a-z A-Z
  i=$((i+1))
done >/dev/null

Dla porównania, skrypty mierzyłem także pod ATT ksh93 i pod myślnikiem (z wyjątkiem tego herestring.bash, że myślnik nie ma opisów).

Oto mediana z trzech razy:

$ time bash ./herestring.bash 10000
./herestring.bash 10000  0.32s user 0.79s system 15% cpu 7.088 total
$ time ksh ./herestring.bash 10000
ksh ./herestring.bash 10000  0.54s user 0.41s system 17% cpu 5.277 total
$ time bash ./heredoc.bash 10000
./heredoc.bash 10000  0.35s user 0.75s system 17% cpu 6.406 total
$ time ksh ./heredoc.bash 10000  
ksh ./heredoc.sh 10000  0.54s user 0.44s system 19% cpu 4.925 total
$ time sh ./heredoc.bash 10000  
./heredoc.sh 10000  0.08s user 0.58s system 12% cpu 5.313 total
$ time bash ./echo.bash 10000
./echo.bash 10000  0.36s user 1.40s system 20% cpu 8.641 total
$ time ksh ./echo.bash 10000
ksh ./echo.sh 10000  0.47s user 1.51s system 28% cpu 6.918 total
$ time sh ./echo.sh 10000
./echo.sh 10000  0.07s user 1.00s system 16% cpu 6.463 total

Wnioski:

  • Heredoc jest szybszy niż herestring.
  • echoa rura jest zauważalnie, ale nie dramatycznie szybsza. (Należy pamiętać, że jest to program zabawkowy: w prawdziwym programie większość czasu przetwarzania byłaby w tym, co troznacza połączenie).
  • Jeśli chcesz prędkości, rzuć dash bash i zadzwoń dash lub jeszcze lepiej ksh. Funkcje Basha nie nadrabiają względnej powolności, ale ksh ma zarówno cechy, jak i szybkość.

Oprócz wydajności istnieje także przejrzystość i przenośność. <<<to rozszerzenie ksh93 / bash / zsh, które jest mniej znane niż echo … |lub <<. Nie działa w ksh88 / pdksh ani w POSIX sh.

Jedynym miejscem, w którym <<<jest zapewne znacznie wyraźniejsze, jest heredoc:

foo=$(tr a-z A-Z <<<'hello world')

vs

foo=$(tr a-z A-Z <<'EOF'
hello world
EOF
)

(Większość powłok nie radzi sobie z zamykaniem nawiasów na końcu wiersza zawierającego <<EOF.)

Gilles „SO- przestań być zły”
źródło
2
Zauważ, że <<<pochodzi rci został po raz pierwszy wprowadzony do świata muszli podobnych do Bourne'a zsh. rcnie dodać do nowej linii spływu, ale wszystkich bash, zshi ksh93zrobić AFAICT. rcwykorzystuje rurę tam, podczas bash, zshi ksh93użyć pliku tymczasowego jak dla heredocs.
Stéphane Chazelas,
@StephaneChazelas Ups, powinienem był sprawdzić, dzięki. To sprawia, że ​​jest <<<jeszcze mniej przydatny.
Gilles „SO- przestań być zły”
Zauważ również, że zshma optymalizację w =(<<<foo)celu efektywnego utworzenia pliku tymczasowego zawierającego „foo”, który nie wymaga rozwikłania procesu =(echo foo).
Stéphane Chazelas,
Wspomnę również, że pdksh nie obejmuje <<<.
kurtm
@kurtm „To nie działa w ksh88 / pdksh ani w POSIX sh.”
SO Gillesa - przestań być zły ”
6

Innym powodem używania heredoków (jeśli nie masz ich wystarczająco dużo) jest to, że echo może zawieść, jeśli strumień nie zostanie zużyty. Rozważ pipefailopcję „bash” :

set -o pipefail
foo=yawn
echo $foo | /bin/true ; echo $?  # returns 0

/bin/truenie zużywa standardowych danych wejściowych, ale echo yawnmimo to kończy działanie. Jeśli jednak echo zostanie poproszone o wydrukowanie dużej ilości danych, nie zostanie ono ukończone, dopóki truenie zostanie zakończone:

foo=$(cat /etc/passwd)
# foo now has a fair amount of data

echo $foo | /bin/true ; echo $?  # returns 0 sometimes 141
echo $foo$foo$foo$foo | /bin/true ; echo $?  # returns mostly 141

141 to SIGPIPE (128 + 13) (dodano 128, ponieważ bash robi to zgodnie z bash (1):

Kiedy polecenie kończy się na fatalnym sygnale N, bash używa wartości 128 + N jako statusu wyjścia.

Heredocs nie mają tego problemu:

/bin/true <<< $foo$foo$foo$foo ; echo $?  # returns 0 always
mogsie
źródło
Dzięki, ten szczególny niuans sprawiał, że mój skrypt czasami przerywał się w przypadkowych miejscach, bardziej na serwerach Amazon niż na hostach KVM, ale od czasu do czasu zawodził, w miejscach pozornie niemożliwych do awarii. Dobrze, że pipefaildomyślnie jest wyłączony!
mogsie
2
Och, włączam pipefailna początku każdego skryptu. Daj mi wszystkie błędy!
l0b0
@ l0b0 Hmmm. Może dodam pipefail do j.mp/safebash Jestem za otrzymywaniem wszystkich błędów podczas programowania!
Bruno Bronosky
2

Jednym z powodów, dla których możesz chcieć użyć echa, jest przejęcie kontroli nad znakiem nowej linii dodawanym na końcu heredoków i instrukcji:

Trzy znaki foomają długość 3:

$ echo -n foo | wc -c
3

Jednak ten ciąg znaków składa się z czterech znaków:

$ wc -c <<< foo
4

Hredok składający się z trzech postaci:

$ wc -c << EOF
foo
EOF
4

Czwarty znak to znak nowej linii 0x0a.

Jakoś to magicznie pasuje do sposobu, w jaki bash usuwa te znaki nowej linii podczas pobierania wyjścia z podpowłoki:

Oto polecenie, które zwraca cztery znaki: fooi \n. „\ N” jest dodawane przez echo, zawsze dodaje znak nowej linii, chyba że podasz -nopcję:

$ echo foo
foo
$ echo foo | wc -c
4

Jednak przypisując to do zmiennej, końcowy znak nowej linii dodany przez echo jest usuwany:

$ foo=$(echo foo)
$ echo "$foo" # here, echo adds a newline too.
foo

Więc jeśli miksujesz pliki i zmienne i używasz ich w obliczeniach (np. Nie możesz używać heredoków lub ciągów znaków, ponieważ dodadzą one nową linię.

foo=abc
echo -n 'abc' > something.txt
if [ $(wc -c <<< "$foo") -eq $(wc -c < something.txt) ] ; then
  echo "yeah, they're the same!"
else
  echo "foo and bar have different lengths (except, maybe not)"
fi

Jeśli zmienisz instrukcję if do przeczytania

if [ $(echo -n "$foo" | wc -c) -eq $(wc -c < something.txt) ] ; then

wtedy test przechodzi.

mogsie
źródło