os

~OSとのやりとりを行うモジュール~
Nimの標準パッケージの一部

カテゴリー:ユーティリティ
対応Nimバージョン:2.2.8
公式レポジトリ:https://github.com/nim-lang/Nim




osとは?


概要

os は、Nim で OS まわりの処理を書くときの入口になるモジュールである。
公式文書でも、環境変数の取得、ディレクトリ操作、シェルコマンド実行などの基本的な OS 機能を含むと説明されている。
さらに実体としては、ospaths2osfilesosdirscmdlineenvvarsoserrors など複数の内部モジュールを取り込み、その主要 API をまとめて使える構成になっている。
つまり os は、単一機能の小パッケージではなく、OS 関連処理の総合窓口と捉えると理解しやすい。

初心者にとって重要なのは、os を使うと「今いるディレクトリはどこか」「このファイルは存在するか」「ディレクトリを作りたい」「環境変数を読みたい」「起動時引数を取りたい」といった、実際のアプリで頻出する処理を、同じ系統の名前と流儀でまとめて扱える点である。
たとえばパス分解の splitPathsplitFile、カレントディレクトリ取得の getCurrentDir、ディレクトリ作成の createDir、ファイル存在確認の fileExists、環境変数取得の getEnv、引数取得の commandLineParams などがその典型である。

現在の Nim では、より型安全な pathsfilesdirs といった分割モジュールも整備されている。
これらは Path 型を使う高水準 API を提供する。一方で os は文字列ベースの従来 API を一か所で使いやすくまとめているため、学習初期や小規模ツールでは依然として扱いやすい。
実務では、まず os で全体像をつかみ、必要に応じて std/paths などへ進む流れが理解しやすい。

特徴

os の第一の強みは、OS 依存の処理を Nim らしい API で吸収してくれる点にある。
たとえばパス結合、ファイル移動、ディレクトリ走査、環境変数、実行ファイル探索などを、同じ標準ライブラリ群の中で一貫した書き方で扱える。
さらに公式スタイルガイドでも、標準ライブラリは import std/os のように std 接頭辞を用いる形が推奨されており、学習資料や実務コードでの見通しもよい。

第二の強みは、低水準 API よりかなり扱いやすい点である。
公式の posix 文書は、そこが生の POSIX インターフェースであり、cstring や戻り値ベースのエラー処理を使うと明記している。
そのうえで、例外や Nim らしいインターフェースが欲しいなら os を使うよう案内している。
つまり os は、OS 機能を「Nim の文字列や例外で普通に扱える形」に持ち上げてくれる層である。
初心者が最初から posix に降りる必要はほぼない。

第三の強みは、機能の横断性である。
os は単なるファイルモジュールではない。
ファイルとディレクトリの操作だけでなく、getEnvputEnv のような環境変数 API、paramCountcommandLineParams のような引数 API、execShellCmdquoteShellCommand のようなコマンド実行補助も備える。
そのため、CLI ツール、ビルド補助、ローカル自動化、設定ファイル処理の前段などを一つの import で始めやすい。

ただし、何でも os だけで完結させるのが最善とは限らない。
たとえばプロセス通信まで必要なら osproc がより高度であり、公式文書でも execShellCmd を越える用途では osproc を見るよう示している。
また、新しい pathsfilesdirsPath 型によって文字列パスの混乱を減らせる。
したがって os の立ち位置は、「まず最初に覚える総合 OS モジュール」であり、用途が深くなったら専門モジュールへ分岐する入口である。

使いどころ

最もわかりやすい使いどころは、ファイル整理や自動化スクリプトである。
たとえば「作業用フォルダを作る」「ログファイルが存在するか確認する」「古いファイルを削除する」「ファイルを別ディレクトリへ移動する」といった処理は、createDirdirExistsfileExistsremoveFilemoveFile といった API で素直に書ける。
Windows と Linux の両方を意識する小道具でも、まずは os を使うだけで相当進められる。

