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

test

ユニケージ新コードレビュー(Vol.63掲載)

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

著者:岡田 健

ユニケージでは、小さな道具の「コマンド」をシェルスクリプトで組み合わせて、さまざまな業務システムを構築しています。本連載では、毎回あるテーマに従ってユニケージによるシェルスクリプトの記述例を分かりやすく紹介します。第10回は、グラフ可視化ソフト「Graphviz」を用いたグラフィカルなコード設計書の作成方法について解説します。

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

図1 粗利を計算するシェルスクリプト(Source1.sh)

######################################
#| input:      [原価マスタ]    PRICE
#| output:     [原価マスタ修正] $tmp-price
#| outline:    原価マスタから必要な部分だけ抜き出す
######################################
cat PRICE |
self 1/3  > $tmp-price

######################################
#| input:      [部門マスタ]    CATEGORY
#| output:     [部門マスタ修正] $tmp-category
#| outline:    部門マスタから必要な部分だけ抜き出す
######################################
cat CATEGORY |
self 1/3     > $tmp-category

######################################
#| input:      [原価マスタ修正] $tmp-price
#| input:      [部門マスタ修正] $tmp-category
#| output:     [出力]        $tmp-out
#| outline:    粗利計算をする
######################################
cat SALES                       |
#  1:店舗 2:商品No 3:日付 4:売数
#  5:売上 6:割引
join1 key=2 $tmp-price          | # 原価 / 売価を連結
#  1:店舗 2:商品No 3:原価 4:売価
#  5:日付 6:売数   7:売上 8:割引
join1 key=2 $tmp-category       | # 部門を連結
#  1:店舗 2:商品No 3:部門 4:原価
#  5:売価 6:日付   7:売数 8:売上
#  9:割引
lcalc '$3,$7,$8,$8-$7*$4'       | # 売数 / 売上 / 荒利計算
#  1:部門 2:売数 3:売上 4:粗利
msort key=1                     | # 部門でソート
sm2 1 1 2 4                     | # 売数 / 売上 / 荒利集計
sm5 1 1 2 4                     | # 合計行の付加
divsen 2 3 4                    | # 千で除算
divsen 3 4                      | # 千で再除算
lcalc '$1,$2,$3,$4,100*$4/$3'   | # 荒利率を求める
#  1:部門   2:売数 3:売上 4:粗利
#  5:粗利率
marume 5.1                      | # 四捨五入
join2 key=1 CATEGORY_NAME       | # カテゴリ名の連結
#  1:部門 2:部門名 3:売数 4:売上
#  5:粗利 6:粗利率
comma 3 4 5                     | # カンマ編集
keta                            | # 桁そろえ
keisen +e                       | # 罫線を引く
cat header -                      # 出力

図3 DOT言語で書かれた中間コード(Source1.dot)

digraph sample {
  graph[ layout=dot ];
  node[fontname="IPAゴシック"];
  ID:1 原価マスタから必要な部分だけ抜き出す [ shape=box ];
  ID:2 部門マスタから必要な部分だけ抜き出す [ shape=box ];
  ID:3 粗利計算をする [ shape=box ];
  [原価マスタ] [shape = ellipse, peripheries = 2]
  [部門マスタ] [shape = ellipse, peripheries = 2]
  [出力] [ shape = ellipse, style = bold];
  [原価マスタ修正] [ shape = ellipse, style = bold];
  [部門マスタ修正] [ shape = ellipse, style = bold];
  [原価マスタ] -> ID:1 原価マスタから必要な部分だけ抜き出す [color = blue, style = bold, arrowsize = 1]
  ID:1 原価マスタから必要な部分だけ抜き出す -> [原価マスタ修正] [color = red, style = bold, arrowsize = 1]
  [部門マスタ] -> ID:2 部門マスタから必要な部分だけ抜き出す [color = blue, style = bold, arrowsize = 1]
  ID:2 部門マスタから必要な部分だけ抜き出す -> [部門マスタ修正] [color = red, style = bold, arrowsize = 1]
  [原価マスタ修正] -> ID:3 粗利計算をする [color = blue, style = bold, arrowsize = 1]
  [部門マスタ修正] -> ID:3 粗利計算をする [color = blue, style = bold, arrowsize = 1]
  ID:3 粗利計算をする -> [出力] [color = red, style = bold, arrowsize = 1]
}

Webアプリケーションの正しい作り方(Vol.63記載)

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

著者:しょっさん

ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第3回は、前回のプロジェクトの計画や方針に基づいて2回の開発サイクル(イテレーション)を回します。

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

図4 TypeScriptに書き換えたメインのプログラム(index.ts)

