2006-02-22 に書いた「シェルスクリプト自身のファイル名を取得できる特殊変数 $0」についてメールで指摘を頂いた。ありがとうございます。
私はスラッシュなどのパス指定文字列を除去するために basename コマンドを使っていたのだが、そうしなくても済む方法があるとのこと。
試してみると、確かに ${0##*/} でスクリプト自身の名前を取得できた。
上記を get_script_name.sh という名前で保存し、相対パス指定で実行した結果。
1行目の $0 のみを指定したもの以外は、見事にファイル名を取得できている。
次に、フルパスでスクリプトファイルを指定して実行してみる。
指摘の通り、3行目の ${0##*/} は見事にファイル名だけを取得できているが、4行目に書いた ${0#*/} だと余計な文字が入ってしまっている。
上記を一度読んでもすぐには理解できなかった。順番に読んでいく。
・word に指定されたものについて、パス名展開と同じような展開がなされてパターンが作られる。
・パターンが parameter の先頭とマッチする場合、そこからパターンを削除する。
・word の前が ## の場合は最長一致、# の場合は最短一致。
わかってきた。あとはパス名展開の定義を調べておこう。
ということは、*/ は「空文字を含む任意の文字列にスラッシュが続く」という文字列に展開されるということか。で、その文字列が最長一致で parameter の先頭とマッチしたときに parameter から取り除かれるんだな。なるほど、やっとわかった。こういう意味だったのか。勉強になったなあ。
私はスラッシュなどのパス指定文字列を除去するために basename コマンドを使っていたのだが、そうしなくても済む方法があるとのこと。
http://sonic64.com/2006-02-22.html
で言及さている 「basename コマンドを使わなくても済む特殊変数」ですが、
echo "This script name is ${0##*/}" でお望みの動作になるはずです。
今回の例に限っては ${0#*/} でも良いですが、Full Path を指定して実行した場合に意図しない結果になると思われます。
man bash 内を ${parameter##word}で検索してみてください。
試してみると、確かに ${0##*/} でスクリプト自身の名前を取得できた。
#!/bin/sh
echo "This script name is $0"
echo "This script name is `basename $0`"
echo "This script name is ${0##*/}"
echo "This script name is ${0#*/}"
上記を get_script_name.sh という名前で保存し、相対パス指定で実行した結果。
$ ./get_script_name.sh
This script name is ./get_script_name.sh
This script name is get_script_name.sh
This script name is get_script_name.sh
This script name is get_script_name.sh.
1行目の $0 のみを指定したもの以外は、見事にファイル名を取得できている。
次に、フルパスでスクリプトファイルを指定して実行してみる。
$ /home/sonic64/tmp/get_script_name.sh
This script name is /home/sonic64/tmp/get_script_name.sh
This script name is get_script_name.sh
This script name is get_script_name.sh
This script name is home/sonic64/tmp/get_script_name.sh
指摘の通り、3行目の ${0##*/} は見事にファイル名だけを取得できているが、4行目に書いた ${0#*/} だと余計な文字が入ってしまっている。
- ${parameter##word} の意味を調べる
さて、${parameter##word} ってどういう意味だろう? man bash して ${parameter##word} を検索してみる。あった。${parameter##word}
word が展開され、パス名展開の場合と同じようなパターンを作ります。このパターンが parameter の値の先頭部分とマッチする場合、展開して得られる値は parameter を展開した値から最短一致パターン (``#''の場合) または最長一致パターン (``##'' の場合) を取り除いたものになります。 parameter が @ または * である場合、パターンを削除する操作は全ての位置パラメータに順番に適用され、展開結果はリストとして得られます。 parameter が @ または * が添字になっている配列変数である場合、パターンを削除する操作は配列の全ての要素に順番に適用され、展開結果はリストとして得られます。
上記を一度読んでもすぐには理解できなかった。順番に読んでいく。
・word に指定されたものについて、パス名展開と同じような展開がなされてパターンが作られる。
・パターンが parameter の先頭とマッチする場合、そこからパターンを削除する。
・word の前が ## の場合は最長一致、# の場合は最短一致。
わかってきた。あとはパス名展開の定義を調べておこう。
パス名展開
* 空文字列を含む、任意の文字列にマッチします。
ということは、*/ は「空文字を含む任意の文字列にスラッシュが続く」という文字列に展開されるということか。で、その文字列が最長一致で parameter の先頭とマッチしたときに parameter から取り除かれるんだな。なるほど、やっとわかった。こういう意味だったのか。勉強になったなあ。
パラメータ $0 でシェルスクリプト自身の名前を取得できる。
上記スクリプトを get_script_name.sh というファイル名で保存し、実行。
スクリプトの呼び出し方によってはパス指定文字列が入ってしまうので、二行目では basename コマンドでファイル名部分だけを取り出している。basename コマンドを使わなくても済む特殊変数って無いのかな? man bash してみたけど見つけられなかった。
追記。
2006-02-27 に「シェルのパラメータ展開でスクリプト自身のファイル名を取得する」という記事を書いた。
basename コマンドを使わなくても、${0##*/} でスクリプト自身の名前を取得できる。
#!/bin/sh
echo "This script name is $0"
echo "This script name is `basename $0`"
上記スクリプトを get_script_name.sh というファイル名で保存し、実行。
$ ./get_script_name.sh
This script name is ./get_script_name.sh
This script name is get_script_name.sh
スクリプトの呼び出し方によってはパス指定文字列が入ってしまうので、二行目では basename コマンドでファイル名部分だけを取り出している。basename コマンドを使わなくても済む特殊変数って無いのかな? man bash してみたけど見つけられなかった。
追記。
2006-02-27 に「シェルのパラメータ展開でスクリプト自身のファイル名を取得する」という記事を書いた。
basename コマンドを使わなくても、${0##*/} でスクリプト自身の名前を取得できる。
.bash_profile の記述を変更して以下のようにし、プロンプトに時刻が表示されるようにした。プロンプトに時刻が入っていると、コマンドの実行を開始したおおよその時間がわかるので便利。
時刻を表示してくれているのは [\t] の部分。\u はユーザ名、\h はホスト名。ちなみに先頭の \[\033[1;32m\] と 末尾の [\033[0m\] は、プロンプトを緑色にするための設定。私は端末の背景色を黒にしているので、緑色が映える。それに、プロンプトの色を地の文の色と変えておくと、長い出力があった後にスクロールバーを操作してプロンプトを探そうとしたときに見つけやすい。
実際のプロンプトは以下のように表示される。
export PS1="\[\033[1;32m\][\t][\u@\h \w]$\[\033[0m\] "
時刻を表示してくれているのは [\t] の部分。\u はユーザ名、\h はホスト名。ちなみに先頭の \[\033[1;32m\] と 末尾の [\033[0m\] は、プロンプトを緑色にするための設定。私は端末の背景色を黒にしているので、緑色が映える。それに、プロンプトの色を地の文の色と変えておくと、長い出力があった後にスクロールバーを操作してプロンプトを探そうとしたときに見つけやすい。
実際のプロンプトは以下のように表示される。
[23:06:33][hiroaki@sonic64 ~]$
cron で毎分スクリプトを回したいが、一定の確率で何もしないでそのまま終了するようにしたい。
とあるスクリプトに、ランダムに仕事をしないで怠けるような機能を追加したい。乱数を使えばいいのだが、乱数を得るコマンドってあったかな? Perl のワンライナーで rand() を呼べばできるけど、それは最後の手段だ。
Manpage of BASH
http://www.linux.or.jp/JM/html/GNU_bash/man1/bash.1.html
とあるスクリプトに、ランダムに仕事をしないで怠けるような機能を追加したい。乱数を使えばいいのだが、乱数を得るコマンドってあったかな? Perl のワンライナーで rand() を呼べばできるけど、それは最後の手段だ。
- シェル変数 RANDOM
bash の man page を読んでみる。シェル変数 RANDOM が使えそうだ。Manpage of BASH
http://www.linux.or.jp/JM/html/GNU_bash/man1/bash.1.html
RANDOM
このパラメータが参照される度に、 0 から 32767 までのランダムな整数が生成されます。 RANDOM に値を代入すると、乱数の列を初期化できます。 RANDOM を unset すると、この変数の特殊な性質は無くなります。後で再び set しても元には戻りません。
- 乱数を n で割った余りが0だったら何もしない
乱数を n で割った余りを求め、0だったら何もせずに終了する。これで「n 分の1の確率で何もしない」という処理ができる。今回の用途にはこれで十分だ。# 確率3分の1で何もせずに終了する
probability=3
if [ `expr $RANDOM % $probability` -eq 0 ]; then
echo unlucky!
exit
fi
/cygdrive ディレクトリを作っておけば、bash で TAB キーを押したときに /cygdrive が補完対象になる。
/cygdrive は私の cygwin では補完が効かず毎回手で入力していた。ただ、cyg というスペルは私の指と相性が悪いらしく、cgy と打ち間違えることが多かった。そのたびに補完できると良いのになあと思っていたけど、こんな簡単な解決法があるとはね。ディレクトリがないなら作ってあげればいい。実に明快だ。
[linux-users:103268] Re: cygwinの設定について
http://search.luky.org/linux-users.a/msg03218.html
さらに d を入力して TAB キーを押す。
/cygdrive は私の cygwin では補完が効かず毎回手で入力していた。ただ、cyg というスペルは私の指と相性が悪いらしく、cgy と打ち間違えることが多かった。そのたびに補完できると良いのになあと思っていたけど、こんな簡単な解決法があるとはね。ディレクトリがないなら作ってあげればいい。実に明快だ。
[linux-users:103268] Re: cygwinの設定について
http://search.luky.org/linux-users.a/msg03218.html
> > $ cd /c/cygdrive
> > BASH: cd: /c/cygdrive: No such file or directory
>
> /cygdrive/c/ の勘違いですね。きっと。
昔との互換性で「c:/」表記も使えたと思います。「cygdrive」
の部分の文字列は設定次第で変更可能ですが、こちらの表記であれ
ば環境に依存せず不変ですね。
そもそも /cygdrive だとか /proc だとかの pseudo directory
が / を readdir() した際に fetch 出来ないという中途半端さが
混乱の元のような気がします。
bash の pathname 補完のためだけに /cygdrive や /proc を敢
えて作成しておく人も少なくないらしいですね。
- 補完の便利さを味わおう
さあ、補完の便利さを味わってみよう。/cygdrive/c/Program Files/ にカレントディレクトリを移動したいとする。$ cd /cここまで入力したら、すかさず TAB キーを押すッ!
$ cd /cygyg が補完された。素敵。あと2回 TAB キーを押すと、候補が表示される。補完だけなら候補を表示させる必要はないんだけど、便利さを味わうんだったら全機能を使っておきたいのやっておく。
$ cd /cyg
cygdrive cygwin.bat cygwin.ico
さらに d を入力して TAB キーを押す。
$ cd /cygdrive/ああ楽だ。cgywin などと間違って入力することもない。これが補完の醍醐味。
- cygwin.bat が cygwin.bat cygwin.ico cygwin.bat 邪魔ばっかしやがって あ゛っっ あいつさえいなきゃ!! ぜーーーんぶ 上手くいくのにっ あ゛っ あははは はははは はははは どこまでだって 追いつめてって 消してやるわ!! なめんじゃねえよ
しかし、cygwin.bat や cygwin.ico は邪魔だな。これらのファイルが無ければ、/c と入力して TAB を押せば /cygdrive まで補完してくれるんだけどなー。消してしまえッ! ・・・と思ったけど、これらのファイルって cygwin を起動したときに使われるファイルなのね。ショートカットのリンク先と cygwin.bat の中身を修正してやれば他のパスに移動しても大丈夫かもしれない。でもパッケージのアップデート時などに何か問題があったら困るので、今回はこのままにしておく。cygwin.bat は命拾いしたな。シェルのリダイレクトについての理解が不十分なためにやってしまった失敗。
標準出力も標準エラー出力も /dev/null に捨てたいとき、間違えて以下のようにしてしまうことがときどきあった。最近はやらなくなったが。
正しくは以下のように記述する。こうすることで、両方とも /dev/null に向けられる。
ここで問題なのは、2>&1 を「合流」と考えてしまっていることだ。さらに、「2番を1番に合流させているんだから、1番をリダイレクトすれば両者ともに出力先が変わる」と考えてしまっていることだ。実際のリダイレクトはそういった処理はしていない。
シェルのファイルディスクリプタ操作
http://home.catv.ne.jp/pp/ginoue/memo/sh-fd.html
まずは正しい例。
次に、私が間違った例。
合流とかそういうものではなく、毎回毎回ファイルディスクリプタのオープンとクローズが行われていることがわかる。
標準エラー出力を1番のファイルディスクリプタに向けようとしたのだが、この記述だとやりたかったこととはまったく違う結果になる。まず、& が指定されているためコマンドはバックグラウンドで実行される。そして2番の標準エラー出力が 1 というファイルに向けられる。
普通ならば「バックグラウンド実行されたよ」という旨のメッセージが表示されるため、すぐに間違いに気づく。しかし、悪いことにこれを crontab に書いていたため、発見が遅れてしまった。cron の結果を確認しようとしたところ、なぜか 1 というファイルができており、そのときになって初めて気づいたのだった。
標準出力も標準エラー出力も /dev/null に捨てたいとき、間違えて以下のようにしてしまうことがときどきあった。最近はやらなくなったが。
# 間違いこれだと command の標準出力は /dev/null に向けられるが、command の標準エラー出力は画面に向いてしまう。
$ command 2>&1 >/dev/null
正しくは以下のように記述する。こうすることで、両方とも /dev/null に向けられる。
# 正しい
$ command >/dev/null 2>&1
- なぜ間違ってしまうのか?
間違った記述をしてしまう理由は、リダイレクトを誤解しているからだ。そのときの私の思考は以下のようなものだ。標準エラー出力は2番だから、まず2番を1番が指している先に合流させるために 2>&1 とする。
で、1番を >/dev/null として /dev/null に向ければ、両方とも /dev/null に向く。
ここで問題なのは、2>&1 を「合流」と考えてしまっていることだ。さらに、「2番を1番に合流させているんだから、1番をリダイレクトすれば両者ともに出力先が変わる」と考えてしまっていることだ。実際のリダイレクトはそういった処理はしていない。
- 実際のリダイレクト処理
実際のリダイレクトの処理は、コマンドラインの左から右へ、ファイルディスクリプタをオープン/クローズしながら進められる。以下のサイトの説明を読んだ方が早い。シェルのファイルディスクリプタ操作
http://home.catv.ne.jp/pp/ginoue/memo/sh-fd.html
まずは正しい例。
ls >outfile.txt 2>&1
また実装例から示します。
fd = open("outfile.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
dup2(fd, 1);
close(fd);
dup2(1, 2);
「ls >outfile.txt 2>&1」が左から処理されていく過程を追います。「>outfile.txt」は上と同じで、ファイルを指すfdが作られた後、1番が一旦クローズされてfdの複製として再生され、ついでにfdがクローズされます。「2>&1」は、2番が一旦クローズされて1番の複製として再生されます。
1番 => [screen]
2番 => [screen]
|
| fd = open();
| dup2(fd, 1);一旦1番をクローズ。
| 1番がfdの複製として再生される。
| close(fd);
V
1番 => [file]
2番 => [screen]
|
| dup2(1, 2);一旦2番をクローズ。
| 2番が1番の複製として再生される。
V
1番 => [file]
2番 => [file]
結局、1番も2番もファイル(outfile.txt)を指すことになります。つまり、lsの標準出力も標準エラー出力もファイルに出力されます。
次に、私が間違った例。
ls 2>&1 >outfile.txt
いつものように実装例から。
dup2(1, 2);
fd = open("outfile.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
dup2(fd, 1);
close(fd);
「2>&1」も「>outfile.txt」も、上で説明したとおりの動作をします。左から処理が進むので、処理の順番が異なるだけですが、最終形態も異なります。
1番 => [screen]
2番 => [screen]
|
| dup2(1, 2);一旦2番をクローズ。
| 2番が1番の複製として再生される(結局、画面のまま)。
V
1番 => [screen]
2番 => [screen]
|
| fd = open();
| dup2(fd, 1);一旦1番をクローズ。
| 1番がfdの複製として再生される。
| close(fd);
V
1番 => [file]
2番 => [screen]
(fd => [file])
結局、1番がファイルを指して、2番は画面のままです。
合流とかそういうものではなく、毎回毎回ファイルディスクリプタのオープンとクローズが行われていることがわかる。
- さらにすごい間違い
私がやった失敗をもう一つ。$ command >/dev/null &2>1
標準エラー出力を1番のファイルディスクリプタに向けようとしたのだが、この記述だとやりたかったこととはまったく違う結果になる。まず、& が指定されているためコマンドはバックグラウンドで実行される。そして2番の標準エラー出力が 1 というファイルに向けられる。
普通ならば「バックグラウンド実行されたよ」という旨のメッセージが表示されるため、すぐに間違いに気づく。しかし、悪いことにこれを crontab に書いていたため、発見が遅れてしまった。cron の結果を確認しようとしたところ、なぜか 1 というファイルができており、そのときになって初めて気づいたのだった。
シェルのファイルディスクリプタ操作 Shell file descriptor manipulation
http://home.catv.ne.jp/pp/ginoue/memo/sh-fd.html
リダイレクトやパイプがどんな仕組みで成り立っているのかを図解つきで解説。普段は抽象化された入出力ライブラリに頼りっきりでファイルディスクリプタをあまり意識していないが、非常にわかりやすかった。
http://home.catv.ne.jp/pp/ginoue/memo/sh-fd.html
リダイレクトやパイプがどんな仕組みで成り立っているのかを図解つきで解説。普段は抽象化された入出力ライブラリに頼りっきりでファイルディスクリプタをあまり意識していないが、非常にわかりやすかった。
カレントディレクトリにあるファイルとディレクトリへのシンボリックリンクを、/tmp/lntest に作成するシェルスクリプト。大したことをやってるわけじゃなくて、for 文でループさせてるだけ。実を言うと、単に bash での for 文の使い方を知りたかっただけだ。
いちおう削除も書いておこう。
$ for i in *; do ln -s "`pwd`/$i" "/tmp/lntest/$i"; doneダブルクオートしておくと、ファイル名にスペースが入っていても大丈夫。
いちおう削除も書いておこう。
$ for i in /tmp/lntest/*; do rm -f "$i"; done
grep した結果を表示させたり、込み入ったログを表示させたりすると、
ターミナルのどこにプロンプトがあるのかわかりづらい。
そのため、どこから表示が始まり、どこで終わったのかを瞬時に把握できなくなる。
プロンプトに色を付ければ、一目でわかる。
Bash Prompt HOWTO http://www.linux.or.jp/JF/JFdocs/Bash-Prompt-HOWTO.htm
ターミナルのどこにプロンプトがあるのかわかりづらい。
そのため、どこから表示が始まり、どこで終わったのかを瞬時に把握できなくなる。
プロンプトに色を付ければ、一目でわかる。
PS1="\[\033[1;32m\][\u@\h \w]$\[\033[0m\] "これでライトグリーンになる。
Bash Prompt HOWTO http://www.linux.or.jp/JF/JFdocs/Bash-Prompt-HOWTO.htm
- 2003-03-31 に書いた bash の補完で悩む。
chown は 第2引数にファイル名をとるのだが、complete -u {su,finger,chown}だと引数の位置にかかわらずユーザ名しか補完してくれない。
- tcsh ならば引数位置による補完対象を指定できるようだが、bash では指定できないようだ。
- Working more productively with bash 2.x
http://www.caliban.org/bash/index.shtml#completionではどうやってるのかな、と見てみたが、何やってるか良くわからん。
以下は bash-completion-20030419.tar.gz からの抜粋。
# chown(1) completion
#
_chown()
{
local cur prev user group i
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
prev=${COMP_WORDS[COMP_CWORD-1]}
# options completion
if "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W '-c -h -f -R -v --changes \
--dereference --no-dereference --from= --silent --quiet \
--reference= --recursive --verbose --help --version' -- $cur ) )
return 0
fi
# first parameter on line or first since an option?
if [ $COMP_CWORD -eq 1 ]; then
if [[ "$cur" == [a-zA-Z]*[.:]* ]] && \
[[ ${BASH_VERSINFO[1]} > 04 ]]; then
user=${cur%%?(\\)[.:]*}
group=${cur#*[.:]}
COMPREPLY=( $( compgen -P $user':' \
-g -- $group 2>/dev/null) )
else
COMPREPLY=( $( compgen -S ':' -u $cur ) )
fi
else
_filedir
fi
return 0
}
complete -F _chown $filenames chown
ざっと読んだ限りでは、_chown() という関数を作り、それをchown の補完対象として呼び出すようにしている。_chwon() の前半は chwon のオプションを補完。後半は引数の補完だが、第一引数かどうかを判定し、第一引数だったらユーザ名、そうでなかったらファイル名を補完しているようだ。
なんか面倒だなあ。zsh に乗り換えた方が良いのかなあ。
bash に限らず、シェルはカスタマイズ次第で使い勝手が大きく変わる。
http://pc.2ch.net/test/read.cgi/unix/1013019416/
Working more productively with bash 2.x
http://www.caliban.org/bash/index.shtml#completion
以下、説明。
全く同じコマンドは重複して記録しないように ignoredups を設定。
- 参考になるページ
bash で補完スレhttp://pc.2ch.net/test/read.cgi/unix/1013019416/
Working more productively with bash 2.x
http://www.caliban.org/bash/index.shtml#completion
- 私の /etc/profile の中身
HISTSIZE=10000
HISTFILESIZE=10000
HISTCONTROL=ignoredups
export HISTSIZE HISTFILESIZE HISTCONTROL
PS1='[\u@\h \W]\$ '
PS2='> '
PS4='+ '
alias ll='ls -l'
alias la='ls -aF'
alias lla='ls -al'
alias top='nice -10 top -d 2'
complete -d cd
complete -u {su,finger,chown}
complete -c man
complete -A stopped -P '%' bg
complete -j -P '%' fg jobs disown
complete -c command type which
PATH="$PATH":/usr/local/pgsql/bin
PG=/usr/local/pgsql
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH":$PG/lib
export PGLIB=$PG/lib
export PGDATA=$PG/data
export http_proxy=http://10.241.4.1:10241/
export ftp_proxy=http://10.241.4.1:10241/
export https_proxy=http://10.241.4.1:10241/
以下、説明。
- 履歴とプロンプト
十分な数のコマンド履歴を保存したいので、十万までヒストリーを拡張。全く同じコマンドは重複して記録しないように ignoredups を設定。
HISTSIZE=10000
HISTFILESIZE=10000
HISTCONTROL=ignoredups
export HISTSIZE HISTFILESIZE HISTCONTROL
PS1='[\u@\h \W]\$ '
PS2='> '
PS4='+ '
- エイリアスやコマンド補完
complete を使うと状況に応じて補完する対象を限定できる。たとえば、complete -d cd だと cd コマンドの 引数には、ファイルに cd することはあり得ないのでディレクトリのみ表示する、など。alias ll='ls -l'
alias la='ls -aF'
alias lla='ls -al'
alias top='nice -10 top -d 2'
complete -d cd
complete -u {su,finger,chown}
complete -c man
complete -A stopped -P '%' bg
complete -j -P '%' fg jobs disown
complete -c command type which
- PostgreSQL 用設定
ほとんどのユーザが DB を利用するので追加。PATH="$PATH":/usr/local/pgsql/bin
PG=/usr/local/pgsql
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH":$PG/lib
export PGLIB=$PG/lib
export PGDATA=$PG/data
- wget 用 proxy 設定
export http_proxy=http://10.241.4.1:10241/
export ftp_proxy=http://10.241.4.1:10241/
export https_proxy=http://10.241.4.1:10241/
bash をカスタマイズして使いやすくする。
http://www.atmarkit.co.jp/flinux/rensai/theory09/theory09b.h ...
history の個数を設定するには、HISTFILESIZE と HISTSIZE を設定する。
- @IT の特集記事。
Windowsユーザーに教えるLinuxの常識 第9回 bashの便利な機能を使いこなそうhttp://www.atmarkit.co.jp/flinux/rensai/theory09/theory09b.h ...
history の個数を設定するには、HISTFILESIZE と HISTSIZE を設定する。
- 私好みのヒストリー設定
過去のコマンドを再利用しやすいように、history の件数は十分に確保する。HISTSIZE=100000
HISTFILESIZE=100000
HISTCONTROL=ignoredups