次に多いのは、パス処理である。アプリを実行した場所を起点に設定ファイルを探したり、拡張子だけ差し替えたり、親ディレクトリを取り出したりする場面は非常に多い。
splitPathsplitFileparentDirchangeFileExtgetCurrentDirrelativePathisAbsolute などは、この種の処理でよく使う。
特に初心者は文字列連結でパスを組み立てがちだが、区切り文字の違いを考えると、専用 API を使う方が安全である。

三つ目は、CLI ツール作成である。
commandLineParamsparamCountparamStr を使えば、ドラッグアンドドロップされたファイル名や、コマンドライン引数で渡された設定値を受け取れる。
Nim で「簡単な変換ツール」「バッチ処理ツール」「教材用コマンドアプリ」を作るなら、この用途は極めて重要である。
なお、parseCmdLine もあるが、公式には本格的な引数解析には parseopt の方が向いていると明記されている。

四つ目は、環境変数を使う設定管理である。
たとえば API キーや出力先ディレクトリなどを、コードに直書きせず OS 側から受け取りたいとき、getEnvexistsEnv が役立つ。
開発と本番で設定を切り替えるときにも便利である。さらに envPairs を使えば、現在の環境変数一覧を走査できる。
小規模なツールでも、設定の分離という意味で学習価値が高い。

五つ目は、ディレクトリの探索である。
walkDirwalkDirRec を使えば、指定ディレクトリ内のファイル一覧やサブディレクトリ一覧を順に処理できる。
画像一括変換、ソースコード一括検査、教材ファイル収集のような処理では非常に頻繁に使う。
walkDir は非再帰、walkDirRec は再帰であるという違いを最初に押さえると混乱しにくい。

六つ目は、簡易な外部コマンド実行である。
execShellCmd を使えばシェルコマンドを実行できる。
ただしこれはあくまで簡易用途向けであり、シェルを介さず安全にプロセスを起動したい、標準出力を受け取りたい、といった場面では osproc を使う方が適切である。
ここを区別できると、雑に書いた自動化スクリプトから一歩先へ進める。

使い方

まずは推奨される import の書き方から始める。
現在の公式スタイルでは import std/os が推奨である。
古い記事では import os も見かけるが、今から書くなら std 付きで覚えた方がよい。

                    
import std/os
                    
                

これで os の API を使う準備ができた。
以降は、公式文書の説明に沿って、必要な機能を呼び出す形でコードを書いていく。


最初に覚えたいのは、カレントディレクトリとパス操作である。
今どこでプログラムが動いているかを知り、そこを基準にファイルを組み立てるのが基本になる。

                    
import std/os

echo "現在の作業ディレクトリ: ", getCurrentDir()

let src = getCurrentDir() / "data" / "input.txt"
echo src

let parts = splitFile(src)
echo "dir = ", parts.dir
echo "name = ", parts.name
echo "ext = ", parts.ext

echo parentDir(src)
echo changeFileExt(src, "bak")
                    
                

このコードは、今どこでプログラムが動いているかを知り、その場所を基準にファイルパスを組み立てて、あとから分解する例である。
getCurrentDir() は現在の作業ディレクトリを返す。そこに "data" と "input.txt" をつないで、src というファイルパスを作っている。
splitFile(src) は、そのパスを「ディレクトリ部分」「ファイル名本体」「拡張子」に分解する。
最後の parentDir(src) は親ディレクトリを取り出し、changeFileExt(src, "bak") は拡張子を .bak に変えた新しいパス文字列を返す。
これらはすべて、ファイル名を直接文字列操作でいじるより安全に扱うための基本操作である。

初心者がここで気をつけるべきなのは、パスをただの文字列連結で雑に作らないことである。
Windows と Unix 系では見た目や扱いが微妙に異なるため、まずは getCurrentDirsplitFileparentDirchangeFileExt のような専用 API を使う方がよい。
とくに拡張子の変更は、後ろにただ文字列を足すのではなく、changeFileExt に任せた方が意図がはっきりする。