import Express from 'express';
const app = Express();
import { Expense } from './models/expense';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import session from 'express-session';

const users = {
  'user01': 'p@ssw0rd',
  'user02': 'ewiojfsad'
};

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
  secret: 'secret',
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 24 * 30 * 60 * 1000
  }
}));

app.get('/login', (req: Express.Request, res: Express.Response): void => {
  res.send('<h1>LOGIN</h1><form action="/login" method="post">ユーザーID:<input type="text" name="user" size="40"><br />パスワード<input type="password" name="password"><input type="submit" value="ログイン">');
});

app.post('/login', (req: Express.Request, res: Express.Response): void => {
  if (eval("users." + req.body.user) === req.body.password) {
    if (req.session) {
      req.session.user = req.body.user;
    }
  }
  res.redirect('/');
});

app.post('/expense', (req: Express.Request, res: Express.Response): void => {
  Expense.create(req.body)
    .then(() => {
      res.redirect('/');
    });
});

app.get('/', (req: Express.Request, res: Express.Response): void => {
  const user = req!.session!.user || '名無しの権兵衛';
  res.writeHead(200, { "Content-Type": "text/html" });
  res.write(<h1>Hello ${user}</h1><table><tr><th>ID</th><th>申請者名</th><th>日付</th><th>経費タイプ</th><th>経費詳細</th><th>金額</th></tr>);
  Expense.findAll()
    .then(results => {
      for (let i in results) {
        res.write(<tr><td>${results[i].id}</td><td>${results[i].user_name}</td><td>${results[i].date}</td><td>${results[i].type}</td><td>${results[i].description}</td><td>${results[i].amount}</td></tr>);
      }
      res.write('</table><a href="/login">ログイン</a><a href="/submit">経費入力</a>');
      res.end();
    });
});

app.get('/submit', (req: Express.Request, res: Express.Response): void => {
  const user = req!.session!.user || '名無しの権兵衛';
  res.send(<h2>経費入力</h2><form action="/expense" method="post">申請者名:<input type="text" name="user_name" value="${user}"><br />日付:<input type="date" name="date"><br />経費タイプ:<input type="text" name="type"><br />経費詳細:<input type="text" name="description"><br />金額:<input type="number" name="amount"><br /><input type="submit" value="経費申請">);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(http://localhost:${port});
})

export default app;

図5 TypeScriptで作成した自作のモデルファイル(expense.ts)

import { Sequelize, Model, DataTypes } from 'sequelize';

// todo: データベース接続を定義する Typescript モジュール
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
let sequelize;
if (config.use_env_variable) {
  const config_url: any = process.env[config.use_env_variable];
  sequelize = new Sequelize(config_url, config);
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, config);
}

class Expense extends Model {
  public id!: number;
  public user_name!: string;
  public date!: Date;
  public type!: string;
  public description!: string | null;
  public amount!: number;
  public readonly careated_at!: Date;
  public readonly updated_at!: Date;
}

Expense.init({
  id: {
    type: DataTypes.INTEGER.UNSIGNED,
    autoIncrement: true,
    allowNull: false,
    primaryKey: true,
  },
  user_name: {
    type: DataTypes.STRING(256),
    allowNull: false,
    defaultValue: ''
  },
  date: {
    type: DataTypes.DATE,
    allowNull: false
  },
  type: {
    type: DataTypes.STRING(256),
    allowNull: false
  },
  description: {
    type: DataTypes.TEXT,
  },
  amount: {
    type: DataTypes.INTEGER.UNSIGNED,
    allowNull: false
  }
}, {
  tableName: 'expenses',
  underscored: true,
  sequelize: sequelize
});

export { Expense };

図6 トランスパイルオプションファイル(tsconfig.json)

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "lib": [
      "es2018"
    ],
    "sourceMap": true,
    "outDir": "./dist",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": [
    "./src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "**/*.test.ts"
  ]
}

図7 gulpfile.jsファイルの内容

const { src, dest, parallel, series } = require('gulp');
const ts = require('gulp-typescript');
const tsconfig = require('./tsconfig.json');

// gulp固有の設定
const config = {
  output: 'dist/',
  json: {
    source: 'src/**/*.json'
  }
};

// typescript のトランスパイルオプション ← tsconfig.json を再利用する
const typescript = () => {
  return src(tsconfig.include)
    .pipe(ts(tsconfig.compilerOptions))
    .pipe(dest(config.output));
};

// json ファイルのアウトプットディレクトリへのコピーを司る指令
const json = () => {
  return src(config.json.source)
    .pipe(dest(config.output));
};

