1

「onewhich」という小さなスクリプトがあります。その目的は のように動作することですがwhich、オプションとして指定された実行可能ファイルの最初のオカレンスのみを、パスに表示される順序で検出します。

たとえば、パスがで、との/opt/bin:/usr/bin:/bin両方がある場合、コマンドは を返します。/opt/bin/runme/usr/bin/runmeonewhich runme/opt/bin/runme

ただし、 もある場合は/usr/bin/doit、代わりにコマンドonewhich doit runmeが返さ/usr/bin/doitれます。

アイデアは、パスをたどり、指定された各実行可能ファイルをチェックし、存在する場合はそれを表示して終了することです。

ここまでのスクリプトです。

#!/bin/sh

for what in "$@"; do
  for loc in `echo "${PATH}" | awk -vRS=: 1`; do
    if [ -f "${loc}/${what}" ]; then
      echo "${loc}/${what}"
      exit 0
    fi
  done
done

exit 1

問題は、特殊文字を含む PATH ディレクトリについて改善したいということです。StackOverflow に関する 2 番目のシェルの質問は、awk や sed などのツールでパスを解析することがいかに悪いかについて語っています。それについてのbash faq エントリさえあります。(但し書き: これには bash を使用していませんが、推奨事項は引き続き有効です。)

そこで、このようにパイプでパスを区切るようにスクリプトを書き直してみました」

#!/bin/sh

for what in "$@"; do
  echo "${PATH}" | awk -vRS=: 1 | while read loc ; do
    if [ -f "${loc}/${what}" ]; then
      echo "${loc}/${what}"
      exit 0
    fi
  done
done

exit 1

これが本当の利点をもたらすかどうかはわかりません$locが(まだ引用符の中にあるため)、何らかの理由で無視されているように見えるため、機能しません。exit 0または...何か(おそらくパイプを終了するwhileループを含むサブシェル)を終了しますが、スクリプトは1毎回の値で終了します。

${PATH}特殊文字が混乱を招くリスクなしに、ディレクトリをステップスルーするより良い方法は何ですか?

あるいは、車輪の再発明ですか? 既存のシェルツールに組み込まれているこれを行う方法はありますか?

これは Linux と FreeBSD の両方で実行する必要があるため、bash ではなく Bourne で記述しています。

ありがとう。

4

3 に答える 3

1

解析の必要性をまったく回避する別の方法PATHは、組み込みtypeコマンドを新しいシェルで実行することです (つまり、検索する関数やエイリアスはまったくありません。参照env -i sh -c 'type cmd 2>/dev/null)。

# using `cmd` instead of $(cmd) for portability
onewhich() {
  ec=0  # exit code
  for cmd in "$@"; do
    command -p env -i PATH="$PATH" sh -c '
      export LC_ALL=C LANG=C
      cmd="$1"
      path="`type "$cmd" 2>/dev/null`"
      if [ X"$path" = "X" ]; then
        printf "%s\n" "error: command \"${cmd}\" not found in PATH" 1>&2
        exit 1
      else
        case "$path" in
          *\ /*)
            path="/${path#*/}"
            printf "%s\n" "$path";;
          *)
            printf "%s\n" "error: no disk file: $path" 1>&2
            exit 1;;
        esac
        exit 0
      fi
    ' _ "$cmd"
    [ $? != 0 ] && ec=1
  done
  [ $ec != 0 ] && return 1
}

onewhich awk ls sed
onewhich builtin 
onewhich if

which2 つのコマンドが引数として指定されている場合、成功すると 2 つの完全なコマンド パスが返されるため、上記exit 0の最初のonewhichスクリプトでは、プログラムが途中で中止されます。さらに、 の引数として 2 つのコマンドが指定されている場合、コマンドの検索が 1 つだけ失敗した場合でもwhich、 の終了コードwhichは に設定されます (参照)。コマンドのこの動作を模倣するには、 2 つの変数 (および以下)のオン/オフを切り替える必要があります。1which awk sedxyz ls; echo $?whichcntnomatches

onewhich() (
   IFS=":"
   nomatches=0
   for cmd in "$@"; do
      cnt=0
      for loc in $PATH  ; do
         if [ $cnt = 0 ] && [ -x "$loc"/"$cmd" ]; then
            echo "$loc"/"$cmd"
            cnt=1
         fi
      done
      [ $cnt = 0 ] && nomatches=1
   done
   [ $nomatches = 1 ] && exit 1 || exit 0  # exit 1: at least one cmd was not in PATH
)


onewhich awk ls sed
onewhich awk lsxyz sed
onewhich builtin 
onewhich if
于 2014-11-24T13:56:38.473 に答える
1

あなたの最初の方法は、うまくいくように見えます。実際には、実際$PATHに検索する場合、ディレクトリにスペースや改行が埋め込まれている可能性はほとんどありません。その場合は、おそらくリファクタリングの時期です。

それでも、変数を引用符で囲んでいるため、悪い名前がループを破壊する可能性から危険にさらされているとは思いません。最悪の場合、奇妙な有効な実行可能ファイルを見逃す可能性があると思いますが、スクリプトがどのようにエラーを生成するのかわかりません。(スクリプトが有効な実行可能ファイルをどのように見逃すのかわかりません。また、テストしていません。一見問題がないと言っているだけです。)

ループについての 2 番目の質問については、頭に釘を打ったと思います。のようなパイプを実行すると、パイプthis | that | while condition; do things; doneの最後にある独自のシェルで while ループが実行されます。そのシェルを終了すると、パイプのアクションが終了する可能性がありますが、親シェルに戻るだけで、終了する独自の実行スレッドがありますexit 1

これを行うためのより良い方法については、を検討しwhichます。

#!/bin/sh

for what in "$@"; do
  which "$what"
done | head -1

また、終了値も本当に必要な場合は、次のようにします。

#!/bin/sh

for what in "$@"; do
  which "$what" && exit 0
done

exit 1

2 つ目は、ファイル ハンドルを開いて をパイプする必要がないため、リソースがさらに少なくなる可能性がありますhead

を使用してパスを分割することもできますIFS。たとえば、ループを逆にラップしたい場合は、次のようにします。

#!/bin/sh

IFS=":"

for loc in $PATH; do
  for what in "$@"; do
    if [ -x "$loc"/"$what" ]; then
      echo "$loc"/"$what"
      exit 0
    fi
  done
done

exit 1

通常の状況では、 の古い値を保存したい場合がありますが$IFS、スタンドアロン スクリプトで実行しているように見えるため、スクリプトが終了すると「新しい」値がスローされることに注意してください。

上記のコードはすべてテストされていません。YMMV。

于 2013-03-11T19:36:34.553 に答える
1

これはあなたの質問に直接答えるものではありませんが、解析する必要はまったくありませんPATH:

onewhich () {
    for what in "$@"; do
        which "$what" 2>/dev/null && break
    done
}

whichこれは、一致するものが見つかるまで、入力リストの各コマンドを呼び出すだけです。


を解析するPATHには、単純に「IFS=':'」と設定します。

if [ "${IFS:-x}" = "${IFS-x}" ]; then
    # Only preserve the value of IFS if it is currently set
    OLDIFS=$IFS
fi
IFS=":"
for f in $PATH; do  # Do not quote $PATH, to allow word splitting
    echo $f
done
if [ "${OLDIFS:-x}" = "${OLDIFS-x}" ]; then
    IFS=$OLDIFS
fi

PATHディレクトリのいずれかに実際にコロンが含まれている場合、上記は失敗します。

于 2013-03-11T17:18:14.020 に答える