Làm cách nào để tôi có thể sử dụng một tệp trong một lệnh và chuyển hướng đầu ra đến cùng một tệp mà không phải cắt ngắn nó?


98

Về cơ bản, tôi muốn lấy văn bản đầu vào từ một tệp, xóa một dòng khỏi tệp đó và gửi đầu ra trở lại cùng một tệp. Một cái gì đó dọc theo những dòng này nếu điều đó làm cho nó rõ ràng hơn.

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > file_name

tuy nhiên, khi tôi làm điều này, tôi kết thúc với một tệp trống. Có suy nghĩ gì không?


Câu trả lời:


84

Bạn không thể làm điều đó bởi vì bash xử lý các chuyển hướng trước, sau đó thực hiện lệnh. Vì vậy, vào thời điểm grep nhìn vào file_name, nó đã trống. Bạn có thể sử dụng một tệp tạm thời.

#!/bin/sh
tmpfile=$(mktemp)
grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > ${tmpfile}
cat ${tmpfile} > file_name
rm -f ${tmpfile}

như vậy, hãy xem xét sử dụng mktempđể tạo tmpfile nhưng lưu ý rằng nó không phải là POSIX.


47
Lý do tại sao bạn không thể làm điều đó: bash xử lý các chuyển hướng trước, sau đó thực thi lệnh. Vì vậy, vào thời điểm grep nhìn vào file_name, nó đã trống.
glenn Jackman

1
@glennjackman: bởi "chuyển hướng quy trình, bạn có nghĩa là trong trường hợp> nó mở tệp và xóa nó và trong trường hợp >> nó chỉ mở nó"?
Razvan

2
có, nhưng lưu ý trong trường hợp này, việc >chuyển hướng sẽ mở tệp và cắt ngắn nó trước khi trình bao khởi chạy grep.
glenn Jackman

1
Xem câu trả lời của tôi nếu bạn không muốn sử dụng tệp tạm thời, nhưng vui lòng không tán thành nhận xét này.
Zack Morris

Thay vì điều này, câu trả lời sử dụng spongelệnh sẽ được chấp nhận.
vlz

96

Sử dụng miếng bọt biển cho loại nhiệm vụ này. Một phần của moreutils của nó.

Hãy thử lệnh này:

 grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | sponge file_name

4
Cảm ơn vì câu trả lời. Là một bổ sung có thể hữu ích, nếu bạn đang sử dụng homebrew trên Mac, có thể sử dụng brew install moreutils.
Anthony Panozzo

2
Hoặc sudo apt-get install moreutilstrên các hệ thống dựa trên Debian.
Jonah

3
Chỉ trích! Cảm ơn vì đã giới thiệu cho tôi moreutils =) một số chương trình hay ở đó!
netigger

cảm ơn bạn rất nhiều, moreutils vì đã giải cứu! bọt biển như một ông chủ!
aqquadro

3
Lưu ý, "bọt biển" có tính phá hoại, vì vậy nếu bạn gặp lỗi trong lệnh của mình, bạn có thể xóa tệp đầu vào của mình (như tôi đã làm lần đầu tiên dùng miếng bọt biển). Đảm bảo rằng lệnh của bạn hoạt động và / hoặc tệp đầu vào được kiểm soát phiên bản nếu bạn đang cố gắng lặp lại để lệnh hoạt động.
user107172 27/12/16

18

Sử dụng sed thay thế:

sed -i '/seg[0-9]\{1,\}\.[0-9]\{1\}/d' file_name

1
iirc -ilà phần mở rộng chỉ GNU, chỉ cần lưu ý.
c00kiemon5ter

3
Trên * BSD (và do đó cũng có thể là OSX), bạn có thể nói -i ''vì vậy phần mở rộng không phải là bắt buộc hoàn toàn, nhưng -itùy chọn này yêu cầu một số đối số.
tripleee

14

hãy thử cái đơn giản này

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

Tệp của bạn sẽ không bị trống lần này :) và đầu ra của bạn cũng được in ra thiết bị đầu cuối của bạn.


1
Tôi thích giải pháp này! Và nếu bạn không muốn nó được in trong thiết bị đầu cuối, bạn vẫn có thể chuyển hướng đầu ra đến /dev/nullhoặc những nơi tương tự.
Frozn

4
Thao tác này cũng xóa nội dung tệp ở đây. Đó có phải là do sự khác biệt GNU / BSD không? Tôi đang sử dụng macOS ...
ssc

7

Bạn không thể sử dụng toán tử chuyển hướng ( >hoặc >>) cho cùng một tệp, vì nó có mức độ ưu tiên cao hơn và nó sẽ tạo / cắt ngắn tệp trước khi lệnh được gọi. Để tránh điều đó, bạn nên sử dụng các công cụ thích hợp như tee, sponge, sed -ihoặc bất kỳ công cụ khác có thể viết kết quả vào file (ví dụ sort file -o file).

