web-dev-qa-db-ja.com

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

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

作成または上書きの動作は、ファイルを>出力リダイレクト演算子の右側に配置する、またはteeに引数として渡す(実際には、 teeの引数としては、まさに私がやっていることです)。

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

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

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

はい、許可を「前もって」チェックすることはNIX Way™ではないことに気づきました。むしろ、操作を試し、後で許しを求めるべきです。しかし、私の特定のスクリプトでは、これはユーザーエクスペリエンスの低下につながり、責任のあるコンポーネントを変更することはできません。

15
BeeOnRope

明らかなテストは次のとおりです。

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を自由に作成し、それを使用して自由に実行できます。

11
DopeGhoti

私が集めているものから、使用するときにそれを確認したい

_tee -- "$OUT_FILE"
_

(_--_であるか、-で始まるファイル名では機能しないことに注意してください)、teeは書き込み用にファイルを開くことに成功します。

それはそれ:

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

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

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-like Shellで、単に

_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:h_は、dir=$(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##*:}
_

(エラーメッセージendssyserror()ENOENTに変換され、その変換に_:_が含まれていないと仮定します)、そして代わりに_[ -e "$file" ]_の例:

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

そして、ENOENTエラーをチェックしてください

_case $err in
  (*:"$msg_for_ENOENT") ...
esac
_

_$file:P_の部分は、bashで、特にFreeBSDで達成するのが最も難しいです。

FreeBSDにはrealpathコマンドと、_-f_オプションを受け入れるreadlinkコマンドがありますが、ファイルがシンボリックリンクでない場合は使用できません解決します。これはPerlCwd::realpath()と同じです。

pythonos.path.realpath()zsh _$file:P_と同様に機能するように見えるため、少なくとも1つのバージョンのpythonがインストールされており、それらの1つを参照するpythonコマンドです(これはFreeBSDでは提供されていません)。

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

しかし、その後、pythonですべてを行うこともできます。

または、これらすべてのコーナーケースを処理しないことを決定できます。

8

検討したいオプションの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を使用してエラー時にファイルをクリーンアップすることもできます。これは一般的な方法です。

このようにして、ファイルを作成できる権限のrealチェックを取得すると同時に、それが失敗した場合に十分早い段階でファイルを実行できます長いチェックを待つ時間を費やしていません。


UPDATED:チェックが失敗した場合にファイルを破壊しないようにするには、bashの fd<>file redirection を使用しますファイルをすぐに切り捨てないでください。 (ファイルからの読み取りについては気にしません。これは単なる回避策であるため、切り捨てません。>>を追加してもおそらく機能しますが、これはもう少しエレガントになりがちですが、 O_APPENDフラグを画像から除外します。)

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

6
filbranden

ユーザーエクスペリエンスが疑問を引き起こしているとのことですが、 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 .. >構文)、ターゲットファイルが必要なときに書き込み可能でなかった場合、ユーザーに手動での作業を強制し、一時ファイルをそのまま残します(セキュリティまたはメンテナンスの問題がある可能性があります)。

4
bishop

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変数を最後の/までのディレクトリパスに設定します。それ以外の場合、引数が/で始まらず、/(サブディレクトリを指定)が含まれている場合、dirを現在の作業ディレクトリ+サブディレクトリパスに設定します。それ以外の場合は、現在の作業ディレクトリを想定します。次に、そのディレクトリが書き込み可能かどうかを確認します。

4
jesse_b

以下に概説するような通常の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
4
Jaleks

TL; DR:

_: >> "${userfile}"
_

_<> "${userfile}"_または_touch "${userfile}"_とは異なり、これはファイルのタイムスタンプに誤った変更を加えず、書き込み専用ファイルでも機能します。


OPから:

ファイルを作成/上書きできるかどうかは妥当なチェックをしたいが、実際には作成したくない。

そしてあなたのコメントから Xの観点からの私の答え

これが失敗する大部分の時間は、提供されたパスが無効であるためです。不正ユーザーが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でこれを軽減します。

(他の回答では)最もユーザーフレンドリーなアプローチは、ユーザーが移動する必要のある一時ファイルを作成することだと主張していますが、これは最小限のユーザー敵意アプローチで完全に入力を精査します。

2
bishop