bash 함수에서 "grep | grep" 명령을 문자열로 어떻게 실행할 수 있나요?

bash 함수에서 "grep | grep" 명령을 문자열로 어떻게 실행할 수 있나요?

bash 함수에서 한 grep 명령의 결과를 다른 grep 명령으로 파이프하는 명령을 작성하려고 합니다. 궁극적으로 나는 명령이 다음과 같이 실행되기를 원합니다.

grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:

내가 작성 중인 함수는 명령의 첫 번째 부분을 문자열에 저장한 다음 두 번째 부분을 추가합니다.

grep_cmd="grep -I -r $pattern $@"

if (( ${#file_types[@]} > 0 )); then
    file_types="${file_types[@]}"
    file_types=.${file_types// /':\|.'}:

    grep_cmd="$grep_cmd | grep $file_types"
fi

echo "$grep_cmd"
${grep_cmd}

첫 번째 부분의 출력 후에 오류가 발생합니다.

grep: |: No such file or directory
grep: grep: No such file or directory
grep: .c:\|.h:: No such file or directory

마지막 줄을 에서 으로 변경하면 ${grep_cmd}"$grep_cmd"번째 부분의 출력이 표시되지 않고 다른 오류가 발생합니다.

bash: grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:: No such file or directory

이 SO 답변, 마지막 줄을 $(grep_cmd). 또 다른 오류가 발생합니다.

bash: grep_cmd: command not found

이 SO 답변을 사용하는 것이 좋습니다 eval $grep_cmd. 이렇게 하면 오류가 억제되지만 출력도 억제됩니다.

이 하나을 사용하는 것이 좋습니다 eval ${grep_cmd}. 이는 동일한 결과를 갖습니다(오류 및 출력을 억제함). bash에서 디버깅을 활성화하려고 시도했는데(사용 set -x) 다음과 같은 결과를 얻었습니다.

+ eval grep -I -r FooBar /code/internal/dev/ /code/public/dev/ '|' grep '.c:\|.h:'
++ grep -I -r FooBar /code/internal/dev/ /code/public/dev/
++ grep '.c:|.h:'

파이프가 이스케이프되는 것처럼 보이므로 쉘은 명령을 두 개의 명령으로 해석합니다. 파이프 문자를 적절하게 이스케이프하여 하나의 명령으로 해석하려면 어떻게 해야 합니까?

답변1

설명에서 언급했듯이, 명령을 변수에 저장한 다음 나중에 해당 명령을 실행하려고 하기 때문에 많은 어려움이 있습니다.

명령을 저장하려고 시도하는 대신 즉시 명령을 실행하면 훨씬 더 나은 행운을 누릴 수 있습니다.

예를 들어, 다음은 달성하려는 작업을 수행해야 합니다.

if (( ${#file_types[@]} > 0 )); then
    regex="${file_types[*]}"
    regex="\.\(${regex// /\|}\):"
    grep -I -r "$pattern" "$@" | grep "$regex"
else
    grep -I -r "$pattern" "$@"
fi

답변2

튜토리얼에서 종종 명확하게 설명되지 않는 쉘 프로그래밍에 대해 명심해야 할 한 가지는 두 가지 유형의 데이터가 있다는 것입니다.문자열 및 문자열 목록. 문자열 목록은 줄 바꿈이나 공백 구분 기호가 있는 문자열과 동일하지 않으며 그 자체입니다.

명심해야 할 또 다른 점은 대부분의 확장은 셸이 파일을 구문 분석할 때만 적용된다는 것입니다. 명령 실행에는 확장이 포함되지 않습니다.

변수 값에 일부 확장이 발생합니다. $foo즉, "변수 값을 가져와서 foo공백을 구분 기호로 사용하여 문자열 목록으로 분할하고 목록의 각 요소를 확장되는 와일드카드 패턴으로 해석합니다"를 의미합니다. 이 확장은 목록을 호출하는 컨텍스트에서 변수가 사용되는 경우에만 발생합니다. 문자열을 요구하는 맥락에서 $foo"변수의 값을 취한다 foo"는 뜻입니다. 큰따옴표는 문자열 컨텍스트를 적용하므로 다음과 같은 조언이 있습니다.변수 대체 및 명령 대체는 항상 큰따옴표로 묶어서 사용하세요 "$foo"."$(somecommand)"². (변수와 마찬가지로 보호되지 않은 명령 대체에도 동일한 확장이 발생합니다.)

구문 분석과 실행의 차이로 인해 단순히 명령을 문자열에 넣고 실행할 수는 없다는 것입니다. 를 작성하면 ${grep_cmd}파싱이 아닌 분할과 글로빙만 발생하므로 와 같은 문자는 |특별한 의미가 없습니다.

쉘 명령을 문자열에 반드시 입력해야 한다면 eval다음과 같이 할 수 있습니다.

eval "$grep_cmd"

큰따옴표에 유의하세요. 변수 값에는 쉘 명령이 포함되어 있으므로 정확한 문자열 값이 필요합니다. 그러나 이 접근 방식은 복잡한 경향이 있습니다. 실제로 셸 소스 구문에 뭔가가 있어야 합니다. 예를 들어 파일 이름이 필요한 경우 이 파일 이름을 올바르게 인용해야 합니다. 따라서 거기에 $patternand 를 넣을 수는 없으며 $@구문 분석할 때 패턴을 포함하는 단일 단어와 인수를 포함하는 단어 목록을 생성하는 문자열을 작성해야 합니다.

요약:쉘 명령을 변수에 넣지 마십시오. 대신에,기능을 사용하다. 파이프라인과 같이 복잡한 명령이 아닌 인수가 있는 간단한 명령이 필요한 경우 배열을 대신 사용할 수 있습니다(배열 변수는 문자열 목록을 저장합니다).

가능한 접근 방식은 다음과 같습니다. run_grep표시된 코드에는 이 기능이 실제로 필요하지 않습니다. 나는 이것이 더 큰 스크립트의 작은 부분이고 더 많은 중간 코드가 있다는 가정하에 여기에 포함시켰습니다. 이것이 실제로 전체 스크립트라면 파이프할 내용을 알고 있는 지점에서 grep을 실행하면 됩니다. 또한 필터를 작성하는 코드가 올바르지 않은 것처럼 수정했습니다(예를 들어 .정규 표현식에서 "모든 문자"를 의미하지만 문자 그대로 점이 필요한 것 같습니다).

grep_cmd=(grep -I -r "$pattern" "$@")

if (( ${#file_types[@]} > 0 )); then
    regexp='\.\('
    for file_type in "${file_types[@]}"; do
      regexp="$regexp$file_type\\|"
    done
    regexp="${regexp%?}):"
    run_grep () {
      "${grep_cmd[@]}" | grep "$file_types"
    }
else
  run_grep () {
    "${grep_cmd[@]}"
  }
fi

run_grep

¹ 보다 일반적으로 IFS.
² 전문가 전용: 변수 및 명령 대체를 생략하면 올바른 효과가 나타나는 이유를 이해하지 않는 한 항상 큰따옴표를 사용하십시오.
3 전문가 전용: 쉘 명령을 변수에 입력해야 하는 경우 인용에 매우 주의하십시오.


당신이 하고 있는 일은 지나치게 복잡하고 신뢰할 수 없는 것 같습니다. 를 포함하는 파일이 있다면 어떻게 될까요 foo.c: 42? GNU grep에는 --include재귀 순회에서 특정 파일만 볼 수 있는 옵션이 있습니다. 그냥 사용하세요.

grep_cmd=(grep -I -r)
for file_type in "${file_types[@]}"; do
  grep_cmd+=(--include "*.$file_type")
done
"${grep_cmd[@]}" "$pattern" "$@"

답변3

command="grep $regex1 filelist | grep $regex2"
echo $command | bash

관련 정보