sironekotoroの日記

Perl と Mac の初心者の備忘録

1 行増えただけじゃん

長い前置き

平穏な時代

経理のお仕事で、仕訳というのがあります。複式簿記に則ってお金や債権・債務の流れを記録していきます。

いろいろなやり方があると思うのですが、うちの場合、 Google Sheet でマスターデータを書いておいて、それを Google Apps で処理して会計ソフト(弥生会計)取り込み用の csv ファイルにする、という方法をとっています。

マスタデータはこんな感じです。

名前 発生 金額 手数料負担 支払手数料 摘要
白猫商事 2021.12 10,000 得意先 176 猫用ホットカーペット

ひとつの振込先につき支払いはひとつ、という前提があり、こんなフローで仕訳 csv を作成します。

振り込み手数料は 30,000 円未満は 176円、30,000 円以上は275円です。

  1. マスターデータから 1 行取り込む
  2. 銀行手数料が得意先負担の場合には、以下の2行を作って仕訳シートに追加する
    1. 支払額から支払い手数料を算出する
    2. 支払額から支払手数料を引いた金額を支払額にする
    3. 支払手数料の行を追加する
  3. 銀行手数料が当方負担の場合には、金額をそのままに1行作って仕訳シートに追加する

データ構造としてはこんな感じです

[
    { 名前 => 白猫商事, 金額 => 10000, 手数料負担 => 得意先, 支払手数料 => 176, ... },
]

で、仕訳を作るとこんな感じです。

勘定科目 金額 勘定科目 金額 摘要
未払金 10,000 普通預金 9,824 白猫商事/2021.12/猫用ホットカーペット
支払手数料 176 白猫商事/2021.12/振込手数料

相手への支払い義務のあるお金(未払金)を、普通預金から出金して支払うという仕訳です。

ただし、白猫商事は振込手数料を先方が負担するので、手数料分を引いた金額を振込みます。

崩れる前提

ひとつの振込先につき支払いはひとつ、という前提があり、仕訳csvを作る時は

はい。ある日突然、一つの振込先に対する支払いの行を複数にすることになります。

名前 発生 金額 手数料負担 支払手数料 摘要
黒猫農業 2021.12 10,000 得意先 176 マタタビ
黒猫農業 2021.12 30,000 得意先 176 高級マタタビ

そして、振り込まれる側(この場合は黒猫農業)としては、一括で振り込んで欲しい(一つの支払いごとに支払い手数料を負担したくない)のも当然ですね。

最終的にはこういう仕分けになります。振り込み額が30,000円を超えるので、銀行手数料が変更になります。

勘定科目 金額 勘定科目 金額 摘要
未払金 10,000 普通預金 39,725 黒猫農業/2021.12/マタタビ
未払金 30,000 黒猫農業/2021.12/高級マタタビ
支払手数料 275 黒猫農業/2021.12/振込手数料

ぎゃー!

こうなってしまうと、マスターデータから1行読み込んで処理するという方法は取れません。

マスターデータ側で合算して 1 行にしてしまうのが一番簡単ですが、会計ソフト上でマタタビ・高級マタタビでそれぞれの費用を分析する場合には不向きになります。

となると、このような感じで処理することになります。

  1. ハッシュのデータ構造にする。key を名前に、 value には配列を格納するハッシュ(辞書型)のデータ構造を作る
  2. 同じ key の場合には、value に push する
  3. 出来上がったデータ構造を key ごとに処理する
    1. value の要素が 1 つだけの場合(白猫商事、黒猫物産の場合)は従来通り
    2. value の要素が複数の場合
      1. 支払額を合算
      2. 支払手数料も合算した額で算出
      3. 合算した支払額を1行目に集約
      4. 2行目以降は未払金のみ記載、支払額欄は空欄
      5. 最後の行に支払手数料を入れる

となります。この処理を行うためには、こんなデータ構造にする必要があります。

{
    茶虎農業 => [ 
        { 金額 => 10000, 手数料負担 => 得意先, ... }, 
        { 金額 => 30000, 手数料負担 => 得意先, ...  },
     ],
}

ここまでデータ構造の当たりがつけば、要件を満たすことができそうです。

