LoginSignup
9
9

More than 3 years have passed since last update.

Swift でスクリプトを書くためのファイル入出力操作まとめ

Posted at

はじめに

Swift でスクリプトを書こうとすると、標準で使いやすいファイル入出力周りのクラスやメソッドがないので辛いですよね………。そこで、処理に必要になるクラスや extension を整理してみました。

ファイルパスの扱い

ファイルパスの表現

URL クラスでパスを表現します。URL はほとんどの場合はリモートのサーバーを表すために利用しますが、ローカルのファイルに対しても利用できます。

# 絶対パス
let file1 = URL(fileURLWithPath: "/Users/taisukeh/bar.swift")

# 相対パス
let file2 = URL(fileURLWithPath: "bar.swift")

# 相対パス + ルートディレクトリ
let file3 = URL(fileURLWithPath: "bar.swift", relativeTo: URL(fileURLWithPath: "/Users/taisukeh/"))

# 明示的にディレクトリのパスであることを指定
let file4 = URL(fileURLWithPath: "/Users/taisukeh/bar", isDirectory: true)

ファイルパスの結合、削除

let file1 = URL(string: "/Users/taisukeh")

# パス追加
let file2 = file1.appendingPathComponent("foo")

print(file2) // --> file:///Users/taisukeh/foo

# 拡張子追加
let file3 = file1.appendingPathExtension("foo")

print(file3) // --> file:///Users/taisukeh.foo
let file1 = URL(fileURLWithPath: "/Users/taisuke/foo.swift")

# 最後のパス削除
let file2 = file1.deletingLastPathComponent()

print(file2) // --> file:///Users/taisuke/

# 拡張子削除
let file3 = file1.deletingPathExtension()

print(file3) // --> file:///Users/taisuke/foo

ファイルパスの分解、結合

let file1 = URL(fileURLWithPath: "/Users/taisukeh/foo.swift")

# 分割
print(file1.pathComponents) // --> ["/", "Users", "taisukeh", "foo.swift"]

# 最後のパス
print(file1.lastPathComponent) // --> foo.swift

# 拡張子
print(file1.pathExtension) // --> swift

分割したパスの要素からパスを再構築する機能は URL にはありません。NSStirng にあるので、以下のような extension を用意しておくと良さそうです。

extension String {
    public static func path(withComponents components: [String]) -> String {
        return NSString.path(withComponents: components)
    }
}

文字列への変換

let file1 = URL(fileURLWithPath: "/Users/taisukeh/foo.swift")

# file:// スキーム付きの絶対パス
print(file1.absoluteString) // --> file:///Users/taisukeh/foo.swift

# パス
print(file1.path) // --> /Users/taisukeh/foo.swift

ディレクトリのパスの扱い

let file1 = URL(fileURLWithPath: "/foo/bar")
let file2 = URL(fileURLWithPath: "/foo/bar/")
let file3 = URL(fileURLWithPath: "/foo/bar", isDirectory: true)

print(file1.hasDirectoryPath) // --> false
print(file2.hasDirectoryPath) // --> true
print(file3.hasDirectoryPath) // --> true

... の正規化

print(URL(fileURLWithPath: "/foo/bar/.././bar/baz.swift").standardized.path)
// --> /foo/bar/baz.swift

~ 展開

ホームディレクトリを表す ~ を展開する機能は URL にはありません。そこで、NSString にある expandingTildePath を利用します。String に以下のような extension を定義しておくと良さそうです。

public extension String {
    public var expandingTildeInPath: String {
        return NSString(string: self).expandingTildeInPath
    }
}

ファイルの操作

FileManager クラスを利用します。

ホームディレクトリ、カレントディレクトリ、一時ディレクトリ

let fm = FileManager.default

# ホームディレクトリ
print(fm.homeDirectoryForCurrentUser)

# 特定のユーザーのホームディレクトリ。ユーザーが存在しなければ nil
print(fm.homeDirectory(forUser: "tahori"))

# 一時ディレクトリ
print(fm.temporaryDirectory)

# カレントディレクトリ
print(fm.currentDirectoryPath)

# カレントディレクトリ変更
fm.changeCurrentDirectoryPath("/")

ファイル作成

# 空ファイル
let created = fm.createFile(atPath: "/Users/tahori/hoge", contents: nil)

# Data でファイルの内容を指定して作成
let created = fm.createFile(atPath: "/Users/tahori/hoge", contents: "foo".data(using: .utf8))


# パーミッションを指定して作成
let r = fm.createFile(atPath: "/Users/tahori/hoge", contents: nil,
                      attributes: [
                       FileAttributeKey.posixPermissions: 0o777,
  ])

ファイル削除