// 実行時オプション
exports.typescript = typescript;
exports.default = series(parallel(typescript, json));

図8 jest.config.jsの内容

module.exports = {
  coverageDirectory: "coverage",
  preset: 'ts-jest',
  testEnvironment: "node",
};

図9 index.test.tsの内容

test('1 adds 2 is equal 3', () => {
   expect(1 + 2).toBe(3);
 })

図11 super.test.tsファイルの内容

import app from '../src';
import request from 'supertest';

describe('Root', () => {
  it('Root index is valid', async () => {
    const response = await request(app).get("/");
    expect(response.status).toBe(200);
  });
});

describe('Login', () => {
  const userCredentials = {
    user: 'user01',
    password: 'p@ssw0rd'
  };
  it('login form is varid', async () => {
    const response = await request(app).get("/login");
    expect(response.status).toBe(200);
  });
  it('login with user credential is valid', async () => {
    const response = await request(app).post("/login")
      .send(userCredentials);
    expect(response.status).toBe(302);
  })
});

describe('submit', () => {
  it('A submit form is valid', async () => {
    const response = await request(app).get("/submit");
    expect(response.status).toBe(200);
  });
});

図13 .circleci/config.ymlファイルの内容

# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
  build:
    docker:
      # specify the version you desire here
      - image: circleci/node:12.10.0

      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/mongo:3.4.4

    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "package.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run: npm install

      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}

      # npm migrate
      - run: npm run migrate

      # run tests!
      - run: npm test

図14 書き換えたsuper.test.tsの内容

import app from '../src';
import request from 'supertest';

describe('Root', () => {
  it('Root index is valid', async () => {
    const response = await request(app).get("/");
    expect(response.status).toBe(200);
  });
});

describe('Login', () => {
  const userCredentials = {
    user: 'user01',
    password: 'p@ssw0rd'
  };
  it('login form is varid', async () => {
    const response = await request(app).get("/login");
    expect(response.status).toBe(200);
  });
  it('login with user credential is valid', async () => {
    const response = await request(app).post("/login")
      .send(userCredentials);
    expect(response.status).toBe(302);
  })
});

describe('submit', () => {
  it('A submit form is valid', async () => {
    const response = await request(app).get("/expenses/submit");
    expect(response.status).toBe(200);
  });
});

describe('payment', () => {
  it('A payment list is valid', async () => {
    const response = await request(app).get("/expenses/payment");
    expect(response.status).toBe(200);
  });
});

// エラーテスト
describe('/expense', () => {
  it('This URI is not valid', async () => {
    const response = await request(app).get("/expense");
    expect(response.status).toBe(404);
  });
});

図15 index.tsファイルの内容

import Express from 'express';
const router = Express.Router();

// GET / 最初に開く画面
router.get('/', (req: Express.Request, res: Express.Response) => {
  const user = req!.session!.user || '名無しの権兵衛';
  res.send(<h1>Hello ${user}</h1><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>);
})

export default router;

図16 login.tsファイルの内容

import Express from 'express';
const router = Express.Router();

// ユーザー&パスワード
const users = {
  'user01': 'p@ssw0rd',
  'user02': 'ewiojfsad'
};

// GET /login ユーザーログインフォーム
router.get('/', (req: Express.Request, res: Express.Response): void => {
  res.send('<h1>LOGIN</h1><form action="/login" method="post">ユーザーID:<input type="text" name="user" size="40"><br />パスワード<input type="password" name="password"><input type="submit" value="ログイン"><br /><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>');
});

// POST / ユーザーの認証処理
router.post('/', (req: Express.Request, res: Express.Response): void => {
  if (eval("users." + req.body.user) === req.body.password) {
    if (req.session) {
      req.session.user = req.body.user;
    }
  }
  res.redirect('/');
});

export default router;

図17 payment.tsファイルの内容

import Express from 'express';
const router = Express.Router();
import { Expense } from '../../models/expense';

// GET /expenses/payment もともと、最初に開かれる画面だった部分
router.get('/', (req: Express.Request, res: Express.Response): void => {
  const user = req!.session!.user || '名無しの権兵衛';
  res.writeHead(200, { "Content-Type": "text/html" });
  res.write(<h1>Hello ${user}</h1><table><tr><th>ID</th><th>申請者名</th><th>日付</th><th>経費タイプ</th><th>経費詳細</th><th>金額</th></tr>);
  Expense.findAll()
    .then(results => {
      for (let i in results) {
        res.write(<tr><td>${results[i].id}</td><td>${results[i].user_name}</td><td>${results[i].date}</td><td>${results[i].type}</td><td>${results[i].description}</td><td>${results[i].amount}</td></tr>);
      }
      res.write('</table><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>');
      res.end();
    });
});