というわけで

この「ぎゃー!」ってなってから実装し直すまで結構時間がかかった(試行錯誤で 1 日半くらい)ので記念に書いてみました。

こうやって書き出して思い出してみると、たいしたことないところで悩んでたなー、とか思うんですが、まぁ、喉元過ぎれば熱さ忘れるってやつですね。

FC2 ブログに記事を投稿する Perl モジュール作った

はい。タイトルのまんまです。

github.com

元々は、ちょっと表に出せないような欲望から始まったんですが、その途中でブログに投稿できるモジュールが欲しいなー、ということになり、気づいてみると欲望置き去りにして完成してたのがこのモジュールです。

目的と手段がよくすり変わるという自覚はあります。

手段たーのしー!

今回は目的別にモジュール分けるってのをやってみました。こんなんです。

$ tree lib/WebService/FC2
lib/WebService/FC2
├── Blog
│   ├── Delete.pm
│   ├── Edit.pm
│   ├── Get.pm
│   ├── Media.pm
│   └── New.pm
└── Blog.pm

問題は全然テスト書いてないことです。

このモジュールは XML-RPC を使って各種の操作を行うのですが、その XML-RPC のモック?を用意しなきゃいけない?の?

ってところで時間切れというか気力がつきました。

Windows のバッチファイルにファイルをドラッグ&ドロップして、Perl でなんか処理する

Windows 使ってます

ということもあり、これらの Windows には Strawberry Perlストロベリー パールを入れています。

strawberryperl.com

Windows で簡単にインストールできる Perl としては Active Perl もあるんですが、Strawberry Perl は cpanm が最初からセットアップされていたりで使いやすく、気に入っています。

業務用途だと、プロダクション環境と開発環境で Perl のバージョンを合わせる必要があり Strawberry Perl だと大変なんじゃないかなー、と思うのですが・・・個人で使う分には Strawberry Perl で良いのではないかと思います。

というか、WindowsPerl 動かしてる業務環境はなかなかレアですねー。皆無とは思わないですが、実例知らない・・・

バッチファイル書いていく

Windows でも Perl 使うことができるんですが、Windows では GUI をフルに生かして使いたい!

ってことで、バッチファイルにドラッグ&ドロップしたファイルを Perl に引数として渡すってのを紹介します。

基本的なバッチファイルはこちらで、これに付け足していきます。

メモ帳(notepad.exe)で書いていきます。ちなみに、いまメモ帳のデフォルト文字コードutf8 なんすよ。

f:id:sironekotoro:20210131162043p:plain

@echo off
setlocal
cd /d %~dp0

rem %~f1 ドラッグ&ドロップされたファイルのフルパス
echo %~f1

pause

この内容で arg.bat とかいう名前で保存します。

f:id:sironekotoro:20210131132039p:plain

最初の3行はいわばバッチファイルを書くときの「お約束」ですね。3行目はバッチファイル実行時に、このバッチファイルがあるフォルダを実行場所にするためのものです。

@echo off
setlocal
cd /d %~dp0

その次の rem から始まるのがコメント行、Perl でいうと # です。

%~f1 が引数のフルパスを示すものです。

rem %~f1 ドラッグ&ドロップされたファイルのフルパス
echo %~f1

適当なファイルをこのバッチファイルにドラッグ&ドロップするとこうなります。

f:id:sironekotoro:20210131132400p:plain

キーボードの適当なキーを押すと終了します。

バッチファイルから Perl 起動して引数渡す

バッチファイルから起動する Perlスクリプトはこちらです。Hello! [コマンドライン引数] と表示するものです。

これもメモ帳で書きます。

#!/usr/bin/env perl
use strict;
use warnings;

print "Hello! @ARGV";

バッチファイルはちょっとだけ手直し。

@echo off
setlocal
cd /d %~dp0

rem ドラッグ&ドロップされたファイルのフルパスを Perl のコマンドライン引数に渡す

perl hello.pl %~f1

pause

この Perl スクリプト とバッチファイルを同じ場所に置いて、適当なファイルをバッチファイルにドラッグ&ドロップ。

f:id:sironekotoro:20210131141930p:plain

