bashでファイルを作成または切り捨て/上書きできるかどうかを確認するにはどうすればよいですか?


15

ユーザーは、foo.sh file.txtまたはのようなスクリプトのある時点で作成または上書きされるファイルパスを使用してスクリプトを呼び出しますfoo.sh dir/file.txt

create-or-overwriteの動作は、>出力リダイレクト演算子の右側にファイルを置くか、引数としてファイルを渡すための要件によく似ていますtee(実際、引数として渡すことteeはまさに私がやっていることです) )。

スクリプトの内容に入る前に、ファイル作成/上書きできるかどうかを合理的にチェックしたいのですが、実際には作成しません。このチェックは完全である必要はありません。はい、チェックとファイルが実際に書き込まれるポイントの間で状況が変わる可能性があることを理解しています-しかし、ここではベストエフォートタイプのソリューションで大丈夫ですので、早期に救済できますファイルパスが無効な場合。

ファイルを作成できなかった理由の例:

  • ファイルにはディレクトリコンポーネントが含まれていますdir/file.txtが、ディレクトリdirは存在しません
  • ユーザーは、指定されたディレクトリ(またはディレクトリが指定されていない場合はCWD)に書き込み権限を持っていません

はい、「事前に」許可を確認することはUNIX Way™ではないことを理解しています。むしろ、操作を試し、後で許しを求める必要があります。ただし、私の特定のスクリプトでは、これによりユーザーエクスペリエンスが低下し、責任のあるコンポーネントを変更できません。


引数は常に現在のディレクトリに基づいていますか、またはユーザーがフルパスを指定できますか?
Jesse_b

@Jesse_b-ユーザーがのような絶対パスを指定できると思います/foo/bar/file.txt。基本的には、コマンドラインで渡される場所のteeようtee $OUT_FILEにパスをOUT_FILE渡します。絶対パスと相対パスの両方で「機能する」はずですよね?
BeeOnRope

@BeeOnRope、tee -- "$OUT_FILE"少なくともあなたは必要ないでしょう。ファイルが既に存在するか、存在するが、通常のファイル(ディレクトリ、シンボリックリンク、FIFO)ではない場合はどうなりますか?
ステファンシャゼラス

@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制限を超えない
  • ファイルは(シンボリックリンク解決後)存在し、タイプディレクトリではなく、書き込み許可があります。
  • ファイルが存在しない場合、ファイルのディレクトリ名は(シンボリックリンク解決後)ディレクトリとして存在し、ファイルへの書き込みおよび検索許可があり、ファイル名の長さはディレクトリが存在するファイルシステムのNAME_MAX制限を超えません。
  • または、ファイルは存在しないファイルを指すシンボリックリンクであり、シンボリックリンクループではありませんが、上記の基準を満たします

ファイル名に含まれるバイト値、ディスククォータ、プロセス制限、selinux、apparmorまたはその他のセキュリティメカニズム、フルファイルシステム、iノードが残っていない、デバイスに制限があるvfat、ntfs、hfsplusなどのファイルシステムは今のところ無視します何らかの理由でその方法で開くことができないファイル、現在何らかのプロセスアドレス空間にマップされている実行可能ファイルはすべて、ファイルを開くまたは作成する機能に影響を与える可能性があります。

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、または任意のBourneシェルのような、単に置き換えます

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

で:

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

ここでのzsh固有の機能は、$ERRNO(最後のシステムコールのエラーコードを公開する)および$errnos[]対応する標準Cマクロ名に変換する連想配列です。そして$var:h(cshから)および$var:P(zsh 5.3以降が必要)。

bashには同等の機能がまだありません。

$file:hdir=$(dirname -- "$file"; echo .); dir=${dir%??}またはGNU で置き換えることができますdirnameIFS= read -rd '' dir < <(dirname -z -- "$file")

の場合$errnos[ERRNO] == ENOENT、アプローチls -Ldはファイルで実行し、エラーメッセージがENOENTエラーに対応するかどうかを確認することです。しかし、これを確実かつ移植性のある方法で行うのは難しいです。

1つのアプローチは次のとおりです。

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がありますが、ファイルが解決しないシンボリックリンクの場合は使用できません。それは同じだperlさんCwd::realpath()

pythonさんは、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.ベンKnoble

ありがとう、ビショップ、@ D.BenKnoble、編集を参照してください。
ステファンシャゼル

