シェルスクリプトマガジン

test

Vol.58

投稿日:2019.01.25 | カテゴリー: バックナンバー

 インターネット上のサイトで欠かせないサーバーソフトウエアが、データを格納・管理する「データベース管理システム」です。特集1では、オープンソースのデータベース管理システムとして人気が高い「MySQL」を紹介します。入手方法、Linuxディストリビューション「CentOS」へのインストール方法だけでなく、データベースの核となる「ストレージエンジン」、運用管理やトラブルシューティングなどで重要な「ログ」、データベース処理で知っておきたい「トランザクション」や「ロック」に触れます。
 特集2では、小型コンピュータボード「Raspberry Pi」上で「IchigoJam BASIC RPi」による「BASIC」プログラミングを紹介します。BASICは、古くからある言語で、プログラミングの基本が学べます。子供から大人まで楽しめますので、親子で一緒にプログラミングを始めましょう。
 特別企画では、サイボウズの業務改善プラットフォーム「kintone」上で実用的な業務システムの「交通費申請システム」を作成します。入力フォームにテキストボックスやボタンなどの部品を配置し、簡単なワークフローを設定するだけなので、プログラムを一切書く必要はありません。無料のアカウントを登録すれば、すぐに試せます。
 このほか、「中小企業手作りIT化奮戦記」「センサーボードで学ぶ 電子回路の制御」などの人気連載も掲載しています。
 今月も読み応え十分のシェルスクリプトマガジン Vol.58。お見逃しなく!

※記事掲載のコードはこちら。記事の補足情報はこちら

※読者アンケートはこちら

香川大学SLPからお届け!(Vol.58掲載)

投稿日:2019.01.25 | カテゴリー: コード

著者:竹原 一駿
2018年秋に広島と香川で「オープンソースカンファレンス」(OSC)が開催されました。SLPは、両OSCでブースを出展し、OSC香川では3人の学生がライトニングトーク(LT)を行いました。今回は、これらの活動の内容や、それによって得られた体験などについて報告します。OSCへの一般参加や出展などを考えている読者の方の参考になれば幸いです。

シェルスクリプトマガジン Vol.58は以下のリンク先でご購入できます。

図2 自作コンテナエンジンのソースコードの一部
unshareシステムコールによるLinux Namespaceの分離
unshare(
  CloneFlags::CLONE_NEWPID
    | CloneFlags::CLONE_NEWIPC
    | CloneFlags::CLONE_NEWUTS
    | CloneFlags::CLONE_NEWNS
    | CloneFlags::CLONE_NEWUSER,)
