단일 패스로 여러 문자열 교체

단일 패스로 여러 문자열 교체

저는 일반적인 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입니다.

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

메모나는 올바른 일반적인 솔루션만을 찾고 있습니다. 특정 입력(입력 파일, 검색 및 바꾸기 쌍)에 대해 실패하는 솔루션을 제안하지 마십시오. 그러나 그럴 것 같지는 않습니다.

답변1

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

자리 표시자나 대체 항목 모두 NUL 문자를 포함할 수 없지만 s가 필요한 경우 \와 같이 표준 C 이스케이프를 사용할 수 있습니다 (따라서 a를 원하는 경우 작성해야 함 ).\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"
}

\필요한 경우 인수에서 that이 이미 이스케이프되었다고 가정 하지만, 큰따옴표가 있으면 이스케이프해야 합니다. 이것이 두 번째 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

답변2

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를 쓰지 않습니다. 하지만 이것은하다원하는 경우 null 구분 기호를 안정적으로 처리할 수 있습니다. 보다이 답변그것이 어떻게 작동하는지. 포함된 특수 쉘 문자나 이와 유사한 문자에는 문제가 없습니다.~이다ASCII 로케일에 따라 다릅니다. 즉, od같은 줄에 멀티바이트 문자를 출력하지 않고 한 줄에 하나씩만 출력합니다. 이것이 문제라면 iconv.

답변3

해결책 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

관련 정보