export default router;

図18 submit.tsファイルの内容

import Express from 'express';
const router = Express.Router();
import { Expense } from '../../models/expense';

// GET /expenses/submit 入力フォーム
router.post('/', (req: Express.Request, res: Express.Response): void => {
  Expense.create(req.body)
    .then(() => {
      res.redirect('/');
    });
});

// POST /expenses/submit 経費の申請
router.get('/', (req: Express.Request, res: Express.Response): void => {
  const user = req!.session!.user || '名無しの権兵衛';
  res.send(<h2>経費入力</h2><form action="/expenses/submit" method="post">申請者名:<input type="text" name="user_name" value="${user}"><br />日付:<input type="date" name="date"><br />経費タイプ:<input type="text" name="type"><br />経費詳細:<input type="text" name="description"><br />金額:<input type="number" name="amount"><br /><input type="submit" value="経費申請"><br /><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>);
});

export default router;

図20 分割後のindex.tsファイルの内容

import Express from 'express';
const app = Express();
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import session from 'express-session';

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
  secret: 'secret',
  resave: false,
  saveUninitialized: false,
  cookie: {
    maxAge: 24 * 30 * 60 * 1000
  }
}));

// ルーティング
import index from './routes/index';
import login from './routes/login'; // ログイン機能
import payment from './routes/expenses/payment'; // 支払い機能
import submit from './routes/expenses/submit'; // 請求機能

app.use('/', index);
app.use('/login', login);
app.use('/expenses/payment', payment);
app.use('/expenses/submit', submit);

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(http://localhost:${port});
})

export default app;

図21 package.jsonファイルの内容

{
  "name": "webapp",
  "version": "0.3.0",
  "description": "This application is sample code of Shell Script Magazine",
  "main": "dist/index.js",
  "scripts": {
    "dev": "ts-node ./src/index.ts",
    "test": "jest",
    "clean": "rimraf dist/*",
    "migrate": "sequelize db:migrate",
    "transpile": "gulp",
    "build": "npm run clean && npm run migrate && npm run transpile",
    "start": "node ."
  },
  "engines": {
    "node": "12.10.0"
  },
  "author": "しょっさん",
  "license": "ISC",
  "dependencies": {
    "cookie-parser": "^1.4.4",
    "express": "^4.16.4",
    "express-session": "^1.16.1",
    "gulp": "^4.0.2",
    "gulp-cli": "^2.2.0",
    "gulp-typescript": "^5.0.1",
    "pg": "^7.12.1",
    "rimraf": "^3.0.0",
    "sequelize": "^5.18.0",
    "sequelize-cli": "^5.5.1"
  },
  "devDependencies": {
    "@types/bluebird": "^3.5.27",
    "@types/cookie-parser": "^1.4.2",
    "@types/express": "^4.17.1",
    "@types/express-session": "^1.15.14",
    "@types/jest": "^24.0.18",
    "@types/node": "^12.7.3",
    "@types/pg": "^7.11.0",
    "@types/supertest": "^2.0.8",
    "@types/validator": "^10.11.3",
    "jest": "^24.9.0",
    "jest-junit": "^8.0.0",
    "sqlite3": "^4.1.0",
    "supertest": "^4.0.2",
    "ts-jest": "^24.0.2",
    "ts-loader": "^6.0.4",
    "ts-node": "^8.3.0",
    "typescript": "^3.6.2"
  }
}

中小企業手作りIT化奮戦記

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

著者:菅 雄一

WordやExcel、PDFビューアなどで外字を含む文書を表示したり、外字を含む文章をメールで受信して閲覧したりすると、文字化けが起きる場合がある。今回は、私が長年、外字と向き合って格闘してきた話を書くことにする。

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

図4 CSSファイルに追加する記述

@font-face {
  font-family: 'gaiji';
  src:url('./gaiji.woff') format('woff');
}
div.gaiji {
  font-family: 'gaiji';
}

図5 独自外字を表示するためのHTMLファイルの記述例

<!DOCTYPE html>
<html lang="ja">
<head>
<title>データ検索</title>
<meta charset="Shift_JIS">
<link rel="stylesheet" type="text/css" href="gaiji.css">
</head>
<body>
(略)
<div class="gaiji">
ここに独自外字を含む文字列が並ぶ
</div>
(略)
</body>
</html>

漢のUNIX(Vol.63掲載)

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

著者:後藤 大地

