LoginSignup
3

More than 5 years have passed since last update.

Mac でランダムな文字列(パスワード生成用など)を作るコマンドラインプログラム

Posted at

序言

アカウント作成するとき、パスワード考えるのだるいですよね。
まあ同じパスワードを使うという手もなくはないけどセキュリティー的な観点で言うと完全にアウトですね。
幸い最近の Safari は自動でパスワードを生成してくれたりもするけど、覚えやすさのためか3文字ごとに「-」が入ってて、サービスによっては受け付けないこともあったりするしw
あとはまあ Keychain Access アプリを使ってパスワードを作ってもらう手もあるけどまあいちいち立ち上げて新規パスワード項目作ってもらうのもひと手間だしダルいですね
というわけで、プログラマたるもの、足りないものがあれば自分でなんとするもんです。

要件

  • 指定した長さのランダム文字列を作ってもらう
  • 文字列には英字の大文字・小文字及び数字が含まれる
  • ただし必要に応じて特定な文字セット(大文字英字など)を除外することも可能
  • 作られた文字列は即座にクリップボードにコピーし、cmd + v でそのままペーストできる

コーディング

とりあえずソースコードそのまま貼ります:

hanako.swift
import Cocoa

private extension Int {

    static func createRandom(under limit: Int) -> Int {
        let random = arc4random_uniform(UInt32(limit))
        return Int(random)
    }

}

private extension String {

    var characterArray: [String] {
        return self.characters.map({ (c) -> String in
            return "\(c)"
        })
    }

    func containsCharacterInArray(array: [String]) -> Bool {

        for character in array {
            if self.containsString(character) {
                return true
            }
        }

        return false

    }

}

private extension Array {

    func getRandomElement() -> Element {
        let randomIndex = Int.createRandom(under: self.count)
        return self[randomIndex]
    }

}

private let uppercaseStrings: [String] = {
    let string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    return string.characterArray
}()

private let lowercasedStrings: [String] = {
    let string = "abcdefghijklmnopqrstuvwxyz"
    return string.characterArray
}()

private let numberStrings: [String] = {
    let string = "0123456789"
    return string.characterArray
}()

private enum CharacterType {
    case UppercasedAlphabet
    case LowercasedAlphabet
    case Numeral
}

private var types: Set<CharacterType> = [.UppercasedAlphabet, .LowercasedAlphabet, .Numeral]
private var shouldCopyToPasteboard = true
private var length = 10
private var hyphenFrequency = 0

func printHelp() {

    let help =
    "hanako is a random string generator.\n" +
    "\n" +
    "Usage:\n" +
    "\t$ hanako [options]\n" +
    "\n" +
    "Arguments:\n" +
    "\t-h / --help: Help. (This will ignore all other arguments and won't perform an output.)\n" +
    "\t-nu / --no-uppercase: Don't use uppercased string for the output.\n" +
    "\t-nl / --no-lowercase: Don't use lowercased string for the output.\n" +
    "\t-nn / --no-numeral: Don't use numeral string for the output.\n" +
    "\t-nc / --no-copy: Don't copy the result to pasteboard" +
    "\t-l [length] / --length [length]: Output length. Replace [length] with a number. Default length is 10.\n" +
    "\t-hf [frequency] / --hyphen-frequency [frequency]: Insert a hyphen after every certain characters. Replace [frequncy] with a number. e.g.: abc-def-ghi if you set frequency to 3, abcdefghi if you set frequncy to 0. Default frequency is 0, and hyphens are also counted in the length.\n"

    print(help)

}

func printError() {

    let errorMessage = "Invalid arguments. Please enter -h to get help.\n"
    print(errorMessage)

}

func createRandomString() -> String {

    guard types.count > 0 else {
        let errorMessage = "No characters able to use in random string, please check your setting.\n"
        print(errorMessage)
        exit(EXIT_FAILURE)
    }

    let characterList = types.reduce([]) { (list, type) -> [String] in
        let nextList: [String]
        switch type {
        case .UppercasedAlphabet:
            nextList = uppercaseStrings

        case .LowercasedAlphabet:
            nextList = lowercasedStrings

        case .Numeral:
            nextList = numberStrings
        }
        return list + nextList
    }

    let randomString: String
    if hyphenFrequency > 0 {
        let hyphenPosition = hyphenFrequency + 1
        randomString = (1 ... length).reduce("") { (string, index) -> String in
            if index % hyphenPosition == 0 {
                return string + "-"
            } else {
                return string + characterList.getRandomElement()
            }
        }

    } else {
        randomString = (1 ... length).reduce("") { (string, _) -> String in
            return string + characterList.getRandomElement()
        }
    }

    if length >= types.count {

        for type in types {

            let characterArray: [String]
            switch type {
            case .UppercasedAlphabet:
                characterArray = uppercaseStrings

            case .LowercasedAlphabet:
                characterArray = lowercasedStrings

            case .Numeral:
                characterArray = numberStrings
            }

            guard randomString.containsCharacterInArray(characterArray) else {
                return createRandomString()
            }

        }
    }

    return randomString

}