次は、ディレクトリ作成とファイル存在確認である。
createDir は途中の親ディレクトリも含めて作成でき、すでに存在していても通常は失敗扱いしない。
対して existsOrCreateDir は「もうあったか、今作ったか」を真偽値で知りたいときに向く。

                    
import std/os

let outDir = "output/logs"

createDir(outDir)

if fileExists(outDir / "app.log"):
  echo "ログファイルは存在する"
else:
  echo "まだログファイルはない"

let alreadyExisted = existsOrCreateDir("cache")
echo alreadyExisted
                    
                

このコードは、出力用ディレクトリを作り、その中にログファイルがあるか確認し、別のキャッシュ用ディレクトリも準備する例である。
createDir(outDir) はディレクトリを作る関数であり、途中のサブディレクトリがまだなくてもフルパスを作成できる。
すでに存在している場合でも通常は失敗扱いにならない。
次の fileExists(...) は、そのパスに通常ファイルまたはシンボリックリンクが存在するかを調べる。
最後の existsOrCreateDir("cache") は、指定ディレクトリがすでにあれば true、なければ作成して false を返す。

ここで重要なのは、createDirexistsOrCreateDir の違いである。
createDir は親ディレクトリも含めて必要な分を作れるが、existsOrCreateDir は親ディレクトリを自動で作らない。
親がない場所に対して existsOrCreateDir("a/b") のように呼ぶと OSError になる可能性がある。
そのため、階層ごとまとめて用意したいときは createDir、単に「この一つのフォルダがあるか、なければ作る」でよいときは existsOrCreateDir と使い分けるとよい。


ファイルのコピー、移動、削除もよく使う。
copyFile はコピー先の親ディレクトリが存在している必要がある。
moveFile はリネーム用途にも使える。
removeFile は対象が存在しなくても失敗しないが、「削除に成功したかどうか」だけ知りたいなら tryRemoveFile が向いている。

                    
import std/os

createDir("backup")

copyFile("memo.txt", "backup/memo.txt")
moveFile("backup/memo.txt", "backup/memo_old.txt")

if tryRemoveFile("backup/old.log"):
  echo "削除できた"
else:
  echo "削除できないか、別要因で失敗した"
                    
                

このコードは、バックアップ用フォルダを作り、ファイルをコピーし、その後で名前を変え、不要ファイルを削除する流れを示している。
copyFile("memo.txt", "backup/memo.txt") は memo.txt を backup に複製する。
moveFile はファイルを移動する関数であり、同じディレクトリ内で使えば実質的にリネームとしても使える。
tryRemoveFile は削除に成功したかどうかを真偽値で返すため、処理を分岐させたいときに便利である。
moveFile はシンボリックリンクをたどらず、リンクそのものを移動する挙動である点にも注意がいる。

このコードのつまずきやすい点は、copyFile のコピー先である。
公式ドキュメントでは、copyFile のコピー先は 親ディレクトリがすでに存在している必要がある とされている。
だから最初に createDir("backup") を入れているわけである。これを省くと、backup が存在しない環境では OSError になる。
つまりこの例は、単に順番に書いているのではなく、失敗しないための前準備を先にしている構成である。


ディレクトリ走査では walkDirwalkDirRec を使い分ける。
walkDir は一段だけ、walkDirRec は再帰的に全体を見ていく。
ファイルの種類も知りたいときは walkDir が便利である。

                    
import std/os

for kind, path in walkDir("src"):
  echo kind, " -> ", path

for path in walkDirRec("src"):
  echo path
                    
                

このコードは、ディレクトリ内に何があるかを列挙する例である。
walkDir("src") は src の直下だけを調べ、各項目について「種類」と「パス」を返す。
種類は PathComponent で表され、通常ファイル、ディレクトリ、リンクなどを区別できる。
一方 walkDirRec("src") は再帰的に走査するため、src 配下のさらに下の階層まで含めて順にたどっていく。
つまり一段だけ見たいなら walkDir、配下全体を見たいなら walkDirRec である。