Rustは、Mozilla Foundationが開発を支援している比較的新しいプログラミング言語だ。「マルチパラダイムシステムプログラミング言語」と呼ばれており、C/C++の代わりに利用できるプログラミング言語と言われている。C/C++のように見えるが、関数型プログラミングのパラダイムも織り込まれていて、かなり厳密なコーディングができるようになっている。本連載では、しばらく、このRustを取り上げていく。

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

図20 Hello Worldのコード

fn main() {
    println!("Hello, world!");
}

バーティカルバーの極意(Vol.63掲載)

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

著者:飯尾 淳

本連載は、「縦棒」(バーティカルバー)をキーワードにデータ処理や効果的なアルゴリズムについて考えるものです。ここ何回かバーティカルバーから離れて自由なテーマで論じ過ぎていたような気がするので、今回は初心に戻ってヒストグラムを用いたデータ分析の議論を紹介してみることにしましょう。
テーマは入力効率の問題です。普段、皆さんがお使いのキーボードによるタイピングとスマートフォンのフリック入力、はたしてどちらが効率的に入力できるのでしょうか?

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

図3 hoge hogeタイピングにおけるデータ送信部のコード

function update_field(){
  if(count < 12 ){
    $("#answer2").html($("#answer1").val());
    typed = $("#answer1").val();
    endTime2 = new Date();
    totalTime2 = endTime2 - startTime2;
    startTime2 = new Date();
    flag = 0;

    $.ajax({
      type: "post",
      url: "./hoge.php",
      data: {
        info: info,
        flag: flag,
        uuid: uuid, seid: seid,
        num: totalTime2,
        test: question,
        typed: typed,
        userAgent: userAgent
      },
    });
  }
}

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

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

著者:清水 赳

 こんにちは、香川大学修士1年の清水です。2年ぶりの登板です。
 今回から2回にわたり、OSSのシステム監視ツール「Prometheus」と「Itamae」というプロビジョニングツールを使って、サーバー監視システムを構築する方法を紹介します。今回は、Prometheusの配備とサーバー稼働状況の簡単な可視化について解説します。

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

図5 「./cookbooks/prometheus/default.rb」ファイルに記述する内容

url_head = "https://github.com/prometheus/prometheus/releases/download"
url_ver  = "v2.13.1"
origin_dir = "Prometheus-2.13.1.linux-amd64"
install_dir = "/usr/local/bin"
config_dir = "/etc/prometheus"
## Prometheusをダウンロードする
execute "download prometheus" do
  cwd "/tmp"
  command "wget #{File.join(url_head, url_ver, origin_dir)}.tar.gz"
end
## Prometheusを展開する
execute "extract prometheus" do
  cwd "/tmp"
  command "tar xvfz #{origin_dir}.tar.gz"
end
## Prometheusを所定のディレクトリに配置
execute "install prometheus" do
  cwd "/tmp"
  command "mv #{File.join(origin_dir, "prometheus")} #{install_dir}"
end
## Systemd設定ファイルの転送
remote_file "/etc/systemd/system/prometheus.service" do
  owner "root"
  group "root"
  source "files/etc/systemd/system/prometheus.service"
end
## Prometheusの設定ファイルを配置する
remote_directory "/etc/prometheus" do
  owner "root"
  group "root"
  source "files/etc/prometheus"
end
## Prometheusサービスの開始
service "prometheus" do
  action :restart
end

図6 「./cookbooks/prometheus/etc/systemd/system/prometh
eus.service」ファイルに記述する内容

[Unit]
Description=Prometheus

[Service]
ExecStart=/usr/local/bin/prometheus --config.file /etc/prometheus/prometheus.yml \
    --storage.tsdb.path /var/lib/prometheus/ \
    --web.console.templates=/etc/prometheus/consoles/ \
    --web.console.libraries=/etc/prometheus/console_libraries/

[Install]
WantedBy=multi-user.target

図8 「./cookbooks/node_exporter/default.rb」ファイルに記述する内容

url_head = "https://github.com/prometheus/node_exporter/releases/download"
url_ver  = "v0.18.1"
origin_dir = "node_exporter-0.18.1.linux-amd64"
install_dir = "/usr/local/bin"
## node_exporterをダウンロードする
execute "download node_exporter" do
  cwd "/tmp"
  command "wget #{File.join(url_head, url_ver, origin_dir)}.tar.gz"
end
## node_exporterを展開する
execute "extract node_exporter" do
  cwd "/tmp"
  command "tar xvfz #{origin_dir}.tar.gz"
end
## node_exporterを所定のディレクトリに配置
execute "install node_exporter" do
  cwd "/tmp"
  command "mv #{File.join(origin_dir, "node_exporter")} #{install_dir}"
