단일 패스에서 여러 문자열 교체


11

템플릿 파일의 자리 표시 자 문자열을 일반적인 Unix 도구 (bash, sed, awk, perl)를 사용하여 구체적인 값으로 바꾸는 방법을 찾고 있습니다. 교체는 단일 패스로 수행하는 것이 중요합니다. 즉, 이미 스캔 / 교체 된 것을 다른 교체로 고려해서는 안됩니다. 예를 들어,이 두 가지 시도는 실패합니다.

echo "AB" | awk '{gsub("A","B");gsub("B","A");print}'
>> AA

echo "AB" | sed 's/A/B/g;s/B/A/g'
>> AA

이 경우 올바른 결과는 물론 BA입니다.

일반적으로 솔루션은 주어진 대체 문자열 중 하나와 가장 긴 일치를 위해 왼쪽에서 오른쪽으로 입력을 스캔하고 각 일치에 대해 대체를 수행하고 입력의 해당 지점부터 계속하는 것과 동일해야합니다. 이미 읽은 입력이나 수행 된 교체는 일치하는 것으로 간주해야합니다. 실제로 교체의 결과가 전체 또는 일부의 다른 교체에 대해 고려되지 않는다는 사실만으로 세부 사항은 중요하지 않습니다.

참고 올바른 일반 솔루션 만 찾고 있습니다. 특정 입력 (입력 파일, 검색 및 바꾸기 쌍)에 실패한 솔루션을 제안하지 마십시오.


나는 그들이 한 문자보다 길다고 가정합니까? 이를 위해을 사용할 수 있습니다 tr AB BA.
케빈

3
솔직히, 누군가가 당신의 노트를 약간 무례하다고 생각해도 놀라지 않을 것입니다.
peterph 2016 년

1
샘플 입력 또는 출력을 제공하지 않았을 때 어떻게 "올바른 솔루션 만 얻을 것"을 기대하십니까?
jasonwryan

1
나는 당신이 당신이 그것을 묘사하고있는 것처럼 정확하게해야 할 것을 두려워합니다. 처음부터 구문 분석하고 갈 때 대체하십시오. 즉 정규 표현식이 아닙니다.
peterph 2016 년

2
이것은 공정한 질문이지만 답은 상태 머신 파서가 필요하다는 것입니다 . 이것은 rici의 답변이 제공하는 것입니다 (진정한 해커 스타일). 즉, "일반적으로 정규식으로 (HT | X) ML을 구문 분석하고 싶습니다"-> 대답은 아니오입니다. sed를 사용할 수 는 없습니다 . awk를 사용할 수 는 없습니다 . AFAIK 기존 도구를 즉시 사용할 수있는 도구가 없습니다. Sans rici의 익스플로잇을 위해서는 코드를 작성해야합니다.
goldilocks

답변:


10

좋아, 일반적인 해결책. 다음 bash 함수에는 2k인수 가 필요합니다 . 각 쌍은 자리 표시 자와 교체로 구성됩니다. 문자열을 적절하게 인용하여 함수에 전달하는 것은 사용자의 몫입니다. 인수 수가 홀수이면 암시적인 빈 인수가 추가되어 마지막 자리 표시 자의 발생을 효과적으로 삭제합니다.

자리 표시 자나 대체물에는 NUL 문자가 포함될 수 없지만 필요한 경우 \와 같은 표준 C 이스케이프를 사용할 수 있습니다 (따라서 원하는 경우을 작성 해야 함 ).\0NUL\\\

posix와 유사한 시스템 (lex 및 cc)에 있어야하는 표준 빌드 도구가 필요합니다.

replaceholder() {
  local dir=$(mktemp -d)
  ( cd "$dir"
    { printf %s\\n "%option 8bit noyywrap nounput" "%%"
      printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\\"}"
      printf %s\\n "%%" "int main(int argc, char** argv) { return yylex(); }"
    } | lex && cc lex.yy.c
  ) && "$dir"/a.out
  rm -fR "$dir"
}

\인수에서 필요한 경우 이미 이스케이프 된 것으로 가정 하지만 큰 따옴표가 있으면 이스케이프해야합니다. 그것이 두 번째 printf에 대한 두 번째 주장이하는 것입니다. lex기본 동작은 이므로 ECHO걱정할 필요가 없습니다.

예제 실행 (회의론자의 타이밍과 함께; 싸구려 상품 노트북 일 뿐임) :

$ time echo AB | replaceholder A B B A
BA

real    0m0.128s
user    0m0.106s
sys     0m0.042s
$ time printf %s\\n AB{0000..9999} | replaceholder A B B A > /dev/null

real    0m0.118s
user    0m0.117s
sys     0m0.043s

더 큰 입력의 경우에 최적화 플래그를 제공하는 것이 유용 할 수 있으며 cc현재 Posix 호환성을 위해 사용하는 것이 좋습니다 c99. 더욱 야심 찬 구현은 생성 된 실행 파일을 매번 생성하는 대신 캐시하려고 시도 할 수 있지만 생성 비용이 정확하게 들지는 않습니다.

편집하다

tcc 가 있으면 임시 디렉토리를 작성하는 번거 로움을 피하고 일반적인 크기의 입력에 도움이되는 빠른 컴파일 시간을 즐길 수 있습니다.

treplaceholder () { 
  tcc -run <(
  {
    printf %s\\n "%option 8bit noyywrap nounput" "%%"
    printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\\"}"
    printf %s\\n "%%" "int main(int argc, char** argv) { return yylex(); }"
  } | lex -t)
}

$ time printf %s\\n AB{0000..9999} | treplaceholder A B B A > /dev/null

