コンテンツにスキップ

Go/os/Args

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go

第7章 コマンドライン引数の処理 - os.Args

[編集]

はじめに

[編集]

Goプログラムを作成する際、コマンドラインから引数を受け取る必要がある場面は多々あります。os.Argsは、コマンドライン引数にアクセスするための最も基本的で重要な仕組みです。この章では、os.Argsの使い方から実践的なパターンまでを詳しく解説します。

os.Argsの基本概念

[編集]

os.Argsとは

[編集]

os.Argsは、[]string型のスライスで、コマンドライン引数を格納しています。このスライスには以下の特徴があります:

  • インデックス0には実行ファイル名(またはプログラム名)が格納される
  • インデックス1以降に実際のコマンドライン引数が格納される
  • 引数は文字列として扱われる
  • 空白で区切られた各要素が一つの引数となる

基本的な使用例

[編集]
package main

import (
    "fmt"
    "os"
)

func main() {
    // コマンドライン引数の数を表示
    fmt.Printf("引数の数: %d\n", len(os.Args))
    
    // すべての引数を表示
    for i, arg := range os.Args {
        fmt.Printf("Args[%d]: %s\n", i, arg)
    }
}

実行例:

$ go run main.go hello world 123
引数の数: 4
Args[0]: /tmp/go-build123456789/b001/exe/main
Args[1]: hello
Args[2]: world
Args[3]: 123

実用的なパターン

[編集]

引数の数をチェックする

[編集]
package main

import (
    "fmt"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("使用法: program <引数>")
        os.Exit(1)
    }
    
    fmt.Printf("第1引数: %s\n", os.Args[1])
}

複数の引数を処理する

[編集]
package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    if len(os.Args) < 3 {
        fmt.Println("使用法: calculator <num1> <num2>")
        os.Exit(1)
    }
    
    num1, err1 := strconv.ParseFloat(os.Args[1], 64)
    num2, err2 := strconv.ParseFloat(os.Args[2], 64)
    
    if err1 != nil || err2 != nil {
        fmt.Println("エラー: 数値を入力してください")
        os.Exit(1)
    }
    
    fmt.Printf("%.2f + %.2f = %.2f\n", num1, num2, num1+num2)
}

オプションフラグの簡単な処理

[編集]
package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    var verbose bool
    var files []string
    
    // os.Args[1:]で実行ファイル名を除外
    for _, arg := range os.Args[1:] {
        if strings.HasPrefix(arg, "-") {
            switch arg {
            case "-v", "--verbose":
                verbose = true
            default:
                fmt.Printf("不明なオプション: %s\n", arg)
                os.Exit(1)
            }
        } else {
            files = append(files, arg)
        }
    }
    
    if verbose {
        fmt.Println("詳細モードが有効です")
    }
    
    fmt.Printf("処理するファイル: %v\n", files)
}

高度な使用例

[編集]

サブコマンドの実装

[編集]
package main

import (
    "fmt"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        printUsage()
        os.Exit(1)
    }
    
    command := os.Args[1]
    args := os.Args[2:]
    
    switch command {
    case "create":
        handleCreate(args)
    case "delete":
        handleDelete(args)
    case "list":
        handleList(args)
    default:
        fmt.Printf("不明なコマンド: %s\n", command)
        printUsage()
        os.Exit(1)
    }
}

func printUsage() {
    fmt.Println("使用法:")
    fmt.Println("  program create <name>")
    fmt.Println("  program delete <name>")
    fmt.Println("  program list")
}

func handleCreate(args []string) {
    if len(args) < 1 {
        fmt.Println("エラー: 作成する項目名を指定してください")
        return
    }
    fmt.Printf("'%s'を作成しています...\n", args[0])
}

func handleDelete(args []string) {
    if len(args) < 1 {
        fmt.Println("エラー: 削除する項目名を指定してください")
        return
    }
    fmt.Printf("'%s'を削除しています...\n", args[0])
}

func handleList(args []string) {
    fmt.Println("項目一覧を表示しています...")
}