無事、Perlスクリプトにファイルのパスが渡りました。めでたしめでたし!

ドラッグ&ドロップしたテキストファイルの中身を表示すると・・・文字化け!

・・・で、終わればいいんですけどね。

今度は、ドラッグ&ドロップしたテキストファイルの中身を表示してみましょう。

テキストファイルを作ります。中身はこんな感じ。ファイル名は Perl入学式.txt

Perl入学式 2月開催をお楽しみに!

f:id:sironekotoro:20210131152252p:plain

まずはこのファイルをドラッグ&ドロップ。

はい、ちゃんとファイル名が出ましたね。

f:id:sironekotoro:20210131151454p:plain

では、ファイルの中身を表示すべく Perl 側のスクリプトを新たに作ります。ファイル名は open_text.pl とします。

素直な、ファイル開いて1行1行表示するものです。

#!/usr/bin/env perl
use strict;
use warnings;

open my $FH, '<', $ARGV[0];
for my $line(<$FH>){
    chomp $line;
    print $line . "\n";
}
close $FH;

バッチファイルも呼び出す Perlスクリプト名を変更します。

@echo off
setlocal
cd /d %~dp0

rem バッチファイルから起動するスクリプトを変更
perl open_text.pl %~f1

pause

f:id:sironekotoro:20210131152634p:plain

はい文字化けー

ということで、PerlWindows で動かすときの大きな障壁の一つがこの文字化けだと思います。

自分もかつて、ここでつまづきました。

これは、Windowsコマンドプロンプトcp932 という文字コードであることに対し、Perl で出力する際の標準の文字コードutf8 であることが原因です。

f:id:sironekotoro:20210131153529p:plain

ですので、これを正しく表示するためには

のいずれかが手取り早いです。

「郷にいれば郷に従え」で、最初の Perl文字コードを変更する方法が良いのではないかなぁと思います。

Perlスクリプト側で入出力する文字コードWindows 環境に合わせる

スクリプト中にコメントで書きましたが、Encodeエンコード モジュールを使います。

それぞれどの文字コードで読み込むのか、出力するのかを明示したものです。

#!/usr/bin/env perl
use strict;
use warnings;

use Encode;  # 文字コードを扱うモジュール

open my $FH, '<:utf8', $ARGV[0];     # 入力する文字コードが何なのかを明示する
for my $line(<$FH>){
    chomp $line;
    print encode('cp932', $line) . "\n";    # 出力する文字コードは何なのかを明示する 
}
close $FH;

Windowsコマンドプロンプト文字コードを utf8 にする

コマンドプロンプトchcp 65001 というコマンドを実行することで、コマンドプロンプト文字コードを utf8 に変更できます。

この方法をとる場合、変更すべきはバッチファイルの方になります。

@echo off
setlocal
cd /d %~dp0

rem コマンドプロンプトの文字コードを utf8 に変更
chcp 65001

perl open_text.pl %~f1

pause

f:id:sironekotoro:20210131155825p:plain

こんな感じで

Perl の便利さ + Windows での慣れた GUI 操作 両方のいいとこ取りで、仕事を楽にしていきましょう!

おまけ

7年くらい前、Windows マシンで Perl を勉強していた自分は、この文字コードがどうとかエンコードとか理解できませんでした。

そして Mac を買うという解決策をとったのでした。

Mac はターミナル(Windows でいうところのコマンドプロンプト)の文字コードが utf8 なので文字コードを意識する必要がなかったんですね。

あと、当時はみんな Mac だったなぁという。形から入りました。

・・・とはいえ、ネットから落としてきた何かや、スクレイピングで得た文字列を扱うときには文字コードの問題はついて回るので、結局逃げられなかったという感じです。

追記

バッチファイルから Perl にファイルパスを渡す時なのですが

perl hello.pl %~f1

よりも

perl hello.pl "%~f1"

の方が良いです。

ファイルパス中にスペースがあった場合、スペース以降の文字列を引数として認識してしまいます。

このため、ファイルパスをダブルクォーテーション " " で囲むことで、ひとまとまりの文字列として扱うことができます。

もちろん、この問題でしっかりハマりました。

