GNU/Linux >> Znalost Linux >  >> Linux

Proč by se měl v Bash vyhnout eval a co bych měl místo toho použít?

V tomto problému je více, než se na první pohled zdá. Začneme tím, co je zřejmé:eval má potenciál spouštět „špinavá“ data. Špinavá data jsou jakákoli data, která nebyla přepsána jako bezpečná pro použití v situaci XYZ; v našem případě je to jakýkoli řetězec, který nebyl naformátován tak, aby byl bezpečný pro vyhodnocení.

Dezinfekce dat vypadá na první pohled snadno. Za předpokladu, že si projdeme seznam možností, bash již poskytuje skvělý způsob, jak dezinfikovat jednotlivé prvky, a další způsob, jak dezinfikovat celé pole jako jeden řetězec:

function println
{
    # Send each element as a separate argument, starting with the second element.
    # Arguments to printf:
    #   1 -> "$1\n"
    #   2 -> "$2"
    #   3 -> "$3"
    #   4 -> "$4"
    #   etc.

    printf "$1\n" "${@:2}"
}

function error
{
    # Send the first element as one argument, and the rest of the elements as a combined argument.
    # Arguments to println:
    #   1 -> '\e[31mError (%d): %s\e[m'
    #   2 -> "$1"
    #   3 -> "${*:2}"

    println '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit "$1"
}

# This...
error 1234 Something went wrong.
# And this...
error 1234 'Something went wrong.'
# Result in the same output (as long as $IFS has not been modified).

Nyní řekněme, že chceme přidat možnost přesměrování výstupu jako argument pro println. Mohli bychom samozřejmě jen přesměrovat výstup println při každém volání, ale pro příklad to neuděláme. Budeme muset použít eval , protože proměnné nelze použít k přesměrování výstupu.

function println
{
    eval printf "$2\n" "${@:3}" $1
}