fork/execシステムコールによるコンテナ内子プロセス生成
match fork() {
  Ok(ForkResult::Parent { child, .. }) => {
    match waitpid(child, None).expect("waitpid faild") {
      WaitStatus::Exited(_, _) => {}
      WaitStatus::Signaled(_, _, _) => {}
      _ => eprintln!("Unexpected exit."),
    }
  }
  Ok(ForkResult::Child) => {
    sethostname(&self.name).expect("Could not set hostname");
    fs::create_dir_all("proc").unwrap_or_else(|why| {
      eprintln!("{:?}", why.kind());
    });
    println!("Mount procfs ... ");
    mounts::mount_proc().expect("mount procfs failed.");
    let cmd = CString::new(self.command.clone()).unwrap();
    let default_shell = CString::new("/bin/bash").unwrap();
    let shell_opt = CString::new("-c").unwrap();
    execv(&default_shell, &[default_shell.clone(), shell_opt, cmd]).expect("execution faild.");
  }
  Err(_) => eprintln!("Fork failed"),
}
図5 「数字に隠された真実」のソースコードの一部
数値を1桁ずつ分解して、すべての和を求める関数
function add_1(number, result_text, c) {
  var num = [];
  if (parseInt(number / 10) == 0 && (number != 6 || number != 9)) { return 0; } // 1桁になって悪魔の数字じゃなければ失敗
  num = num_split_onedigit(number); // 数値を1桁ずつ分解
  sum = num_add_onedigit(num); // すべて加算
  result_text[c] = array_value_text(num, sum, "+"); // 計算過程の文字列を作成
  var check = akumanumber_check(sum, result_text); // 悪魔の数字かどうかチェック
  if (check != 0) { return check; } // 悪魔の数字なら終了
  var result = add_1(sum, result_text, c + 1); // 処理1を実行(再帰)
  if (result != 0) { return result; } else { result_t  ext.splice(c, 1); }
  var result = mult_1(sum, result_text, c + 1); // 処理2を実行(相互再帰)
  if (result != 0) { return result; } else { result_text.splice(c, 1); }
  return 0;
}
数値を1桁ずつ分解して、すべての積を求める関数
function mult_1(number, result_text, c) {
  var num = [];
  if (parseInt(number / 10) == 0 && (number != 6 || number != 9)) { return 0; } // 1桁になって悪魔の数字じゃなければ失敗
  num = num_split_onedigit(number); // 数値を1桁ずつ分解
  sum = num_mult_onedigit(num); // すべて乗算
  result_text[c] = array_value_text(num, sum, "×"); // 計算過程の文字列を作成
  var check = akumanumber_check(sum, result_text); // 悪魔の数字かどうかチェック
  if (check != 0) { return check; } // 悪魔の数字なら終了
  var result = add_1(sum, result_text, c + 1); // 処理1を実行(相互再帰)
  if (result != 0) { return result; } else { result_text.splice(c, 1); }
  var result = mult_1(sum, result_text, c + 1); // 処理2を実行(再帰)
  if (result != 0) { return result; } else { result_text.splice(c, 1); }
  return 0;
}
図9 サーバー監視システムのソースコードの一部
int SystemAnalyzer::GetPreTick_(void)
{
  // 演算に使用されたTick値を取得
  FILE *infile = fopen("/src/proc/stat", "r");
  if (NULL == infile) {
    cout << "[GetCPUUsage]<<Cannot open /src/proc/stat"
         << endl;
    return 0;
  }
  int usr, nice, sys;
  char buf[1024]; // 文字列"cpu"の部分の入力用
  int result = fscanf(infile, "%s %d %d %d",
                      buf, &usr, &nice, &sys);
  if (result == -1) {
    cout << "[GetCPUUsage]<<Cannot read fscanf"
         << endl;
    return 0;
  }
  fclose(infile);
  return usr + nice + sys;
}

Vol.58 補足情報

投稿日:2019.01.25 | カテゴリー: コード

Techパズル 第11回

「タテのカギ」の「18 現実」が抜けていました。お詫びして訂正いたします。

バーティカルバーの極意

p.73のタイトル部分の辰己先生の「己」が「巳」になっていました。お詫びして訂正いたします *

* 2019年1月25日に掲載した訂正情報にも誤りがあり、当該の先生にもご迷惑をおかけしたことをお詫び申し上げます。

情報は随時更新致します。

Node.js/Expressで楽々Webアプリ開発(Vol.58掲載)

投稿日:2019.01.25 | カテゴリー: コード

著者:しょっさん
プログラミング言語「JavaScript」の実行環境「Node.js」と「Express」フレームワークを使って、基本となるWebアプリの開発手法を習得しましょう。第4回は「蔵書管理アプリケーション」のサンプルプログラムで認証機能を実現す
る方法を解説します。

シェルスクリプトマガジン Vol.58は以下のリンク先でご購入できます。