Perl つかって 8 年目でディレクトリをコピーをしようとしてハマる

ディレクトリをコピーする機会がなかった

意外なことに、いままでディレクトリのコピーをする用件がなかった。

で、ハマった。

Perl でファイルコピーと言ったら File::Copy という標準モジュール。それで事足りてきた 8 年間。

だもんで、File::Copy でディレクトリもコピーできるだろうと・・・思ったけど、うまくいかない。

「ははーん、これは先に移行先のディレクトリ 作っておけってことね」と思って先行してコピー先のディレクトリを作っておくもダメ。

Mac 上で書いて、Windows 上で動かすコードだったので、文字コードの違いかなー?

と思うも違う。むー

で、File::Copy::Recursive モジュールを使ってあっさり解決した。そうか、File::Copy でディレクトリのコピーはできんかったのか・・・できると思い込んでた。

www.nqou.net

perldoc.jp

せっかくなので Windows で日本語ファイル名・フォルダを扱う時の注意

Windows文字コードcp932 ってやつで、ファイル名やパスもこの cp932 です。

そう、ファイル名だけじゃなくて パスも cp932エンコードしてあげる必要があります。

フォルダのコピーがうまくいかない時、ちゃんとファイルを認識しているんかな?と思って -e $path ってファイルテスト演算子使ってみたのですが、みごとに認識しておりませんでした。

-e encode('cp932', $path) とすることでちゃんとファイルパスを認識してくれました。

コードにするとこんな感じです。utf8 で保存したスクリプトWindows で実行ってのを想定してます。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;

use Encode qw/encode/;
use File::Spec;

# ダブルクォーテーションだと \ をエスケープ文字と認識してしまい失敗するよ
my $path = 'D:\Desktop\新しいフォルダー (2)';

if ( -e $path ) {
    print "$path is exist\n";
}
else {
    # Windowsで実行すると、ここを通る(文字化けもする)
    print "$path is not exist\n";
}

# utf8のスクリプト上に書かれたパス(9行目)を、パス含めて cp932 でエンコード
my $cp932_path = encode( "cp932", $path );

if ( -e $cp932_path ){
    # Windowsで実行すると、ここを通る(文字化けしない)
    print "$cp932_path is exist\n";
}
else {
    print "$cp932_path is not exist\n";
}

追記

さーてバリバリ続き書いてくかー!

って思ったら、ファイルコピー失敗。おいおいなんだよー、ファイルコピーは問題ないはずだろ!・・・と思ったがこういうオチだった。

f:id:sironekotoro:20210127192645p:plain

コピー先の容量不足。

ファイルコピー道は険しい。

Perl入学式 オンライン 2020 第4回お疲れ様でした

受講された方、サポーターの方、お疲れ様でした。 講師をやったパーカーの人です。

講義に利用したスライド・動画類は以下 Perl 入学式の公式サイトで公開しています。参加された方も、参加できなかった方も、ぜひ復習に使ってください。

appslideshare.tugougaii.site

/workshop-basic-online/blob/master/4th/slide.md:embed:cite]

問題の意味がわからない、とか、このような解答例はどうだろう?という方は Discord の Perl 入学式チャンネル(招待コード)や、twitterハッシュタグ #Perl入学式 をつけて聞いてみてください。

Perl入学式 オンライン 2020 第4回

今回は以下の項目について学習しました。

  • 配列を操作する関数
  • ハッシュ

Perl のイベント紹介

来月、Perl のオンラインイベントが開催されます。

コロナ禍によりオフラインイベントである YAPC::Japan は開催できない状況が続いていますが、オンラインで Perl についての話が聞ける貴重な機会です。楽しみですね〜

yapcjapan.connpass.com

次回オンライン第 5 回!

2021 年の 2 月 27 日開催予定です。

次回はハッシュの操作関数とサブルーチン・正規表現をやります。

過去の動画や講義資料は Perl 入学式の公式ページに掲載しております。

ぜひ閲覧いただいた上で参加ください!お待ちしております!!

Mac で Ansible によるインストール時にエラーが出るようになった

エラーが出る前にやったこと

多分これが原因かなぁ。