end
## Systemd設定ファイルの転送
remote_file "/etc/systemd/system/node_exporter.service" do
  owner "root"
  group "root"
  source "files/etc/systemd/system/node_exporter.service"
end
## node_exporterサービスの開始
service "node_exporter" do
  action :restart
end

図9 「./cookbooks/node_exporter/etc/systemd/system/node
_exporter.service」ファイルに記述する内容

[Unit]
Description=NodeExporter

[Service]
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target

図10 「./cookbooks/prometheus/files/etc/prometheus/prome
theus.yml」ファイルの編集内容

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090']

# ここから下を追加
  - job_name: 'node'
    static_configs:
    - targets: ['192.168.100.12:9100']

センサーボードで学ぶ電子回路の制御(Vol.63掲載)

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

著者:米田 聡

シェルスクリプトマガジンでは、小型コンピュータボード「Raspberry Pi」(ラズパイ)向けのセンサー搭載拡張ボード「ラズパイセンサーボード」を制作しました。第10 回では、I2C 接続のモノクロ有機ELディスプレイをGroveコネクタにつなぎ、ラズパイセンサーボードだけでセンサーから情報をディスプレイ上に表示します。

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

図6 有機ELディスプレイに日本語を表示するためのライブラリ(OLED.py)

import time
import Adafruit_SSD1306

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont


class OLED(Adafruit_SSD1306.SSD1306_128_64):

	WIDTH = 128
	HEIGHT = 64
	
	# DEFAULT_FONT = '/usr/share/fonts/truetype/fonts-japanese-gothic.ttf'
	DEFAULT_FONT = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'
	FONT_SIZE = 12
	_LINE_HEIGHT = 16
	
	def __init__(self):
		super().__init__(rst=24)
		self._image = Image.new('1', (self.WIDTH, self.HEIGHT) ,0)
		self._draw = ImageDraw.Draw(self._image)
		self._font = ImageFont.truetype(self.DEFAULT_FONT, self.FONT_SIZE, encoding='unic')
	
	def image(self, image):
		self._image = image
		super().image(self._image)
	
	def drawString(self, str, line=0):
		self._draw.rectangle((0, line*self._LINE_HEIGHT, self.WIDTH,line*self._LINE_HEIGHT+self._LINE_HEIGHT), fill=(0))
		self._draw.text((0, line*self._LINE_HEIGHT), str, font=self._font, fill=1)
		self.image(self._image)

図7 BME280のデータを表示するサンプルプログラム(sample.py)

#!/usr/bin/env python3
#
# apt install python3-pip
# sudo pip3 install RPi.BME280
#

import time
import smbus2
import bme280

from OLED import OLED

BME280_ADDR = 0x76
BUS_NO = 1

# BME280
i2c = smbus2.SMBus(BUS_NO)
bme280.load_calibration_params(i2c, BME280_ADDR)

# OLEDパネル
oled = OLED()
oled.begin()
oled.clear()
oled.display()

try:
	while True:
		data = bme280.sample(i2c, BME280_ADDR)
		oled.drawString('気温 :' + str(round(data.temperature,1)) + '℃', 0)
		oled.drawString('湿度 :' + str(round(data.humidity,1)) + '%', 1)
		oled.drawString('気圧 :' + str(round(data.pressure,1)) + 'hPa', 2)
		oled.display()
		
		time.sleep(1)
except KeyboardInterrupt:
	pass

特別企画 PartiQLをはじめよう(Vol.63記載)

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

著者:岡本 秀高

米Amazon Web Services(AWS)社が2019年8月に発表した「PartiQL」は、RDBだけでなくKVSやJSONデータ、CSVデータに対しても問い合わせが可能な便利なクエリー言語です。文法はSQLのサブセットになっていて、SQLを知っている人であればすぐに使えます。PartiQL対応の実サービスも提供され始めた今、この新しいクエリー言語を始めてみましょう。

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

図13 「sample.csv」ファイルの内容

3,Bob Smith
4,Susan Smith
6,Jane Smith

図15 項目名を追加したCSVファイル「with_header.csv」の内容

id,name
3,Bob Smith
4,Susan Smith
6,Jane Smith

図20 生成されたCSVファイル「out.csv」の内容

name,securityProjectsNum
Bob Smith,2
Susan Smith,0
Jane Smith,1

図22 生成されたAmazon Ion形式ファイル「out.ion」の内容

{
  name:"Bob Smith",
  securityProjectsNum:2
}
{
  name:"Susan Smith",
  securityProjectsNum:0
}
{
  name:"Jane Smith",
  securityProjectsNum:1
}

特集1 古いラズパイの活用術(Vol.63記載)

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