Về cơ bản chuyển hướng đầu vào đến cùng một tệp gốc không có ý nghĩa và bạn nên sử dụng các trình chỉnh sửa tại chỗ thích hợp cho điều đó, ví dụ: trình chỉnh sửa Ex (một phần của Vim):

ex '+g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' -scwq file_name

Ở đâu:

  • '+cmd'/ -c- chạy bất kỳ lệnh Ex / Vim nào
  • g/pattern/d- loại bỏ các dòng khớp với một mẫu bằng global ( help :g)
  • -s- chế độ im lặng ( man ex)
  • -c wq- thực thi :write:quitlệnh

Bạn có thể sử dụng sedđể đạt được như nhau (như đã trình bày trong câu trả lời khác), tuy nhiên tại chỗ ( -i) là phần mở rộng FreeBSD phi tiêu chuẩn (có thể làm việc khác nhau giữa Unix / Linux) và về cơ bản nó là một s tream ed itor, không một trình soạn thảo tập tin . Xem: Chế độ Ex có bất kỳ công dụng thực tế nào không?


6

Thay thế một lớp lót - đặt nội dung của tệp dưới dạng biến:

VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' > file_name

4

Vì câu hỏi này là kết quả hàng đầu trong các công cụ tìm kiếm, đây là một lớp lót dựa trên https://serverfault.com/a/547331 sử dụng vỏ con thay vì sponge(thường không phải là một phần của cài đặt vani như OS X) :

echo "$(grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name)" > file_name

Trường hợp chung là:

echo "$(cat file_name)" > file_name

Chỉnh sửa, giải pháp trên có một số lưu ý:

  • printf '%s' <string>nên được sử dụng thay vì echo <string>để các tệp chứa -nkhông gây ra hành vi không mong muốn.
  • Lệnh thay thế dải các dòng mới theo sau ( đây là một lỗi / tính năng của shell như bash ), vì vậy chúng ta nên thêm một ký tự postfix xvào đầu ra và xóa nó ra bên ngoài thông qua mở rộng tham số của một biến tạm thời như ${v%x}.
  • Việc sử dụng một biến tạm thời sẽ làm $vgiảm giá trị của bất kỳ biến hiện có nào $vtrong môi trường shell hiện tại, vì vậy chúng ta nên lồng toàn bộ biểu thức trong dấu ngoặc đơn để bảo toàn giá trị trước đó.
  • Một lỗi / tính năng khác của shell như bash là thay thế lệnh sẽ loại bỏ các ký tự không thể in được như nulltừ đầu ra. Tôi đã xác minh điều này bằng cách gọi dd if=/dev/zero bs=1 count=1 >> file_namevà xem nó trong hex với cat file_name | xxd -p. Nhưngecho $(cat file_name) | xxd -p bị tước. Vì vậy, câu trả lời này không nên được sử dụng trên các tệp nhị phân hoặc bất kỳ thứ gì sử dụng các ký tự không in được, như Lynch đã chỉ ra .

Giải pháp chung (bạch tạng chậm hơn một chút, tốn nhiều bộ nhớ hơn và vẫn loại bỏ các ký tự không in được) là:

(v=$(cat file_name; printf x); printf '%s' ${v%x} > file_name)

Kiểm tra từ https://askubuntu.com/a/752451 :

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do (v=$(cat file_uniquely_named.txt; printf x); printf '%s' ${v%x} > file_uniquely_named.txt); done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Nên in:

hello
world

Trong khi gọi cat file_uniquely_named.txt > file_uniquely_named.txt trong shell hiện tại:

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do cat file_uniquely_named.txt > file_uniquely_named.txt; done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

In một chuỗi trống.

Tôi chưa thử nghiệm điều này trên các tệp lớn (có thể trên 2 hoặc 4 GB).

Tôi đã mượn câu trả lời này từ Hart Simhakos .


2
Tất nhiên nó sẽ không hoạt động với tệp lớn. Đây không thể là một giải pháp tốt hoặc hoạt động mọi lúc. Điều đang xảy ra là bash thực hiện lệnh đầu tiên và sau đó tải stdout của catvà đặt nó làm đối số đầu tiên echo. Tất nhiên các biến không in được sẽ không xuất ra đúng cách và làm hỏng dữ liệu. Đừng cố chuyển hướng một tệp trở lại chính nó, nó không thể tốt được.
Lynch

1

Ngoài ra còn có ed(thay thế cho sed -i):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' H 'g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' wq |  ed -s file_name

1

Bạn có thể làm điều đó bằng cách sử dụng quá trình thay thế .