図3 「models/user.js」ファイルにLibrariesテーブルとの関係性を追記
user.associate = function (models) {
  // associations can be defined here
  user.hasMany(models.Library, { foreignKey: 'user_id' });
};
return user;return user;
図4 モデルファイル「models/library.js」の修正
'use strict';
module.exports = (sequelize, DataTypes) => {
  var Library = sequelize.define('Library', {
    book_title: DataTypes.STRING,
   author: DataTypes.STRING,
    publisher: DataTypes.STRING,
    image_url: DataTypes.STRING(2048),
    user_id: DataTypes.INTEGER
  },
    {
      underscored: true
    });
  Library.associate = function (models) {
    // associations can be defined here
    Library.hasMany(models.Comment, { foreignKey: 'book_id'});
  };
  return Library;
};
図5 マイグレーションファイルの追記内容
user_id: {
  type: Sequelize.INTEGER,
  allowNull: false,
  foreignKey: true,
  references: {
    model: 'users',
    key: 'id',
  },
  onUpdate: 'RESTRICT',
  onDelete: 'RESTRICT',
},
図6 作成したseed(日付-demo-user.js)
'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    const models = require('../models');
    return models.user.bulkCreate([
      {
        id: 1,
        email: 'tak@oshiire.to',
        password: '$2b$10$t73WMpPlvyhkWuL.ALWe..OKbU1q1ssR4K5ezVTXLlvaDMtUuAqve',
        salt: '$2b$10$t73WMpPlvyhkWuL.ALWe..'
      },
      {
        id: 2,
        email: 'sho@oshiire.to',
        password: '$2b$10$t73WMpPlvyhkWuL.ALWe..OKbU1q1ssR4K5ezVTXLlvaDMtUuAqve',
        salt: '$2b$10$t73WMpPlvyhkWuL.ALWe..'
      },
      {
        id: 3,
        email: 'shosan@oshiire.to',
        password: '$2b$10$t73WMpPlvyhkWuL.ALWe..OKbU1q1ssR4K5ezVTXLlvaDMtUuAqve',
        salt: '$2b$10$t73WMpPlvyhkWuL.ALWe..'
      }
    ]);
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('users', null, {});
  }
};
図8 passportストラテジの定義
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;

const hashPassword = (password, salt) => {
  var bcrypt = require('bcrypt');
  var hashed = bcrypt.hashSync(password, salt);
  return hashed;
};

passport.use(new LocalStrategy(
  {
    usernameField: 'email',
    passwordField: 'password'
  },
  (username, password, done) => {
    models.user.findOne({ where: { email: username } }).then(user => {
      if (!user)
        return done(null, false, { message: 'Incorrect email.' });
      if (hashPassword(password, user.salt) !== user.password)
        return done(null, false, { message: 'Incorrect password.' });
      return done(null, user.get());
    });
  }
));
図9 セッション連携の関数「serializeUser」と「deserializeUser」
var session = require('express-session');

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  models.user.findById(id).then(user => {
    if (user) {
      done(null, user.get());
    } else {
      done(user.errors, null);
    }
  });
});
図10 ミドルウエアで認証済みか否かの判定
app.use((req, res, next) => {
  if (req.isAuthenticated())
    return next();
  if (req.url === '/' || req.url === '/login')
    return next();
  res.redirect('/');
});

app.use('/', index);
app.use('/books', books);
図11 テストコード(supertest-spec.js)の主要部分
/* eslint-env jasmine */

// routing テスト
const request = require('supertest-session');
const app = require('../app');

//let's set up the data we need to pass to the login method
const userCredentials = {
  email: 'tak@oshiire.to',
  password: 'password'
};
const wrongEmailCredentials = {
  email: 'foo',
  password: 'password'
};
const wrongPasswordCredentials = {
  email: 'tak@oshiire.to',
  password: 'foo'
};

//now let's login the user before we run any tests
const authenticatedUser = request(app);

describe('POST /login', () => {
  it('should redirect to / with unloggined user', (done) => {
    request(app)
      .get('/books/')
      .expect(302)
      .expect('Location', '/', done);
  });
  it('should success login with correct user', (done) => {
    request(app)
      .post('/login')
      .send(userCredentials)
      .expect(302)
      .expect('Location', '/books/', done);
  });
  it('should deny login with wrong email', (done) => {
    request(app)
      .post('/login')
      .send(wrongEmailCredentials)
      .expect(302)
      .expect('Location', '/', done);
  });
  it('should deny login with wrong password', (done) => {
    request(app)
      .post('/login')
      .send(wrongPasswordCredentials)
      .expect(302)
      .expect('Location', '/', done);
  });
});

describe('with Login', () => {
  beforeAll((done) => {
    authenticatedUser
      .post('/login')
      .send(userCredentials)
      .expect(302, done);
  });

  describe('GET /', () => {
    it('respond with http', (done) => {
      authenticatedUser
        .get('/')
        .set('Accept', 'text/html')
        .expect(200, done);
    });
  });
 :
(中略)
 :
});

