🎍

Haskellで、Unix(Linux,...)の基本コマンドのようなものを書く

2024/12/21に公開

はじめに

Haskell Advent Calender 2024の21日めの記事です。
カレンダー枠が空いていたので勢いで書きました。勢いで書いたので説明不足ぎみですが失礼します。

この記事では、Unix系の基本コマンドのような挙動をするHaskellの簡単なプログラムの例を紹介します。引数処理やエラー処理などは省略して、骨格部分の雰囲気のみを紹介します。

Haskellにおいて入出力や文字列処理等を手軽に実現できそうな雰囲気を紹介できればと思います。

catコマンドのようなもの

Unix系のcatコマンドのような挙動を実現するプログラムの例です。つまり、入力された複数の行をそのまま出力するプログラムの単純な例です。

getContents関数は、標準入力からの入力全体を1つの文字列にして返します。以下の例では入力の全体(つまり複数の行)が、変数xsに入ります。
putStr関数は、文字列を標準出力に出力します。ここでは、変数xsの内容(つまり全ての入力行)を丸ごと出力しています。

cat.hs
main :: IO ()
main = do
    xs <- getContents
    putStr xs

次のように、Haskellの標準的な処理系であるGHCに付属しているrunhaskellコマンド を用いて、Haskellのプログラムを明示的にコンパイルすることなくスクリプト的に実行して挙動を試せます[1]

$ runhaskell cat.hs
ABCD
ABCD
$ runhaskell cat.hs < FILE
   :

sortコマンドのようなもの

Unix系のsortコマンドのような挙動を実現する例です。つまり、入力を行単位でソートして出力する例です。

lines関数は、文字列を改行コードで区切られた個々の要素に分割して、リストにして返します。ここではlines関数を用いて、複数の行のかたまりである文字列xsを、個々の行を要素とするリストに変換しています。
sort関数は、リストの要素をソートします。
unlines関数は、リストの各要素(つまり各行)の末尾に改行コードを付加した上で、全ての要素を結合します。

sort.hs
import Data.List (sort)

main :: IO ()
main = do
    xs <- getContents
    let xs2 = unlines . sort . lines $ xs
    putStr xs2

uniqコマンドのようなもの

Unix系のuniqコマンドのような挙動を実現する例です。つまり、入力行の重複を削除して出力します[2]

nub関数はリストの重複する要素を削除します。

uniq.hs
import Data.List (nub)

main :: IO ()
main = do
    xs <- getContents
    let xs2 = unlines . nub . lines $ xs
    putStr xs2

head -5コマンドのようなもの

Unix系のheadコマンドのような挙動を実現する例です。つまり、入力の先頭から指定された個数の行のみを出力します。この例では、先頭から5行のみを出力します。

take関数を用いて、リストの先頭の5つの要素(つまり5つの行)のみを取得しています。

head.hs
main :: IO ()
main = do
    xs <- getContents
    let xs2 = unlines . take 5 . lines $ xs
    putStr xs2

grep "PATTERN"コマンドのようなもの

Unix系のgrep(fgrep)コマンドのような挙動を実現する例です。つまり、指定されたパターンを含んでいる行のみを出力します。この例では、文字列"PATTARN"を含む行のみを出力します。

filter関数を用いて、条件に一致するリストの要素(つまり行)のみを抽出しています。
isInfixOf関数は、指定した文字列を含む場合に条件が真(True)になります。

fgrep.hs
import Data.List (isInfixOf)

main :: IO ()
main = do
    xs <- getContents
    let xs2 = unlines . filter (isInfixOf "PATTERN") . lines $ xs
    putStr xs2

egrep "(Jan|Jul)"コマンドのようなもの

Unix系のegrepコマンドのような挙動を実現する例です。つまり、指定された正規表現によるパターンを含んでいる行のみを出力します。この例では、"Jan"または"Jul"を含む行のみを出力します。

filter関数の条件に、正規表現のパターンマッチを行うための=~関数を用いています。
=~関数を使うためには、regex-posix packageをインストールする必要があります[3]

egrep.hs
import Text.Regex.Posix ((=~))

main :: IO ()
main = do
    xs <- getContents
    let xs2 = unlines . filter (=~ "(Jan|Jul)") . lines $ xs
    putStr xs2

tr [:lower:] [:upper:]コマンドのようなもの

Unix系のtrコマンドのような挙動を実現する例です。つまり、指定した文字列の対応関係で入力を変換して出力します。この例では、入力中の英小文字を英大文字に変換しています。

toUpper関数は、英小文字を大文字に変換します。
map関数によって、入力の全ての文字に対して、toUpper関数を適用しています。

tr.hs
import Data.Char (toUpper)

main :: IO ()
main = do
    xs <- getContents
    let xs2 = map toUpper xs
    putStr xs2

wc -lコマンドのようなもの

Unix系のwc -lコマンドのような挙動を実現する例です。つまり、入力の行数を数えて出力します。

length関数で、リストの要素数(つまり行の個数)を数えています。

wc.hs
main :: IO ()
main = do
    xs <- getContents
    let xs2 = length . lines $ xs
    putStrLn $ show xs2

seq 1 10コマンドのようなもの

Unix系のseqコマンドのような挙動を実現する例です。つまり、指定する範囲の数値を出力します。この例では、1から10までの数値を出力します。

リスト内包表記を用いて1から10の値からなるリストを生成しています。
mapM_関数を用いて、リストの要素(つまり値)ごとにputStrLn . showを適用しています。
show関数を用いて、数値型(Int型)の値を文字列型(String型)に変換しています。
putStrLn関数で、各々の値を出力しています。

seq.hs
main :: IO ()
main = do
    mapM_ (putStrLn . show) [1 .. 10]

cut -c1-20コマンドのようなもの

