Czy ewaluacja $ BASH_COMMAND jest bezpieczna?

11

Pracuję nad skryptem powłoki, który konstruuje złożone polecenie ze zmiennych, np. Takich jak ten (z techniką, której nauczyłem się z Bash FAQ ):

#!/bin/bash

SOME_ARG="abc"
ANOTHER_ARG="def"

some_complex_command \
  ${SOME_ARG:+--do-something "$SOME_ARG"} \
  ${ANOTHER_ARG:+--with "$ANOTHER_ARG"}

Ten skrypt dynamicznie dodaje parametry --do-something "$SOME_ARG"i --with "$ANOTHER_ARG"aby some_complex_command, jeśli te zmienne są zdefiniowane. Jak dotąd działa to dobrze.

Ale teraz chcę też móc wydrukować lub zalogować polecenie, gdy je uruchamiam, na przykład gdy mój skrypt jest uruchomiony w trybie debugowania. Więc kiedy mój skrypt działa some_complex_command --do-something abc --with def, chcę również mieć to polecenie w zmiennej, aby np. Móc zalogować się do syslog.

Bash FAQ pokazuje technikę wykorzystania DEBUGpułapki i $BASH_COMMANDzmiennej (na przykład do celów debugowania) w tym celu. Próbowałem tego z następującym kodem:

#!/bin/bash

ARG="test string"

trap 'COMMAND="$BASH_COMMAND"; trap - DEBUG' DEBUG
echo "$ARG"

echo "Command was: ${COMMAND}"

To działa, ale nie rozwija zmiennych w poleceniu:

host ~ # ./test.sh
test string
Command was: echo "$ARG"

Myślę, że muszę użyć eval, by rozwinąć się echo "$ARG"do echo test string(przynajmniej nie znalazłem sposobu bez którego eval). Działa to:

eval echo "Command was: ${COMMAND}"

Daje następujące dane wyjściowe:

host ~ # ./test.sh
test string
Command was: echo "$ARG"
Command was: echo test string

Ale nie jestem pewien, czy mogę evalbezpiecznie używać tego w ten sposób. Bezskutecznie próbowałem wykorzystać niektóre rzeczy:

#!/bin/bash

ARG="test string; touch /x"
DANGER='$(touch /y; cat /etc/shadow)'

trap 'COMMAND="$BASH_COMMAND"; trap - DEBUG' DEBUG
echo "$ARG" $DANGER

echo "Command was: ${COMMAND}"
eval echo "Command was: ${COMMAND}"

Wygląda na to, że dobrze sobie z tym radzi, ale jestem ciekawy, czy ktoś widzi problem, który mi umknął.

Martin von Wittich
źródło
Tego używam od lat w opakowaniu sudo, nigdy nie zauważyłem problemu.
w00t

Odpowiedzi:

7

Jedną z możliwości jest utworzenie funkcji otoki, która jednocześnie wydrukuje polecenie i wykona je w następujący sposób:

debug() {
    # This function prints (to stdout) its arguments and executes them
    local args=() idx=0 IFS=' ' c
    for c; do printf -v args[idx++] '%q' "$c"; done
    printf "%s\n" "${args[*]}"
    # Execute!
    "$@"
}

Aby w skrypcie można wykonać:

debug echo "$ARG"

Nie musisz majstrować przy pułapce. Wadą jest to, że dodaje kilka debugsłów kluczowych w całym kodzie (ale to powinno być w porządku, często takie rzeczy, jak aserty itp.).

Możesz nawet dodać zmienną globalną DEBUGi zmodyfikować debugfunkcję w następujący sposób:

debug() {
    # This function prints (to stdout) its arguments if DEBUG is non-null
    # and executes them
    if [[ $DEBUG ]]; then
        local args=() idx=0 IFS=' ' c
        for c; do printf -v args[idx++] '%q' "$c"; done
        printf "%s\n" "${args[*]}"
    fi
    # Execute!
    "$@"
}

Następnie możesz wywołać skrypt jako:

$ DEBUG=yes ./myscript

lub

$ DEBUG= ./myscript

Lub tylko

$ ./myscript

w zależności od tego, czy chcesz mieć informacje debugowania, czy nie.

Napisałem wielką DEBUGliterę, ponieważ należy ją traktować jako zmienną środowiskową. DEBUGto trywialna i popularna nazwa, więc może kolidować z innymi poleceniami. Może nazwać GNIOURF_DEBUGlub MARTIN_VON_WITTICH_DEBUGlub UNICORN_DEBUGjeśli lubisz jednorożce (i to prawdopodobnie jak kucyki zbyt).

Uwaga. W debugfunkcji ostrożnie sformatowałem każdy argument za pomocą printf '%q', aby dane wyjściowe były poprawnie zastępowane i cytowane, tak aby były wielokrotnego użytku dosłownie z bezpośrednim kopiowaniem i wklejaniem. Pokaże także dokładnie to, co zobaczyła pocisk, ponieważ będziesz w stanie zrozumieć każdy argument (w przypadku spacji lub innych śmiesznych symboli). Ta funkcja używa również bezpośredniego przypisania z -vprzełącznikiem printf, aby uniknąć niepotrzebnych podpowłok.

gniourf_gniourf
źródło
1
Ta funkcja działa świetnie, ale niestety moje polecenie zawiera przekierowania i operator sterujący „excute in background” &. Musiałem przenieść je do funkcji, aby działało - niezbyt fajnie, ale nie sądzę, że jest lepszy sposób. Ale nic więcej eval, więc muszę to zrobić, co jest miłe :)
Martin von Wittich
5

eval "$BASH_COMMAND" uruchamia polecenie

printf '%s\n' "$BASH_COMMAND" wypisuje dokładnie określone polecenie plus nowy wiersz.

Jeśli polecenie zawiera zmienne (tj. Jeśli jest coś podobnego cat "$foo"), wówczas wydrukowanie polecenia powoduje wydrukowanie tekstu zmiennej. Niemożliwe jest wydrukowanie wartości zmiennej bez wykonania polecenia - pomyśl o takich poleceniach variable=$(some_function) other_variable=$variable.

Najprostszym sposobem na uzyskanie śladu po uruchomieniu skryptu powłoki jest ustawienie xtraceopcji powłoki przez uruchomienie skryptu jako bash -x /path/to/scriptlub wywołanie set -xwewnątrz powłoki. Śledzenie jest drukowane z błędem standardowym.

Gilles „SO- przestań być zły”
źródło
1
Wiem o tym xtrace, ale nie daje mi to zbyt dużej kontroli. Próbowałem „set -x; ...; set + x”, ale: 1) wypisano też polecenie „set + x” w celu wyłączenia xtrace 2) Nie mogę poprzedzić wyjścia np. Znacznikiem czasu 3) Mogę zaloguj się do syslog.
Martin von Wittich
2
„Niemożliwe jest wydrukowanie wartości zmiennej bez wykonania polecenia” - dobry przykład, nie rozważałem takiego przypadku. Byłoby miło, gdyby bash miał dodatkową zmienną obok, BASH_COMMANDktóra zawiera rozwiniętą komendę, ponieważ w pewnym momencie musi wykonać ekspansję zmiennej na komendzie podczas jej wykonywania :)
Martin von Wittich