[unix] 내부에서 파일을 수정하는 방법이 있습니까?

상당히 큰 파일 (35Gb)이 있고이 파일을 제자리에서 필터링하고 싶습니다 (예 : 다른 파일을위한 충분한 디스크 공간이 없음). 특히 grep하고 일부 패턴을 무시하고 싶습니다. 다른 파일을 사용하지 않고이 작업을 수행합니까?

foo:예를 들어 포함하는 모든 줄을 필터링하고 싶다고 가정 해 봅시다 .



답변

시스템 호출 레벨에서 가능해야합니다. 프로그램은 대상 파일을 자르지 않고 쓰기 위해 열고 stdin에서 읽은 내용을 쓰기 시작할 수 있습니다. EOF를 읽을 때 출력 파일이 잘릴 수 있습니다.

입력에서 행을 필터링하므로 출력 파일 쓰기 위치는 항상 읽기 위치보다 작아야합니다. 이것은 새로운 출력으로 입력을 손상시키지 않아야한다는 것을 의미합니다.

그러나이를 수행하는 프로그램을 찾는 것이 문제입니다. 출력 파일을 열 때 자르지 않는 dd(1)옵션 conv=notrunc이 있지만 마지막 부분도 자르지 않고 grep 내용 뒤에 원래 파일 내용을 남겨 둡니다 (와 같은 명령 사용 grep pattern bigfile | dd of=bigfile conv=notrunc)

시스템 호출 관점에서 매우 간단하기 때문에 작은 프로그램을 작성하여 작은 (1MiB) 전체 루프백 파일 시스템에서 테스트했습니다. 원하는 것을 수행했지만 실제로 다른 파일로 먼저 테스트하려고합니다. 항상 파일을 덮어 쓸 위험이 있습니다.

overwrite.c

/* This code is placed in the public domain by camh */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)
{
        int outfd;
        char buf[1024];
        int nread;
        off_t file_length;

        if (argc != 2) {
                fprintf(stderr, "usage: %s <output_file>\n", argv[0]);
                exit(1);
        }
        if ((outfd = open(argv[1], O_WRONLY)) == -1) {
                perror("Could not open output file");
                exit(2);
        }
        while ((nread = read(0, buf, sizeof(buf))) > 0) {
                if (write(outfd, buf, nread) == -1) {
                        perror("Could not write to output file");
                        exit(4);
                }
        }
        if (nread == -1) {
                perror("Could not read from stdin");
                exit(3);
        }
        if ((file_length = lseek(outfd, 0, SEEK_CUR)) == (off_t)-1) {
                perror("Could not get file position");
                exit(5);
        }
        if (ftruncate(outfd, file_length) == -1) {
                perror("Could not truncate file");
                exit(6);
        }
        close(outfd);
        exit(0);
}

당신은 그것을 다음과 같이 사용할 것입니다 :

grep pattern bigfile | overwrite bigfile

나는 주로 당신이 그것을 시도하기 전에 다른 사람들이 의견을 남길 수 있도록 이것을 게시하고 있습니다. 아마도 다른 누군가가 더 많은 테스트를 거친 비슷한 프로그램을 알고있을 것입니다.


답변

sed파일을 편집 하는 데 사용할 수 있습니다 (그러나 중간 임시 파일이 생성됨).

다음을 포함하는 모든 줄을 제거하려면 foo:

sed -i '/foo/d' myfile

다음을 포함하는 모든 줄을 유지하려면 foo:

sed -i '/foo/!d' myfile


답변

필자는 필터 명령이 접두사 축소 필터 라고 부르는 것으로 가정합니다.이 필터 는 출력의 바이트 N이 N 바이트 이상의 입력을 읽기 전에 절대 쓰지 않는 속성을 갖습니다. grep이 속성을 가지고 있습니다 (만 필터링하고 일치하는 줄 번호 추가와 같은 다른 일을하지 않는 한). 이러한 필터를 사용하면 입력을 덮어 쓸 수 있습니다. 물론 파일의 시작 부분에서 덮어 쓴 부분이 영구적으로 손실되므로 실수를하지 않아야합니다.

대부분의 유닉스 도구는 파일을 덮어 쓰지 않고 파일에 추가하거나자를 수 있습니다. 표준 도구 상자의 한 가지 예외 dd는 출력 파일을 자르지 않도록 지시 할 수 있습니다. 따라서 계획은로 명령을 필터링하는 것 dd conv=notrunc입니다. 파일 크기는 변경되지 않으므로 새 내용의 길이를 잡고 파일을 해당 길이로 자릅니다 (와 함께 dd). 이 작업은 본질적으로 강력하지 않습니다. 오류가 발생하면 사용자가 스스로해야합니다.