設定ファイルパスの指定

[編集]
package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    configPath := getConfigPath()
    fmt.Printf("設定ファイル: %s\n", configPath)
}

func getConfigPath() string {
    // デフォルトの設定ファイルパス
    defaultConfig := filepath.Join(os.Getenv("HOME"), ".myapp", "config.json")
    
    // コマンドライン引数をチェック
    for i, arg := range os.Args {
        if arg == "-config" || arg == "--config" {
            if i+1 < len(os.Args) {
                return os.Args[i+1]
            } else {
                fmt.Println("エラー: -configオプションにはパスが必要です")
                os.Exit(1)
            }
        }
    }
    
    return defaultConfig
}

注意点とベストプラクティス

[編集]

引数のバリデーション

[編集]
package main

import (
    "fmt"
    "os"
    "regexp"
    "strconv"
)

func validatePort(portStr string) (int, error) {
    port, err := strconv.Atoi(portStr)
    if err != nil {
        return 0, fmt.Errorf("ポート番号が不正です: %s", portStr)
    }
    
    if port < 1 || port > 65535 {
        return 0, fmt.Errorf("ポート番号は1-65535の範囲で指定してください: %d", port)
    }
    
    return port, nil
}

func validateEmail(email string) error {
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(email) {
        return fmt.Errorf("メールアドレスの形式が不正です: %s", email)
    }
    return nil
}

func main() {
    if len(os.Args) != 3 {
        fmt.Println("使用法: program <email> <port>")
        os.Exit(1)
    }
    
    email := os.Args[1]
    portStr := os.Args[2]
    
    if err := validateEmail(email); err != nil {
        fmt.Printf("エラー: %v\n", err)
        os.Exit(1)
    }
    
    port, err := validatePort(portStr)
    if err != nil {
        fmt.Printf("エラー: %v\n", err)
        os.Exit(1)
    }
    
    fmt.Printf("Email: %s, Port: %d\n", email, port)
}

引数の安全な処理

[編集]
package main

import (
    "fmt"
    "os"
    "path/filepath"
    "strings"
)

func sanitizeFilename(filename string) string {
    // 危険な文字を除去または置換
    filename = strings.ReplaceAll(filename, "..", "_")
    filename = strings.ReplaceAll(filename, "/", "_")
    filename = strings.ReplaceAll(filename, "\\", "_")
    
    // ファイル名の長さ制限
    if len(filename) > 255 {
        filename = filename[:255]
    }
    
    return filename
}

func main() {
    if len(os.Args) < 2 {
        fmt.Println("使用法: program <filename>")
        os.Exit(1)
    }
    
    rawFilename := os.Args[1]
    safeFilename := sanitizeFilename(rawFilename)
    fullPath := filepath.Join("./data", safeFilename)
    
    fmt.Printf("入力されたファイル名: %s\n", rawFilename)
    fmt.Printf("安全なファイル名: %s\n", safeFilename)
    fmt.Printf("完全パス: %s\n", fullPath)
}

os.Argsの限界と代替案

[編集]

os.Argsの限界

[編集]

os.Argsは基本的な機能しか提供しないため、以下のような場合には限界があります:

  • 複雑なオプション解析
  • ヘルプメッセージの自動生成
  • 型安全な引数処理
  • 設定ファイルとの統合

代替ライブラリ

[編集]

より高度な機能が必要な場合は、以下のようなライブラリを検討してください:

// flagパッケージ(標準ライブラリ)
import "flag"

// サードパーティライブラリの例
// - github.com/spf13/cobra
// - github.com/urfave/cli
// - github.com/alecthomas/kong

基本的なflagパッケージの例:

package main

import (
    "flag"
    "fmt"
)

func main() {
    var name = flag.String("name", "World", "挨拶する対象")
    var count = flag.Int("count", 1, "繰り返し回数")
    
    flag.Parse()
    
    for i := 0; i < *count; i++ {
        fmt.Printf("Hello, %s!\n", *name)
    }
    
    // 残りの引数(非フラグ引数)
    fmt.Printf("残りの引数: %v\n", flag.Args())
}