func copyStringToPasteboard(string: String) {

    let board = NSPasteboard.generalPasteboard()
    board.clearContents()

    let item = NSPasteboardItem()
    item.setString(string, forType: NSPasteboardTypeString)
    board.writeObjects([item])

}

func parseCommand() {

    let arguments = Array(Process.arguments.dropFirst())

    if arguments.contains("-h") || arguments.contains("--help") {
        printHelp()
        exit(EXIT_SUCCESS)

    } else {
        var arguments = arguments
        while let argument = arguments.first {
            switch argument {
            case "-nu", "--no-uppercase":
                types.remove(.UppercasedAlphabet)
                arguments.removeFirst()

            case "-nl", "--no-lowercase":
                types.remove(.LowercasedAlphabet)
                arguments.removeFirst()

            case "-nn", "--no-numeral":
                types.remove(.Numeral)
                arguments.removeFirst()

            case "-nc", "--no-copy":
                shouldCopyToPasteboard = false
                arguments.removeFirst()

            case "-l", "--length":
                guard arguments.count > 1, let lengthParameter = Int(arguments[1]) else {
                    printError()
                    exit(EXIT_FAILURE)
                }
                length = lengthParameter
                arguments.removeFirst(2)

            case "-hf", "--hyphen-frequency":
                guard arguments.count > 1, let frequencyParameter = Int(arguments[1]) else {
                    printError()
                    exit(EXIT_FAILURE)
                }
                hyphenFrequency = frequencyParameter
                arguments.removeFirst(2)

            default:
                printError()
                exit(EXIT_FAILURE)
            }
        }

        let result = createRandomString()
        print("Generated string: \(result)")

        if shouldCopyToPasteboard {
            copyStringToPasteboard(result)
            print("Result has been copied to your clipboard, you can use cmd + v to paste it.")
        }

        print()

    }

}

parseCommand()

コード解説

macOS 用のコマンドラインプログラムなので、もちろんいつもの iPhone 用プログラムとは少々勝手が違うし、普通の C 言語プログラムともちょっと違います。iPhone プログラムみたいに AppDelegateViewController などのものがありませんのでそこらへんのライフサイクルとかもきにする必要はありません。そして C 言語みたいに明示的な void main() 関数も必要ありません。なのでものすっごい長いコードが記述されていますが、実際コマンドとして実行されるのは最後の 1 行の parseCommand() のみです。それ以外のものは全てこの parseCommand() の中身です。

コマンドラインプログラムだから、命令から引数を取得することも可能です。C 言語では void main(int argc, char *argv[]) のような記述で取っていますが、Swift は main() 関数がない代わりに、プログラムの中に Process.arguments で引数を取れます。引数はコマンドラインを " "(半角スペース)で区切られたもので撮りますが、一番最初のものは自分自身のプログラム名ですので基本要りません。

そしてクリップボードに結果をコピーするのは NSPasteboard を使います。具体的な使い方はソースコードの func copyStringToPasteboard(string: String) を読めばわかるかと思います。

ビルド

ファイルをそのまま保存して、ターミナルから保存したディレクトリーに移動し、$ swiftc -o hanako ファイル名 を実行すれば hanako というバイナリファイルが吐き出されます。このファイルを /usr/local/bin の中にコピーすればターミナルからいつでも hanako でランダム文字列を生成できます。もちろん引数に対応しているので、$ hanako のコマンドの後ろに -h を追加すればヘルプが出るし、-nu を入れれば大文字が文字セットから外されるし、-l 12 を入れれば 12 文字の長さの文字列が返されるし、-hf 3 を入れれば Safari の自動生成パスワードみたいに 3 文字ごとに「-」が入るし、-nc を入れればクリップボードにコピーせず文字列の生成のみ行われます。

Github レポジトリ

一応 MIT License で Github にプロジェクトを公開しています

余談

プロジェクト名が「hanako」になっている理由

あんハピ♪という作品に、「花小泉 杏」というキャラが登場してるんですよ。そのキャラがものすごい不運の持ち主なのに常にポジティブな姿勢で笑顔が絶えない、とっても健気なキャラクターです。個人的に今期(2016年4月期)アニメのキャラランキングでは余裕でトップスリーに入るくらい大好きです。ちなみにもともとは名前「杏」だけど、あだ名が「花小泉」略して「はなこ」になりました。もうはなこマジ天使。

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
3