初心者はこの二つを混同しやすい。
walkDir は非再帰であり、深い階層までは見ない。逆に walkDirRec は再帰的に探索するので、対象ディレクトリが大きいと出力数も多くなる。
ファイル整理ツールや一括変換ツールでは、この違いを理解していないと「一部しか見つからない」「逆に見つかりすぎる」という問題が起こる。
まずは「一段だけか、全部か」で使い分けると覚えるとよい。


環境変数の取得は、設定値を外から渡したいときの基本である。
getEnv は環境変数が存在しないと空文字列を返す。そのため、空文字そのものと区別したい場合は existsEnv も併用する。

                    
import std/os

let apiKey = getEnv("MY_APP_API_KEY")

if existsEnv("MY_APP_API_KEY"):
  echo "APIキーが設定されている"
else:
  echo "APIキーが未設定である"

echo getEnv("NOT_FOUND", "default-value")
                    
                

このコードは、環境変数から設定値を受け取る例である。
getEnv("MY_APP_API_KEY") は指定した名前の環境変数の値を返す。
もしその環境変数が存在しない場合、既定では空文字列 "" を返す。
そこで existsEnv("MY_APP_API_KEY") を併用すると、「未設定なのか」「空文字で設定されているのか」を区別できる。
最後の getEnv("NOT_FOUND", "default-value") は、環境変数が存在しなければ "default-value" を返す使い方である。

この例で大切なのは、getEnv の戻り値だけでは未設定か空文字か区別できないという点である。
たとえば API キーが空文字になることをありえない前提にしてしまうと、設定ミスの発見が遅れる。
だから重要な設定値では、getEnv を単独で使うより existsEnv と組み合わせる方が安全である。
環境変数は、コードに秘密情報を書きたくないときや、開発環境と本番環境で設定を切り替えたいときに特に有効である。


コマンドライン引数を読むと、CLI ツールらしくなる。
commandLineParams() は配列でまとめて取りたいときに便利であり、paramCount()paramStr() は一個ずつ扱いたいときに向く。
paramStr(0) は実行ファイル名っぽい値になることがあるが OS 依存なので、実行ファイル名が必要なら getAppFilename() を使う方がよい。

                    
import std/os

echo "実行ファイル: ", getAppFilename()
echo "実行ファイルのある場所: ", getAppDir()

for p in commandLineParams():
  echo "param: ", p

echo "引数の数: ", paramCount()

if paramCount() >= 1:
  echo "最初の引数: ", paramStr(1)
                    
                

このコードは、プログラム起動時に渡された引数を読む例である。
getAppFilename() は実行ファイル名を返し、getAppDir() はその実行ファイルが置かれているディレクトリを返す。
commandLineParams() は実行ファイル名を除いた引数一覧だけを返すため、CLI ツールでは一番わかりやすい入口になる。
paramCount() は引数の個数を返し、引数なしで起動した場合は 0 になる。
paramStr(1) は一番目の引数を返す。

ここでの注意点は二つある。
ひとつは、paramStr(i) の i は 1..paramCount() の範囲で使う必要があり、範囲外では IndexDefect が起きることである。
だからこのコードでは、先に if paramCount() >= 1: と確認している。
もうひとつは、paramStr(0) を実行ファイル名代わりに使わないことだ。
公式文書では paramStr(0) は OS 依存の内容になるため避け、実行ファイル名が必要なら getAppFilename() を使うよう案内している。
つまりこのコードは、安全な順番で引数を読む書き方になっている。


簡易に外部コマンドを叩くなら execShellCmd が使える。
ただし文字列をそのままシェルに渡す性質上、外部入力を雑に連結するのは危険である。
引数を安全にシェル向け文字列へまとめたい場合は quoteShellquoteShellCommand を使う。
より本格的なプロセス制御が必要なら osproc に切り替える。

                    
import std/os

let filename = "my file.txt"
let cmd = "type " & quoteShell(filename)   # Windows では type、Posix では cat に読み替える
echo cmd