Unix系のcutコマンドのような挙動を実現する例です。つまり、各入力行について、指定した桁の間の文字を出力します。この例では、各行について1文字目から20文字目までを出力します。

map関数を用いて、リストの各要素(つまり各行)に対して、take 20を適用しています。
take 20によって、リストの各要素(つまり各行)に対して、行頭の文字20個を取得しています。

cut.hs
main :: IO ()
main = do
    xs <- getContents
    let xs2 = unlines . map (take 20) . lines $ xs
    putStr xs2

sed -e 's/ABC/XYZ/g'コマンドのようなもの

Unix系のsedコマンドのような挙動を実現する例です。ここではsedのs(置換)サブコマンドの挙動を模倣します。つまり各入力行について、1つめのパターンを2つめのパターンに変換して出力します。この例では、各入力行について文字列"ABC"を"XYZ"に変換します。

splitOn関数を用いて、文字列を"ABC"で区切り、区切られた要素からなるリストにして返します。
intercalate関数を用いて、リストの要素を、"XYZ"を区切りとした文字列に結合しています。
splitOn関数を使うには、split packageをインストールする必要があります[4]

sed.hs
import Data.List (intercalate)
import Data.List.Split (splitOn)

main :: IO ()
main = do
    xs <- getContents
    let xs2 = unlines . map (intercalate "XYZ" . splitOn "ABC") . lines $ xs
    putStr xs2

awk '{print $4}'コマンドのようなもの

Unix系のawkコマンドのような挙動を実現する例です。ここでは、空白で区切られたフィールドを取り出す挙動を模倣します。この例では、各入力行について、4つめのフィールドを出力します。

map関数を用いて、lines関数で区切られたリストの各要素(つまり各行)に対して、concat . take 1 . drop 3 . wordsを適用しています。
words関数を用いて、入力された個々の行について、さらに空白コードで区切られた個々の単語に分解してリストにしています。
drop関数で、リストの先頭の3つの要素を削除しています。つまり、先頭3つの単語を削除します。
take関数で、リストの先頭の1つの要素のみを取得しています。take関数は、入力となるリストの要素が0個の場合でもエラーにはならず、[]を出力します。
concat関数で、リストから要素(文字列)を取り出しています[5]

awk.hs
main :: IO ()
main = do
    xs <- getContents
    let xs2 = unlines . map (concat . take 1 . drop 3 . words) . lines $ xs
    putStr xs2

すこしだけ補足

最後に少しだけ補足です。ここで紹介したような簡単なプログラムをさらに本格的に作る場合等に有用な関数をいくつか紹介します。

Haskellでも、CやPythonやPerl, ...などのプログラミング言語のように、入出力などの多様な機能が提供されています。以下に少しだけ一例を紹介します。

以下は、基本的な入出力関連の関数の例です。

  • getLine ... 標準入力から1行を入力します。
  • getChar ... 標準入力から1文字を入力します。

以下は、環境関連の関数等です。

  • getArgs ... プログラムの実行時の引数をリストで返します。
  • getEnv ... プログラムの実行時の環境変数を返します。
  • exitSuccess ... プログラムの途中でプログラムを正常終了させます。
  • system ... OSのコマンドを実行します。

以下は、ファイル操作関連の関数です。

  • readFile ... ファイルから入力します。
  • writeFile ... ファイルへ出力します。

以下は、バッファ操作等の関数です。

  • hSetBuffering ... 入出力時のバッファリングの可否を制御します。
  • hSetEcho ... 入力時のエコーの可否を制御します。

以下は、その他の便利そうな関数いろいろです。

  • printf ... 文字列をC言語のprintf関数風に加工します(文字の加工のみで出力は行いません)。
  • trace ... 純粋関数の中からでも、標準出力へ文字列を出力できます。デバッグ用途です。
  • timeout ... タイマーです。指定した時間後に指定した関数を実行します。
  • randomRIO ... 手頃な乱数生成器です。

まとめ

Haskellは、よく見慣れたプログラミング言語とシンタックスが少し違うだけの普通のプログラミング言語の1つです。

Haskellでも、多くのプログラミング言語と同様に、入力や出力を行うための様々な関数が提供されています。そしてリストに対する様々な処理によって、基本的な文字列の処理を簡単に実現できます[6]

以上のように、Haskellを用いてUnix系の基本コマンドのような挙動をするプログラムなどをカジュアルに実現できます。また、runhaskellコマンドを使うことでHaskellプログラムをスクリプト風に実行できるので、Perl, Python, Ruby等のように手軽に使えます。

では、Happy Haskelling!

参考情報など

脚注
  1. runhaskellコマンドを含むGHCの処理系をインストールする方法は、例えば、https://zenn.dev/mod_poppo/articles/haskell-setup-2023 を参照してください。なお、runhaskellにはrunghcという別名がありますが両者の実体は同じものです。 ↩︎

  2. この例は、全体の行の中から重複する行を削除する挙動ですので、Unix系のuniqコマンドとは少し挙動が異なります。 ↩︎

  3. 例えば、cabal install regex-posix --lib のようにして regex-posixパッケージをインストールできます。 ↩︎

  4. 例えば、cabal install split --lib のようにして splitパッケージをインストールできます。 ↩︎

  5. この例では、入力行の要素数が3個以下の場合にエラーとならないように、head関数ではなくて、takeとconcatの組み合わせを用いています。 ↩︎

  6. 文字列をより効率的(高速、省メモリなど)に扱うためには、様々なパッケージが提供されています。例えば https://hackage.haskell.org/package/texthttps://hackage.haskell.org/package/bytestring などがあります。なお、日常使いのカジュアルなプログラムくらいであれば、基本的なString型で性能的にも問題ないでしょう。 ↩︎

Discussion