ฉันจะตรวจสอบว่าไฟล์สามารถสร้างหรือตัดทอน / เขียนทับในทุบตีได้อย่างไร?


15

ผู้ใช้เรียกสคริปต์ของฉันกับเส้นทางของไฟล์ที่จะได้รับการอย่างใดอย่างหนึ่งจะสร้างหรือเขียนทับในบางจุดในสคริปต์เหมือนหรือfoo.sh file.txtfoo.sh dir/file.txt

พฤติกรรมการสร้างหรือเขียนทับเป็นเหมือนข้อกำหนดสำหรับการวางไฟล์ทางด้านขวาของ>โอเปอเรเตอร์เอาท์พุทหรือส่งต่อเป็นอาร์กิวเมนต์ไปที่tee(อันที่จริงการส่งเป็นอาร์กิวเมนต์เพื่อteeเป็นสิ่งที่ฉันกำลังทำอยู่ )

ก่อนที่ฉันจะเข้าไปในความกล้าของสคริปต์ฉันต้องการตรวจสอบอย่างสมเหตุสมผลว่าไฟล์สามารถสร้าง / เขียนทับได้ แต่ไม่ได้สร้างจริง การตรวจสอบนี้ไม่จำเป็นต้องสมบูรณ์แบบและใช่ฉันรู้ว่าสถานการณ์สามารถเปลี่ยนแปลงได้ระหว่างการตรวจสอบและจุดที่ไฟล์ถูกเขียนจริง - แต่ที่นี่ฉันโอเคด้วยวิธีแก้ไขปัญหาความพยายามที่ดีที่สุดเพื่อให้ฉันสามารถประกันตัวได้เร็ว ในกรณีที่เส้นทางของไฟล์ไม่ถูกต้อง

ตัวอย่างของสาเหตุที่ไฟล์ไม่สามารถสร้างได้:

  • ไฟล์มีองค์ประกอบไดเรกทอรีเช่นdir/file.txtแต่dirไม่มีไดเรกทอรี
  • ผู้ใช้ไม่มีสิทธิ์ในการเขียนในไดเรกทอรีที่ระบุ (หรือ CWD หากไม่มีการระบุไดเรกทอรี

ใช่ฉันรู้ว่าการตรวจสอบการอนุญาต "ล่วงหน้า" ไม่ใช่The UNIX Way ™แต่ฉันควรลองใช้งานและขอการให้อภัยในภายหลัง ในสคริปต์เฉพาะของฉันสิ่งนี้นำไปสู่ประสบการณ์การใช้งานที่ไม่ดีและฉันไม่สามารถเปลี่ยนองค์ประกอบที่รับผิดชอบได้


อาร์กิวเมนต์จะยึดตามไดเรกทอรีปัจจุบันเสมอหรือผู้ใช้สามารถระบุเส้นทางแบบเต็มได้หรือไม่
jesse_b

@Jesse_b - /foo/bar/file.txtฉันคิดว่าผู้ใช้สามารถระบุเส้นทางที่แน่นอนเช่น โดยทั่วไปฉันผ่านเส้นทางไปteeเช่นtee $OUT_FILEที่OUT_FILEจะถูกส่งผ่านในบรรทัดคำสั่ง ที่ควร "เพียงแค่ทำงาน" กับทั้งเส้นทางแน่นอนและญาติใช่มั้ย
BeeOnRope

@BeeOnRope คุณไม่ต้องการtee -- "$OUT_FILE"อย่างน้อย ถ้าไฟล์มีอยู่แล้วหรือมีอยู่ แต่ไม่ใช่ไฟล์ปกติ (ไดเรกทอรี, symlink, Fifo)
Stéphane Chazelas

@ StéphaneChazelas - ฉันกำลังใช้tee "${OUT_FILE}.tmp"อยู่ หากไฟล์มีอยู่แล้วให้teeเขียนทับซึ่งเป็นลักษณะการทำงานที่ต้องการในกรณีนี้ ถ้าเป็นไดเรกทอรีteeจะล้มเหลว (ฉันคิดว่า) symlink ฉันไม่แน่ใจ 100%?
BeeOnRope

คำตอบ:


11

การทดสอบที่ชัดเจนจะเป็น:

if touch /path/to/file; then
    : it can be created
fi

แต่จริงๆแล้วมันจะสร้างไฟล์ถ้ามันยังไม่มีอยู่ เราสามารถทำความสะอาดตัวเองหลังจาก:

if touch /path/to/file; then
    rm /path/to/file
fi

แต่สิ่งนี้จะลบไฟล์ที่มีอยู่แล้วซึ่งคุณอาจไม่ต้องการ

อย่างไรก็ตามเรามีวิธีแก้ไข:

if mkdir /path/to/file; then
    rmdir /path/to/file
fi

คุณไม่สามารถมีไดเรกทอรีที่มีชื่อเดียวกันกับวัตถุอื่นในไดเรกทอรีนั้น ฉันไม่สามารถนึกถึงสถานการณ์ที่คุณจะสามารถสร้างไดเรกทอรีได้ แต่ไม่สามารถสร้างไฟล์ได้ หลังจากการทดสอบนี้สคริปต์ของคุณจะมีอิสระในการสร้างแบบดั้งเดิม/path/to/fileและทำสิ่งที่มันพอใจ


2
ที่จัดการกับปัญหามากมายอย่างเรียบร้อย! ความคิดเห็นเกี่ยวกับรหัสอธิบายและการแก้ปัญหาที่ผิดปกติอาจเกินความยาวของคำตอบของคุณ :-)
jpaugh