let exitCode = execShellCmd(cmd)
echo "終了コード: ", exitCode
                    
                

このコードは、文字列として組み立てたコマンドをシェル経由で実行する例である。
quoteShell(filename) は、スペースを含むファイル名などをシェル引数として安全に引用しやすくする補助である。
execShellCmd(cmd) はそのコマンド文字列を実行し、終了コードを返す。
std/os の文書でも、シェルコマンド実行はこのモジュールの代表的機能の一つとして説明されている。

ただし、このコードは便利な反面、雑に使うべきではない。
execShellCmd はシェルを経由してコマンドを実行するため、外部入力をそのまま連結すると意図しない解釈が起こりうる。
公式文書でも、より高度なプロセス制御や通信が必要な場合は osproc を使う方向が示されている。
したがって、この例は「簡易自動化には便利だが、安全性や複雑な制御が必要な場面では上位手段へ進むべき」という理解で使うのがよい。


最後に、ファイル情報の取得である。getFileInfo を使うと、サイズ、種類、権限、時刻などを一度に取得できる。
単なる存在確認より一歩進んだツールを書くときに有効である。

                    
import std/os

if fileExists("memo.txt"):
  let info = getFileInfo("memo.txt")
  echo "size = ", info.size
  echo "kind = ", info.kind
  echo "links = ", info.linkCount
  echo "special = ", info.isSpecial
                    
                

このコードは、ファイルが存在することを確認したうえで、その詳細情報を取得する例である。
まず fileExists("memo.txt") で対象が通常ファイルまたはシンボリックリンクとして存在するかを確認している。
そのうえで getFileInfo("memo.txt") を呼び、サイズ、種類、リンク数、特殊ファイルかどうかなどをまとめて取得している。
getFileInfo(path)followSymlink = true を既定値として持つため、シンボリックリンクの場合は通常その先をたどった情報になる。

この例の意図は、単なる「ある・ない」だけで終わらず、対象の中身の属性まで調べるところにある。
たとえばバックアップツールや監視ツールでは、サイズや種別を見て処理を分岐することがある。
そのとき FileInfo は非常に便利である。ただし、リンクそのものを調べたいのに既定値のまま呼ぶと、リンク先の情報を見てしまう可能性がある。
リンクの扱いが重要な場面では、followSymlink の既定挙動を意識しておくべきである。


以上、std/os の基本的な使い方と注意点を紹介した。
これらの API を理解して使いこなせるようになると、ファイル操作や環境変数管理、コマンド実行など、OS に関わる多くの処理が Nim でスムーズに書けるようになる。
ただし、機能の横断性がある分、用途や状況に応じて適切な API を選ぶことが重要である。
まずは基本的な使い方を押さえたうえで、実際のプロジェクトで必要に応じて活用してみるとよい。

注意点

最初の注意点は、existsDirexistsFile をそのまま覚えないことだ。
os の文書ではこれらは deprecated とされ、代わりに dirExistsfileExists を使うよう明記されている。
古い記事を見てコピペすると、いきなり古い書き方を覚えてしまう。今から学ぶなら新しい名前で統一した方がよい。

二つ目は、getEnv の戻り値である。
環境変数が存在しない場合、getEnv(key) は空文字列を返す。
つまり「未設定」なのか「空文字で設定されている」のかを、戻り値だけでは区別できない。
その区別が必要なら existsEnv(key) を先に見るべきである。これは初心者がかなり踏みやすい罠である。

三つ目は、paramStr(i) の添字である。
公式文書では 1..paramCount() の範囲外だと IndexDefect が起きると説明されている。
したがって、引数を読む前に必ず paramCount() を確認する癖をつけるべきである。
また paramStr(0) は OS 依存の内容であり、実行ファイル名を取りたい用途では避け、getAppFilename() を使う。

