USP Magazine 2014年11月号「シェル芸勉強会後追い企画 Haskellでやってはいかんのか?
Mon May 4 12:48:33 JST 2015 (modified: Sun Oct 1 10:50:27 JST 2017)
views: 4463, keywords: この記事は最終更新日が7年以上前のものです。
8. シェル芸勉強会後追い企画: Haskellでやってはいかんのか?¶
産業技術大学院大学・USP研究所・USP友の会 上田隆一
USP友の会のシェル芸勉強会 (脚注:シェルのワンライナー勉強会)は、 日々、他の言語からの他流試合に晒されているのである。 そこで上田は、Haskellで自ら他流試合を行い、 さらにシェル芸勉強会をいじめる自傷行為に手を 染めるのであった。
8.1. はじめに¶
こんちには。富山の元虚弱少年、ヘルパンギーナ上田です。 かれこれ10日間以上、喉にヘルパンギーナビールスを 飼っておりますが、なかなかこいつら死にません。 名前が似ているのでビールスはビールで消毒するとよいと思い、 毎日ビールを1リッター注入しているのですが、 死なないどころか悪化しております。 きっと私の中のビールス人口は、増えているものと思われます。 おそらくこの原稿がUSP Magazineに掲載される頃には、 ビールスで私の体が置換されて、 ビールスが本連載の原稿を書いていることでしょう。
8.2. 前回のおさらい¶
さてそんな前置きはどうでもいいんです。 体がしんどいですが今回も第1回シェル芸勉強会の3問目の途中から。 問題はこんなのです。
/etc の下にあるすべてのbashスクリプト ( #!/bin/bash で始まるもの) について以下の操作をしてください。
- ~/hoge というディレクトリにコピー
- その際、 「 #!/bin/bash 」を「 #!/usr/local/bin/bash 」 に変更
前回は /etc/ 下のbashスクリプトを発見するところまで書きました。 リスト1に前回最後のコードを再掲します。
- リスト1: q1_3_6.hs
1 2 3 4 5 6 7 8 9 | ueda@remote:~/Study1_Q3$ cat q1_3_6.hs
import FileTools
import qualified Data.ByteString.Lazy.Char8 as B
main = find "/etc" >>= mapM judgeBashFile >>= print
judgeBashFile :: FilePath -> IO (String,Bool)
judgeBashFile file = cat file >>= return . ((,) file) . isBash
where isBash bs = B.take 11 bs == B.pack "#!/bin/bash"
|
実行結果をリスト2に示します。 出力に grep をかけて発見した bashスクリプトのリストだけを表示しています。 この4つのファイルを ~/hoge/ ディレクトリに移し、 そのときに「 #!/bin/bash 」を「 #!/usr/local/bin/bash 」 に変換すれば問題完了です。
- リスト2: q1_3_6の実行
1 2 3 4 5 | ueda@remote:~/Study1_Q3$ sudo ./q1_3_6 | tr ')' '\\n' | grep True
,("/etc/alternatives/fakeroot",True
,("/etc/alternatives/lzfgrep",True
,("/etc/alternatives/lzmore",True
,("/etc/alternatives/lzdiff",True
|
8.3. ファイルをディレクトリにコピー¶
さて、作業を開始します。 まず、ファイルを ~/hoge/ 下にコピーするコードを書いてみました。 リスト3にコードを示します。
- リスト3: ファイルをコピーするコードを追加したq1_3_7.hs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ueda@remote:~/Study1_Q3$ cat q1_3_7.hs
import FileTools
import System.FilePath
import qualified Data.ByteString.Lazy.Char8 as B
main = find "/etc" >>= mapM judgeBashFile >>= mapM handleBashFile
judgeBashFile :: FilePath -> IO (String,Bool)
judgeBashFile file = cat file >>= return . (,) file . isBash
where isBash bs = B.take 11 bs == B.pack "#!/bin/bash"
handleBashFile :: (String,Bool) -> IO ()
handleBashFile (file,False) = return ()
handleBashFile (file,True) = cat file >>= B.writeFile dest
where dest = "/home/ueda/hoge/" ++ takeFileName file
|
main 関数で judgeBashFile の後ろに mapM handleBashFile と関数をつなげ、 12〜15行目のように handleBashFile を実装します。 13〜14行目ではパターンマッチをやっています。 13行目はbashスクリプトでないものを無視する処理、 14行目はコピー処理です。 13行目の return () ですが、 12行目の型が示すように型は IO () です。 なにも返すものがないときにこう書きます。 ちなみに print 関数も型は IO () です。 print は画面に何か書き出しますが、 関数としては何も返さないのでこうなります。
- リスト4: printの型はIO ()
1 2 3 4 | Prelude> :t print
print :: Show a => a -> IO ()
Prelude> :t print "hell"
print "hell" :: IO ()
|
この話はこれで終わりにしておきます。 とにかく型を合わせる方法として覚えておくので 構わないと考えます。
処理として実際に重要なのは14, 15行目です。 14行目では、先月号で FileTools.hs 内に定義した cat 関数で引っ張りだして、 >>= で右側の B.writeFile に渡しています。 B.writeFile はその名の通りファイルに データを書き出す関数です。 リスト5のように型を見ておきましょう。
- リスト5: writeFileの型
1 2 3 | Prelude> import qualified Data.ByteString.Lazy.Char8 as B
Prelude B> :t B.writeFile
B.writeFile :: FilePath -> B.ByteString -> IO ()
|
B.writeFile に渡している引数 dest は、書き出し先のファイル名です。 これは15行目で作っています。 file からディレクトリの部分を除去して、 /home/ueda/hoge/ と行き先のディレクトリを ハードコーディングしています [6] 。
8.4. 改ざん処理を加える¶
さて、さっさと終わらせましょう。 1行目のシバン( #!/bin/bash ) を( #!/usr/local/bin/bash ) に改ざん [7] します。
- リスト6: シバンを変更する処理を加えたq1_3_8.hs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ueda@remote:~/Study1_Q3$ cat q1_3_8.hs
import FileTools
import System.FilePath
import qualified Data.ByteString.Lazy.Char8 as B
main = find "/etc" >>= mapM judgeBashFile >>= mapM handleBashFile
judgeBashFile :: FilePath -> IO (String,Bool)
judgeBashFile file = cat file >>= return . (,) file . isBash
where isBash bs = B.take 11 bs == B.pack "#!/bin/bash"
handleBashFile :: (String,Bool) -> IO ()
handleBashFile (file,False) = return ()
handleBashFile (file,True) = cat file >>= B.writeFile dest . chg
where dest = "/home/ueda/hoge/" ++ takeFileName file
chg cs = B.append (B.pack "#!/usr/local/bin/bash\\n")
(B.unlines $ drop 1 $ B.lines cs)
|
リスト6に完成したコードを示します。 copyBashFile 関数の名前は、 処理にあわせて handleBashFile に変更しました。 ファイルの中身を改ざんするため、 16行目に chg という関数を定義して、 14行目の一番後ろにくっつけています。 これで、 B.writeFile dest に変更された内容が渡されます。 16行目の B.append は二つの B.ByteString をくっつけるて返す関数です。 B.append の最初の引数は1行目のシバンと改行文字、 次の引数は元のファイルの2行目以降です。
実行してみましょう。リスト7のように、 hoge ディレクトリ中のファイルのシバンが #!/usr/local/bin/bash になっていることが分かります。 めでたしめでたし。
- リスト7: q1_3_8の実行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ueda@remote:~/Study1_Q3$ sudo ./q1_3_8
ueda@remote:~/Study1_Q3$ cd ~/hoge
ueda@remote:~/hoge$ head -n 1 *
==> fakeroot <==
#!/usr/local/bin/bash
==> lzdiff <==
#!/usr/local/bin/bash
==> lzfgrep <==
#!/usr/local/bin/bash
==> lzmore <==
#!/usr/local/bin/bash
|
8.5. 第一回勉強会4問目¶
さて、地獄のような3問目がようやく終了したので、 次に4問目に参ります [8] 。4問目はこんな問題。
次のような ages ファイルを作り、 ans のように集計してください。
ages と ans は図8のようなファイルです。 どっちもシェル芸で簡単に [9] 作れます。
- リスト8: インプットする ages ファイルと解答ファイル ans
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ###agesは0〜109(年齢)をランダムに書いたもの###
ueda@remote:~/Study1_Q4/data$ while : ; do echo $((RANDOM % 110)) ; done |
head -n 100000 > ages
ueda@remote:~/Study1_Q4/data$ head -n 5 ages
91
35
11
100
94
###ansはagesの度数分布表###
ueda@remote:~/Study1_Q4/data$ cat ages | awk '{print int($1/10)}' |
sort -n | count 1 1 | awk '{print $1*10"〜"$1*10+9,$2}' > ans
ueda@remote:~/Study1_Q4/data$ cat ans
0〜9 9158
10〜19 9142
20〜29 9052
30〜39 9208
40〜49 9081
50〜59 9161
60〜69 8938
70〜79 8959
80〜89 9143
90〜99 9047
100〜109 9111
|
8.6. 文字列を数字に変換¶
では、まず ages ファイルを読んでみましょう。 読み込む際に、文字列から数字(Int型)に変換します。 まず、リスト9の q1_4_1.hs を作ってみました。
- リスト9: 標準入力からファイルを読み込んで数字のリストにするq1_4_1.hs
1 2 3 | ueda@remote:~/GIT/UspMagazineHaskell/Study1_Q4$ cat q1_4_1.hs
main = do cs <- getContents
print [ read c :: Int | c <- lines cs ]
|
実行してみましょう。リスト10のように、 Haskellのプログラム内部で数字のリストになっているのが分かります [10] 。
- リスト10: q1_4_1 の出力
1 2 | ueda@remote:~/Study1_Q4$ cat ./data/ages | ./q1_4_1 | head -c 30
[91,35,11,100,94,9,72,105,97,1
|
リスト9のコードでは、 6月号で出て来たリスト内包表記が使われています。 lines cs の出力は読み込んだテキストファイルを 1行1要素のリストにしたもので、そこから一つずつ read 関数で数字に変換しています。 Python書く人にはなじみ深いかと思います。
次に read c :: Int を説明します。 この呪文は、入力 c を Int型にして出力するという意味になります。 次のように、出力の型が決まっていないので、 なんらかの方法で指定しなければいけませんが、 リストのように :: Int と書いてInt型を指定します。 ちょっと不格好ですね。
1 2 | Prelude> :t read
read :: Read a => String -> a
|
次に、何十代かをカウントするというお題なので、 各年齢を、例えば19なら10、8なら0と1の位をゼロにします。
- リスト12: q1_4_1 の出力
1 2 3 | ueda@remote:~/Study1_Q4$ cat q1_4_2.hs
main = do cs <- getContents
print [ 10 * ( (read c :: Int) `div` 10 ) | c <- lines cs ]
|
整数の割り算には div を使います。 次のように、divと / では扱う型が違います。 div で整数を割ると、余りは切り捨てられます。 リスト12の計算は、 10で割って余りを切り捨てて後から 10をかけるというものになっています。
1 2 3 4 | Prelude> :t div
div :: Integral a => a -> a -> a
Prelude> :t (/)
(/) :: Fractional a => a -> a -> a
|
ところで、 div の両側にはバッククォートがついていますが、 つけるとつけないでは、このように書く順番が変わります。
1 2 3 4 | Prelude> 6 `div` 3
2
Prelude> div 6 3
2
|
要は数字を両側に置かないと演算子っぽくないので、 バッククォートをつけると真ん中に置けるよという 決まりになっているのです。 考えてみれば関数も演算子も、 引数をいくつかとって答えを出力するものなので、 実は本質的な違いがないものだと言えます。
では、今月はここまでにしましょう。 でもこの問題、3問目と違ってすぐに終わりそうですね。
8.7. おわりに¶
今回は第一回シェル芸勉強会の3問目の解答を完成させ、 4問目に突入しました。 3問目はHaskell入門というには難しすぎましたが、 4問目はなんとかなりそうです。 本連載、シェル芸勉強会で出題した順で扱っているので、 難易度は簡単になったり難しくなったりします。 ですので、ちょっと待っていれば自分のスキルにあった 問題が出るかもしれません。辛抱強くお付き合いを。