rcmdnk's blog

デーモン君のソース探検―BSDのソースコードを探る冒険者たちのための手引き書 (BSD magazine Books)

MacはBSD UNIXベースなのでGNU Linuxのコマンドと比べて 同じコマンドでも少し振舞いが違う所があります。

これまでGNU Linuxだけ使ってきてFree BSD等使ったことが無いので、 基本的にGNU系のコマンドに慣れていて、 たまにMacでコマンドを打つと予想外の事が起きて戸惑っています。

今回、特に気になっていたcpについてwrapper関数(bash用)を作って Mac上でもGNU的な振る舞いにするようにしてみました。

cp Wrapper関数

次の様な関数を定義して.bashrc等に書いておきます。

cp_wrapper_for_BSDtoGNU.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# cp wraper for BSD cp (make it like GNU cp){{{
# Remove the end "/" and change -r to -R
if ! cp --version 2>/dev/null |grep -q GNU;then
  function cp {
    local opt=""
    local source=""
    local dest=""
    while [ $# -gt 0 ];do
      if [[ "$1" == -* ]];then
        if [ "$1" == "-r" ];then
          opt="$opt -R"
        else
          opt="$opt $1"
        fi
      elif [ $# -eq 1 ];then
        dest="$1"
      else
        source="${source} ${1%/}"
      fi
      shift
    done
    command cp $opt $source $dest
  }
fi
# }}}

cpがGNUのものでない場合これらのwrapper関数を適用します。

特に気になったのが、コピーする元のディレクトリの指定が 最後に/を付けた状態だとディレクトリではなく、中身だけ 持ってきてしまうことです。 (GNUの場合は/あるなしに関わらずディレクトリそのものがコピーされる。) Tabで補完してディレクトリ名を入力すると/まで補完されるので、 ディレクトリをコピーしたい場合はいちいち消さないといけないし、 忘れると中身をコピーしてきてしまって、 中身が大量のディレクトリの場合には酷いことになります。

元からあったファイルと区別して移動し直すのも大変ですが、 同じファイル名の物があった日には上書きされてしまってどうしようもないことに…

GNU Linuxと両方使ってる場合はとっても危険なのでとても気をつけないといけません。

通常、ディレクトリ内の物を移動したい時はdir/*使う様にします。 ただこれだとdir内の.で始まる隠しファイルは移動出来ません。

BSDコマンドの場合だと、

$ cp -R ../dir/ ./

とすれば/dirの中身が.で始まるファイルも含めてコピー出来るのですが、 GNUと合わせて使いたいのでこの機能は封印することになります。 (意図してない限り知らないうちに.git/等も含めて複製したりすることになるので、 この機能を普段から意図的に使ってない場合はむしろ混乱の元になるかと。)

もう1つの変更が-rオプションで、GNUの場合は-r-Rと同様、単なる recursiveとしてのオプションですが、 BSDの場合はmanしてみると

COMPATIBILITY
     Historic versions of the cp utility had a -r option.  This implementation
     supports that option; however, its use is strongly discouraged, as it
     does not correctly copy special files, symbolic links, or fifo's.

の様に、使わないように、となっています。

これまで普通にcp -rしてきて上の/問題以外には余り気付かなかったのですが、 特にディレクトリ内にシンボリックリンクがある場合に微妙な振る舞いをします。

GNU cpだとcp -rで他のオプション無しだと中にあるシンボリックリンクを シンボリックリンクとしてコピーしますが、BSD cpだとシンボリックの 元の実態をコピーしようとします。

一方でcp -RとすればGNUのcp -rの様な振る舞いになります。

基本的に-rとしている場合でもシンボリックリンクはシンボリックリンクとして コピーされるものだと身に付いているので、これも-Rに変換しておきます。

シンボリックリンクを追うかどうかは-L/Pでオン/オフ出来ますが、 BSDでは-rはこれらのオプションとは同時に使えないため、 やはり-rは使うべきでは無いようです。

ちょっと使ってみた感じ、問題なく動いているのでしばらく使ってみようと思っていますが、 cpという頻繁に使うコアなコマンドなので、もし使う際には自己責任で注意して使って下さい。

おまけ

cp以外にも色々と細かい所で違いがあってたまにイラッとしますが (まだ単に気づいてないだけの所も多々あると思いますが…)、 気になった所でGNU/BSDの互換性をもたせようと思って使っている物をついでに紹介しておきます。

sed

sedもGNUとBSDでちょっと違う所があります。

ファイルを直接書き換えるコマンドで

$ sed -i 's/foo/bar/g' test.txt

とGNU sedですると、test.txtの中のfoobarに変換されます。 -iに続いて.bak等とサフィックスを与えれば元のファイルがtest.txt.bak と言う風にバックアップされます。

これがBSD sedだと

$ sed -i 's/foo/bar/g' test.txt
sed: 1: "test.txt": command a expects \ followed by text

等と怒られます。BSDの場合では-iに続いて必ずサフィックスを指定しなくてはならず、 バックアップが要らない場合でも空の""を与える必要があります。

それぞれmanしてみるとGNUの場合:

 -i[SUFFIX], --in-place[=SUFFIX]

        edit files in place (makes backup if extension supplied)

BSDの場合:

 -i extension
         Edit files in-place, saving backups with the specified extension.
         If a zero-length extension is given, no backup will be saved.  It
         is not recommended to give a zero-length extension when in-place
         editing files, as you risk corruption or partial content in situ-
         ations where disk space is exhausted, etc.

となっています。BSDの方では親切にno backupは危ないよ、と言う思想で、 どうしてもと言うなら意図的に""を与えなさい、と言う感じ。

前置きが長くなりましたが、これの対処としてはsedにWrapper関数かけるのは 色々複雑すぎて面倒なので、 aliasで別コマンドにして処置します。

ただ、この""を与える際、GNU sedの場合、-iと離すと(sed -i ""とすると)次の引数として 取られるのでエラーが出ます。

一方、BSD sedの場合、-iとくっつけて渡すと(sed -i""とすると)エラーになります。

これらも考慮して、

if sed --version 2>/dev/null |grep -q GNU;then
  alias sedi='sed -i"" '
else
  alias sedi='sed -i "" '
fi

といったものを.bashrcに書いておいてファイル内のfooをすべてbarにしたい場合、

$ sedi 's/foo/bar/g' test.txt

としています。(これで余計なファイルを作らずに変換出来ます。)

その他のsedでの違い:

GNU/BSDでのsedにおける正規表現の扱いの違い

sedで改行を出力する

tail: ファイルの中身を逆さにする

BSDのtailコマンドには-rオプションがあり、

$ cat << EOF > test.txt
> abc
> def
> 123
> EOF
$ cat test.txt
abc
def
123
$ tail -r test.txt
123
def
abc
$

こんな感じでファイルの中身を逆にします。 引数を与えない場合は標準入力を変換します。

$ cat test.txt|tail -r
123
def
abc

一方、GNU tailには-rがありません。 ただし、大概のLinuxにはGNUのtacと言うコマンドが入っていてこれがtail -rと同じ 働きをします(BSDにはtail -rがあるからtacは無い?)。

これら気にせず使うために、これもaliasで

# Revert lines in the file/std input
if ! type tac >& /dev/null && \
   ! tail --version 2>/dev/null|grep -q GNU;then
  alias tac='tail -r'
fi

こんな感じでBSDの場合でもtail -rtacで呼べる様にしてあります。

余談ですが、これを最初考えてる時、

if type tac >/dev/null 2>&1;then
  alias rev="tac"
elif ! tail --version 2>/dev/null |grep -q GNU;then
  alias rev="tail -r"
fi

こんな感じでrev(reverse)と言う名前で統一しようかと思ったのですが、 実はrevと言うコマンドは存在していることにこの時気づきました。 各行の文字列をそれぞれ反転される、というコマンドです。 (LinuxにもMacにもありました。)

実行すると

$ rev test.txt
cba
fed
321

の様に、各行が逆から書かれています。 正直、このコマンドを使う場面が余り思いつかないし、 使っているところを見たことありませんが、 一応潰さないように。

まとめ

(おまけの方が長くなった…)

MacにもGNUコマンド入れれば良いじゃん、と言ってしまえばそれまでですが、 設定ファイル(.bashrc)1つで手軽に変えられる方法、と言うことで。

cpに関してはcpなんて使わずrsyncでしょ、とか、find+cpioだろ、とか、 さらにはtar使うでしょ、という話もある様ですが 1、 通常のファイルコピー同様cpで慣れてるのでまだcp使いたいな、と。

そのうち他の物も気になったら加えて行こうとおもっていますが、 もし他にもクリティカルな違いがあれば教えて頂けるとうれしいです。

Sponsored Links
Sponsored Links

« Cygwin内外でリンクを共有する Vimインストール »

}