ฉันยังได้ใช้if mkdir /var/run/lockdirเป็นการทดสอบการล็อค นี่คืออะตอมในขณะที่if ! [[ -f /var/run/lockfile ]]; then touch /var/run/lockfileมีเวลาเล็กน้อยในระหว่างการทดสอบและtouchตัวอย่างของสคริปต์ที่เป็นปัญหาอื่นสามารถเริ่มต้นได้
DopeGhoti

8

จากสิ่งที่ฉันรวบรวมคุณต้องการตรวจสอบว่าเมื่อใช้

tee -- "$OUT_FILE"

(โปรดทราบ--หรือไม่สามารถใช้งานได้กับชื่อไฟล์ที่ขึ้นต้นด้วย -) teeจะเปิดไฟล์เพื่อเขียนได้สำเร็จ

นั่นคือ:

  • ความยาวของพา ธ ไฟล์ไม่เกินขีด จำกัด PATH_MAX
  • ไฟล์มีอยู่ (หลังจากการแก้ปัญหา symlink) และไม่ใช่ไดเรกทอรีประเภทและคุณได้รับอนุญาตให้เขียน
  • หากไฟล์ไม่มีอยู่ dirname ของไฟล์จะมีอยู่ (หลังจาก symlink resolution) เป็นไดเร็กทอรีและคุณมีสิทธิ์ในการเขียนและค้นหาไฟล์และความยาวชื่อไฟล์ไม่เกินขีด จำกัด NAME_MAX ของระบบไฟล์ที่มีไดเร็กทอรีอยู่
  • หรือไฟล์นั้นเป็น symlink ที่ชี้ไปยังไฟล์ที่ไม่มีอยู่และไม่ใช่ symlink loop แต่ตรงตามเกณฑ์ด้านบน

เราจะเพิกเฉยต่อระบบไฟล์ในขณะนี้เช่น vfat, ntfs หรือ hfsplus ที่มีข้อ จำกัด ว่าชื่อไฟล์ค่าไบต์อาจมีโควต้าดิสก์ จำกัด กระบวนการ จำกัด selinux, apparmor หรือกลไกความปลอดภัยอื่น ๆ ในทางระบบแฟ้มเต็มไม่มีเหลือ inode ไฟล์ที่ไม่สามารถเปิดได้ด้วยเหตุผลอย่างใดอย่างหนึ่งไฟล์ที่ปฏิบัติการได้แมปในพื้นที่ที่อยู่กระบวนการบางอย่างซึ่งทั้งหมดนี้อาจมีผลต่อความสามารถในการเปิดหรือสร้างไฟล์

ด้วยzsh:

zmodload zsh/system
tee_would_likely_succeed() {
  local file=$1 ERRNO=0 LC_ALL=C
  if [ -d "$file" ]; then
    return 1 # directory
  elif [ -w "$file" ]; then
    return 0 # writable non-directory
  elif [ -e "$file" ]; then
    return 1 # exists, non-writable
  elif [ "$errnos[ERRNO]" != ENOENT ]; then
    return 1 # only ENOENT error can be recovered
  else
    local dir=$file:P:h base=$file:t
    [ -d "$dir" ] &&    # directory
      [ -w "$dir" ] &&  # writable
      [ -x "$dir" ] &&  # and searchable
      (($#base <= $(getconf -- NAME_MAX "$dir")))
    return
  fi
}

ในbashหรือเปลือกคล้ายบอร์นใด ๆ เพียงแค่แทนที่

zmodload zsh/system
tee_would_likely_succeed() {
  <zsh-code>
}

ด้วย:

tee_would_likely_succeed() {
  zsh -s -- "$@" << 'EOF'
  zmodload zsh/system
  <zsh-code>
EOF
}

zshคุณสมบัติ -specific ที่นี่$ERRNO(ซึ่งหมายความว่ารหัสข้อผิดพลาดของระบบโทรที่ผ่านมา) และ$errnos[]อาเรย์ที่จะแปลให้สอดคล้องชื่อแมโคร C มาตรฐาน และ$var:h(จาก csh) และ$var:P(ต้องการ zsh 5.3 ขึ้นไป)

bash ยังไม่มีคุณสมบัติที่เทียบเท่า

$file:hสามารถถูกแทนที่ด้วยdir=$(dirname -- "$file"; echo .); dir=${dir%??}หรือ GNU :dirnameIFS= read -rd '' dir < <(dirname -z -- "$file")

สำหรับ$errnos[ERRNO] == ENOENTวิธีการอาจใช้ls -Ldกับไฟล์และตรวจสอบว่าข้อความแสดงข้อผิดพลาดสอดคล้องกับข้อผิดพลาด ENOENT หรือไม่ การทำสิ่งนั้นอย่างน่าเชื่อถือและพกพานั้นเป็นเรื่องยุ่งยาก

วิธีการหนึ่งอาจเป็น:

msg_for_ENOENT=$(LC_ALL=C ls -d -- '/no such file' 2>&1)
msg_for_ENOENT=${msg_for_ENOENT##*:}

(สมมติว่าข้อความแสดงข้อผิดพลาดลงท้ายด้วยการsyserror()แปลENOENTและการแปลนั้นไม่รวม:) และจากนั้นแทนที่จะ[ -e "$file" ]ทำ:

err=$(ls -Ld -- "$file" 2>&1)

และตรวจสอบข้อผิดพลาด ENOENT ด้วย

case $err in
  (*:"$msg_for_ENOENT") ...
esac

$file:Pส่วนหนึ่งเป็นยากที่จะประสบความสำเร็จในbashโดยเฉพาะอย่างยิ่งใน FreeBSD

FreeBSD มีrealpathคำสั่งและreadlinkคำสั่งที่ยอมรับ-fตัวเลือก แต่ไม่สามารถใช้ในกรณีที่ไฟล์นั้นเป็น symlink ที่ไม่สามารถแก้ไขได้ นั่นเป็นเช่นเดียวกันกับ'sperlCwd::realpath()

python's os.path.realpath()ไม่ปรากฏในการทำงานคล้าย ๆ กับzsh $file:Pดังนั้นสมมติว่าอย่างน้อยหนึ่งรุ่นของpythonการติดตั้งและว่ามีpythonคำสั่งที่หมายถึงหนึ่งของพวกเขา (ซึ่งไม่ได้เป็นได้รับใน FreeBSD) คุณสามารถทำ:

dir=$(python -c '
import os, sys
print(os.path.realpath(sys.argv[1]) + ".")' "$dir") || return
dir=${dir%.}

แต่แล้วคุณก็อาจทำทุกสิ่งpythonได้เช่นกัน

หรือคุณสามารถตัดสินใจที่จะไม่จัดการคดีมุมทั้งหมด


ใช่คุณเข้าใจเจตนาของฉันถูกต้อง ในความเป็นจริงคำถามเดิมไม่ชัดเจน: ตอนแรกฉันพูดเกี่ยวกับการสร้างไฟล์ แต่จริงๆแล้วฉันใช้มันด้วยteeดังนั้นความต้องการจึงเป็นจริงที่ไฟล์นั้นสามารถสร้างได้หากไม่มีอยู่หรือถูกตัดให้เหลือศูนย์และเขียนทับ ถ้ามัน (หรืออื่น ๆteeจัดการที่)
BeeOnRope

ดูเหมือนว่าการแก้ไขครั้งล่าสุดของคุณจะลบการจัดรูปแบบ
D. Ben Knoble

ขอบคุณ @ bishop, @ D.BenKnoble ดูการแก้ไข
Stéphane Chazelas

1
@DennisWilliamson ใช่แล้วเชลล์เป็นเครื่องมือในการเรียกใช้โปรแกรมและทุกโปรแกรมที่คุณเรียกใช้ในสคริปต์จะต้องติดตั้งเพื่อให้สคริปต์ทำงานได้โดยไม่ต้องพูดอะไร macOS มาพร้อมกับ bash และ zsh ติดตั้งโดยค่าเริ่มต้น FreeBSD มาพร้อม ๆ OP อาจมีเหตุผลที่ดีที่ต้องการทำเช่นนั้นในทุบตีอาจเป็นสิ่งที่พวกเขาต้องการเพิ่มในสคริปต์ทุบตีที่เขียนไว้แล้ว
Stéphane Chazelas

1
@pipe อีกครั้งเชลล์คือตัวแปลคำสั่ง คุณจะเห็นว่าหลายข้อความที่ตัดตอนมารหัสเปลือกเขียนนี่วิงวอนขอล่ามภาษาอื่น ๆ เช่นperl, awk, sedหรือpython(หรือแม้กระทั่งsh, bash... ) การเขียนสคริปต์เชลล์เกี่ยวกับการใช้คำสั่งที่ดีที่สุดสำหรับงาน ที่นี่แม้perlจะไม่เทียบเท่ากับzsh's $var:P(มันCwd::realpathทำงานเหมือน BSD realpathหรือreadlink -fในขณะที่คุณต้องการให้มันทำตัวเหมือน GNU readlink -fหรือpythonของos.path.realpath())
Stéphane Chazelas

6

ทางเลือกหนึ่งที่คุณอาจต้องการพิจารณาคือการสร้างไฟล์ แต่เนิ่น ๆ แต่จะเติมในภายหลังในสคริปต์ของคุณ คุณสามารถใช้execคำสั่งเพื่อเปิดไฟล์ใน file descriptor (เช่น 3, 4, ฯลฯ ) จากนั้นใช้การเปลี่ยนเส้นทางไปยัง file descriptor ( >&3และอื่น ๆ ) เพื่อเขียนเนื้อหาไปยังไฟล์นั้นในภายหลัง

สิ่งที่ต้องการ:

#!/bin/bash

# Open the file for read/write, so it doesn't get
# truncated just yet (to preserve the contents in
# case the initial checks fail.)
exec 3<>dir/file.txt || {
    echo "Error creating dir/file.txt" >&2
    exit 1
}

# Long checks here...
check_ok || {
    echo "Failed checks" >&2
    # cleanup file before bailing out
    rm -f dir/file.txt
    exit 1
}

# We're ready to write, first truncate the file.
# Use "truncate(1)" from coreutils, and pass it
# /dev/fd/3 so the file doesn't need to be reopened.
truncate -s 0 /dev/fd/3

# Now populate the file, use a redirection to write
# to the previously opened file descriptor.
populate_contents >&3

นอกจากนี้คุณยังสามารถใช้ a trapเพื่อล้างไฟล์เมื่อเกิดข้อผิดพลาดซึ่งเป็นวิธีปฏิบัติทั่วไป

วิธีนี้คุณจะได้รับจริงการตรวจสอบสิทธิ์ที่คุณจะสามารถสร้างไฟล์ในขณะที่ในเวลาเดียวกันความสามารถในการดำเนินการได้เร็วพอว่าถ้าล้มเหลวคุณยังไม่ได้ใช้เวลารอเวลาสำหรับการตรวจสอบนาน


อัปเดต:เพื่อหลีกเลี่ยงการอุดตันไฟล์ในกรณีที่การตรวจสอบล้มเหลวให้ใช้การfd<>fileเปลี่ยนเส้นทางของ bash ซึ่งไม่ตัดทอนไฟล์ทันที (เราไม่สนใจเกี่ยวกับการอ่านจากไฟล์นี่เป็นเพียงวิธีการแก้ปัญหาดังนั้นเราจึงไม่ตัดทอนการต่อท้าย>>อาจจะใช้ได้เช่นกัน แต่ฉันมักจะพบว่าไฟล์นี้มีความสง่างามกว่าเล็กน้อย ของภาพ)

เมื่อเราพร้อมที่จะแทนที่เนื้อหาเราจำเป็นต้องตัดทอนไฟล์ก่อน (มิฉะนั้นถ้าเราเขียนไบต์น้อยกว่าที่เคยมีในไฟล์ก่อนหน้านี้ไบต์ต่อท้ายจะอยู่ที่นั่น) เราสามารถใช้การตัดทอน (1)คำสั่งจาก coreutils เพื่อจุดประสงค์นั้นและเราสามารถส่งผ่านตัวอธิบายไฟล์แบบเปิดที่เรามี (ใช้/dev/fd/3ไฟล์หลอก) ดังนั้นจึงไม่จำเป็นต้องเปิดไฟล์อีกครั้ง (อีกครั้งบางสิ่งบางอย่างในทางเทคนิคที่เรียบง่ายกว่า: >dir/file.txtอาจจะใช้งานได้ แต่ไม่ต้องเปิดไฟล์อีกครั้งเป็นวิธีที่สวยงามยิ่งกว่า)


ฉันได้พิจารณาเรื่องนี้แล้ว แต่ปัญหาคือถ้าเกิดข้อผิดพลาดที่ไร้เดียงสาเกิดขึ้นระหว่างตอนที่ฉันสร้างไฟล์และจุดต่อมาที่ฉันจะเขียนมันในภายหลังผู้ใช้อาจจะอารมณ์เสียเมื่อพบว่าไฟล์ที่ระบุถูกเขียนทับ .
BeeOnRope

1
@BeeOnRope ตัวเลือกที่จะไม่ข่มขี่ไฟล์คือการพยายามที่จะผนวกอีกอะไรกับมัน: หรือecho -n >>file true >> fileแน่นอน ext4 มีไฟล์ต่อท้ายเพียงไฟล์เดียว แต่คุณสามารถใช้งานไฟล์เหล่านั้นในเชิงบวกได้
mosvy

1
@BeeOnRope หากคุณต้องการเก็บรักษา (a) ไฟล์ที่มีอยู่หรือ (b) ไฟล์ใหม่ (สมบูรณ์) นั่นเป็นคำถามที่แตกต่างจากที่คุณถาม คำตอบที่ดีคือการย้ายไฟล์ที่มีอยู่ไปยังชื่อใหม่ (เช่นmy-file.txt.backup) ก่อนที่จะสร้างไฟล์ใหม่ อีกวิธีหนึ่งคือการเขียนไฟล์ใหม่ไปยังไฟล์ชั่วคราวในโฟลเดอร์เดียวกันแล้วคัดลอกไฟล์เก่าหลังจากสคริปต์อื่นที่เหลือประสบความสำเร็จ --- หากการดำเนินการครั้งสุดท้ายล้มเหลวผู้ใช้สามารถแก้ไขปัญหาด้วยตนเองโดยไม่สูญเสีย ความก้าวหน้าของเขาหรือเธอ
jpaugh

1
@BeeOnRope หากคุณไปหาเส้นทาง "atomic replace" (ซึ่งเป็นสิ่งที่ดีมาก!) โดยทั่วไปแล้วคุณจะต้องเขียนไฟล์ชั่วคราวในไดเรกทอรีเดียวกันกับไฟล์สุดท้าย (ต้องอยู่ในระบบไฟล์เดียวกันเพื่อเปลี่ยนชื่อ (2) เพื่อให้สำเร็จ) และใช้ชื่อที่ไม่ซ้ำกัน (ดังนั้นหากมีสองอินสแตนซ์ของสคริปต์ที่ทำงานอยู่พวกเขาจะไม่ปิดบังไฟล์ชั่วคราวของกันและกัน) การเปิดไฟล์ชั่วคราวสำหรับการเขียนในไดเรกทอรีเดียวกันมักจะเป็นตัวบ่งชี้ที่ดี จะสามารถเปลี่ยนชื่อได้ในภายหลัง (ดียกเว้นว่ามีเป้าหมายสุดท้ายอยู่และเป็นไดเรกทอรี) ดังนั้นในระดับหนึ่งก็สามารถตอบคำถามของคุณได้เช่นกัน
filbranden

1
@jpaugh - ฉันค่อนข้างลังเลที่จะทำเช่นนั้น มันจะนำไปสู่คำตอบที่พยายามแก้ปัญหาระดับสูงของฉันอย่างหลีกเลี่ยงไม่ได้ซึ่งอาจยอมรับวิธีแก้ปัญหาที่ไม่มีอะไรเกี่ยวข้องกับคำถาม "ฉันจะตรวจสอบได้อย่างไร ... " ไม่ว่าปัญหาระดับสูงของฉันคืออะไรฉันคิดว่าคำถามนี้ยืนอยู่คนเดียวเป็นสิ่งที่คุณอาจต้องการทำอย่างมีเหตุผล มันจะไม่ยุติธรรมสำหรับผู้ที่ตอบคำถามนั้นหากฉันเปลี่ยนเป็น "แก้ปัญหาเฉพาะที่ฉันมีด้วยสคริปต์ของฉัน" ฉันรวมรายละเอียดเหล่านี้ไว้ที่นี่เพราะถูกถาม แต่ฉันไม่ต้องการเปลี่ยนคำถามหลัก
BeeOnRope

4

ฉันคิดว่าโซลูชันของ DopeGhoti นั้นดีกว่า แต่ก็ควรใช้งานได้:

file=$1
if [[ "${file:0:1}" == '/' ]]; then
    dir=${file%/*}
elif [[ "$file" =~ .*/.* ]]; then
    dir="$(PWD)/${file%/*}"
else
    dir=$(PWD)
fi

if [[ -w "$dir" ]]; then
    echo "writable"
    #do stuff with writable file
else
    echo "not writable"
    #do stuff without writable file
fi

สิ่งแรกถ้าโครงสร้างตรวจสอบว่าอาร์กิวเมนต์เป็นเส้นทางแบบเต็ม (เริ่มต้นด้วย / ) และกำหนดตัวแปรเส้นทางไดเรกทอรีถึงสุดท้ายdir /มิฉะนั้นถ้าอาร์กิวเมนต์ไม่ได้เริ่มต้นด้วย/แต่มี/(ระบุไดเรกทอรีย่อย) มันจะถูกตั้งค่าdirเป็นไดเรกทอรีการทำงานปัจจุบัน + เส้นทางไดเรกทอรีย่อย มิฉะนั้นจะถือว่าไดเรกทอรีทำงานปัจจุบัน จากนั้นตรวจสอบว่าไดเร็กทอรีนั้นเขียนได้หรือไม่


4

สิ่งที่เกี่ยวกับการใช้testคำสั่งปกติเช่นที่ระบุไว้ด้านล่าง?

FILE=$1

DIR=$(dirname $FILE) # $DIR now contains '.' for file names only, 'foo' for 'foo/bar'

if [ -d $DIR ] ; then
  echo "base directory $DIR for file exists"
  if [ -e $FILE ] ; then
    if [ -w $FILE ] ; then
      echo "file exists, is writeable"
    else
      echo "file exists, NOT writeable"
    fi
  elif [ -w $DIR ] ; then
    echo "directory is writeable"
  else
    echo "directory is NOT writeable"
  fi
else
  echo "can NOT create file in non-existent directory $DIR "
fi

ฉันเกือบทำซ้ำคำตอบนี้! แนะนำตัวดี แต่คุณอาจพูดถึงman testและนั่น[ ]เทียบเท่ากับการทดสอบ (ไม่ใช่ความรู้ทั่วไปอีกต่อไป) นี่คือหนึ่งในสายการบินที่ฉันกำลังจะใช้ในคำตอบที่ต่ำกว่าของฉันเพื่อครอบคลุมกรณีที่แน่นอนสำหรับลูกหลาน:if [ -w $1] && [ ! -d $1 ] ; then echo "do stuff"; fi
Iron Gremlin

4

คุณพูดถึงประสบการณ์ของผู้ใช้ที่ผลักดันคำถามของคุณ ฉันจะตอบจากมุม UX เนื่องจากคุณได้รับคำตอบที่ดีทางด้านเทคนิค

แทนที่จะทำการตรวจสอบล่วงหน้าวิธีการเขียนผลลัพธ์ลงในไฟล์ชั่วคราวในตอนท้ายสุดแล้ววางผลลัพธ์ลงในไฟล์ที่ผู้ใช้ต้องการ? ชอบ:

userfile=${1:?Where would you like the file written?}
tmpfile=$(mktemp)

# ... all the complicated stuff, writing into "${tmpfile}"

# fill user's file, keeping existing permissions or creating anew
# while respecting umask
cat "${tmpfile}" > "${userfile}"
if [ 0 -eq $? ]; then
    rm "${tmpfile}"
else
    echo "Couldn't write results into ${userfile}." >&2
    echo "Results available in ${tmpfile}." >&2
    exit 1
fi

ข้อดีด้วยวิธีการนี้: สร้างการดำเนินการที่ต้องการในสถานการณ์เส้นทางความสุขปกติขั้นตอนด้านปัญหาอะตอมมิกทดสอบและกำหนดรักษาสิทธิ์ของไฟล์เป้าหมายในขณะที่สร้างหากจำเป็นและง่ายต่อการใช้งาน

หมายเหตุ: ถ้าเราใช้mvเราจะรักษาสิทธิ์ของไฟล์ชั่วคราว - เราไม่ต้องการสิ่งนั้นฉันคิดว่า: เราต้องการรักษาสิทธิ์ตามที่ตั้งไว้ในไฟล์เป้าหมาย

ตอนนี้ข้อเสีย: ต้องใช้พื้นที่สองครั้ง ( cat .. >สร้าง) บังคับให้ผู้ใช้ทำงานด้วยตนเองหากไฟล์เป้าหมายไม่สามารถเขียนได้ในเวลาที่ต้องการและปล่อยให้ไฟล์ชั่วคราววางรอบ (ซึ่งอาจมีความปลอดภัย หรือปัญหาการบำรุงรักษา)


ในความเป็นจริงนี่เป็นสิ่งที่ฉันทำมากขึ้นหรือน้อยลง ฉันเขียนผลลัพธ์ส่วนใหญ่ไปยังไฟล์ชั่วคราวจากนั้นทำขั้นตอนการประมวลผลสุดท้ายและเขียนผลลัพธ์ไปยังไฟล์สุดท้าย ปัญหาคือฉันต้องการประกันตัวเร็ว (ตอนเริ่มต้นสคริปต์) หากขั้นตอนสุดท้ายนั้นน่าจะล้มเหลว สคริปต์อาจทำงานโดยอัตโนมัติโดยไม่ต้องดูแลเป็นเวลาหลายนาทีหรือหลายชั่วโมงดังนั้นคุณต้องการทราบล่วงหน้าว่ามันจะล้มเหลวอีกต่อไป!
BeeOnRope

แน่นอน แต่มีหลายวิธีที่สิ่งนี้อาจล้มเหลว: ดิสก์สามารถเติมได้ไดเรกทอรีอัปสตรีมอาจถูกลบสิทธิ์อาจเปลี่ยนแปลงไฟล์เป้าหมายอาจถูกใช้สำหรับสิ่งสำคัญอื่น ๆ และผู้ใช้ลืมว่าเขามอบหมายให้ไฟล์เดียวกันนี้ถูกทำลายโดยสิ่งนี้ การทำงาน ถ้าเราพูดถึงเรื่องนี้จากมุมมอง UX ที่บริสุทธิ์บางทีสิ่งที่ถูกต้องที่ต้องทำก็คือปฏิบัติต่อมันเหมือนการส่งงาน: ในตอนท้ายเมื่อคุณรู้ว่ามันทำงานอย่างถูกต้องจนเสร็จสมบูรณ์เพียงแค่บอกผู้ใช้ว่าเนื้อหาที่นำเสนออยู่ปัญหาคำสั่งสำหรับพวกเขาที่จะย้ายตัวเอง
บิชอป

ในทางทฤษฎีใช่มีวิธีมากมายที่สิ่งนี้อาจล้มเหลวได้ ในทางปฏิบัติส่วนใหญ่ที่ครอบงำเวลานี้ล้มเหลวเป็นเพราะเส้นทางที่ให้ไว้ไม่ถูกต้อง ฉันไม่สามารถป้องกันผู้ใช้ที่หลอกลวงบางคนไม่สามารถแก้ไข FS พร้อมกันเพื่อแบ่งงานในระหว่างการดำเนินการได้ แต่ฉันสามารถตรวจสอบสาเหตุความล้มเหลวอันดับที่ 1 ของเส้นทางที่ไม่ถูกต้องหรือไม่สามารถเขียนได้
BeeOnRope

2

TL; DR:

: >> "${userfile}"

ซึ่งแตกต่างจาก<> "${userfile}"หรือtouch "${userfile}"สิ่งนี้จะไม่ทำการแก้ไขใด ๆ ที่เป็นลวงตากับเวลาของไฟล์และจะทำงานกับไฟล์เขียนอย่างเดียว


จาก OP:

ฉันต้องการตรวจสอบอย่างสมเหตุสมผลว่าไฟล์สามารถสร้าง / เขียนทับได้ แต่ไม่สามารถสร้างได้จริง

และจากความคิดเห็นของคุณถึงคำตอบของฉันจากมุมมอง UX :

ส่วนใหญ่ที่ครอบงำเวลานี้ล้มเหลวเป็นเพราะเส้นทางที่ให้ไว้ไม่ถูกต้อง ฉันไม่สามารถป้องกันผู้ใช้ที่หลอกลวงบางคนได้อย่างมีเหตุผลบางคนทำการปรับเปลี่ยน FS เพื่อแบ่งงานในระหว่างการดำเนินการในเวลาเดียวกัน แต่ฉันสามารถตรวจสอบสาเหตุความล้มเหลวอันดับที่ 1 ของเส้นทางที่ไม่ถูกต้องหรือไม่สามารถเขียนได้

การทดสอบที่เชื่อถือได้เพียงอย่างเดียวคือ open(2)ยังไฟล์เพราะสิ่งเดียวที่แก้ไขคำถามทุกข้อเกี่ยวกับความสามารถในการเขียน: เส้นทาง, ความเป็นเจ้าของ, ระบบไฟล์, เครือข่าย, บริบทด้านความปลอดภัย ฯลฯ การทดสอบอื่น ๆ จะเน้นที่ความสามารถในการเขียนบางส่วน หากคุณต้องการชุดย่อยของการทดสอบในที่สุดคุณจะต้องเลือกสิ่งที่สำคัญสำหรับคุณ

แต่นี่คือความคิดอื่น จากสิ่งที่ฉันเข้าใจ:

  1. กระบวนการสร้างเนื้อหาใช้เวลานานและ
  2. ไฟล์เป้าหมายควรอยู่ในสถานะที่สอดคล้องกัน

คุณต้องการตรวจสอบล่วงหน้าเนื่องจาก # 1 และคุณไม่ต้องการเล่นกับไฟล์ที่มีอยู่เพราะ # 2 ดังนั้นทำไมคุณไม่เพียงแค่ขอให้เชลล์เปิดไฟล์เพื่อผนวก แต่จริงๆแล้วไม่ได้ผนวกอะไรเลย?

$ tree -ps
.
├── [dr-x------        4096]  dir_r
├── [drwx------        4096]  dir_w
├── [-r--------           0]  file_r
└── [-rw-------           0]  file_w

$ for p in file_r dir_r/foo file_w dir_w/foo; do : >> $p; done
-bash: file_r: Permission denied
-bash: dir_r/foo: Permission denied

$ tree -ps
.
├── [dr-x------        4096]  dir_r
├── [drwx------        4096]  dir_w
   └── [-rw-rw-r--           0]  foo
├── [-r--------           0]  file_r
└── [-rw-------           0]  file_w

ภายใต้ประทุนนี้จะช่วยแก้ปัญหาการเขียนได้อย่างที่ต้องการ:

open("dir_w/foo", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3

แต่ไม่มีการแก้ไขเนื้อหาหรือข้อมูลเมตาของไฟล์ ตอนนี้ใช่วิธีนี้:

  • ไม่ได้แจ้งให้คุณทราบหากไฟล์นั้นมีการต่อท้ายเท่านั้นซึ่งอาจเป็นปัญหาเมื่อคุณดำเนินการอัปเดตเมื่อสิ้นสุดการสร้างเนื้อหา คุณสามารถตรวจจับสิ่งนี้ได้ในระดับหนึ่งด้วยlsattrและตอบสนองตามนั้น
  • สร้างแฟ้มที่ไม่ได้อยู่ก่อนหน้านี้ถ้าเป็นกรณีดังกล่าว: rmลดนี้กับที่เลือก

ในขณะที่ฉันยืนยัน (ในคำตอบอื่น ๆ ของฉัน) ว่าวิธีที่ง่ายที่สุดสำหรับผู้ใช้คือการสร้างไฟล์ชั่วคราวที่ผู้ใช้ต้องย้ายฉันคิดว่านี่เป็นวิธีที่ผู้ใช้ที่เป็นมิตรน้อยที่สุดในการตรวจสอบอินพุตของพวกเขา

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.