著者:麻生 二郎

小型コンピュータボードの最新機種「Raspberry Pi 4 Model B」が国内で発売できる状態になりました。Raspberry 4 Model Bは、高機能かつ高性能なハードウエアです。このラズパイが登場することでラズパイの適用範囲が広がりますが、同時に古いモデルが不要になります。いらなくなったラズパイを有効利用する三つの方法を紹介します。

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

活用例1

図5 DMを一斉送信するシェルスクリプト(raspi_dm.sh)

#!/bin/sh

SOURCE_ADDRESS="自分のGmailアドレス"
DISTINATION_LIST="sendlist.txt"
USER_ID="Googleアカウントのユーザー名"
PASSWORD="Googleアカウントのパスワード"
MESSAGE_TEMPLATE_FILE="message.txt"

NUMBER_SEND=$(cat ${DISTINATION_LIST} | wc -l)
sed "s/%source_address%/${SOURCE_ADDRESS}/" ${MESSAGE_TEMPLATE_FILE} > /tmp/tmp_message.txt

for i in $(seq ${NUMBER_SEND})
do
  DISTINATION_ADDRESS=$(sed -n ${i}p ${DISTINATION_LIST} | cut -f 1)
  DISTINATION_NAME=$(sed -n ${i}p ${DISTINATION_LIST} | cut -f 2)
  sed "s/%name%/${DISTINATION_NAME}/g" /tmp/tmp_message.txt | sed -e "s/%distination_address%/${DISTINATION_ADDRESS}/" > /tmp/message.txt
  curl -s -k --url 'smtps://smtp.gmail.com:465' --mail-rcpt ${DISTINATION_ADDRESS} --mail-from ${SOURCE_ADDRESS} --user ${USER_ID}:${PASSWORD} --upload-file /tmp/message.txt
done

rm /tmp/tmp_message.txt /tmp/message.txt

図6 送信メッセージのテンプレートファイル(message.txt)

To: %distination_address%
From: %source_address%
Subject: 同窓会のご案内
Content-Type: text/plain; charset="UTF-8"

%name%

お元気ですか?
ご無沙汰しております。
早速ですが、下記の日程で同窓会を開催いたします。お手数をおかけしますが、出欠をメールにてご返信ください。%name%にお会いできるのを楽しみにしています。
                       記

日時:2019年11月30日(土曜日) 16時から
場所:〇〇〇高等学校 体育館
会費:5000円

                                       以上

図7 送信先のメールアドレスと、個別に書き換えたい情報を保存し
たタブ区切りテキストファイル(sendlist.txt)

taro@example.co.jp    シェルマグ太郎先生
hanako@example.com    シェルマグ花子さん
jiro@example.com    マガジン二郎君

活用例2

図11 受信メッセージをLINEに転送するシェルスクリプト (raspi_mail_line.sh)

#!/bin/sh
 
POP_SERVER="pop.gmail.com"
USER_ID="Googleアカウントのユーザー名"
PASSWORD="Googleアカウントのパスワード"
LINE_TOKEN="LINE Notifyのアクセストークン"

SUBJECT_BASE64="44CQ6YeN6KaB44CR"

expect -c "
  set timeout 30
  spawn openssl s_client -connect ${POP_SERVER}:995
  expect \"+OK Gpop ready\"
  send \"user ${USER_ID}\n\"
  expect \"+OK send PASS\"
  send \"pass ${PASSWORD}\n\"
  expect \"+OK Welcome.\"
  send \"stat\n\"
  expect \"+OK\"
  send \"quit\n\"
  expect \"+OK Farewell.\"
  exit 0
" > receive.log
 
RECEIVE_COUNT=$(grep +OK receive.log |tail -n2 |head -n 1 | cut -d " " -f 2)
 
for i in $(seq ${RECEIVE_COUNT})
do
  expect -c "
    set timeout 30
    spawn openssl s_client -connect ${POP_SERVER}:995
    expect \"+OK Gpop ready\"
    send \"user ${USER_ID}\n\"
    expect \"+OK send PASS\"
    send \"pass ${PASSWORD}\n\"
    expect \"+OK Welcome.\"
    send \"retr 1\n\"
    expect \".\"
    send \"quit\n\"
    expect \"+OK Farewell.\"
    exit 0
  " > message.log
 
  SUBJECT=$(cat message.log | grep "Subject: =" | sed "s/Subject: =?UTF-8?B?//g "| cut -c 1-16)
 
  if [ ${SUBJECT} = ${SUBJECT_BASE64} ] ; then
    cat message.log | awk '/Content-Language\: en-US/,/^\./' | head -n -1 | tail -n +2 > message.txt
    curl -s -X POST -H "Authorization: Bearer ${LINE_TOKEN}" -F "message=$(cat message.txt)" https://notify-api.line.me/api/notify
  fi
