と書いてみたけど、ほとんどbashの話題。 FreeBSDだと一般ユーザーではbashでないshが、スーパーユーザーではcsh(tcsh)が立ち上がったりするけど、使いにくいので真っ先にportsからbashを入れて置き換えてしまう・・・。 最新のは追いかけてないけど今でもそうなのかなぁ。
24 Oct 2014 | エイリアス追加。 |
以前メモ帳にまとめたんだけどな。 bashの場合、インタラクティブシェルかどうか、ログインシェルかどうかで読み込む起動スクリプトが違う。 また、リンクを張ってshとして起動されるとまた違う動作になる。
ログインシェルとは、argv[0][0]=='-'
であるか、--login
オプションと共に起動されたシェルのことである。
argv[0][0]=='-'
というのは、起動するときに例えば -bash というコマンド名で起動した場合、である。
これはリンクを張って起動した場合も当てはまるが、多くの場合はloginコマンドがargv[0]に-bashを指定してexecした場合だろう
(exec系の関数は実際に実行するプログラムのパスと、そのプログラムに渡す「最初の引数」=argv[0]=「通常はコマンド名」を別々に指定できる)。
インタラクティブ(対話)シェルとは、入力とエラー出力が端末に接続されていて、-cオプションが指定されておらず、オプション以外の引数が指定されていない状態で起動されたシェルのことである。 つまり、キーボードから入力できて、出力は端末画面に表示されて、シェルスクリプトを実行するために起動されたシェルではない、ということ。
上の条件を満たさなくても-i
オプションを指定して起動すると強制的にインタラクティブ扱いになる。
インタラクティブ扱いだと環境変数PS1(プロンプト指定)がセットされ、変数$-
にi
が含まれる。
echo $-
としてみれば意味が分かるだろう。
◆ログインシェルの場合:
まず /etc/profile を実行。
続いて ~/.bash_profile・~/.bash_login・~/.profile のうち最初に見つかったものを実行。
--noprofile
を付けるとこの挙動が抑制される。
◆ログインシェルが終了するとき:
~/.bash_logoutを実行。
◆ログインシェルではないインタラクティブシェルとして起動された場合:
~/.bashrcを実行。
-norc
でこの挙動を抑制、--rcfile
でファイル名を指定できる。
◆非インタラクティブシェルとして起動された場合:
BASH_ENV環境変数に指定されたファイルを実行。
あとで実験するが、インタラクティブでなければログインシェルでもBASH_ENVに指定されたファイルが実行される。
◆ログインシェルの場合:
/etc/profileを実行。
~/.profileを実行。
--noprofile
を付けるとこの挙動が抑制される。
◆インタラクティブシェルとして起動された場合:
ENV環境変数で指定されたファイルを実行。 bashと違って、インタラクティブならばログインシェルの場合でもこの動作が行われる。
◆インタラクティブではない場合:
何も実行されない。
例えばsshdがbashをexecしたような場合。
.bashrcを実行。
-norc
でこの挙動を抑制、--rcfile
でファイル名を指定できるが、rshdやsshdから指定できなければならない。
あとで実験するが、ログインシェルではなく、ネットワークから接続された場合はインタラクティブ扱い、ということらしい。 ログインシェルならば非インタラクティブ扱いのまま。
-pオプションをつけない限り、スタートアップファイルは実行されず、環境からシェル関数を引き継がない。 他にも若干違いがある。 -pを付けると通常と同じ動作になるが、EUIDはそのままらしい。 マニュアルの受け売りで深く考えたわけではないのでよく分からん。
ログイン | 非ログイン | |
---|---|---|
インタラクティブ | 1. /etc/profile 2. ~/.bash_profile・~/.bash_login・~/.profile のうち最初に見つかったもの | ~/.bashrc |
ネットワーク | 1. /etc/profile 2. ~/.bash_profile・~/.bash_login・~/.profile のうち最初に見つかったもの 3. BASH_ENVで指定されたファイル | |
非インタラクティブ | BASH_ENVで指定されたファイル |
bashでログインすると~/.bash_profileが読み込まれるので、こんなのを書いておく。
if [ -r ~/.profile ]; then . ~/.profile; fi if [ -r ~/.bashrc ]; then . ~/.bashrc; fi
すると、bashでもshでもログインすると~/.profileが読み込まれる。 ~/.profileにはこんなのを書いておく。 FreeBSDではデフォルトでこうなっていた。
export ENV=~/.shrc
shの場合にはインタラクティブならログインシェルでも$ENVが読み込まれるので、これで~/.shrcが読み込まれる。 bashの場合にはログインシェルだとこの動作が行われないので、上の例のように~/.bash_profileから~/.bashrcを読み込むようにし、~/.bashrcで
if [ -r "$ENV" ]; then . "$ENV"; fi
とすることで、~/.shrcが読み込まれる。 以後、shをインタラクティブに起動するとENVで指定した~/.shrcが読み込まれ、bashをインタラクティブに起動すると~/.bashrc経由でENVで指定した~/.shrcが読み込まれる。
システムワイドな設定(PRINTERとかかねぇ)は/etc/profileに書く。 常駐モノ(ssh-agentとかだな)や共通の環境(CVS_RSHとかだな)は~/.profileに書く。 bashじゃないと怒られるものは~/.bash_profileに書く。 shを起動したときに設定したい環境変数は~/.shrcに、bashを起動したときに設定したい環境変数は~/.bashrcで$ENVを読み込んだ後に書く。 shでログインした後、bashを起動したらPS1を再設定とかね。
・・・っていうことでいいのかな。
.bash_profileと.bashrcにechoを入れて実験してみる。
BASH_ENV環境変数についても実験するため、~/.bash_environというファイルを作り、echoを書いておいて、.bashrcでexport BASH_ENV='~/.bash_environ'
しておく。
bashのバージョンはCentOS5.5をインストールしてyum updateしたやつで、
$ bash --version GNU bash, version 3.2.25(1)-release (i686-redhat-linux-gnu) Copyright (C) 2005 Free Software Foundation, Inc.
ちょっと古いな。 Cygwinのは4.1.10だった。
Login: user Password: Last login: ... [~/.bash_profile is read] [reading ~/.bashrc] [~/.bashrc is read]
普通にインタラクティブログインシェル。
ログインシェルだとインタラクティブでも.bashrcが実行されないので、.bash_profileの中に. ~/.bash_rc
などと書いてあり([reading ~/.bashrc]
の部分がそう)、そのおかげで.bashrcが実行されている。
$ bash [~/.bashrc is read]
普通にインタラクティブシェル。
$ bash -c 'ls -l /proc/$$/fd' [~/.bash_environ is read] 合計 0 lrwx------ 1 user user 64 7月 7 22:59 0 -> /dev/pts/0 lrwx------ 1 user user 64 7月 7 22:59 1 -> /dev/pts/0 lrwx------ 1 user user 64 7月 7 22:59 2 -> /dev/pts/0 lr-x------ 1 user user 64 7月 7 22:59 3 -> /proc/3651/fd
非ログイン・非インタラクティブなので、BASH_ENV環境変数で指定したファイルが実行される。
余談だが、CentOS5.6を使ったらデフォルトでLESSOPEN='|/usr/bin/lesspipe.sh %s'
となっていて、lesspipe.shが実行されるときに.bash_environが実行されてechoが出てしまい、これが原因でlessの動作が台無しに。
以後、echo '[~/.bash_environ is read]' >&2
として実験。
これは標準エラー出力もファイルに出したいときによく使う2>&1
の逆だね。
$ bash --login -c 'ls -l /proc/$$/fd' [~/.bash_profile is read] [reading ~/.bashrc] [~/.bash_rc is read] [~/.bash_environ is read] 合計 0 lrwx------ 1 user user 64 7月 7 23:16 0 -> /dev/pts/0 lrwx------ 1 user user 64 7月 7 23:16 1 -> /dev/pts/0 lrwx------ 1 user user 64 7月 7 23:16 2 -> /dev/pts/0 lr-x------ 1 user user 64 7月 7 23:16 3 -> /proc/3888/fdログインシェルに対する.bashrcと違って、ログインでも非インタラクティブならBASH_ENVで指定されたスクリプトが読み込まれるようだ。 あと、CentOS5.6では.bash_logoutに/usr/bin/clearが書いてあって、ログアウトすると画面がクリアされるのだが、この場合はクリアされなかった。 これはちょっとマズいんでない? Cygwinのbash-4.1.10でも.bash_logoutが実行されなかった。
$ ssh localhost Last login: ... [~/.bash_profile is read] [reading ~/.bashrc] [~/.bashrc is read] $ ls -l /proc/$$/fd 合計 0 lrwx------ 1 user user 64 7月 7 23:14 0 -> /dev/pts/1 lrwx------ 1 user user 64 7月 7 23:14 1 -> /dev/pts/1 lrwx------ 1 user user 64 7月 7 23:14 2 -> /dev/pts/1 lrwx------ 1 user user 64 7月 7 23:14 255 -> /dev/pts/1
当然インタラクティブログインシェル。
$ ssh -T localhost [~/.bash_profile is read] [reading ~/.bashrc] [~/.bash_rc is read] stty: 標準入力: 無効な引数です [~/.bash_environ is read] ls -l /proc/$$/fd 合計 0 lrwx------ 1 user user 64 7月 7 22:46 0 -> socket:[13920] lrwx------ 1 user user 64 7月 7 22:46 1 -> socket:[13920] lrwx------ 1 user user 64 7月 7 22:46 2 -> socket:[13922] ps x PID TTY STAT TIME COMMAND 2826 ? S 0:00 sshd: user@pts/0 2827 pts/0 Ss 0:00 -bash 3152 pts/0 S+ 0:00 ssh -T localhost 3155 ? S 0:00 sshd: user@notty 3156 ? Ss 0:00 -bash 3187 ? R 0:00 ps x
sshdがハイフン付きでbashを起動していてログインシェルとして扱われていることが分かる。 しかし、入出力は端末ではなく、-iも指定されていないのでインタラクティブではない。 したがってPS1が設定されず、プロンプトが出ない。 細かいことを言うとプロンプトはシステムのbashrcスクリプトが設定していたりするが、PS1が設定されているときのみPS1を再設定する形になっていて、PS1が元々設定されていないとこの例のようにプロンプトが出ない。 sttyが文句を言っているのは.bash_profileにstty -ixon -ixoffが書いてあるから。 非インタラクティブなので.bash_environも実行されている。
$ ssh localhost 'ls -l /proc/$$/fd' [~/.bashrc is read] 合計 0 lrwx------ 1 user user 64 7月 7 23:03 0 -> socket:[20828] lrwx------ 1 user user 64 7月 7 23:03 1 -> socket:[20828] lrwx------ 1 user user 64 7月 7 23:03 2 -> socket:[20830] lr-x------ 1 user user 64 7月 7 23:03 3 -> /proc/4611/fd
.bash_profileが実行されていないのでログインシェルではない。 また、コマンドが指定されているのでインタラクティブシェルでもない。 sshは後ろにコマンドを指定するとログイン扱いにせず、端末も割り当てないようだ。 まあ当たり前か。 この結果、ネットワーク越しのルールが適用されて、インタラクティブではないが.bashrcが実行され、マニュアルには書いてないがBASH_ENVは無視されて.bash_environは実行されていない。 つまり「インタラクティブ扱い」ということらしい。
ということは、トンネル使って
$ ssh user@remote 'tar cfvz - outgoing' | tar xf -なんてやるときに.bashrcが変なものを表示してると、ゴミが混ざるということか・・・。
$ ssh -t localhost 'ls -l /proc/$$/fd' [~/.bash_environ is read] 合計 0 lrwx------ 1 user user 64 7月 7 23:08 0 -> /dev/pts/1 lrwx------ 1 user user 64 7月 7 23:08 1 -> /dev/pts/1 lrwx------ 1 user user 64 7月 7 23:08 2 -> /dev/pts/1 lr-x------ 1 user user 64 7月 7 23:08 3 -> /proc/4781/fd Connection to localhost closed.
コマンドが指定されているのでsshはbashを非ログインシェルとして起動し、bashは非インタラクティブと判断する。 入力が端末なのでネットワーク越しのルールも適用されない。 よって.bash_profileも.bashrcも実行されず、.bash_environだけが実行される。
$ alias hoge=fuga $ alias alias hoge=fuga $ bash -c alias
BASH_ALIASESという連想配列があるが、これもexportできないようだ。
$ alias hoge="echo hoge"; hoge -bash: hoge: command not found $ hoge hoge $ unalias hoge; hoge hoge $ hoge -bash: hoge: command not found
最初のaliasでは定義しているのに見つからず、次のunaliasでは抹消したのにまだ実行できている。
$ bash -c 'alias hoge="echo fuga" > hoge' bash: line 1: hoge: command not found $ bash -i -c 'alias hoge="echo fuga" > hoge' fuga
aliasの定義と実行の行は別にしないといけないので、コマンド文字列の途中で改行している。
この挙動のため、たとえBASH_ENVを使ってaliasを定義したとしても、シェルスクリプトなどから呼ばれた場合はaliasは実行されない。 この挙動は変更できて、expand_aliasesシェルオプションを有効にするとaliasが解釈されるようになる。
$ bash -O expand_aliases -c 'alias hoge="echo fuga" > hoge' fuga $ bash -c 'shopt -s expand_aliases; alias hoge="echo fuga" > hoge' fuga
どちらも意味は同じ。 前者はbash起動時のコマンドラインオプションで指定、後者はbash起動後にshoptコマンドで設定。
Subversionを最新のものに変えたときにこの挙動に気づいた。 うちのSubversionはコミットログ入力の際に秀丸が立ち上がるようになっている。 Subversionを最新のものに変えたら、秀丸からsvn ciすると、
svn: E205001: コミットに失敗しました (詳しい理由は以下のとおりです): svn: E205001: 非対話的にログメッセージを取得するためのエディタを呼び出せませんと言われてコミットできなくなった。 この訳だと何が原因でエディタが呼び出せないのかいまひとつはっきりしない。 実は原文では、
svn: E205001: Cannot invoke editor to get log message when non-interactiveなので、「非対話モードの場合は、ログメッセージを取得するためにエディタを起動できません」が正しいっぽい。 秀丸のコマンド実行は入出力がリダイレクトされた状態になっているので、Subversionが対話モードではない、と判断したようだ。 CygwinのminttyからSlikSVNを起動するような場合も引っかかる(CygwinのSubversionなら当然大丈夫のはず)。
svnのオプションとして--force-interacitveを付ければ動くというのは分かったが、シェルスクリプトやシェル関数にするのも面倒なので、bashのaliasにしてみた。 すると、minttyからはうまく動くのに、秀丸からbash経由で実行してもうまく動かない。 秀丸からbashを立ち上げると、bashは非対話モードで動くのでaliasが解釈されず、生のsvnがそのまま実行されていた。 仕方がないので、秀丸マクロの方でbashに-O expand_aliasesを渡して解決することにした。
これってUnixシェルの普通の挙動なのかな?
$ bash -c 'ps w' PID TTY STAT TIME COMMAND 4221 pts/0 Ss 0:00 -bash 5241 pts/0 R+ 0:00 ps w $ bash -c 'ps w; ps w' PID TTY STAT TIME COMMAND 4221 pts/0 Ss 0:00 -bash 5242 pts/0 R+ 0:00 bash -c ps w; ps w 5243 pts/0 R+ 0:00 ps w PID TTY STAT TIME COMMAND 4221 pts/0 Ss 0:00 -bash 5242 pts/0 R+ 0:00 bash -c ps w; ps w 5244 pts/0 R+ 0:00 ps w
4221が親のbashで、先頭がハイフンなのでログインシェル。 psを一回だけ指定すると子bashがない。 forkせずにexecしたようだ。 それに対してpsを2回実行すると5242として子bashが現れる。
これはssh経由で起動した場合も同じで、sshのマニュアルでは単に「コマンドラインの最後に書かれたコマンドを実行」となっているが、
$ ssh localhost 'ps wx' [~/.bashrc is read] PID TTY STAT TIME COMMAND 4220 ? S 0:01 sshd: user@pts/0 4221 pts/0 Ss 0:00 -bash 5405 pts/0 S+ 0:00 ssh localhost ps wx 5408 ? S 0:00 sshd: user@notty 5409 ? Rs 0:00 ps wx
.bashrcが実行されるのでシェルが介在しているにもかかわらず、forkせずにpsがexecされて子bashが出てこない。 psを2回実行すると、
$ ssh localhost 'ps wx; ps wx' [~/.bashrc is read] PID TTY STAT TIME COMMAND 4220 ? S 0:01 sshd: user@pts/0 4221 pts/0 Ss 0:00 -bash 5434 pts/0 S+ 0:00 ssh localhost ps wx; ps wx 5437 ? S 0:00 sshd: user@notty 5438 ? Ss 0:00 bash -c ps wx; ps wx 5463 ? R 0:00 ps wx PID TTY STAT TIME COMMAND 4220 ? S 0:01 sshd: user@pts/0 4221 pts/0 Ss 0:00 -bash 5434 pts/0 S+ 0:00 ssh localhost ps wx; ps wx 5437 ? S 0:00 sshd: user@notty 5438 ? Ss 0:00 bash -c ps wx; ps wx 5464 ? R 0:00 ps wx
子bashが出る(5438)。
$ foo=hoge $ sh -c "/bin/echo $foo; foo=fuga; /bin/echo $foo"
親環境で$fooが全部展開されてしまうので、出力はhoge / hoge
。
$ foo=hoge $ sh -c '/bin/echo $foo; foo=fuga; /bin/echo $foo'
シングルクオートは変数を展開しない。
また、fooはexportしてないから、 / fuga
$ foo=hoge $ { /bin/echo $foo; foo=fuga; /bin/echo $foo }
hoge / fuga
になる。
その後親シェルでfooを調べると、fuga
になる。
$ foo=hoge $ ( /bin/echo $foo; foo=fuga; /bin/echo $foo )
これが不思議。
hoge / hoge
でも / fuga
でもなく、hoge / fuga
になる。
その後親シェルでfooを調べると、当然hoge
のまま。
さらに、
$ foo=hoge $ ( /bin/echo \$foo; foo=fuga; /bin/echo \$foo )
これは$foo / $foo
になる。
sh -c "..."
に渡せば当然 / fuga
になるのに。
$ echo `echo '\\'` \ $ echo $(echo '\\') \\
バッククオートと$( )
ではバックスラッシュの扱いが違うらしい。
'$foo'
と"$foo"
は同じ結果になる。
user@host workdir$
というシンプルなもの。
一台のマシンから複数のホストへ接続するから、ホスト名は入れておかないと。
ESC]0;
というのはxtermのエスケープシーケンスで、アイコン名とウィンドウタイトル、つまり、端末ウィンドウのタイトルバーを変更する。
文字列は直後に続け、^G
、つまりBELLで終わる。
ちなみに0を1にするとアイコン名のみ、2にするとウィンドウタイトルのみの変更。
で、文字列の部分は${USER}@${HOSTNAME%%.*}:${PWD/#$HOME/~}
になっているから、「ユーザー名@ホスト名:カレントディレクトリ」になる。
%%.*
は最長一致でサフィックスを削除だから、ホスト名はピリオドの直前まで。
/#$HOME/~
は先頭からの一致部分を置換だから、ホームディレクトリはチルダに置換されるが、ホームディレクトリの外にいる場合はそのままフルパスになる。
結局、コマンドプロンプトが出るたびにタイトルバーがプロンプトと同じように変わる。 タイトルバーを変えておくと端末ウィンドウを[Alt]+[Tab]なんかで切り替えるときに便利。
某所の某ホストはホスト名がlocalhostのままになっていて、このままだと非常に紛らわしいので、
if [ "$PROMPT_COMMAND" != "" ]; then export PROMPT_COMMAND=${PROMPT_COMMAND/'${HOSTNAME%%.*}'/name-of-this-host} fi
とわざわざ置き換えていたりする(笑)。
sttyをベタに書くと入出力が端末ではなかったときに先ほどの例のように文句を言われる。 それがいやなら、
if [ -t 0 ]; then stty -ixoff -ixon; fi
とすればよい。[ -t 0 ]
はファイルディスクリプタ0、つまり標準入力が端末につながっている場合に真。
addpath () { if [ -z "$1" ]; then echo "usage: addpath <dir>"; return 1; fi PATH=$PATH:`cd "$1" && pwd` }
指定するディレクトリは相対パスでいい。指定されたディレクトリにcdしてpwdすることで絶対パスを得ている。 コマンドによる文字列の置き換えはサブシェルを起動する(bashのマニュアルには書いてないがPOSIXのマニュアルには書いてあった)ので、カレントディレクトリを覚えておく必要はない。 realpathというコマンドがあるシステムもあるし、pushd・popdを使ってもできそうだが、これだとPOSIXの範囲でできて、しかも簡単だ。 PATHが空だと頭に余計に:が付くが、空だとpwdが実行できないからまぁいいだろう。
remove-path () { if [ -z "$1" ]; then echo "usage: remove-path <dir>" >&2; return 1; fi abspath=$(cd "$1"; pwd | sed -e 's!/!\\/!g' -e 's/$/\\\/\\?/') PATH=$( echo $PATH | sed -e "s/^$abspath://" | sed -e "s/:$abspath\$//" | sed -e "s/:$abspath:/:/" | sed -e "s/^$abspath\$//" ) echo $PATH }sedで処理するために/を\/に置き換え、さらに最後に/があってもなくてもいいように/?を付け加えて、頭・最後・途中・単独のものを順に削除している。 相対パスで指定すればいいのはaddpathと同じ。 addpathの場合は存在しないパスを指定することはまずないだろうが、remove-pathの場合は実際には存在しないパスを削除したいこともある(PATHにゴミが残っている場合)。 が、パスが存在してないとcdできないので削除できない。
list-path () { echo $PATH | sed -e 's/:/\n/g' }
man which
の例からパクってきたんだと思う。
エイリアスやシェル関数も探してくれる。
which () { (alias; declare -f) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot $@ }
他に、ディレクトリだけ表示するために、
lsdir () { if [ $# -eq 0 ]; then ls -l -F --color "$@" | grep ^d else ls -ld -F --color "$@" | grep ^d fi }なんてのが定義してある。 ls -l すると、行の先頭に
d
が付くので、それをgrepで拾っている。
パラメータがないとls -l
になるので、lsdirだけ打ち込むとカレントディレクトリにあるディレクトリが表示される。
パラメータを指定するとls -ld
になるので、指定されたファイル名のうちディレクトリであるものだけが表示される。
例えばlsdir a*
とやれば、aで始まるディレクトリが表示される。
--color
は強制的にカラー表示にする。
デフォルトでは端末につながっているときだけカラー表示するが、出力をパイプでgrepに渡している関係でこうしないとカラーにならない。
ただ、こうしてしまうとリダイレクトやパイプラインでもカラーのままなので、less -R に渡してもカラーになる反面、sedに渡したりファイルに吐いて編集するような場合は都合が悪い。
まあ、そういうときは生のlsを使っておくれ。
どうしてもという人はif [ -t 1 ]
で--colorを指定したりしなかったりすればよい。
mount-binary () { if [ -z "$1" ]; then echo "usage: mount-binary <dir>"; return 1; fi; mount -o binary,noacl `cygpath -wa "$1"` `cygpath -ua "$1"` }
cygpathを使って指定されたパスをWin32絶対パスとCygwin絶対パスに変換しているので、引数に相対パスをひとつだけ指定すればいいところがミソ。
mount-binary bindir
とすれば、カレントディレクトリにあるbindirというディレクトリがバイナリマウントされるし、mount-binary .
とすれば、カレントディレクトリをバイナリマウントしてくれる。
Copyright (C) 2011-2012 You SUZUKI
$Id: shellscript.htm,v 1.11 2015/09/23 02:00:58 you Exp $