実践的なプロジェクト例

[編集]

ファイル処理ツール

[編集]
package main

import (
    "fmt"
    "io"
    "os"
    "path/filepath"
    "strings"
)

func main() {
    if len(os.Args) < 3 {
        printUsage()
        os.Exit(1)
    }
    
    operation := os.Args[1]
    
    switch operation {
    case "copy":
        if len(os.Args) != 4 {
            fmt.Println("使用法: program copy <source> <dest>")
            os.Exit(1)
        }
        copyFile(os.Args[2], os.Args[3])
    case "rename":
        if len(os.Args) != 4 {
            fmt.Println("使用法: program rename <old> <new>")
            os.Exit(1)
        }
        renameFile(os.Args[2], os.Args[3])
    case "info":
        if len(os.Args) != 3 {
            fmt.Println("使用法: program info <file>")
            os.Exit(1)
        }
        showFileInfo(os.Args[2])
    default:
        fmt.Printf("不明な操作: %s\n", operation)
        printUsage()
        os.Exit(1)
    }
}

func printUsage() {
    fmt.Println("ファイル処理ツール")
    fmt.Println("使用法:")
    fmt.Println("  program copy <source> <dest>   - ファイルをコピー")
    fmt.Println("  program rename <old> <new>     - ファイルをリネーム")
    fmt.Println("  program info <file>            - ファイル情報を表示")
}

func copyFile(src, dst string) {
    sourceFile, err := os.Open(src)
    if err != nil {
        fmt.Printf("ソースファイルを開けません: %v\n", err)
        os.Exit(1)
    }
    defer sourceFile.Close()
    
    destFile, err := os.Create(dst)
    if err != nil {
        fmt.Printf("宛先ファイルを作成できません: %v\n", err)
        os.Exit(1)
    }
    defer destFile.Close()
    
    _, err = io.Copy(destFile, sourceFile)
    if err != nil {
        fmt.Printf("ファイルのコピーに失敗しました: %v\n", err)
        os.Exit(1)
    }
    
    fmt.Printf("'%s' を '%s' にコピーしました\n", src, dst)
}

func renameFile(oldName, newName string) {
    err := os.Rename(oldName, newName)
    if err != nil {
        fmt.Printf("リネームに失敗しました: %v\n", err)
        os.Exit(1)
    }
    
    fmt.Printf("'%s' を '%s' にリネームしました\n", oldName, newName)
}

func showFileInfo(filename string) {
    info, err := os.Stat(filename)
    if err != nil {
        fmt.Printf("ファイル情報を取得できません: %v\n", err)
        os.Exit(1)
    }
    
    fmt.Printf("ファイル名: %s\n", info.Name())
    fmt.Printf("サイズ: %d bytes\n", info.Size())
    fmt.Printf("モード: %s\n", info.Mode())
    fmt.Printf("更新時刻: %s\n", info.ModTime())
    fmt.Printf("ディレクトリ: %t\n", info.IsDir())
    fmt.Printf("拡張子: %s\n", filepath.Ext(filename))
}

まとめ

[編集]

os.Argsは、Goでコマンドライン引数を処理するための基本的で強力なツールです。この章で学んだポイント:

  1. 基本概念: os.Args[0]は実行ファイル名、os.Args[1:]が実際の引数
  2. バリデーション: 引数の数や形式を必ずチェックする
  3. エラーハンドリング: 不正な引数に対して適切にエラー処理を行う
  4. セキュリティ: ユーザー入力は必ずサニタイズする
  5. 使い分け: 複雑な要件にはflagパッケージやサードパーティライブラリを検討する

シンプルなツールやスクリプトにはos.Argsで十分ですが、より複雑なCLIアプリケーションを作成する場合は、専用のライブラリの使用を検討してください。適切なツールを選択することで、保守しやすく使いやすいアプリケーションを作成できます。