describe('GET /logout', () => {
  beforeAll((done) => {
    authenticatedUser
      .post('/login')
      .send(userCredentials)
      .expect(302, done);
  });

  it('should go back to login', (done) => {
    authenticatedUser
      .get('/logout')
      .set('Accept', 'text/html')
      .expect(302)
      .expect('Location', '/', done);
  });
});

シェルスクリプトマガジンvol.58 Web掲載記事まとめ

投稿日:2019.01.25 | カテゴリー: コード

シェルスクリプトマガジン Vol.58で掲載しているコードをまとめています。

プレゼント&アンケートページはこちら

シェルスクリプトマガジン Vol.58は以下のリンク先でご購入できます。

004 レポート Linuxカーネル5.0
005 レポート Microsoft社のProject Mu
006 NEWS FLASH
008 特集1 オープンソースのデータベース管理システム MySQL入門/梶山隆輔
030 特集2 ラズパイでBASIC/土肥毅大、岡優樹、納富志津
040 特別企画 kintoneで作る交通費申請システム/佐山ウィリアム 明裕、ぺそ、檀原由香子
050 ラズパイセンサーボードで学ぶ 電子回路の制御/米田聡  コード掲載
055 姐のNOGYO
056 中小企業手作りIT化奮戦記/菅雄一
062 円滑コミュニケーションが世界を救う!/濱口誠一
064 「Visual Studio Code」を便利に使う/山本美穂
068 新年号/桑原滝弥・イケヤシロウ
070 バーティカルバーの極意/飯尾淳
076 法林浩之のFIGHTING TALKS/法林浩之
078 漢のUNIX/後藤大地
084 人間とコンピュータの可能性/大岩元
086 機械学習のココロ/石井一夫  コード掲載
092 Node.js/Expressで楽々Webアプリ開発/しょっさん  コード掲載
100 香川大学SLPからお届け!/竹原一駿  コード掲載
106 UNIXの歴史を振り返る/古寺雅弘
114 ユニケージ新コードレビュー/技術研究員
120 Techパズル/gori.sh
122 コラム「仕事座右の銘」/シェル魔人

機械学習のココロ(Vol.58掲載)

投稿日:2019.01.25 | カテゴリー: コード

著者:石井 一夫
今回は、機械学習による2値分類の方法を紹介します。映画評論のテキストを好評価な内容か悪い評価の 内容かに分類するという例題のTensorFlowのチュートリアルマニュアルに沿って、2値分類のモデリングの定番である「ロジスティック回帰分析」について解説します。

シェルスクリプトマガジン Vol.58は以下のリンク先でご購入できます。

図4 ライブラリのインポートとIMDBデータセットの取り込み
import tensorflow as tf
from tensorflow import keras
import numpy as np
imdb = keras.datasets.imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
図5 IMDBデータセットの中身を表示するコード
print(train_labels[0])
print(train_data[0])
図6 元の単語を表示する関数を定義するコード
# 整数の索引に単語を割り当てる辞書を検索する関数
word_index = imdb.get_word_index()
# 最初の索引で変換
word_index = {k:(v+3) for k,v in word_index.items()}
word_index["<PAD>"] = 0
word_index["<START>"] = 1
word_index["<UNK>"] = 2 # 不明
word_index["<UNUSED>"] = 3
reverse_word_index = dict([(value, key) for (key, value) in word_index.items()])
def decode_review(text):
    return ' '.join([reverse_word_index.get(i, '?') for i in text])
図7 データをテンソル化するコード
train_data = keras.preprocessing.sequence.pad_sequences(
               train_data, value=word_index["<PAD>"],
          padding='post', maxlen=256)
test_data = keras.preprocessing.sequence.pad_sequences(
               test_data, value=word_index["<PAD>"],
               padding='post', maxlen=256)