done

活用例3

図5 エアコン制御のシェルスクリプト(raspi_aircon)

#!/bin/sh

case $1 in

  "on"  ) bto_advanced_USBIR_cmd -d $(cat /var/aircon/start.txt);;
  "off" ) bto_advanced_USBIR_cmd -d $(cat /var/aircon/stop.txt);;

esac

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

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

004 CentOS 8とCentOS Stream公開
005 東京ゲームショウ2019開催
006 特別レポート ハル研究所から超小型PC-8001
008 NEWSFLASH
010 特集1 古いラズパイの活用術/麻生二郎 コード掲載
026 特集2 5Gで広がるモバイルの世界/酒井尚之、安藤高任
035 姐のNOGYO
036 特別企画 PartiQLをはじめよう/岡本秀高 コード掲載
043 ラズパイセンサーボードで学ぶ電子回路の制御/米田聡 コード掲載
046 円滑コミュニケーションが世界を救う!/濱口誠一
048 香川大学SLPからお届け!/清水赳 コード掲載
054 法林浩之のFIGHTING TALKS/法林浩之
056 バーティカルバーの極意/飯尾淳 コード掲載
062 漢のUNIX/後藤大地 コード掲載
070 virus/桑原滝弥・イケヤシロウ
072 MySQL Shellを使おう/梶山隆輔
079 中小企業手作りIT化奮戦記/菅雄一 コード掲載
084 Webアプリの正しい作り方/しょっさん コード掲載
098 ユニケージ新コードレビュー/岡田健 コード掲載
102 Techパズル/gori.sh
104 新しい風が吹いてくる/シェル魔人

Vol.63

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

 人気の小型コンピュータボードの最新版「Raspberry Pi 4 Model B」の国内販売開始が間近にせまっています。Raspberry Pi 4 Model Bは高機能、高性能で、ラズパイの用途が広がることは間違えありません。Raspberry Pi 4 Model Bを購入すると、古いラズパイが不要になるでしょう。そこで、特集1では、シェルスクリプトを用いて、不要になったラズパイを活用する方法を紹介します。メールの一斉配信や転送、エアコン制御を自動化できます。
 2020年に5G(第5世代移動通信方式)の本格サービスが開始します。この5Gによって、無線でつながるあらゆるモバイル環境が大幅に刷新されます。特集2では、5Gの概要や仕組み、5Gが実現する世界について解説します。
 特別企画では、米Amazon Web Services(AWS)社がオープンソースで公開した、データベース問い合わせ言語「SQL」互換の「PartiQL」を紹介します。PartiSQLは、RDB(リレーショナルデータベース)のみならず、JSON(JavaScript Object Notation)データやCSV(カンマ区切りテキスト)ファイルなどに対しても問い合わせが可能です。
 このほか、緊急レポートでは、小型PC-8001の「PasocomMini PC-8001 PCGセット」なども扱っています。
 今回も読み応え十分のシェルスクリプトマガジン Vol.63。お見逃しなく!

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

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

Vol.63 補足情報

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

目次

p.3の連載「MySQL Shellを使おう」の記事タイトル「第3回 MySQL Shellのサーバー運用管理ユーティリティ」は、「第3回 MySQL X DevAPIとドキュメントストア(その1)」の誤りです。お詫びして訂正いたします。

読者プレゼント

p.9の「応募期間」にある「2019年11月25日~2019年1月20日」は「2019年11月25日~2020年1月20日」の誤りです。お詫びして訂正いたします。

MySQL Shellを使おう

p.72の記事タイトル「第3回 MySQL Shellのサーバー運用管理ユーティリティ」は、「第3回 MySQL X DevAPIとドキュメントストア(その1)」の誤りです。お詫びして訂正いたします。

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

第12回 コマンドを作る

投稿日:2019.11.19 | カテゴリー: 記事

 前回、「日本語の文字列をURLエンコーディング」できる適当なコマンドがなく、プログラミング言語「Python」でURLエンコーディングの処理を記述しました。シェルスクリプト内に別のプログラミング言語の記述があるのは、違和感があり、またコードも読みにくいでしょう。

 そこで、URLエンコーディングの処理をコマンド化し、シェルスクリプトと別の言語のプログラムを混在させないようにします(図1)。このように、よく使う処理をコマンド化することで、別のシェルスクリプトからも利用できます。

図1 よく使う処理をコマンド化

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

  • -->