source

Bash에서 평가를 피해야 하는 이유는 무엇이고, 대신 무엇을 사용해야 합니까?

ittop 2023. 4. 27. 22:47
반응형

Bash에서 평가를 피해야 하는 이유는 무엇이고, 대신 무엇을 사용해야 합니까?

가 Bash를 사용하여 응답하는 것을 몇 번이고 봅니다.eval그리고 그 대답들은 그런 "말도 안 되는" 구조의 사용을 위해 말장난 의도로 얻어맞습니다. 왜?eval그렇게 사악한가요?

한다면eval안전하게 사용할 수 없습니다. 대신 무엇을 사용해야 합니까?

이 문제에는 눈에 보이는 것 이상의 것이 있습니다.다음은 당연한 것부터 시작하겠습니다.eval에는 "잘못된" 데이터가 실행될 가능성이 있습니다.더티 데이터는 상황에 따라 안전하게 사용할 수 있는 XYZ로 다시 작성되지 않은 모든 데이터입니다. 이 경우 평가에 안전하도록 포맷되지 않은 문자열입니다.

데이터를 삭제하는 것은 언뜻 보기에 쉬워 보입니다.Bash는 이미 개별 요소를 검사할 수 있는 좋은 방법과 전체 어레이를 단일 문자열로 검사할 수 있는 또 다른 방법을 제공합니다.

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).

이제 println 인수로 출력을 리디렉션하는 옵션을 추가하려고 합니다.물론, 우리는 각 통화에서 println의 출력을 리디렉션할 수 있지만, 예를 들어, 우리는 그렇게 하지 않을 것입니다.는 야합해니다를 해야 할 eval변수를 사용하여 출력을 리디렉션할 수 없기 때문입니다.

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.

맛있겠다, 그치?문제는 eval이 명령줄의 두 배를 구문 분석한다는 것입니다(모든 셸에서).구문 분석의 첫 번째 패스에서는 인용문의 한 레이어가 제거됩니다.따옴표를 제거하면 일부 변수 내용이 실행됩니다.

변수 확장이 내부에서 발생하도록 함으로써 이 문제를 해결할 수 있습니다.eval우리가 해야 할 일은 이중 따옴표를 그대로 둔 채 모든 것을 단일 따옴표로 묶는 것입니다.한 가지 예외: 이전에 리디렉션을 확장해야 합니다.eval따라서 인용문의 범위를 벗어나야 합니다.

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.

이게 통할 겁니다.그것은 또한 안전합니다.$1println절대 더럽지 않습니다.

잠시만 기다려 주십시오.저는 원래 우리가 사용했던 인용되지 않은 동일한 구문을 사용합니다.sudo가 아니라 하는 거지?왜 여기가 아니라 거기서 작동합니까?왜 우리는 모든 것을 한 번에 인용해야 했을까요? sudo는 좀 더 현대적입니다. 수신되는 각 인수를 따옴표로 묶어야 한다는 것을 알고 있지만, 이는 과도한 설명입니다. eval단순히 모든 것을 연결합니다.

유감스럽게도, 다음을 대체할 수 있는 드롭인(drop-in)은 없습니다.eval를 " 을다음같취급다니합이과"와 같이 합니다.sudo하다, ~와 같이eval는 셸 내장형으로, 함수처럼 새 스택과 범위를 만드는 것이 아니라 실행 시 주변 코드의 환경과 범위를 담당하기 때문에 중요합니다.

대안 평가

특정 사용 사례에는 종종 다음과 같은 실행 가능한 대안이 있습니다.eval여기 편리한 목록이 있습니다. command일반적으로 보낼 내용을 나타냅니다.eval당신이 원하는 것은 무엇이든지 대신하세요.

노옵

단순 콜론은 bash에서 no-op입니다.

:

하위 셸 만들기

( command )   # Standard notation

명령 출력 실행

외부 명령에 의존하지 마십시오.항상 반환 값을 제어해야 합니다.다음과 같이 각 행에 표시합니다.

$(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.

변수를 기준으로 리디렉션

호출 코드에서 지도&3 (으)보다 것.&2합니다.

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!

일회성 통화인 경우 전체 셸을 리디렉션할 필요가 없습니다.

func arg1 arg2 3>&2

중인 에서 "" "" "" "" ""로 .&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!

방향 변수

시나리오:

VAR='1 2 3'
REF=VAR

불량:

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

왜죠? REF에 이중 따옴표가 포함되어 있으면 코드가 손상되어 공격할 수 있습니다.REF를 소독하는 것은 가능하지만 다음과 같은 경우에는 시간 낭비입니다.

echo "${!REF}"

맞습니다, bash는 버전 2부터 방향 변수가 내장되어 있습니다.그것은 조금 더 까다로워집니다.eval더 것을 : 더복한작업수을다면같려하음오십시이과행하잡▁if.

# 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\""

그럼에도 불구하고, 새로운 방법은 더 직관적이지만, 익숙한 경험이 있는 프로그래밍된 방법으로는 보이지 않을 수도 있습니다.eval.

연관 배열

연관 배열은 bash 4에서 기본적으로 구현됩니다.한 가지 주의할 점: 다음을 사용하여 생성해야 합니다.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

이전 버전의 bash에서는 다음과 같은 방향의 변수를 사용할 수 있습니다.

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"`"
# ...

eval안전한

eval 안전하게 사용할 수 있지만 모든 인수를 먼저 따옴표로 묶어야 합니다.방법:

이 기능을 사용할 수 있습니다.

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

사용 예:

일부 신뢰할 수 없는 사용자 입력이 지정된 경우:

% input="Trying to hack you; date"

평가할 명령을 구성합니다.

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

겉보기에는 올바른 견적을 사용하여 평가합니다.

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

당신이 해킹당했다는 것을 주목하세요. date문자 그대로 인쇄되는 것이 아니라 실행되었습니다.

에 에신대로.token_quote():

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

eval사악하지요 - 그냥예요 :) ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜ

저는 이 답변을 두 부분으로 나누겠습니다, 제 생각에, 사람들이 유혹에 빠지기 쉬운 사건들의 상당 부분을 다루고 있습니다.eval:

  1. 이상하게 작성된 명령 실행
  2. 동적으로 명명된 변수 처리

이상하게 작성된 명령 실행

배열을 정의하는 동안 확장을 보호하기 위해 큰따옴표와 관련하여 좋은 습관을 가진다면 여러 번 단순 인덱싱된 배열로 충분합니다.

# 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[@]}"

이렇게 하면 생성됩니다.foo bar그리고.plop yo(4개가 아닌 2개의 파일).

때로는 배열에 인수(또는 여러 옵션)만 넣을 수 있는 더 읽기 쉬운 스크립트를 생성할 수 있습니다(적어도 얼핏 보면 실행 중인 것을 알 수 있습니다).

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

추가적으로 어레이는 다음과 같은 이점을 제공합니다.

  1. 특정 인수에 대한 설명을 추가합니다.
cmd=(
    # Important because blah blah:
    -v
)
  1. 배열 정의 내에 공백 행을 남겨 가독성을 위한 인수를 그룹화합니다.
  2. 디버깅 목적으로 특정 인수를 주석 처리합니다.
  3. 명령에 인수를 추가합니다. 때로는 특정 조건에 따라 또는 루프로 동적으로 추가할 수 있습니다.
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. 구성 파일에서 구성 정의 공백이 포함된 인수를 허용하면서 명령을 정의합니다.
readonly ENCODER=(ffmpeg -blah --blah 'yo plop')
# Deprecated:
#readonly ENCODER=(avconv -bloh --bloh 'ya plap')
# […]
"${ENCODER[@]}" foo bar
  1. printf's를 사용하여 실행 중인 내용을 완벽하게 나타내는 강력하게 실행 가능한 명령을 기록합니다.%q:
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' "$@"
    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. 에서보다 더 나은 구문 강조를 즐길 수 있습니다.eval 따옴표를 가 없기 에 따옴표를 사용합니다.$ 것입니다. -s "바로평가않어있것시입다니을에"

나에게, 이 접근법의 주요 장점(그리고 반대로 단점)은eval)는 인용, 확장 등과 관련하여 평소와 동일한 논리를 따를있다는 것입니다.어떤 명령어가 어떤 순간에 어떤 인용문 쌍을 해석할 것인지 파악하는 동안 "미리" 인용문에 인용문을 넣으려고 머리를 짜낼 필요가 없습니다.그리고 물론 위에서 언급한 많은 것들은 더 어렵거나 완전히 불가능합니다.eval.

이것들 때문에, 나는 절대 의지할 필요가 없었습니다.eval지난 6년 정도 동안 (특히 공백이 포함된 인수와 관련하여) 가독성과 견고성이 거의 틀림없이 증가했습니다.당신은 심지어 알 필요도 없습니다.IFS으로 완화되었습니다!물론, 여전히 가장자리에 있는 경우가 있습니다.eval실제로 필요할 수도 있지만(예를 들어 사용자가 대화형 프롬프트 등을 통해 전체 스크립트를 제공해야 하는 경우), 바라건대 그것은 일상적으로 접할 수 있는 것이 아닙니다.

동적으로 명명된 변수 처리

declare -n 그 (또는functions그구요성소부내또-요)▁(▁(local -n및 상편)${!foo}대부분의 시간을 그 묘기를 부립니다.

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

예를 들어 설명하지 않고는 특별히 명확하지 않습니다.

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

(저는 이 트릭 ↑을 좋아합니다. 이 트릭은 마치 객체 지향 언어처럼 제 기능에 객체를 전달하는 것처럼 느껴지기 때문입니다.그 가능성은 상상을 초월합니다.)

에 대해서는${!…}(다른 변수에 의해 명명된 변수의 값을 가져옵니다.)

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

언급URL : https://stackoverflow.com/questions/17529220/why-should-eval-be-avoided-in-bash-and-what-should-i-use-instead

반응형