図8 学習モデルを構築するコード
# 入力データの指定形式は、映画評論に用いられた単語数(10,000 単語)をとる
vocab_size = 10000
model = keras.Sequential()
model.add(keras.layers.Embedding(vocab_size, 16)) # 入力層
model.add(keras.layers.GlobalAveragePooling1D()) # 中間層I
model.add(keras.layers.Dense(16, activation=tf.nn.relu)) # 中間層II
model.add(keras.layers.Dense(1, activation=tf.nn.sigmoid)) # 出力層
図10 モデルをコンパイルするコード
model.compile(optimizer=tf.train.AdamOptimizer(),
              loss='binary_crossentropy',
              metrics=['accuracy'])
図11 訓練用データセットを分割するコード
x_val = train_data[:10000]
partial_x_train = train_data[10000:]
y_val = train_labels[:10000]
partial_y_train = train_labels[10000:]
図12 モデルの訓練用コード
history = model.fit(partial_x_train,
                     partial_y_train,
                     epochs=40,
                     batch_size=512,
                     validation_data=(x_val, y_val),
                     verbose=1)
図13 モデルの性能評価用コード
results = model.evaluate(test_data, test_labels)
print(results)
図14 損失のグラフを表示するコード
%matplotlib inline
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

# 青点グラフ
plt.plot(epochs, loss, 'bo', label='Training loss')
# 青線グラフ
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()

plt.show()
図16 精度のグラフを表示するコード
plt.clf() # 図を初期化
acc_values = history_dict['acc']
val_acc_values = history_dict['val_acc']

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()

plt.show()

ラズパイセンサーボードで学ぶ電子回路の制御(Vol.58掲載)

投稿日:2019.01.25 | カテゴリー: コード

著者:米田 聡
シェルスクリプトマガジンでは、小型コンピュータボード「Raspberry Pi」(ラズパイ)向けのセンサー搭載拡張ボード「ラズパイセンサーボード」を制作しました。第5回では、このボードを使った電子回路制御を取り上げます。具体的には、明るさ・近隣センサーに近づく物体の検出です。

シェルスクリプトマガジン Vol.58は以下のリンク先でご購入できます。

図3 割り込みに対応したVCNL4020ライブラリ(VCNL4020.py)
import smbus
import time
import RPi.GPIO as GPIO
from threading import BoundedSemaphore