Đó là một chút hack mặc dù bash mở tất cả các đường ống một cách không đồng bộ và chúng tôi phải giải quyết vấn đề đó bằng cách sử dụng sleepYMMV.

Trong ví dụ của bạn:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) tạo một tệp tạm thời nhận đầu ra từ grep
  • sleep 1 độ trễ trong một giây để cho thời gian phân tích cú pháp tệp đầu vào của grep
  • cuối cùng cat > file_nameviết đầu ra

1

Bạn có thể sử dụng slurp với POSIX Awk:

!/seg[0-9]\{1,\}\.[0-9]\{1\}/ {
  q = q ? q RS $0 : $0
}
END {
  print q > ARGV[1]
}

Thí dụ


1
Có lẽ nên chỉ ra rằng "slurp" có nghĩa là "đọc toàn bộ tệp vào bộ nhớ". Nếu bạn có một tệp đầu vào lớn, có thể bạn muốn tránh điều đó.
tripleee

1

Điều này rất có thể xảy ra, bạn chỉ cần đảm bảo rằng vào thời điểm bạn ghi đầu ra, bạn đang ghi nó vào một tệp khác. Điều này có thể được thực hiện bằng cách xóa tệp sau khi mở bộ mô tả tệp cho nó, nhưng trước khi ghi vào nó:

exec 3<file ; rm file; COMMAND <&3 >file ;  exec 3>&-

Hoặc từng dòng, để hiểu rõ hơn:

exec 3<file       # open a file descriptor reading 'file'
rm file           # remove file (but fd3 will still point to the removed file)
COMMAND <&3 >file # run command, with the removed file as input
exec 3>&-         # close the file descriptor

Đó vẫn là một điều rủi ro để làm, bởi vì nếu COMMAND không chạy đúng cách, bạn sẽ mất nội dung tệp. Điều đó có thể được giảm thiểu bằng cách khôi phục tệp nếu COMMAND trả về mã thoát khác 0:

exec 3<file ; rm file; COMMAND <&3 >file || cat <&3 >file ; exec 3>&-

Chúng tôi cũng có thể xác định một hàm shell để giúp sử dụng dễ dàng hơn:

# Usage: replace FILE COMMAND
replace() { exec 3<$1 ; rm $1; ${@:2} <&3 >$1 || cat <&3 >$1 ; exec 3>&- }

Thí dụ :

$ echo aaa > test
$ replace test tr a b
$ cat test
bbb

Ngoài ra, lưu ý rằng điều này sẽ giữ một bản sao đầy đủ của tệp gốc (cho đến khi bộ mô tả tệp thứ ba bị đóng). Nếu bạn đang sử dụng Linux và tệp bạn đang xử lý quá lớn để có thể vừa hai lần trên đĩa, bạn có thể kiểm tra tập lệnh này sẽ chuyển tệp đến từng khối lệnh được chỉ định trong khi hủy phân bổ tệp đã được xử lý các khối. Như mọi khi, hãy đọc các cảnh báo trong trang sử dụng.


0

Thử cái này

echo -e "AAA\nBBB\nCCC" > testfile

cat testfile
AAA
BBB
CCC

echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC

Một lời giải thích ngắn hoặc thậm chí các bình luận có thể hữu ích.
Giàu

tôi nghĩ rằng, nó hoạt động vì chuỗi ngoại suy thực hiện trước khi hành chuyển hướng, nhưng tôi không biết chính xác
Виктор Пупкин

0

Những điều sau sẽ thực hiện được điều tương tự mà spongekhông yêu cầu moreutils:

    shuf --output=file --random-source=/dev/zero 

Phần --random-source=/dev/zerothủ thuậtshuf thực hiện công việc của nó mà không thực hiện bất kỳ xáo trộn nào, vì vậy nó sẽ đệm đầu vào của bạn mà không làm thay đổi nó.

Tuy nhiên, đúng là sử dụng tệp tạm thời là tốt nhất, vì lý do hiệu suất. Vì vậy, đây là một chức năng mà tôi đã viết sẽ làm điều đó cho bạn một cách tổng quát:

# Pipes a file into a command, and pipes the output of that command
# back into the same file, ensuring that the file is not truncated.
# Parameters:
#    $1: the file.
#    $2: the command. (With $3... being its arguments.)
# See https://stackoverflow.com/a/55655338/773113

function siphon
{
    local tmp=$(mktemp)
    local file="$1"
    shift
    $* < "$file" > "$tmp"
    mv "$tmp" "$file"
}

-2

Tôi thường sử dụng chương trình phát bóng để làm điều này:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

Nó tự tạo và loại bỏ một tệp tạm thời.


Xin lỗi, teekhông đảm bảo hoạt động. Xem askubuntu.com/a/752451/335781 .
studgeek
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.