1
@DennisWilliamson、そうです、シェルはプログラムを呼び出すためのツールであり、スクリプトで呼び出すすべてのプログラムは、スクリプトが機能するためにインストールする必要があります。macOSには、デフォルトでbashとzshの両方がインストールされています。FreeBSDにはどちらも付属していません。OPは、bashでそれを実行したいという十分な理由があるかもしれません。おそらく、既に記述されているbashスクリプトに追加したいものです。
ステファンシャゼラス

1
@pipe、再び、シェルはコマンドインタープリターです。あなたは多くのシェルコードの抜粋のような他の言語の通訳たinvokeここに書かれていることがわかりますperlawksedまたはpython(偶数またはshbash...)。シェルスクリプトとは、タスクに最適なコマンドを使用することです。ここでもperl相当持っていないzsh$var:P(そのCwd::realpathBSDのように振る舞うのrealpathか、readlink -fあなたがしたいと思いながら、それはGNUのように動作するreadlink -fpythonos.path.realpath()
ステファンChazelas

6

検討したいオプションの1つは、早い段階でファイルを作成することですが、スクリプトの後半でのみ作成することです。このexecコマンドを使用して、ファイル記述子(3、4など)でファイルを開き、後でファイル記述子へのリダイレクト(>&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

trapエラー時にファイルをクリーンアップするためにa を使用することもできます。これは一般的な方法です。

この方法では、ファイルを作成できるアクセス許可の実際のチェックを取得すると同時に、ファイルを作成するのに十分な早さで実行できるため、それが失敗しても長いチェックを待つ時間を費やしていません。


更新:チェックが失敗した場合にファイルが上書きされないようにするには、すぐにファイルを切り捨てないbashのfd<>fileリダイレクトを使用します。(ファイルからの読み取りは気にしません。これは単なる回避策であるため、切り捨てません。を追加して>>もおそらく機能しますが、O_APPENDフラグを外してこれをもう少しエレガントにする傾向があります。写真の。)

内容を置き換える準備ができたら、最初にファイルを切り捨てる必要があります(そうしないと、以前のファイルにあったバイト数より少ないバイトを書き込んでいる場合、末尾のバイトがそこに残ります。)truncate(1)を使用できますその目的のためにcoreutilsからコマンド/dev/fd/3を実行し、ファイルを再度開く必要がないように、(疑似ファイルを使用して)開いているファイル記述子を渡すことができます。(繰り返しますが、技術的にはもっとシンプルなもの: >dir/file.txtがおそらく機能しますが、ファイルを再度開く必要がない方がよりエレガントなソリューションです。)


私はこれを検討していましたが、問題は、ファイルを作成してからしばらくしてから書き込みを行うポイントの間に別の無害なエラーが発生した場合、ユーザーがおそらく指定されたファイルが上書きされたことを見つけることに動揺することです。
BeeOnRope

1
@BeeOnRopeファイルを上書きしない別のオプションは、何も追加しないことです:echo -n >>fileまたはtrue >> file。もちろん、ext4には追加専用のファイルがありますが、その誤検知に耐えることができます。
モスビー

1
@BeeOnRope(a)既存のファイルまたは(b)(完全な)新しいファイルのいずれかを保存する必要がある場合、それはあなたが尋ねたものとは異なる質問です。それに対する良い答えは、新しいファイルをmy-file.txt.backup作成する前に、既存のファイルを新しい名前(例:)に移動することです。別の解決策は、新しいファイルを同じフォルダー内の一時ファイルに書き込み、残りのスクリプトが成功した後に古いファイルにコピーすることです-その最後の操作が失敗した場合、ユーザーは問題を失うことなく手動で修正できます彼または彼女の進歩。
jpaugh

1
@BeeOnRope「アトミック置換」ルート(これは良い方法です!)に進む場合、通常、最終ファイルと同じディレクトリに一時ファイルを書き込みます(名前変更のために同じファイルシステムにある必要があります) (2)成功するため)一意の名前を使用します(したがって、実行中のスクリプトのインスタンスが2つある場合、互いの一時ファイルを上書きしません)。通常、同じディレクトリに書き込むために一時ファイルを開くことは、適切な指標です'後で名前を変更することができます(最終ターゲットが存在し、ディレクトリである場合を除き)ので、ある程度あなたの質問にも対応します。
フィルブランデン

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

最初のifコンストラクトは、引数が(で始まる/)フルパスかどうかをチェックdirし、最後までのディレクトリパスに変数を設定します/。それ以外の場合、引数がa /で始まらないが/(サブディレクトリを指定する)を含む場合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
アイアングレムリン

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場合、一時ファイルのアクセス許可を保持することになります。それは望ましくありません。ターゲットファイルに設定されたアクセス許可を保持する必要があります。

さて、悪い点は、2倍のスペース(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.