export LC_ALL=C
n=$({ grep -v foo <big_file |
      tee /dev/fd/3 |
      dd of=big_file conv=notrunc; } 3>&1 | wc -c)
dd if=/dev/null of=big_file bs=1 seek=$n

견고하게 동등한 Perl을 작성할 수 있습니다. 효율적이지 않은 빠른 구현은 다음과 같습니다. 물론 해당 언어로 직접 초기 필터링을 수행 할 수도 있습니다.

grep -v foo <big_file | perl -e '
  close STDOUT;
  open STDOUT, "+<", $ARGV[0] or die;
  while (<STDIN>) {print}
  truncate STDOUT, tell STDOUT or die
' big_file


답변

Bourne과 같은 쉘 :

{
  cat < bigfile | grep -v to-exclude
  perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile

어떤 이유로, 사람들은 40 살 ¹과 표준 읽기 + 쓰기 리디렉션 연산자 를 잊는 경향이 있습니다.

우리는 열 bigfile읽기 + 쓰기 모드와에 잘림없이 (어떤 일이 대부분 여기에 중요한) stdout동안은 bigfile에 공개 (별도)이다 catstdin. grep종료 된 후 일부 라인이 제거 된 경우 stdout이제 내부 어딘가를 bigfile가리켜 서이 시점 이후의 항목을 제거해야합니다. 따라서 현재 위치에서 perl파일 ( truncate STDOUT) 을 자르는 명령 (에서 반환 tell STDOUT)

( stdin과 stdout이 동일한 파일을 가리키는 경우 그렇지 않으면 catGNU에 대한 것 grep입니다).


¹ 음, <>70 년대 후반부터 Bourne 쉘에 있었지만 처음에는 문서화되지 않았으며 제대로 구현되지 않았습니다 . 그것은 ash1989 년부터 원래 구현되지 않았으며 POSIX sh리디렉션 연산자 (POSIX shksh88항상 가지고 있었던 90 년대 초 이후 ) 였지만 sh2000 년까지 FreeBSD 에 추가되지 않았기 때문에 15 년이되었습니다. 오래된 것이 더 정확할 것입니다. 또한 지정되지 않은 경우 기본 파일 디스크립터 는 2010 년 ksh93t +에서 0에서 1로 변경 <>되었다는 점을 제외하고는 모든 쉘에 ksh93있습니다 (이전 호환성 및 POSIX 준수 중단).


답변

이것은 오래된 질문이지만, 그것은 영원한 질문이며, 지금까지 제안 된 것보다 더 일반적이고 명확한 해결책을 사용할 수 있습니다. 신용이 필요한 신용 : Stéphane Chazelas의 <>업데이트 연산자에 대한 언급을 고려하지 않고 신용 카드를 사용했을 것으로 확신하지 않습니다 .

Bourne 쉘에서 업데이트 할 파일 여는 것은 유틸리티가 제한적입니다. 쉘은 파일을 탐색 할 수있는 방법과 새로운 길이를 설정하는 방법을 제공하지 않습니다 (이전 길이보다 짧은 경우). 그러나 그것은 쉽게 해결되었으므로의 표준 유틸리티가 아니라는 것에 놀랐습니다 /usr/bin.

이것은 작동합니다 :

$ grep -n foo T
8:foo
$ (exec 4<>T; grep foo T >&4 && ftruncate 4) && nl T;
     1  foo

이와 마찬가지로 (Stéphane에게 팁) :

$ { grep foo T && ftruncate; } 1<>T  && nl T;
     1  foo

(GNU grep을 사용하고 있습니다. 그가 답변을 작성한 이후에 변경된 것이있을 수 있습니다.)

단, / usr / bin / ftruncate 가 없습니다 . 수십 줄의 C에 대해서는 아래를 참조하십시오. 이 ftruncate 유틸리티는 임의의 파일 설명자를 임의의 길이로 자르며 기본값은 표준 출력과 현재 위치입니다.

위의 명령 (제 1 예)

  • T업데이트 를 위해 파일 설명자 4를 엽니 다 . open (2)과 마찬가지로이 방법으로 파일을 열면 현재 오프셋이 0에 배치됩니다.
  • 그런 다음 grep이T 정상적으로 처리 되고 셸은 출력을 T설명자 4 를 통해 리디렉션합니다 .
  • ftruncate 는 설명자 4에서 ftruncate (2)를 호출하여 길이를 현재 오프셋의 값 (정확히 grep이 남긴 위치)으로 설정합니다.

서브 쉘이 종료되고 설명자 4가 닫힙니다 . 다음은 ftruncate입니다 .

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main( int argc, char *argv[] ) {
  off_t i, fd=1, len=0;
  off_t *addrs[2] = { &fd, &len };

  for( i=0; i < argc-1; i++ ) {
    if( sscanf(argv[i+1], "%lu", addrs[i]) < 1 ) {
      err(EXIT_FAILURE, "could not parse %s as number", argv[i+1]);
    }
  }

  if( argc < 3 && (len = lseek(fd, 0, SEEK_CUR)) == -1 ) {
    err(EXIT_FAILURE, "could not ftell fd %d as number", (int)fd);
  }


  if( 0 != ftruncate((int)fd, len) ) {
    err(EXIT_FAILURE, argc > 1? argv[1] : "stdout");
  }

  return EXIT_SUCCESS;
}

NB, ftruncate (2)는 이런 식으로 사용하면 이식 할 수 없습니다. 절대적인 일반성을 위해 마지막으로 쓴 바이트를 읽고 O_WRONLY 파일을 다시 열고 찾은 다음 바이트를 쓰고 닫습니다.

질문이 5 살이라는 점을 감안할 때이 솔루션은 명백하지 않습니다. 이는 활용 간부 새로운 디스크립터 및 개방하는 <>비전있는 둘 연산자. 파일 디스크립터로 inode를 조작하는 표준 유틸리티를 생각할 수 없습니다. (구문은 일 수 ftruncate >&4있지만 개선이 확실하지 않습니다.) 그것은 camh의 유능하고 탐구적인 답변보다 상당히 짧습니다. 내가 Perl을 더 좋아하지 않는다면 Stéphane ‘s, IMO보다 조금 더 명확합니다. 누군가가 유용하다고 생각합니다.

동일한 작업을 수행하는 다른 방법은 현재 오프셋을보고하는 lseek (2)의 실행 버전입니다. 출력은 일부 Linuxi가 제공하는 / usr / bin / truncate에 사용될 수 있습니다 .


답변

ed 적절한 위치에서 파일을 편집 할 수있는 올바른 선택 일 수 있습니다.

ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q
END_OF_ED_COMMANDS


답변

당신은 (에 현장을 덮어 쓰기합니다), 다음 파일을 엽니 다 읽기 / 쓰기 파일 설명 떠들썩한 파티를 사용 sed하고 truncate…하지만 물론, 지금까지 데이터가 지금까지 읽을의 변경 사항이 금액보다 큰 것을 허용하지 않습니다 .

다음은 스크립트입니다 (bash 변수 $ BASHPID 사용).

# Create a test file
  echo "going abc"  >junk
  echo "going def" >>junk
  echo "# ORIGINAL file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )
#
# Assign file to fd 3, and open it r/w
  exec 3<> junk
#
# Choose a unique filename to hold the new file size  and the pid
# of the semi-asynchrounous process to which 'tee' streams the new file..
  [[ ! -d "/tmp/$USER" ]] && mkdir "/tmp/$USER"
  f_pid_size="/tmp/$USER/pid_size.$(date '+%N')" # %N is a GNU extension: nanoseconds
  [[ -f "$f_pid_size" ]] && { echo "ERROR: Work file already exists: '$f_pid_size'" ;exit 1 ; }
#
# run 'sed' output to 'tee' ...
#  to modify the file in-situ, and to count the bytes
  <junk sed -e "s/going //" |tee >(echo -n "$BASHPID " >"$f_pid_size" ;wc -c >>"$f_pid_size") >&3
#
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# The byte-counting process is not a child-process,
# so 'wait' doesn't work... but wait we must...
  pid_size=($(cat "$f_pid_size")) ;pid=${pid_size[0]}
  # $f_pid_size may initially contain only the pid...
  # get the size when pid termination is assured
  while [[ "$pid" != "" ]] ; do
    if ! kill -0 "$pid" 2>/dev/null; then
       pid=""  # pid has terminated. get the byte count
       pid_size=($(cat "$f_pid_size")) ;size=${pid_size[1]}
    fi
  done
  rm "$f_pid_size"
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#
  exec 3>&- # close fd 3.
  newsize=$(cat newsize)
  echo "# MODIFIED file (before truncating)";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )  cat junk
#
 truncate -s $newsize junk
 echo "# NEW (truncated) file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )  cat junk
#
exit

테스트 출력은 다음과 같습니다

# ORIGINAL file
going abc
going def
# 2 lines, 20 bytes

# MODIFIED file (before truncating)
abc
def
c
going def
# 4 lines, 20 bytes

# NEW (truncated) file
abc
def
# 2 lines, 8 bytes