$ brew update

ここで、

git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow

するようにメッセージが出たで素直に従う。ただ、これはエラーには関係ないかな、多分。

ついでに $ brew upgrade したり、 Docker のアップデートなどを行った。

エラー内容

Mac のアプリケーションを ansible で管理しているのだけど、実行したところ、エラー

TASK [homebrew cask packages install] ******************************************
failed: [localhost] (item={'name': 'aerial'}) => {"ansible_loop_var": "item", "changed": false, "item": {"name": "aerial"}, "msg": "Warning: Cask 'aerial' is already installed.\n\nTo re-install aerial, run:\n  brew reinstall aerial"}

こんなのが、Ansible で設定している item の数だけ出てくる・・・

Warning: Cask 'aerial' is already installed

ううん?

こっからが長い旅路だった、2時間くらい?

ちなみに aerial ってのは綺麗なスクリーンセーバーアプリ。

github.com

設定を抜粋するとこんな感じ

    homebrew_cask_packages:
      - name: aerial
      - name: alfred
      - name: appcleaner

    - name: homebrew cask packages install
      homebrew_cask: name={{ item.name }} state=installed
      # ※1 GUIツールをインストールする場所を `/Application` に固定
      environment:
        HOMEBREW_CASK_OPTS: "--appdir=/Applications"
      with_items: '{{ homebrew_cask_packages }}'

調べて分かったこと

  • brewGUI アプリをインストールするときのコマンド brew cask install Foo ってのは deprecated 、非推奨だった

  • 現在は brew install --cask Foo 、cask はオプション引数みたいな感じになった

  • Ansible で brew cask する plugin である homebrew_caskbrew cask install Foo の形式に対応していない

  • brew cask install Foo の形式に対応しているプラグインcommunity.general.homebrew_cask

  • このプラグインをインストールするコマンド

    $ ansible-galaxy collection install community.general

プラグインインストール後の設定

    - name: homebrew cask packages install
      community.general.homebrew_cask:
        name: "{{ item.name }}"
        state: 'installed'
        install_options: 'appdir=/Applications'
      with_items: '{{ homebrew_cask_packages }}'

ということでした。

参考

github.com

docs.ansible.com

github.com

github.com

Perl5 で Exercise に挑戦してみる(追記あり)

ってことで、巷で話題の Exercise に挑戦してみました。もちろん Perl5 で。

anatofuz.hatenablog.com

うちは手元にある Macでやってみます。まだ Big Sur にあげておらず、Catalina です。

Exercism への登録は GitHub の ID と連携しました。

ecercism コマンドはいつも通り、homebrew でインストールします。

$ brew install exercism

手元の環境では久々に brew するので色々と警告が出たりしましたが、本題と関係薄いので飛ばします。

exercism コマンドが無事入りました。

この辺りは登録直後のページ右側にあります。モダンな感じのページですね。

f:id:sironekotoro:20210108182310p:plain

早速、最初の課題に取り組みます・・・が、API を設定しなよ!って出てくるので書いてある通りに設定します。親切。

$ exercism download --exercise=hello-world --track=perl5
Error:

    Welcome to Exercism!

    To get started, you need to configure the tool with your API token.
    Find your token at

        https://exercism.io/my/settings

    Then run the configure command:

        exercism configure --token=YOUR_TOKEN

API を登録します。

$ exercism configure --token=YOUR_EXERCISM_TOKEN

You have configured the Exercism command-line client:

Config dir:                       /Users/sironekotoro/.config/exercism
Token:         (-t, --token)      YOUR_EXERCISM_TOKEN
Workspace:     (-w, --workspace)  /Users/sironekotoro/Exercism
API Base URL:  (-a, --api)        https://api.exercism.io/v1

今度こそ最初の問題です。

$ exercism download --exercise=hello-world --track=perl5

Downloaded to
/Users/sironekotoro/Exercism/perl5/hello-world

おなじみ Hello World から。ダウンロードされたものを確認します。

  • HelloWorld.pm
  • README.md
  • hello-world.t

ふむふむ。まずは README.md から

$ cat README.md
# Hello World

The classical introductory exercise. Just say "Hello, World!".

