awk '!a[$0]++'는 어떻게 작동하나요?

awk '!a[$0]++'는 어떻게 작동하나요?

이 단일 라이너는 사전 정렬 없이 텍스트 입력에서 중복된 줄을 제거합니다.

예를 들어:

$ cat >f
q
w
e
w
r
$ awk '!a[$0]++' <f
q
w
e
r
$ 

인터넷에서 찾은 원본 코드는 다음과 같습니다.

awk '!_[$0]++'

Perl에서처럼 awk에서 특별한 의미를 갖는다 고 생각했기 때문에 이것은 나에게 훨씬 더 당혹스러웠지만 _그것은 단지 배열의 이름일 뿐이라는 것이 밝혀졌습니다.

이제 나는 한 줄의 논리를 이해합니다. 각 입력 줄은 해시 배열의 키로 사용되므로 완료 시 해시에는 도착 순서대로 고유한 줄이 포함됩니다.

내가 배우고 싶은 것은 awk가 이 표기법을 정확히 어떻게 해석하는지입니다. 예를 들어 앞머리 기호( !)의 의미와 이 코드 조각의 다른 요소 등이 있습니다.

어떻게 작동하나요?

답변1

다음은 "직관적인" 답변입니다. awk의 메커니즘에 대한 더 자세한 설명은 @Cuonglm의 다음을 참조하세요.

이 경우 !a[$0]++사후 증분은 ++잠시 미뤄둘 수 있으며 표현식의 값은 변경되지 않습니다. 그러므로 !a[$0]. 여기:

a[$0]

현재 행을 $0배열의 키로 사용하여 a거기에 저장된 값을 가져옵니다. 이 특정 키가 이전에 참조된 적이 없는 경우 a[$0]빈 문자열로 평가됩니다.

!a[$0]

!이전 값을 부정합니다 . 비어 있거나 0(거짓)이면 이제 참 결과를 얻게 됩니다. 0이 아닌 경우(참) 결과는 거짓입니다. 전체 표현식이 true로 평가되면(즉, a[$0]시작하도록 설정되지 않았음을 의미) 전체 행이 기본 작업으로 인쇄됩니다.

또한 이전 값에 관계없이 사후 증가 연산자는 에 1을 추가하므로 a[$0]다음에 배열의 동일한 값에 액세스하면 양수가 되고 전체 조건이 실패합니다.

답변2

처리는 다음과 같습니다.

  • a[$0]$0: 연관 배열에서 키 값을 살펴봅니다 a. 존재하지 않는 경우 빈 문자열을 사용하여 자동으로 생성합니다.

  • a[$0]++: 의 값을 증가시키고 a[$0], 이전 값을 표현식의 값으로 반환합니다. 연산자 ++는 숫자 값을 반환하므로 a[$0]처음에 가 비어 있으면 가 0반환되고 a[$0]로 증가됩니다 1.

  • !a[$0]++: 표현식의 값을 부정합니다. a[$0]++반환 되면 0(false 값) 전체 표현식이 true로 평가되고 awk기본 작업을 수행 하게 됩니다 print $0. 그렇지 않고 전체 표현식이 false로 평가되면 추가 조치가 수행되지 않습니다.

참고자료:

를 사용하면 gawk다음을 사용할 수 있습니다.dgawk (또는 awk --debug최신 버전)스크립트 를 디버깅합니다 gawk. 먼저 gawk다음과 같은 스크립트를 만듭니다 test.awk.

BEGIN {                                                                         
    a = 0;                                                                      
    !a++;                                                                       
}

그런 다음 다음을 실행하십시오.

dgawk -f test.awk

또는:

gawk --debug -f test.awk

디버거 콘솔에서:

$ dgawk -f test.awk
dgawk> trace on
dgawk> watch a
Watchpoint 1: a
dgawk> run
Starting program: 
[     1:0x7fe59154cfe0] Op_rule             : [in_rule = BEGIN] [source_file = test.awk]
[     2:0x7fe59154bf80] Op_push_i           : 0 [PERM|NUMCUR|NUMBER]
[     2:0x7fe59154bf20] Op_store_var        : a [do_reference = FALSE]
[     3:0x7fe59154bf60] Op_push_lhs         : a [do_reference = TRUE]
Stopping in BEGIN ...
Watchpoint 1: a
  Old value: untyped variable
  New value: 0