function error
{
    println '>&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

Vypadá to dobře, že? Problém je, že eval analyzuje dvakrát příkazový řádek (v libovolném prostředí). Při prvním průchodu analýzou se odstraní jedna vrstva citací. Po odstranění uvozovek se spustí určitý proměnný obsah.

Můžeme to opravit tím, že necháme rozšiřování proměnné probíhat v rámci eval . Vše, co musíme udělat, je uvést vše v jednoduchých uvozovkách a nechat dvojité uvozovky tam, kde jsou. Jedna výjimka:musíme rozšířit přesměrování před eval , takže to musí zůstat mimo uvozovky:

function println
{
    eval 'printf "$2\n" "${@:3}"' $1
}

function error
{
    println '&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

Tohle by mělo fungovat. Je také bezpečný, pokud je $1 v println není nikdy špinavý.

Teď chvíli počkejte:používám to samé neuvozovek syntaxi, kterou jsme původně použili s sudo Celou dobu! Proč to funguje tam a ne tady? Proč jsme museli všechno uvozovat? sudo je o něco modernější:umí uzavřít do uvozovek každý argument, který obdrží, i když to je přílišné zjednodušení. eval jednoduše vše zřetězí.

Bohužel neexistuje žádná náhrada za eval který zachází s argumenty jako sudo dělá, jako eval je vestavěný shell; to je důležité, protože při provádění přebírá prostředí a rozsah okolního kódu, spíše než vytváření nového zásobníku a rozsahu jako funkce.

eval alternativy

Konkrétní případy použití mají často schůdné alternativy k eval . Zde je užitečný seznam. command představuje to, co byste normálně poslali na eval; nahraďte, čím chcete.

Ne-op

Jednoduchá dvojtečka je v bash nepřípustná:

:

Vytvořte sub-shell

( command )   # Standard notation

Provést výstup příkazu

Nikdy se nespoléhejte na externí příkaz. Návratovou hodnotu byste měli mít vždy pod kontrolou. Dejte je na vlastní řádky:

$(command)   # Preferred
`command`    # Old: should be avoided, and often considered deprecated

# Nesting:
$(command1 "$(command2)")
`command "\`command\`"`  # Careful: \ only escapes $ and \ with old style, and
                         # special case \` results in nesting.

Přesměrování na základě proměnné

Ve volacím kódu namapujte &3 (nebo cokoliv vyššího než &2 ) k vašemu cíli:

exec 3<&0         # Redirect from stdin
exec 3>&1         # Redirect to stdout
exec 3>&2         # Redirect to stderr
exec 3> /dev/null # Don't save output anywhere
exec 3> file.txt  # Redirect to file
exec 3> "$var"    # Redirect to file stored in $var--only works for files!
exec 3<&0 4>&1    # Input and output!

Pokud by šlo o jednorázové volání, nemuseli byste přesměrovat celý shell:

func arg1 arg2 3>&2

V rámci volané funkce přesměrujte na &3 :

command <&3       # Redirect stdin
command >&3       # Redirect stdout
command 2>&3      # Redirect stderr
command &>&3      # Redirect stdout and stderr
command 2>&1 >&3  # idem, but for older bash versions
command >&3 2>&1  # Redirect stdout to &3, and stderr to stdout: order matters
command <&3 >&4   # Input and output!

Proměnná nepřímost

Scénář:

VAR='1 2 3'
REF=VAR

Špatné:

eval "echo \"\$$REF\""

Proč? Pokud REF obsahuje dvojitou uvozovku, dojde k porušení a otevření kódu k exploitům. REF je možné dezinfikovat, ale je to ztráta času, když máte toto:

echo "${!REF}"

Správně, bash má od verze 2 vestavěné proměnné nepřímé směrování. Je to trochu složitější než eval pokud chcete udělat něco složitějšího:

# Add to scenario:
VAR_2='4 5 6'

# We could use:
local ref="${REF}_2"
echo "${!ref}"

# Versus the bash < 2 method, which might be simpler to those accustomed to eval:
eval "echo \"\$${REF}_2\""

Bez ohledu na to je nová metoda intuitivnější, i když zkušeným programátorům, kteří jsou zvyklí na eval, se to tak nemusí zdát. .

Asociativní pole

Asociativní pole jsou v bash 4 implementována vnitřně. Jedno upozornění:musí být vytvořena pomocí declare .

declare -A VAR   # Local
declare -gA VAR  # Global

# Use spaces between parentheses and contents; I've heard reports of subtle bugs
# on some versions when they are omitted having to do with spaces in keys.
declare -A VAR=( ['']='a' [0]='1' ['duck']='quack' )

VAR+=( ['alpha']='beta' [2]=3 )  # Combine arrays

VAR['cow']='moo'  # Set a single element
unset VAR['cow']  # Unset a single element

unset VAR     # Unset an entire array
unset VAR[@]  # Unset an entire array
unset VAR[*]  # Unset each element with a key corresponding to a file in the
              # current directory; if * doesn't expand, unset the entire array

local KEYS=( "${!VAR[@]}" )  # Get all of the keys in VAR

Ve starších verzích bash můžete použít proměnnou indirection:

VAR=( )  # This will store our keys.

# Store a value with a simple key.
# You will need to declare it in a global scope to make it global prior to bash 4.
# In bash 4, use the -g option.
declare "VAR_$key"="$value"
VAR+="$key"
# Or, if your version is lacking +=
VAR=( "$VAR[@]" "$key" )

# Recover a simple value.
local var_key="VAR_$key"       # The name of the variable that holds the value
local var_value="${!var_key}"  # The actual value--requires bash 2
# For < bash 2, eval is required for this method.  Safe as long as $key is not dirty.
local var_value="`eval echo -n \"\$$var_value\""

# If you don't need to enumerate the indices quickly, and you're on bash 2+, this
# can be cut down to one line per operation:
declare "VAR_$key"="$value"                         # Store
echo "`var_key="VAR_$key" echo -n "${!var_key}"`"   # Retrieve

# If you're using more complex values, you'll need to hash your keys:
function mkkey
{
    local key="`mkpasswd -5R0 "$1" 00000000`"
    echo -n "${key##*$}"
}

local var_key="VAR_`mkkey "$key"`"
# ...

Jak vytvořit eval bezpečné

eval může být bezpečně používán - ale všechny jeho argumenty musí být nejprve citovány. Zde je postup:

Tato funkce, která to udělá za vás:

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}

Příklad použití:

Vzhledem k nějakému nedůvěryhodnému uživatelskému vstupu:

% input="Trying to hack you; date"

Vytvořte příkaz k vyhodnocení:

% cmd=(echo "User gave:" "$input")

Vyhodnoťte to, zdánlivě správná citace:

% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018

Všimněte si, že jste byli napadeni. date byl popraven, místo aby byl doslova vytištěn.

Místo toho s token_quote() :

% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%

eval není zlý - je to jen špatně pochopeno :)


Tuto odpověď rozdělím na dvě části , které, myslím, pokrývají velkou část případů, kdy lidé mají tendenci být pokoušeni eval :

  1. Spouštění podivně sestavených příkazů
  2. Pohrávání si s dynamicky pojmenovanými proměnnými

Spouštění podivně sestavených příkazů

Mnoho, mnohokrát, jednoduchá indexovaná pole stačí za předpokladu, že si osvojíte dobré návyky ohledně dvojitých uvozovek, abyste chránili rozšíření při definování pole.

# One nasty argument which must remain a single argument and not be split:
f='foo bar'
# The command in an indexed array (use `declare -a` if you really want to be explicit):
cmd=(
    touch
    "$f"
    # Yet another nasty argument, this time hardcoded:
    'plop yo'
)
# Let Bash expand the array and run it as a command:
"${cmd[@]}"

Tím se vytvoří foo bar a plop yo (dva soubory, ne čtyři).

Všimněte si, že někdy může vytvořit čitelnější skripty, které do pole vloží pouze argumenty (nebo spoustu možností) (alespoň na první pohled víte, co spouštíte):

touch "${args[@]}"
touch "${opts[@]}" file1 file2

Jako bonus vám pole umožňují snadno:

  1. Přidejte komentáře ke konkrétnímu argumentu:
cmd=(
    # Important because blah blah:
    -v
)
  1. Seskupit argumenty pro čitelnost ponecháním prázdných řádků v definici pole.
  2. Zakomentujte konkrétní argumenty pro účely ladění.
  3. Připojte k příkazu argumenty, někdy dynamicky podle konkrétních podmínek nebo v cyklech:
cmd=(myprog)
for f in foo bar
do
    cmd+=(-i "$f")
done
if [[ $1 = yo ]]
then
    cmd+=(plop)
fi
to_be_added=(one two 't h r e e')
cmd+=("${to_be_added[@]}")
  1. Definujte příkazy v konfiguračních souborech a povolte argumenty obsahující mezery definované konfigurací:
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
  1. Zaznamenejte robustně spustitelný příkaz, který dokonale reprezentuje to, co se spouští, pomocí %q printf :
function please_log_that {
    printf 'Running:'
    # From `help printf`:
    # “The format is re-used as necessary to consume all of the arguments.”
    # From `man printf` for %q:
    # “printed in a format that can be reused as shell input,
    # escaping  non-printable  characters with the proposed POSIX $'' syntax.”
    printf ' %q' "[email protected]"
    echo
}

arg='foo bar'
cmd=(prog "$arg" 'plop yo' $'arg\nnewline\tand tab')
please_log_that "${cmd[@]}"
# ⇒ “Running: prog foo\ bar plop\ yo $'arg\nnewline\tand tab'”
# You can literally copy and paste that ↑ to a terminal and get the same execution.
  1. Užijte si lepší zvýraznění syntaxe než u eval řetězce, protože nemusíte vnořovat uvozovky ani používat $ -s, které „nebude vyhodnoceno hned, ale bude v určitém okamžiku“.

Pro mě je hlavní výhoda tohoto přístupu (a naopak nevýhoda eval ) je, že můžete postupovat podle stejné logiky jako obvykle, pokud jde o nabídku, rozšíření atd. Není třeba si lámat hlavu tím, že se budete snažit dávat uvozovky do uvozovek „předem“, zatímco se snažíte zjistit, který příkaz bude v kterou chvíli interpretovat kterou dvojici uvozovek. A samozřejmě mnoho z výše uvedených věcí je těžší nebo přímo nemožné dosáhnout s eval .

S těmito jsem se nikdy nemusel spoléhat na eval v posledních zhruba šesti letech a čitelnost a robustnost (zejména pokud jde o argumenty, které obsahují mezery) se pravděpodobně zvýšily. Nemusíte ani vědět, zda IFS byl temperován s! Samozřejmě stále existují okrajové případy, kdy eval může být skutečně potřeba (předpokládám, že například uživatel musí být schopen poskytnout plnohodnotný skript prostřednictvím interaktivní výzvy nebo čehokoli jiného), ale doufejme, že to není něco, s čím se denně setkáte.

Pohrávání si s dynamicky pojmenovanými proměnnými

declare -n (nebo jeho vnitřních funkcí local -n protějšek), stejně jako ${!foo} , dělejte trik většinu času.

$ help declare | grep -- -n
      -n    make NAME a reference to the variable named by its value

No, bez příkladu to není výjimečně jasné:

declare -A global_associative_array=(
    [foo]=bar
    [plop]=yo
)

# $1    Name of global array to fiddle with.
fiddle_with_array() {
    # Check this if you want to make sure you’ll avoid
    # circular references, but it’s only if you really
    # want this to be robust.
    # You can also give an ugly name like “__ref” to your
    # local variable as a cheaper way to make collisions less likely.
    if [[ $1 != ref ]]
    then
        local -n ref=$1
    fi
    
    printf 'foo → %s\nplop → %s\n' "${ref[foo]}" "${ref[plop]}"
}

# Call the function with the array NAME as argument,
# not trying to get its content right away here or anything.
fiddle_with_array global_associative_array

# This will print:
# foo → bar
# plop → yo

(Miluji tento trik ↑, protože díky němu mám pocit, že svým funkcím předávám objekty, jako v objektově orientovaném jazyce. Možnosti jsou ohromující.)

Pokud jde o ${!…} (což získá hodnotu proměnné pojmenované jinou proměnnou):

foo=bar
plop=yo

for var_name in foo plop
do
    printf '%s = %q\n' "$var_name" "${!var_name}"
done

# This will print:
# foo = bar
# plop = yo

Linux
  1. Jak na to:Co je Git a Github? Jak to mám používat a proč by mě to mělo zajímat?

  2. Linux – proč používáme Su – a nejen Su?

  3. Použít $[ Expr ] místo $(( Expr ))?

  1. Proč nemohu použít CD ve skriptu Bash?

  2. Kdy a proč bych měl používat Apt-get Update?

  3. Jaké je použití $# v Bash

  1. Proč *ne* analyzovat `ls` (a co dělat místo toho)?

  2. Co znamená Env X=() { :;}; Command‘ Bash dělat a proč je to nejisté?

  3. Distribuovaná databáze NoSQL Elasticsearch – co to je a měli byste ji používat?