search
LoginSignup
107
Help us understand the problem. What are the problem?

Go Advent Calendar 2021 Day 1

posted at

updated at

ファイルパーミッションでスロットがしたい

この記事は2021年Goアドベントカレンダー1日目の記事です。

ファイルパーミッションでスロットするコマンドラインツールを作りました。

経緯

ある日、サーバ上のとあるファイルの実行権限を変更する事がありました。

$ ls -lah sample_cmd
-rwxr--r-- 1 jiro4989 jiro4989 0 Nov 21 15:14 sample_cmd*

$ chmod 0777 ./sample_cmd

$ ls -lah sample_cmd
-rwxrwxrwx 1 jiro4989 jiro4989 0 Nov 21 15:14 sample_cmd*

この chmod 0777って数字が3つ並んでるからパチスロみたいだなーーーと唐突に思いました。1
(パチスロやったことないですが)

じゃあ、「ファイルのパーミッションをスロットで設定できたらテンション上がって地
味なサーバオペレーションが華やいでハッピーになるのでは?」と思い至りました。

思い立ったが吉日ということで作りました。

成果物

slotchmodというコマンドを作りました。

demo.gif

demo2.gif

まさにスロットですね。Enterキーを押すと回転してるスロットが停止します。

ファイルパーミッションを変更するという地味な作業が、
ドキドキハラハラなゲームに早変わりしました。

実装

画面UIの描画にはtermbox-goを使用しました。

処理としては、「画面描画用のgoroutine」と「キー入力待ちの無限ループ」という
2つの処理が並列に動作して機能を実現しています。

以下はデータ更新と取得の関係性を表したUML図です。PlantUMLで書きました。

goroutineを起動している処理は部分の処理の抜粋は以下です。

まずSlot object(以降Slot)を初期化し、termboxの初期化処理を行います。
次に画面描画goroutineを起動し、キー入力待ちの無限ループに入ります。

main.go
// 省略

func main() {
    args, err := ParseArgs()
    if err != nil {
        Err(err)
        os.Exit(1)
    }

    interval := slotIntervalTime[args.Level]
    style := styles[args.Style]
    slot := NewSlot(0, interval)

    if err := termbox.Init(); err != nil {
        panic(err)
    }
    defer termbox.Close()
    termbox.SetInputMode(termbox.InputEsc)
    termbox.Flush()

    go clock(slot, style)
    waitKeyInput(slot)
    termbox.Close()

    changeMode(slot, args.Args)
}

func clock(s *Slot, st DrawStyle) {
    for !s.IsFinished() {
        s.Switch()
        DrawSlot(s, st)
        time.Sleep(time.Duration(s.IntervalTime()) * time.Millisecond)
    }
}

func waitKeyInput(s *Slot) {
    for {
        switch ev := termbox.PollEvent(); ev.Type {
        case termbox.EventKey:
            switch ev.Key {
            case termbox.KeyCtrlC, termbox.KeyCtrlD:
                return
            case termbox.KeyEnter:
                s.Select()
            }
            switch ev.Ch {
            case 'q':
                return
            }
        }
        if s.IsFinished() {
            return
        }
    }
}

Slotのフィールドと関数は以下のようになっています。

現在選択中のスロットをcurrentSlotIndexというフィールドで管理しています。
currentSlotIndexはキー入力のEnterで1ずつインクリメントされ、値が2を超えるとSlotは終了状態に変化します。
Slotが終了状態になると、キー入力待ち無限ループもループを脱出し、Slotの結果を踏まえてファイルパーミッションを更新します。

slot.go
const (
    slotMinValue = 0
    slotMaxValue = 7
)

type Slot struct {
    slots            [3]int
    currentSlotIndex int
    isFinished       bool
    intervalTime     int
}

func NewSlot(seed int64, interval int) *Slot {
    if seed == 0 {
        i, _ := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
        seed = i.Int64()
    }
    rand.Seed(seed)

    s := Slot{}
    for i := 0; i < 3; i++ {
        slotValue := rand.Intn(slotMaxValue + 1)
        s.slots[i] = slotValue
    }
    s.intervalTime = interval

    return &s
}

func (s *Slot) Switch() {
    s.slots[s.currentSlotIndex] = s.NextValue()
}

func (s *Slot) Select() {
    if 2 <= s.currentSlotIndex {
        s.isFinished = true
        return
    }
    s.currentSlotIndex++
}

func (s *Slot) IsFinished() bool {
    return s.isFinished
}

func (s *Slot) Slots() [3]int {
    return s.slots
}

func (s *Slot) PreviousValue() int {
    v := s.slots[s.currentSlotIndex] - 1
    if v < slotMinValue {
        v = slotMaxValue
    }
    return v
}

func (s *Slot) CurrentValue() int {
    return s.slots[s.currentSlotIndex]
}

func (s *Slot) CurrentSlotIndex() int {
    return s.currentSlotIndex
}

func (s *Slot) NextValue() int {
    v := s.slots[s.currentSlotIndex] + 1
    if slotMaxValue < v {
        v = slotMinValue
    }
    return v
}

func (s *Slot) IntervalTime() int {
    return s.intervalTime
}

画面描画goroutineが呼び出している処理はこんな感じです。
愚直に文字列を組み立てているだけで、特筆することはありません。

view.go
// 省略

func DrawSlot(s *Slot, st DrawStyle) {
    termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)

    idx := s.CurrentSlotIndex()
    pv := s.PreviousValue()
    nv := s.NextValue()
    slots := s.Slots()

    switch st {
    case DrawStyleSimple:
        drawSimple(slots, idx, pv, nv)
    case DrawStyleBig:
        drawBig(slots, idx, pv, nv)
    default:
        drawSimple(slots, idx, pv, nv)
    }

    termbox.Flush()
}

func drawSimple(slots [3]int, idx, pv, nv int) {
    p := [3]string{" ", " ", " "}
    p[idx] = fmt.Sprintf("%d", pv)

    n := [3]string{" ", " ", " "}
    n[idx] = fmt.Sprintf("%d", nv)

    rows := []string{
        fmt.Sprintf("      %s %s %s", p[0], p[1], p[2]),
        fmt.Sprintf("chmod %d %d %d", slots[0], slots[1], slots[2]),
        fmt.Sprintf("      %s %s %s", n[0], n[1], n[2]),
    }

    for y, row := range rows {
        for x, r := range row {
            termbox.SetChar(x, y, r)
        }
    }
}

ハッピーになれる機能

3つ揃うと「BINGO🎉」と出してくれます。

bingo.PNG

集中しっぱなしで神経を使う本番作業にささやかなお祝いメッセージをお届けしてくれます。
これでハッピー。

それが作業者の意図したパーミッションなのかどうかはslotchmodの関心事ではありません。

まとめ

以下の内容を書きました。

  • ファイルパーミッションでスロットするコマンドラインツールを作りました
    • ライブラリには termbox-go を使用しています
    • 画面描画とキー入力待ちの2つの処理を並列で動かして機能を実現しています
    • 数字が3つ揃うと「BINGO🎉」と出してくれます
  • 自己責任で使いましょう

以上です。


  1. chmod は8進数モード以外にも chmod +x で実行権限をつけたり、chmod u+rwx,o-x,g=rなどuser, group, other別々に文字を使って権限を付与または解除できます。僕は8進数モードを好んで使うのですが、もしこちらのモードを常用していたら、今回のプログラムを思いつかなかったかもしれません 

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
What you can do with signing up
107
Help us understand the problem. What are the problem?