class VCNL4020():

  _ALS_OD       = 0b00010000  # オンデマンド明るさ計測スタート
  _PROX_OD      = 0b00001000  # オンデマンド近接計測スタート
  _ALS_EN       = 0b00000100  # 明るさ繰り返し計測有効
  _PROX_EN      = 0b00000010  # 近接繰り返し計測有効
  _SELFTIMED_EN = 0b00000001  # 内蔵タイマー有効
  
  _CONT_CONV    = 0b10000000  # Continue Conversion有効
  _AMBIENT_RATE = 0b00010000  # 明るさの計測レート(default:2sample/s)
  _AUTO_OFFSET  = 0b00001000  # 自動オフセットモード有効
  _AVERAGING    = 0b00000101  # 平均化(default:32conv)

  _COMMAND_REG       = 0x80  # コマンドレジスタ
  _PID_REG           = 0x81  # プロダクトIDレジスタ
  _PROX_RATE_REG     = 0x82  # 近接測定レートジスタ
  _IR_CURRENT_REG    = 0x83  # 近接測定用赤外線LED電流設定レジスタ(default=20mA)
  _AMBIENT_PARAM_REG = 0x84  # 明るさセンサーパラメータレジスタ

  _AMBIENT_MSB       = 0x85  # 明るさ上位バイト
  _AMBIENT_LSB       = 0x86  # 明るさ下位バイト

  _PROX_MSB          = 0x87  # 近接上位バイト
  _PROX_LSB          = 0x88  # 近接下位バイト

  _INT_CONTROL_REG   = 0x89  # 割り込み制御レジスタ

  _LOW_TH_MSB        = 0x8A  # Lowしきい値(MSB)
  _LOW_TH_LSB        = 0x8B  # Lowしきい値(LSB)
  _HIGH_TH_MSB       = 0x8C  # Highしきい値(MSB)
  _HIGH_TH_LSB       = 0x8D  # Highしきい値(LSB)

  _INT_STATUS_REG    = 0x8E  # 割り込みステータス

  _INT_NO            = 0x06  # int = GPIO6

  # コールバック
  __callbackfunc     = None

  def __init__(self, i2c_addr = 0x13, busno = 1):
    self.addr = i2c_addr
    self.i2c = smbus.SMBus(busno)
    
    self._write_reg(self._COMMAND_REG, self._ALS_OD  |\
                       self._PROX_OD |\
                       self._ALS_EN  |\
                       self._PROX_EN |\
                       self._SELFTIMED_EN )
                       
    self._write_reg(self._IR_CURRENT_REG, 2 )  # 20mA
                       
    self._write_reg(self._AMBIENT_PARAM_REG, self._CONT_CONV    |\
                        self._AMBIENT_RATE |\
                        self._AUTO_OFFSET  |\
                        self._AVERAGING )
    self.semaphore = BoundedSemaphore()

    # GPIO設定
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(self._INT_NO, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.add_event_detect(self._INT_NO, GPIO.FALLING, callback=self.__interruptfunc)

    time.sleep(0.6)      # 初回測定まで待つ

  def _write_reg(self, reg, value):
    self.i2c.write_byte_data(self.addr, reg, value)

  def _read_reg(self, reg):
    return self.i2c.read_byte_data(self.addr, reg)

  # 高値用レジスタ設定
  def set_high_threshold(self, value):
    self.semaphore.acquire()
    h = (value & 0xFF00) >> 8
    l = value & 0x00FF
    self._write_reg(self._HIGH_TH_MSB, h)
    self._write_reg(self._HIGH_TH_LSB, l)
    self.semaphore.release()

  # 低値用レジスタ設定
  def set_low_threshold(self, value):
    self.semaphore.acquire()
    h = (value & 0xFF00) >> 8
    l = value & 0x00FF
    self._write_reg(self._LOW_TH_MSB, h)
    self._write_reg(self._LOW_TH_LSB, l)
    self.semaphore.release()

  # 割り込み有効化
  def enable_interrupt(self, callbackfunc=None, prox=True, samples=1):
    self.semaphore.acquire()

    self.__callbackfunc = callbackfunc
    value = self._read_reg(self._INT_CONTROL_REG)

    if callbackfunc is not None:
      if prox:
        value |= 0b00000010
      else:
        value |= 0b00000011
    else:
        value &= 0b11111100

    # samples
    samples &= 0b00000111
    samples = samples << 5
    value &= 0b00011111
    value |= samples

    self._write_reg( self._INT_CONTROL_REG, value)
    self.semaphore.release()

  # 割り込み関数
  def __interruptfunc(self, ch):
    if ch != self._INT_NO:
      return
    if self.__callbackfunc is not None:
      self.__callbackfunc(self.luminance, self.proximity)


  @property
  def luminance(self):
    self.semaphore.acquire()
    d = self.i2c.read_i2c_block_data(self.addr, self._AMBIENT_MSB, 2)
    self.semaphore.release()
    return (d[0] * 256 + d[1]) / 4
  
  @property
  def proximity(self):
    self.semaphore.acquire()
    d = self.i2c.read_i2c_block_data(self.addr, self._PROX_MSB, 2)
    self.semaphore.release()
    return (d[0] * 256 + d[1])
図4 物体が近づいたら割り込みを発生させるサンプルプログラム(simpletest.py)
#!/usr/bin/env python3
import time
from  VCNL4020 import  VCNL4020

sensor = VCNL4020()

# コールバック関数
def callback(lux, prox):
  print('センサーに何かが接近しています')
  print('現在の明るさ:'+str(lux) )
  print('近接センサー:'+str(prox) )
  # 割り込み再設定
  sensor.enable_interrupt(callback)

# しきい値高を設定
sensor.set_high_threshold(2500)
# しきい値低を設定
sensor.set_low_threshold(0)
# 割り込み有効化
sensor.enable_interrupt(callback)

try:
  while True:
    time.sleep(1)

except KeyboardInterrupt:
  term = True

  • shell-mag ブログの 2019年1月 のアーカイブを表示しています。

  • -->