main() at `test.awk':3
3           !a++;
dgawk> step
[     3:0x7fe59154bfc0] Op_postincrement    : 
[     3:0x7fe59154bf40] Op_not              : 
Watchpoint 1: a
  Old value: 0
  New value: 1
main() at `test.awk':3
3           !a++;
dgawk>

보시다시피 Op_postincrement는 이전에 실행되었습니다 Op_not.

더 명확하게 보려면 or 대신 sior를 사용할 수도 있습니다 .stepisstep

dgawk> si
[     3:0x7ff061ac1fc0] Op_postincrement    : 
3           !a++;
dgawk> si
[     3:0x7ff061ac1f40] Op_not              : 
Watchpoint 1: a
  Old value: 0
  New value: 1
main() at `test.awk':3
3           !a++;

답변3

아 유비쿼터스이지만 불길한 awk 중복 제거 프로그램

awk '!a[$0]++'

이 사랑스러운 아기는 awk의 힘과 간결함을 사랑하는 아이입니다. awk one liner의 정점. 짧지만 강력하고 신비한 느낌을 동시에 줍니다. 순서를 유지하면서 중복을 제거합니다. 인접한 중복 항목만 제거 uniq하거나 달성하지 못 sort -u하거나 중복 항목을 제거하기 위해 순서를 깨야 하는 위업 입니다.

여기에 이 ​​awk one liner가 어떻게 작동하는지 설명하려는 시도가 있습니다. awk를 모르는 사람도 따라할 수 있도록 설명하려고 노력했습니다. 내가 그렇게 할 수 있었으면 좋겠다.

먼저 몇 가지 배경 지식: awk는 프로그래밍 언어입니다. 이 명령은 awk '!a[$0]++'awk 코드에서 awk 인터프리터/컴파일러를 호출합니다 !a[$0]++. python -c 'print("foo")'또는 와 유사합니다 node -e 'console.log("foo")'. awk 코드는 텍스트 필터링을 위해 간결하도록 특별히 설계되었기 때문에 하나의 라이너인 경우가 많습니다.

이제 의사 코드가 있습니다. 이 하나의 라이너가 수행하는 작업은 기본적으로 다음과 같습니다.

for every line of input
  if i have not seen this line before then
    print line
  take note that i have now seen this line

이것이 순서를 유지하면서 어떻게 중복 항목을 제거하는지 볼 수 있기를 바랍니다.

하지만 루프, if, 인쇄, 문자열 저장 및 검색 메커니즘이 8자 awk 코드에 어떻게 들어맞을까요? 대답은 암시적입니다.

루프, if 및 인쇄는 암시적입니다.

설명하기 위해 의사 코드를 다시 살펴보겠습니다.

for every line of input
  if line matches condition then
    execute code block

이것은 어떤 언어로든 코드에 어떤 형식으로든 많이 작성했을 일반적인 필터입니다. awk 언어는 이러한 종류의 필터 작성이 매우 짧도록 설계되었습니다.

awk는 우리를 위해 루프를 수행하므로 루프 내부에 코드를 작성하기만 하면 됩니다. awk의 구문은 if의 상용구를 추가로 생략하므로 조건과 코드 블록만 작성하면 됩니다.

condition { code block }

awk에서는 이것을 "규칙"이라고 합니다.

조건이나 코드 블록 중 하나를 생략할 수 있으며(분명히 둘 다 생략할 수는 없습니다) awk는 누락된 부분을 일부 암시적으로 채울 것입니다.

조건을 생략하면

{ code block }

그러면 그것은 암묵적으로 사실이 될 것입니다

true { code block }

이는 코드 블록이 모든 라인에 대해 실행됨을 의미합니다.

코드 블록을 생략하면

condition

그러면 암시적으로 현재 줄을 인쇄하게 됩니다.

condition { print current line }

원래 awk 코드를 다시 살펴보겠습니다.

!a[$0]++

중괄호 안에 위치하지 않으므로 규칙의 조건부 부분입니다.

암시적 루프를 작성하고 if 및 인쇄해 보겠습니다.

for every line of input
  if !a[$0]++ then
    print line

원래 의사 코드와 비교

for every line of input                      # implicit by awk
  if i have not seen this line before then   # at least we know the conditional part
    print line                               # implicit by awk
  take note that i have now seen this line   # ???

우리는 루프, if 및 인쇄를 이해합니다. 하지만 중복된 줄에서만 false로 평가되도록 하려면 어떻게 작동합니까? 그리고 이미 본 줄을 어떻게 기록합니까?

이 짐승을 분해하자:

!a[$0]++

C나 Java를 알고 있다면 이미 일부 기호를 알고 있어야 합니다. 의미론은 동일하거나 적어도 유사합니다.

느낌표( !)는 부정어입니다. 표현식을 부울로 평가하고 그 결과가 무엇이든 부정됩니다. 표현식이 true로 평가되면 최종 결과는 false이고 그 반대도 마찬가지입니다.

a[..]배열입니다. 연관 배열. 다른 언어에서는 지도 또는 사전이라고 부릅니다. awk에서는 모든 배열이 연관 배열입니다. 특별한 의미 a는 없습니다. 이는 단지 배열의 이름일 뿐입니다. x또는 가 될 수도 있습니다 eliminatetheduplicate.

$0입력의 현재 라인입니다. 이것은 awk 특정 변수입니다.

더하기 더하기( ++)는 사후 증가 연산자입니다. 이 연산자는 두 가지 작업을 수행하기 때문에 약간 까다롭습니다. 변수의 값이 증가합니다. 그러나 추가 처리를 위해 증가되지 않은 원래 값을 "반환"합니다.

   !        a[         $0       ]        ++
negator   array   current line      post increment

그들은 어떻게 함께 일합니까?

대략 이런 순서입니다.

  1. $0현재 라인입니다
  2. a[$0]현재 행에 대한 배열의 값입니다.
  3. 사후 증분( ++)은 에서 값을 가져옵니다 a[$0]. 이를 증가시키고 다시 저장합니다 a[$0]. 그런 다음 원래 값을 줄의 다음 연산자인 부정자에게 "반환"합니다.
  4. 부정자( ) 는 에서 원래 값이었던 !값을 에서 가져옵니다 . 부울로 평가된 다음 부정된 다음 암시적 if에 전달됩니다.++a[$0]
  5. if는 라인을 인쇄할지 여부를 결정합니다.

즉, 해당 행이 인쇄되는지 여부 또는 이 awk 프로그램의 맥락에서 해당 행이 중복되는지 여부는 궁극적으로 의 값에 의해 결정됩니다 a[$0].

확장: 이 줄이 이미 표시되었는지 기록하는 메커니즘은 ++증가된 값을 다시 저장할 때 발생해야 합니다 a[$0].

의사 코드를 다시 살펴보겠습니다.

for every line of input
  if i have not seen this line before then   # decided based on value in a[$0]
    print line
  take note that i have now seen this line   # happens by increment from ++

여러분 중 일부는 이미 이것이 어떻게 진행되는지 알고 있을 수도 있지만 여기까지 진행했습니다. 마지막 몇 단계를 수행하고++

암시적 코드에 포함된 awk 코드로 시작합니다.

for each line as $0
  if !a[$0]++ then
    print $0

작업할 공간을 확보하기 위해 변수를 도입해 보겠습니다.

for each line as $0
  tmp = a[$0]++
  if !tmp then
    print $0

이제 우리는 분해합니다 ++.

이 연산자는 변수의 값을 증가시키고 추가 처리를 위해 원래 값을 반환하는 두 가지 작업을 수행한다는 점을 기억하십시오. 그래서 ++두 줄이 됩니다:

for each line as $0
  tmp = a[$0]       # get original value
  a[$0] = tmp + 1   # increment value in variable
  if !tmp then
    print $0

아니면 다른 말로 하면

for each line as $0
  tmp = a[$0]       # query if have seen this line
  a[$0] = tmp + 1   # take note that has seen this line
  if !tmp then
    print $0

첫 번째 의사 코드와 비교

for every line of input:
  if i have not seen this line before:
    print line
  take note that i have now seen this line

여기까지입니다. 루프, if, 인쇄, 쿼리 및 메모 작성이 있습니다. 의사 코드와 순서가 다릅니다.

8자로 압축

!a[$0]++

awks 암시적 루프, 암시적 if, 암시적 인쇄 및 ++쿼리와 메모 작성을 모두 수행하기 때문에 가능합니다.

한 가지 질문이 남아 있습니다. a[$0]첫 번째 줄 의 값은 무엇입니까 ? 아니면 이전에 본 적이 없는 라인에 대해? 대답은 다시 암시적입니다.

awk에서 처음으로 사용되는 모든 변수는 암시적으로 선언되고 빈 문자열로 초기화됩니다. 배열을 제외하고. 배열은 빈 배열로 선언되고 초기화됩니다.

숫자로 암시적으로 변환 됩니다 ++. 빈 문자열은 0으로 변환됩니다. 다른 문자열은 최선의 알고리즘에 의해 숫자로 변환됩니다. 문자열이 숫자로 인식되지 않으면 다시 0으로 변환됩니다.

부울로의 암시 적 !변환을 수행합니다. 숫자 0과 빈 문자열은 false로 변환됩니다. 다른 것은 모두 true로 변환됩니다.

이는 행이 처음으로 표시되면 a[$0]빈 문자열로 설정됨을 의미합니다. 빈 문자열은 에 의해 0으로 변환됩니다 ++(또한 1로 증가되고 다시 저장됩니다 a[$0]). 0은 에 의해 false로 변환됩니다 !. 결과가 !true이므로 줄이 인쇄됩니다.

이제 의 값은 a[$0]숫자 1입니다.

두 번째 줄이 보이면 a[$0]숫자 1이 true로 변환되고 결과는 !false이므로 인쇄되지 않습니다.

같은 줄을 더 많이 만나면 숫자가 늘어납니다. 0을 제외한 모든 숫자는 true이므로 결과는 !항상 false이므로 해당 줄은 다시 인쇄되지 않습니다.

이것이 중복이 제거되는 방법입니다.

TL;DR: 라인이 얼마나 자주 표시되었는지 계산합니다. 0이면 인쇄합니다. 다른 번호가 있으면 인쇄되지 않습니다. 암시적인 내용이 많기 때문에 짧을 수 있습니다.


보너스: 원 라이너의 일부 변형과 그것이 수행하는 작업에 대한 매우 짧은 설명.

$0(전체 줄)을 (두 번째 열)로 바꾸면 $2중복 항목이 제거되지만 두 번째 열만 기준으로 합니다.

$ cat input 
x y z
p q r
a y b

$ awk '!a[$2]++' input 
x y z
p q r

!(negator)를 (equal to one)으로 바꾸면 ==1중복된 첫 번째 줄이 인쇄됩니다.

$ cat input 
a
b
c
c
b
b

$ awk 'a[$0]++==1' input 
c
b

>0(0보다 큼) 으로 바꾸고 추가하면 {print NR":"$0}줄 번호와 함께 모든 중복 줄이 인쇄됩니다. NR줄 번호(awk 용어의 레코드 번호)를 포함하는 특수 awk 변수입니다.

$ awk 'a[$0]++>0 {print NR":"$0}' input 
4:c
5:b
6:b

이 예제가 위에서 설명한 개념을 더 잘 이해하는 데 도움이 되기를 바랍니다.

답변4

그냥 둘 다 추가하고 싶고 의 약어 expr++입니다 . 하지만++exprexpr=expr+1

$ awk '!a[$0]++' f # or 
$ awk '!(a[$0]++)' f

추가 전 expr++으로 평가되므로 모든 고유 값을 인쇄합니다 .expr

$ awk '!(++a[$0])' f

이 경우에는 항상 0이 아닌 값을 반환하고 부정은 항상 0 값을 반환하므로 ++expr아무 것도 인쇄하지 않습니다 .expr+1

관련 정보