四つ目は、execShellCmd を安易に使わないことである。
これはシェルを介してコマンド文字列を実行する。
したがって、ユーザー入力をそのまま連結すると、意図しないコマンド解釈や失敗を招きやすい。
公式文書でも、シェルを介さず実行したい場合は osproc.execProcess を使うよう案内している。
単に「動いたからよい」ではなく、どの層を通してコマンドが実行されるかを理解しておくべきである。

五つ目は、copyFilemoveFile の挙動差である。
copyFile はコピー先親ディレクトリが存在していなければ失敗する。
また非 Windows では権限が自動で完全保存されるとは限らず、必要なら copyFileWithPermissions を使う。
一方 moveFile はシンボリックリンクをたどらず、リンクそのものを移動する。見た目が似ていても意味は異なる。

六つ目は、getFileInfo(path, followSymlink = true) の既定値である。
デフォルトではシンボリックリンクの先をたどって情報を取る。
そのため、「リンクそのものの情報が欲しかったのに、実体側を見ていた」という混乱が起こりうる。
リンク自体を調べたいなら followSymlink = false を意識的に指定する必要がある。

七つ目は、OS による差異である。
たとえば getCreationTime は POSIX では実際の作成時刻ではなく、属性変更時刻に近い値になることがあると文書に明記されている。
isHidden も Windows では hidden 属性、POSIX では先頭ドットという異なる基準で判定される。
さらに getFilePermissionssetFilePermissions は Windows では事実上 readonly 中心の扱いになる。
os は差を吸収してくれるが、完全に同じ意味になるわけではない。ここを誤解すると、クロスプラットフォームで思わぬズレが出る。

八つ目は、文字列でパスを雑に組み立てないことである。
"dir/" & filename のような連結でも動く場面はあるが、区切り文字や余分なスラッシュ、相対パスと絶対パスの混乱が起きやすい。
/ 演算子や parentDirsplitFilerelativePath などの専用 API を使う方が安全である。
将来的により厳密にしたいなら pathsPath 型へ進むとよい。

型・関数・API

os パッケージは、ただファイルを作ったり消したりするだけのモジュールではない。
実際には、パスの分解や結合、ファイルとディレクトリの存在確認、コピーや移動、環境変数の読み書き、コマンドライン引数の取得、実行ファイルの場所の確認、簡易的なシェル実行までを広く担当する、OS 操作の総合窓口である。
初心者のうちは関数名が多く見えて圧倒されやすいが、役割ごとに分けて見れば整理しやすい。
まずは「ファイル情報を表す型」「パス操作」「ファイル操作」「ディレクトリ操作」「環境変数」「コマンドライン引数」というまとまりで覚えると理解しやすい。

型として特に重要なのは FileInfo である。
これはファイルやディレクトリのサイズ、種類、権限、時刻などをまとめて保持する型であり、単なる存在確認より一歩進んだ処理で使う。
加えて、PathComponent はファイルなのかディレクトリなのか、リンクなのかを表すために使われる。
FilePermission は読み取り・書き込み・実行の権限を扱うための列挙型であり、CopyFlag はコピー時にシンボリックリンクをどう扱うかを指定するための列挙型である。
これらは日常的に毎回書くものではないが、os の API を正しく読むためには知っておく価値が高い。

関数では、まず getCurrentDirsplitFileparentDirchangeFileExt のようなパス処理系が基本になる。
続いて fileExistscopyFilemoveFileremoveFile のようなファイル操作系、dirExistscreateDirwalkDirwalkDirRec のようなディレクトリ操作系を覚えると、実用的なスクリプトを書けるようになる。
さらに getEnvexistsEnv を使えば環境変数を読み取れ、commandLineParamsparamCount を使えばコマンドライン引数も扱える。
簡単な自動化では execShellCmd も便利だが、これはシェルを経由するため、外部入力をそのまま連結するのは避けるべきである。

以下に、os パッケージでよく見かける型・列挙値・関数を一覧としてまとめる。

参考文献

以下は当記事執筆において参考にした文献や資料である。

トップへ戻る


お問い合わせ