まずは事の発端を説明します。
/etc/passwdファイルは、ユーザー情報が1行に1ユーザー分ずつ書かれたテキストファイルです。1行はコロンで区切られた7つのフィールドからなります。僕は、最初のフィールドであるユーザー名と、最後(7番目)のフィールドであるログインシェルだけを確認したかったのです。cat /etc/passwd として、直接目で見るだけ*1でもユーザー名とログインシェルを読み取れるのですが、ちょっと辛い。なるべく見やすく表示したいのです。
この課題を狂言回しにして、Linux/Unixコマンドラインでちょっとした事をやるための地味なコマンド達とシェル構文を紹介します。「スクリプト言語(例えばperl)を使えばいいじゃねえか」というご意見・ご指摘はゴモットモだと思いますが、今回は聞く耳持ちません。(スクリプト言語処理系ではない)コマンドとシェルの範囲内でのもっと良い方法は是非にご教示ください。
([追記]id:Magicantさんに色々ご教示いただきました。ありがとうございます。[/追記])
話題にするコマンド:
- cutコマンド
- trコマンド
- echoコマンド
- printfコマンド
話題にするシェル構文:
- 行の継続
- バッククォート
- エスケープとクォーティング
- for文
内容:
- cutコマンドってけっこう便利
- cutコマンドの出力デリミタを指定する
- コマンドラインからタブ文字を入力するのが一苦労
- コマンドラインにおけるエスケープとクォーティング
- printfコマンドを使ってみる
- printfコマンドの繰り返し実行
cutコマンドってけっこう便利
/etc/passwdのような、区切り文字で区切られたフィールドからなるレコード(テキスト行ですが)を操作するにはcutコマンドが使えます。man cut とか http://www.linux.or.jp/JM/html/gnumaniak/man1/cut.1.html を見てください。今回使うオプションは次の2つです。
- -d DELIM または --delimiter=DELIM
- -f FIELD-LIST または --fields=FIELD-LIST
-d にセミコロン文字「:」、-f に取り出すフィールドの番号を指定します。-fの指定の仕方は:
$ cut -d: -f1 /etc/passwd # 最初のフィールドだけ取り出す $ cut -d: -f1,7 /etc/passwd # 1番目と7番目のフィールドを取り出す $ cut -d: -f5- /etc/passwd # 5番目より後のフィールドを取り出す $ cut -d: -f-3 /etc/passwd # 3番目までのフィールドを取り出す $ cut -d: -f3-6 /etc/passwd # 3番目から6番目までのフィールドを取り出す $ cut -d: -f3,4,5,6 /etc/passwd # 上と同じ(3番目から6番目まで)
cut -d: -f1,7 /etc/passwd を実際にやってみると:
[hiyama@microapplications ~]$ cut -d: -f1,7 /etc/passwd | head -10 root:/bin/bash bin:/sbin/nologin daemon:/sbin/nologin adm:/sbin/nologin lp:/sbin/nologin sync:/bin/sync shutdown:/sbin/shutdown halt:/sbin/halt mail:/sbin/nologin news: [hiyama@microapplications ~]$
別な例として、/bin/ls -l の出力からパーミッション、オーナー、ファイル名だけを取り出すことにしましょう。/bin/ls -l の出力は空白(番号0x20の文字)で区切られた9つのフィールドからなるので、 cut -d ' ' -f1,3,9 を使えばよさそうですが、桁揃えの余分の空白があってうまくいきません。tr の -s (--squeeze-repeats)オプションで重複する空白を潰しておけばOKです。なお、コマンドライン行末に「\」を付けると、どんな場所であっても行を継続してコマンドラインを書き続けることができます。
[hiyama@microapplications ~]$ /bin/ls -l /etc/vsftpd | tr -s ' ' \ > | cut -d ' ' -f1,3,9 合計 -rw-r--r-- root user_list.deny.sample -rw------- root vsftpd.conf -rw------- root vsftpd.conf.orig -rw------- root vsftpd.conf~ [hiyama@microapplications ~]$
最初の行の「合計」ってのが邪魔ですが、まーいいとしましょう。それと、ファイル名に空白が入ると変なことになりそうですが、そこまでは知りません*2。
cutコマンドの出力デリミタを指定する
http://www.linux.or.jp/JM/html/gnumaniak/man1/cut.1.html のmanページには載ってないのですが、たいていGNUのcut実装には --output-delimiter というオプションがあり、出力のデリミタを変更できます。(以下のように、コマンドラインが完結してないときは、行末の「\」がなくても次の行へとコマンドラインを続けることができます。)
[hiyama@microapplications ~]$ cat /etc/passwd | > cut -s -d: -f1,7 --output-delimiter=' --> ' | head -10 root --> /bin/bash bin --> /sbin/nologin daemon --> /sbin/nologin adm --> /sbin/nologin lp --> /sbin/nologin sync --> /bin/sync shutdown --> /sbin/shutdown halt --> /sbin/halt mail --> /sbin/nologin news --> [hiyama@microapplications ~]$ /bin/ls -l /etc/vsftpd | > tr -s ' ' | cut -d ' ' -f1,3,9 --output-delimiter=' | ' 合計 -rw-r--r-- | root | user_list.deny.sample -rw------- | root | vsftpd.conf -rw------- | root | vsftpd.conf.orig -rw------- | root | vsftpd.conf~ [hiyama@microapplications ~]$
--output-delimiter オプションを利用すると、見やすさを改善できますね。
シェルの複数行入力を使うと、改行を出力デリミタにすることもできます。以前、「sedにおける改行の表現とシェルの複数行入力」において次のコマンドラインを紹介しました。
$ echo $PATH | sed -e 's/:/\ > /g'
これと同じことを、cutコマンドを使ってやってみます。
[hiyama@microapplications ~]$ echo $PATH | cut -d: -f1- --output-delimiter=' > ' /usr/local/python/bin /usr/kerberos/bin /usr/local/bin /bin /usr/bin /usr/X11R6/bin /home/hiyama/bin /sbin /usr/sbin [hiyama@microapplications ~]$
コマンドラインからタブ文字を入力するのが一苦労
cat /etc/passwd | cut -s -d: -f1,7 でほぼ当初の目的は達成されるのですが、出力区切り記号をタブにしたらより見やすいのではないかと思いました。cat /etc/passwd | cut -s -d: -f1,7 --output-delimiter='<タブ文字>' とすればいいはずです。
ところが! キーボードからコマンドラインにタブ文字を入力することができない。bashは、タブ文字をファイル名補完命令と解釈してデータ文字として扱いません。いくつかのエスケープ記法や、Emacsからの類推で Ctrl+Q を前置してみましたがダメ。ウーム、わからん。
Googleで検索してみました。https://discussionsjapan.apple.com/message/100302527 に「Terminal.app で,bash に対してキーボードからタブ文字が入力できません.どうすればよいのでしょうか?」という僕の問題と一致する質問がありました。が、答えが的外れのトンチンカンばっかでまったくラチ開かない。例えば:
自分はこの方面には詳しくないので、こうすればいい、と解決策を提供することはできませんが、その方法はあるはずです。
なんなんだよ、コレ(怒)。([追記]Magicantさんのコメントによると、Ctrl+V を前置すればいいということです。[/追記])
で、とりあえずechoコマンドでタブ文字を出力させることに。echoコマンドの -e オプションでエスケープが効く*3ようになるのでこれを使います。
[hiyama@microapplications ~]$ echo -n -e '\t' | od -x 0000000 0009 0000001 [hiyama@microapplications ~]$
確かに0x09番の文字=タブが得られるようです。こいつをコマンドラインのなかに埋め込むにはバッククォート構文 `echo -n -e '\t'` を使えばいいでしょう。
[hiyama@microapplications ~]$ cat /etc/passwd | > cut -s -d: -f1,7 --output-delimiter="`echo -n -e '\t'`" | > head -10 root /bin/bash bin /sbin/nologin daemon /sbin/nologin adm /sbin/nologin lp /sbin/nologin sync /bin/sync shutdown /sbin/shutdown halt /sbin/halt mail /sbin/nologin news [hiyama@microapplications ~]$
あれれ、これでも一部分ガタガタで、桁がそろってません。
コマンドラインにおけるエスケープとクォーティング
先に進む前に、タブや改行のエスケープ表現について少し触れておきます。まずは実験から:
[hiyama@microapplications ~]$ echo push-button push-button [hiyama@microapplications ~]$ echo p\u\sh-\bu\tto\n push-button [hiyama@microapplications ~]$ echo "p\u\sh-\bu\tto\n" p\u\sh-\bu\tto\n [hiyama@microapplications ~]$ echo 'p\u\sh-\bu\tto\n' p\u\sh-\bu\tto\n [hiyama@microapplications ~]$
バックスラッシュ(環境により円マーク)+1文字は、特別な解釈を持つことがあります。
しかし、シェルのコマンドラインでは、バックスラッシュ+1文字は単に文字そのものを表すだけです。「\n」は文字「n」なのです。ダブルクォートやシングルクォートで囲むと、「\」も単なる文字扱いになります。「\」やクォートをどのように使うかというと、例えば:
[hiyama@microapplications ~]$ echo \"double\ quoted\"\ \ \'single\ quoted\' "double quoted" 'single quoted' [hiyama@microapplications ~]$ echo "\"quoted\"" "quoted" [hiyama@microapplications ~]$ perl -e "print \"hello\\n\"" hello [hiyama@microapplications ~]$ echo '\"quoted\"' \"quoted\" [hiyama@microapplications ~]$ echo '"quoted"' "quoted" [hiyama@microapplications ~]$ echo '\' \ [hiyama@microapplications ~]$ echo '\'single\ quoted\'' > ' \single quoted' [hiyama@microapplications ~]$
最後の例から分かるように、シングルクォート内でシングルクォートをエスケープすることはできません。「'\''」(シングルクォート - バックスラッシュ - シングルクォート - シングルクォート)でエスケープできるってハナシもありますが、これはエスケープしてるんじゃないですね -- 単にクォートされた文字列を分割しているだけです。
もっとエグい例は「エスケープ祭り、バックスラッシュの嵐」をどうぞ。
printfコマンドを使ってみる
欄(カラム)をきれいに桁揃えして出力する話に戻りましょう。数値や文字列のフォーマティングといえば、やっぱりprintfでしょ。C言語の関数だけじゃなくて、コマンドとしてもprintfがあります。
[hiyama@microapplications ~]$ printf [%5d]\\n 12 [ 12] [hiyama@microapplications ~]$ printf [%-5d]\\n 12 [12 ] [hiyama@microapplications ~]$ printf [%05d]\\n 12 [00012] [hiyama@microapplications ~]$ printf [%10s]\\n hello [ hello] [hiyama@microapplications ~]$ printf [%-10s]\\n hello [hello ] [hiyama@microapplications ~]$
変数と一緒に使うこともできます。
[hiyama@microapplications ~]$ x=12; printf [%05d]\\n $x [00012] [hiyama@microapplications ~]$ for x in 12 260 7 89; do printf [%05d]\\n $x; done [00012] [00260] [00007] [00089] [hiyama@microapplications ~]$
printfコマンドの繰り返し実行
2つの変数$userと$login_shellに、ユーザー名とログインシェルが順番に入ってくるなら、printf %-10s%s\\n $user $login_shell というコマンドを繰り返し実行すると、きれいに桁揃えされた出力が得られるはずです。シェル(bash)が次のような比較的まともな構文(?)*4をサポートしてくれると嬉しいのですが。
List=`cut -s -d: -f1,7 --output-delimiter=' ' /etc/passwd`;\ for user, login_shell in $List; # リストから2つずつ項目を取り出して2つの変数にバインドする do printf printf %-10s%s\\n $user $login_shell; done
どうも、for文で使える変数は1つだけみたいです。「xargs が使えるかな」とも思ったのですが、標準入力から2つずつワードを取り出してコマンドに引数として渡す方法がわかりません。([追記]Magicantさんのコメントによると、xargs -L 2 とすればOK。また、while と read の組み合わせでワードを2つずつ取り出すこともできる、と。[/追記])
しょうがない。デリミタはコロンのままのリストを作って、printfの引数となる直前にコロンを空白に置き換えることにします。またしても、忌まわしいバッククォート構文の出番です。
[hiyama@microapplications ~]$ List=`cut -s -d: -f1,7 /etc/passwd`;\ > for x in $List; > do > printf %-10s%s\\n `echo $x | tr : ' '`; > done | head -10 root /bin/bash bin /sbin/nologin daemon /sbin/nologin adm /sbin/nologin lp /sbin/nologin sync /bin/sync shutdown /sbin/shutdown halt /sbin/halt mail /sbin/nologin news [hiyama@microapplications ~]$
これで目的達成です。コマンドラインを1行にしたいなら(行継続を使って折り返してますが):
[hiyama@microapplications ~]$ for x in `cut -s -d: -f1,7 /etc/passwd`;\ > do printf %-10s%s\\n `echo $x|tr : ' '`;done|head -10 root /bin/bash bin /sbin/nologin daemon /sbin/nologin adm /sbin/nologin lp /sbin/nologin sync /bin/sync shutdown /sbin/shutdown halt /sbin/halt mail /sbin/nologin news [hiyama@microapplications ~]$
しかしそれにしても、これだからシェルは…