["Hello, World!"](http://en.wikipedia.org/wiki/%22Hello,_world!%22_program) is
the traditional first program for beginning programming in a new language
or environment.

The objectives are simple:

- Write a function that returns the string "Hello, World!".
- Run the test suite and make sure that it succeeds.
- Submit your solution and check it at the website.

If everything goes well, you will be ready to fetch your first real exercise.
## Source

This is an exercise to introduce users to using Exercism [http://en.wikipedia.org/wiki/%22Hello,_world!%22_program](http://en.wikipedia.org/wiki/%22Hello,_world!%22_program)

## Submitting Incomplete Solutions
It's possible to submit an incomplete solution so you can see how others have completed the exercise

次はテスト。おぉ、 Test2 使ってる。このテストに通るのを書けばいいんだな〜

$ cat hello-world.t
#!/usr/bin/env perl
use Test2::V0;

use FindBin qw<$Bin>;
use lib $Bin, "$Bin/local/lib/perl5";    # Find modules in the same dir as this file.

use HelloWorld qw(hello);

plan 2;                                  # This is how many tests we expect to run.

imported_ok qw<hello> or bail_out;

# Run the 'is' sub from 'Test2::V0' with three arguments.
is(
  hello(),                               # Run the 'hello' sub imported from the module.
  'Hello, World!',                       # The expected result to compare with 'hello'.
  'Say Hi!'                              # The test description.
);

で、手を入れる必要があるのはこのファイル HelloWorld.pm と。

おー、モジュールだ。

$ cat HelloWorld.pm
# Declare package 'HelloWorld'
package HelloWorld;
use strict;
use warnings;
use Exporter qw<import>;
our @EXPORT_OK = qw<hello>;

sub hello {

# Remove the comments and write some code here to pass the test suite.
}

1;

って感じです。

この HelloWorld.pm を書いたら、Perl ではおなじみの prove コマンドでテストを実行します。

おおっとテスト失敗!

どんなコード書いてテスト失敗したかはご想像にお任せします・・・

$ prove hello-world.t
hello-world.t .. 1/2
# Failed test 'Say Hi!'
# at hello-world.t line 14.
# +-----+----+---------------+
# | GOT | OP | CHECK         |
# +-----+----+---------------+
# | 1   | eq | Hello, World! |
# +-----+----+---------------+
# Seeded srand with seed '20210108' from local date.
hello-world.t .. Dubious, test returned 1 (wstat 256, 0x100)
Failed 1/2 subtests

Test Summary Report
-------------------
hello-world.t (Wstat: 256 Tests: 1 Failed: 0)
  Non-zero exit status: 1
  Parse errors: Bad plan.  You planned 2 tests but ran 1.
Files=1, Tests=1,  1 wallclock secs ( 0.02 usr  0.01 sys +  0.11 cusr  0.04 csys =  0.18 CPU)
Result: FAIL

改めて書き直し。今度は合格点をもらえました

$ prove hello-world.t
hello-world.t .. ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.11 cusr  0.04 csys =  0.18 CPU)
Result: PASS

さて、課題提出です。

$ exercism submit HelloWorld.pm


    Your solution has been submitted successfully.
    You can complete the exercise and unlock the next core exercise at:

    https://exercism.io/my/solutions/fc5c6ec1b3d7471fbc037e65d970a417

ということで、最後に発行された URL に行くと・・・合格っぽい感じです。

解答を公開することもできるようなので置いておきます。いや、Hello, World! しただけですが・・・あ、多分 Exercism へのログインが必要です。

exercism.io

課題のページには次の問題へ!とあるので、暇な時に進めてみようと思います。

追記

この最初の課題をやるのに必要な知識は

  • Perlでのテスト

  • サブルーチンとその返り値

です。

テスト、特に Perl の Test2 については Gihyo さんに記事があります。

実は、Test2 に触れたの今回が初めてでした。

gihyo.jp

サブルーチンについては 1 月 23 日のPerl入学式でやる予定です。

やれるといいな。

時間が押さなければできるんじゃないだろうか、多分。

perl-entrance.connpass.com

ということで、興味のある方の参加をお待ちしております!