do {
    try fm.removeItem(atPath: "/Users/tahori/hogehoge3")
} catch let e as NSError {
    if e.code == NSFileNoSuchFileError {
        print("file not exist")
    }
}

# removeItem(at: URL) もあります

ディレクトリ作成

do {
    try fm.createDirectory(atPath: "/Users/tahori/hoge5/hoge6", withIntermediateDirectories: false, attributes: [:])
} catch let e as NSError {
    if e.code == NSFileNoSuchFileError {
        print("parent dir not exist")
    }
}

コピー、移動

func copyItem(at: URL, to: URL)
func copyItem(atPath: String, toPath: String)
func moveItem(at: URL, to: URL)
func moveItem(atPath: String, toPath: String)

存在確認

func fileExists(atPath: String) -> Bool
func fileExists(atPath: String, isDirectory: UnsafeMutablePointer<ObjCBool>?) -> Bool

パーミッション確認

func isReadableFile(atPath: String) -> Bool
func isWritableFile(atPath: String) -> Bool
func isExecutableFile(atPath: String) -> Bool
func isDeletableFile(atPath: String) -> Bool

パーミッション設定 ( chmod )

try self.setAttributes([FileAttributeKey.posixPermissions: 0o777], ofItemAtPath: "/foo/bar")

Owner / Group Owner 設定

try self.setAttributes([FileAttributeKey.ownerAccountName: name], ofItemAtPath: "/foo/bar")

Symbolic Link / Hard Link


# Creates a symbolic
func createSymbolicLink(at: URL, withDestinationURL: URL)
func createSymbolicLink(atPath: String, withDestinationPath: String)

# Creates a hard link
func linkItem(at: URL, to: URL)
func linkItem(atPath: String, toPath: String)

# Returns the path of the item pointed to by a symbolic link.
func destinationOfSymbolicLink(atPath: String) -> String

ファイル入出力

ファイル入出力はいくつかのクラスに実装されているのですが、最も使いやすい形になっているのは FileHandle です。
(InputStream / OutputStream クラスにも入出力の機能があります。)

標準エラー出力

print, debugPrint では to: パラメータで出力先を指定できます。値は TextOutputStream プロトコルに準拠する必要があります。標準では TextOutputStream に準拠した標準エラー出力の値はありません。

TextOutputStream に準拠した値を作っておきます。ここでは、FileHandle を準拠させています。

extension FileHandle: TextOutputStream {
    public func write(_ string: String) {
        self.write(string, encoding: .utf8)
    }
}

extension FileHandle {
    static var stderr: FileHandle = FileHandle(fileDescriptor: FileHandle.standardError.fileDescriptor)
    static var stdout: FileHandle = FileHandle(fileDescriptor: FileHandle.standardOutput.fileDescriptor)
}

print("hoge", to: &FileHandle.stderr)

ファイルを開く

read, write, read and write でファイルを開きます。

init?(forReadingAtPath: String)
init(forReadingFrom: URL)
init?(forWritingAtPath: String)
init(forWritingTo: URL)
init?(forUpdatingAtPath: String)
init(forUpdating: URL)

standardError / standardInput / standardOutput / nullDevice

(print に直接渡すことはできないのですが)標準入出力、エラー出力、ヌルデバイスへの FileHandle が定義されています。

class var standardError: FileHandle
Returns the file handle associated with the standard error file.

class var standardInput: FileHandle
Returns the file handle associated with the standard input file.

class var standardOutput: FileHandle
Returns the file handle associated with the standard output file.

class var nullDevice: FileHandle
Returns a file handle associated with a null device.

簡易入出力、seek など

read, write はごく簡単なメソッドしか用意されていません。一行入力などはないですね。

func close()
func offset() -> UInt64
func read(upToCount: Int) -> Data?
func readToEnd() -> Data?
func seek(toOffset: UInt64)
func seekToEnd() -> UInt64
func synchronize()
func truncate(atOffset: UInt64)
func write<T>(contentsOf: T)

一行入力

雑ですが、以下のような extension を生やしています。

extension FileHandle {
    func eachLine(callback: (String) throws -> ()) rethrows {
        try eachLine(encoding: .utf8, callback: callback)
    }

    func eachLine(encoding: String.Encoding, callback: (String) throws -> ()) rethrows {
        try eachLine(encoding: encoding, readData: { () -> Data? in
            return try self.readToEnd()
        }, callback: callback)
    }

    private func eachLine(encoding: String.Encoding, readData: () throws -> Data?, callback: (String) throws -> ()) rethrows {
        guard let data = try readData() else {
            return
        }
        guard let s = String(data: data, encoding: encoding) else {
            return
        }

        try callback(s)
    }
}

さいごに

Server Side Swift がもっと盛り上がるとよいですね。

9
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9