てんこ製作

Tenco Works

プログラムを作るときに考えていることを実況してみる(後編)

なにかプログラムを作ろうと思い立ったとき、何をどう考えてプログラム完成まで至るのか。実際のてんこの実例を書き残してみたいと思います。プログラム自体の書き方と言うよりは、考え方に寄せた記事になります。今回は前回の記事で考えた内容を具体的にプログラミングしていくところ。

いよいよ作り始めます

この時点で2つ作るものがあるのがわかってます。

  • メニュー内容を書いておく.cmune.json
  • プログラム本体のcmenu.py

処理フロー的には最初にjsonファイルを読むところから始まるので、先にデータをキチッと決めておいた方がプログラム書きやすそう。先にjsonファイルを書いてしまいます。

メニューデータ(jsonファイル)

簡単な例として3種類のlsコマンドを選択して実行するメニューを作ってみます。

{
    "main": {
            "List file (ls)": "ls",
            "List file (ls -a)": "ls -a",
            "List file (ls -la)": "ls -la"
    }
}

始めから説明テキスト並べるだけでも良いのですが、後々複数メニューに拡張していけるようにmainという箱の中に入れて階層を作っておきました。では、このデータを使って動作するようプログラムを作っていきます。

この内容でホームディレクトリに.cmenu.jsonという名前で保存しておきます。

プログラム

では次にpythonでプログラムを作っていきます。

#!/usr/bin/python3

# デフォルトメニューの読み込み

# 読み込んだメニューを表示

# メニューの選択入力待ち

# 選択したメニューのコマンド実行

# 終わり

まずは骨組みだけ。先に書いていたフローチャートの通りにコメントだけ先に入れてみました。

デフォルトメニューの読み込み

ホームディレクトリにある.cmenu.jsonを読み込む処理を書きます。

pythonでのjsonの扱い方はマニュアルのこちらに説明がありますが、ここは斜め読みします。

docs.python.org

json.load()メソッドが使えそうです。たぶんこんな感じのはず。

import json
with open("file/to/path") as fp:
    menudata = json.load(fp)

読み込んだjsonデータは辞書形式でmenudataに格納されます。

ここで一つ問題があります。ホームディレクトリにある.cmenu.jsonを読み込むことにしてましたが、ホームディレクトリはどうやって取得すればよいのか。

ググってみたらQiitaの記事が。環境変数から取ってくるのが定石っぽいですね。

qiita.com

import os
home_dir = os.environ["HOME"]

なので、こうします。

import json
import os
with open(os.environ["HOME"]+"/.cmenu.json") as fp:
menudata = json.load(fp)

読み込んだメニューを表示

辞書形式のmenudataのキー部がメニューのテキスト、バリュー部が実行するコマンドになります。jsonファイルの方にはそうなるように書きました。

とりあえず表示してみたいと思います。

print("----- CMENU LIST -----")
i = 1
for k,v in menudata["main"].items():
    print(str(i)+") "+k)
    i = i + 1
else:
    print("q) quit")
print("----------------------")

この書き方をしておけば、メニューがいくつ並んでも対応できるはず。

実行結果。

----- CMENU LIST -----
1) List file (ls)
2) List file (ls -a)
3) List file (ls -la)
q) Quit
----------------------
$ 

表示はできました。

問題発生

この時点までは大丈夫そうなんですが、この後のことでちょっと気になり始めました。

表示した後はメニュー番号をを選んでもらって、選んだメニュー番号で再度menudataの中のコマンドを取り出さないといけないです。なので、表示した番号とメニューの中身をどうにかして紐付けておかないと・・・。

読み込んだデータにメニューの番号も一緒に保存できるように、変数の形を変えることにします。

デフォルトメニューの読み込み(改造版)

import json
import os

with open(os.environ["HOME"]+"/.cmenu.json") as fp:
    jsondata = json.load(fp)

読み込み部分はそのままにして、メニュー選択に使いやすい形に変えて変数に保存できるように形を変えることにします。

menudata = {}
i = 0
for k,v in jsondata["main"].items():
    menudata[str(i)] = {"text":k,"cmd":v}
    i = i+1
menudata["q"] = {"text":"quit","cmd":""}

辞書型を用意してメニュー番号をキーにして、jsonデータのテキストとコマンドを辞書として保存していきます。ただしキーの型は、あとで入力文字列と比較することになるのでわざと文字列にしてmenudata[str(i)]としてます。

終了用のメニューもここで用意してしまいます。

読み込んだメニューを表示(改造版)

print("----- CMENU LIST -----")
for k,v in menudata.items():
  print(k+") "+v["text"])
print("----------------------")

終了メニューも変数に組み込んでしまったので、プログラム側は変数の中身を表示するだけになりました。

メニューの選択入力待ち

次はユーザーにメニューを選んでキーボード入力してもらいます。python3ではユーザーからの入力はinput()メソッドを使用します。

selected = input(" select menu : ")

これで変数selectedにユーザーが入力した文字列が入ります。

問題発生2

表示してるメニューを素直に選んでくれたら問題はないですが、メニューに無い番号を入力されたらどうしましょうか?

考えられる処理としては、

  • そのままエラー終了
  • もう一回入力待ちする
  • もう一回メニューを表示して入力待ちする

このくらい?

どれでもいいんでしょうけど、もう一回入力待ちすることにします。

メニューの選択入力待ち(改造版)

入力待ちをしたあとにメニューの選択肢があるかどうかを判定するようにしました。存在するメニュー番号であればwhileループを抜けるようにしてます。

selected = -1
while selected == -1:
    inputdata = input("select menu : ")
    if inputdata in menudata:
        selected = inputdata

選択したメニューのコマンド実行

入力されたメニュー番号に対応するコマンドを実行する部分に入ります。

ここの処理に来るときには、変数selectedにはユーザが入力したメニュー番号が入ってます。しかも存在しないメニュー番号は入ってません。また実行するコマンドはmenudataに入ってます。menudata[selected]["cmd"]でコマンドを取り出すことができます。

唯一考慮が必要なのはqを入力されたとき。このときだけはコマンドを実行するのではなくて、このプログラムを終了する必要があります。

コマンドの実行に関しては、ググってみるとほぼほぼsubprocessモジュールを使うやり方が多そうだったので従います。

docs.python.org

pythonのマニュアルによれば、python 3.5で追加されているsubprocess.run()関数が推奨とのこと。使い方としては、コマンド文字列をスペースで分解してリストの形にしてrun()関数の引数として指定します。文字列の分解はshlex.split()関数で行います。

マニュアルを読み込んでいくと、この分解をするのはコマンドインジェクション対策(セキュリティ対策の一つ)のためなんだとか。いずれ記事にまとめたいところ。

arglist = shlex.split(menudata[selected]["cmd"].strip())
# コマンド列のコマンドと引数が1個以上ある、かつ1つ目の内容が空ではないならコマンド実行
if (len(arglist) > 0) and (len(arglist[0]) > 0):
    subprocess.run(arglist)