real    0m0.039s
user    0m0.041s
sys     0m0.031s

이것이 농담인지 아닌지는 확실하지 않습니다;)
Ambroz Bizjak

3
@ ambrozbizjak : 작동합니다. 큰 입력에는 빠르며 작은 입력에는 빠릅니다. 생각했던 도구를 사용하지 않을 수도 있지만 표준 도구입니다. 왜 장난일까요?
rici

4
+1 농담이 아닙니다! : D
goldilocks 2014 년

그것은 POSIX 포터블이 될 것 fn() { tcc ; } <<CODE\n$(gen code)\nCODE\n입니다. 그래도 물어볼 수 있습니까-이것은 멋진 답변이며 그것을 읽 자마자 찬성했습니다. 그러나 쉘 배열에 무슨 일이 일어나고 있는지 이해할 수 없습니까? 무엇 않습니다 "${@//\"/\\\"}"이 있습니까?
mikeserv 2016 년

@mikeserv :«각 인수를 따옴표로 묶은 값 ( "$ @")으로 따옴표 (\ ")의 모든 (//) 발생을 백 슬래시 (\\)로 바꾸고 따옴표 (\")로 바꿉니다. ». bash 매뉴얼의 매개 변수 확장을 참조하십시오.
rici

1
printf 'STRING1STRING1\n\nSTRING2STRING1\nSTRING2\n' |
od -A n -t c -v -w1 |
sed 's/ \{1,3\}//;s/\\$/&&/;H;s/.*//;x
     /\nS\nT\nR\nI\nN\nG\n1/s//STRING2/
     /\nS\nT\nR\nI\nN\nG\n2/s//STRING1/
     /\\n/!{x;d};s/\n//g;s/./\\&/g' |
     xargs printf %b

###OUTPUT###

STRING2STRING2

STRING1STRING2
STRING1

이와 같은 것은 대상 문자열이 sed줄 당 한 바이트로 스트림에서 발생할 때마다 각 대상 문자열을 한 번만 대체 합니다. 이것이 내가 상상할 수있는 가장 빠른 방법입니다. 그럼 다시, 나는 C를 쓰지 않는다 그러나 이것은 않습니다 당신이 그것을 원하는 경우 안정적으로 널 구분 기호를 처리합니다. 작동 방식에 대해서는 이 답변 을 참조하십시오 . 이것은 어떤에 문제가 특수 쉘 문자 또는 이와 유사한 포함 없습니다 - 그러나 그것은 이다 ASCII 로케일 특정 또는, 즉, od같은 줄에 출력하지 멀티 바이트 문자 만 당 하나를 수행 할 것입니다. 이것이 문제라면에 추가하고 싶을 것입니다 iconv.


+1 왜 "타겟 문자열의 가장 빠른 발생"만을 대체한다고 말합니까? 출력에서 마치 모든 것을 대체하는 것처럼 보입니다. 나는 그것을 요구하지 않지만, 값을 하드 코딩하지 않고이 방법으로 할 수 있습니까?
goldilocks 5

@goldilocks-예-그러나 발생하는 즉시. 어쩌면 나는 그것을 다시 말해야 할 것입니다. 그리고 네-중간 sed을 추가 하고 널 또는 다른 것을 저장 한 다음이 sed스크립트를 작성하십시오. 또는 쉘 기능에 넣어하고 같은 줄에 하나의 입에서 값주고 "/$1/"... "/$2/"... 어쩌면 나도 그 기능을 쓸 것이다 -
mikeserv

이 자리 표시가있는 경우에 작동하지 않는 것 PLACE1, PLACE2하고 PLA. PLA항상 이깁니다. OP는 " 주어진 대체 문자열 중 하나 와 가장 긴 일치 를 위해 왼쪽에서 오른쪽으로 입력을 스캔하는 것과 같습니다 "(강조 추가)
rici

@rici-감사합니다. 그런 다음 null 구분 기호를 수행해야합니다. 다시 플래시.
mikeserv 2016 년

@rici-방금 다른 버전을 게시하려고했는데 설명을 처리하지만 다시 살펴보아야한다고 생각하지 않습니다. 그는 주어진 교체 문자열 중 하나 에 대해 가장 오래 말합니다 . 그렇게합니다. 한 문자열이 다른 문자열의 하위 집합이라는 표시는 없으며 대체 된 값만있을 수 있습니다. 또한 목록을 반복하는 것이 문제를 해결하는 유효한 방법이라고 생각하지 않습니다. 내가 이해할 때 문제가 발생하면 이것이 작동하는 솔루션입니다.
mikeserv

1

perl솔루션입니다. 일부는 가능하지 않다고 말했지만 일반적으로 간단한 일치 및 교체가 불가능하며 NFA의 역 추적으로 인해 악화되어 결과가 예기치 않을 수 있습니다.

일반적으로이 문제는 대체 튜플의 순서와 길이에 따라 다른 결과를 나타냅니다. 즉 :

A B
AA CC

입력 AAA결과는 BBB또는 CCB입니다.

코드는 다음과 같습니다.

#!/usr/bin/perl

$v='if (0) {} ';
while (($a,$b)=split /\s+/, <DATA>) {
  $k.=$a.'|';
  $v.='elsif ($& eq \''.$a.'\') {print \''.$b.'\'} ';
}
$k.='.';
$v.='else {print $&;}';

eval "
while (<>) {
  \$_ =~ s/($k)/{$v}/geco;
}";  
print "\n";


__DATA__
A    B
B    A
abba baab
baab abbc
abbc aaba

체커 버니 :

$ echo 'ABBabbaBBbaabAAabbc'|perl script
$ BAAbaabAAabbcBBaaba
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.