この処理Perlでどう書く？

[この処理Pythonでどう書く？](https://www.m3tech.blog/entry/python-snippets) の二番煎じです。

実際perlだったらどう書くかなと考えながら練習がてら。できる限りCOREモジュール縛り。

# この処理Perlでどう書く？

## 標準出力・標準エラー出力

```bash
# 標準出力にメッセージ
echo 'HELLO'

# 標準エラーにメッセージ
echo 'ERROR!' >&2
```

```perl
use strict;
use warnings;

# 標準出力にメッセージ
print "HELLO\n";
# もしくは v5.10 以上を use した上で say
use v5.10;
say "HELLO";

# 標準エラーにメッセージ。(PBP p.235 「10.12 ファイルハンドルへの出力」に従う)
print {*STDERR} "ERROR!\n";
# もしくは warn を使う。最後に改行を追加するとwarnが通常付与する行番号の情報を消せる。
warn "ERROR!\n";
```

## ファイル関係

### パス操作

```bash
name="$(basename "/foo/bar/baz.txt")" # => baz.txt          ファイル名
name="$(dirname "/foo/bar/baz.txt")"  # => /foo/bar/        親ディレクトリパス
fullpath="/foo/bar/${name}"           # => /foo/bar/baz.txt パスの連結
```

```perl
use strict;
use warnings;
use File::Basename qw(basename dirname);
use File::Spec;

my $name = basename("/foo/bar/baz.txt");  # => baz.txt
my $dir = dirname("/foo/bar/baz.txt");  # => /foo/bar
my $fullpath = File::Spec->catfile("/foo/bar", $name);  # => /foo/bar/buz.txt
```

### チルダや環境変数が含まれるパスを扱う

```python
import pathlib

bad = pathlib.Path("~/Donwloads/report.txt") # ~ は自動展開されない！

f = pathlib.Path("~/Donwloads/report.txt").expanduser()
g = pathlib.Path("${WORK_DIR}/input.txt").expandvars()
```

```perl
use strict;
use warnings;

my $bad = "~/Downloads/report.txt";

# globを使う。ワイルドカードに注意。
my $f = glob '~/Downloads/report.txt';  # スカラコンテキストでアクセスする。

# %ENV から環境変数を取れる。
my $g = "$ENV{WORK_DIR}/input.txt";
```

### ファイルの読み書き

```python
import pathlib
path = pathlib.Path('foo.txt')

with open(path, 'r', encoding="utf-8") as f:
    for line in f:
        # line を処理

with open(path, 'w', encoding="utf-8") as f:
    print("内容", file=f)

with path.open('r') as f: # Pathのメソッドを使って開くこともできる。
    # ...
```

```perl
use strict;
use warnings;
use open qw/:encoding(UTF-8) :std/;
use utf8;
use autodie;

my $path = 'foo.txt';

READ: {
    open my $fh, '<', $path;

    while (<$fh>) {
        chomp(my $line = $_);
        # $line を処理
    }

    close $fh;
}

WRITE: {
    open my $fh, '>', $path;
    print {$fh} "内容";
    close $fh;
}

1;
```

### 行数を数える（`wc -l`）

```python
with path.open('rb') as f:
    count = sum(1 for line in f)
```

```perl
use strict;
use warnings;
use autodie;
open my $fh, '<:raw', $path;
my $count = () = <$fh>;  # リストコンテキストでファイルハンドルを取り、それにスカラコンテキストでアクセス。
```

### ファイルの列挙

```python
import pathlib
dir = pathlib.Path('/tmp')
for file in dir.glob('tmp.*'):
    # ファイルを処理する
    # fileは文字列ではなく、pathlib.Pathであることに注意
```

```perl
use strict;
use warnings;

for my $file (glob '/tmp/tmp.*') {
    # ファイルを処理する
    # $fileは文字列
}
```

### ファイルの情報（存在確認・作成日時）

```python
import pathlib
f = pathlib.Path('/bin/bash')
f.exists()  # 存在確認

f.is_file() # ファイル？
f.is_dir()  # ディレクトリ？

f.stat().st_ctime # 作成日時
f.stat().st_mtime # 更新日時
f.stat().st_atime # アクセス日時
```

```perl
use strict;
use warnings;
use File::stat;

my $f = '/bin/bash';

my $sb = stat($f);  # 特殊ファイルハンドル "_" がキャッシュされる。
-e _;  # 存在確認
-f _;  # ファイル？
-d _;  # ディレクトリ？

$sb->ctime;  # 作成日時
$sb->mtime;  # 更新日時
$sb->atime;  # アクセス日時
```

### 移動・削除

```python
import pathlib

path_from = pathlib.Path('.bash_history')
path_to = pathlib.Path('/tmp/.bash_history')
path_from.rename(path_to) # 移動
path_from.unlink() # 削除
```

```perl
use strict;
use warnings;
use File::Copy qw(move);
use autodie qw(move);

my $path_from = '.bash_history';
my $path_to = '/tmp/.bash_history';
move $path_from, $path_to;  # 移動
unlink $path_from;  # 削除
```

### コピー

```python
import shutil
import pathlib

path_from = pathlib.Path('.bash_history')
path_to = pathlib.Path('/tmp/.bash_history')
shutil.copy2(path_from, path_to) # コピー
```

```perl
use strict;
use warnings;
use File::Copy qw(copy);
use autodie qw(copy);

my $path_from = '.bash_history';
my $path_to = '/tmp/.bash_history';
copy $path_from, $path_to;
```

## 外部コマンド

### 単純に実行する

```python
import subprocess
subprocess.run(['sl', '-h'], check=True)
```

```perl
use strict;
use warnings;
use autodie qw(system);
system qw(sl -h);
```

### 外部のコマンドを実行し、標準出力を受け取る

```python
import subprocess
r = subprocess.run(['echo', '世界'], check=True, stdout=subprocess.PIPE)
r.stdout # => b'\xe4\xb8\x96\xe7\x95\x8c\n' '世界\n'をUTF-8でエンコードしたもの
r.stdout.decode(sys.getfilesystemencoding()) # => '世界\n'
```

```perl
use strict;
use warnings;
use autodie;
use Encode;

# 実行するコマンドが安全なら
my $stdout = `echo 世界`;
$stdout;  # '世界\n'のバイナリ文字列。utf8フラグは無し。
decode_utf8($stdout);  # '世界\n'、utf8フラグあり。

# 安全ではないなら
my @cmd = qw(echo 世界);
open my $fh, '-|', @cmd;
$stdout = do { local $/; <$fh> };  # 丸呑み
close $fh;

# utf8フラグの事を気にしたくないのなら
{
    use utf8;
    use open qw/:encoding(UTF-8) :std/;
    open my $fh, '-|', @cmd;
    $stdout = do { local $/; <$fh> };  # utf8フラグあり
    close $fh;
}

# 古いシステムでロケールがUTF-8じゃない？正気か？？
{
    no utf8;
    use Encode::Locale;  # non-core
    my @lcmd = map { encode(locale => decode_utf8($_)) } @cmd;
    open my $fh, '-|', @lcmd;
    $stdout = decode(locale => do { local $/; <$fh> });
    close $fh;
}
```

### 環境変数やカレントディレクトリを変更する

```python
env = dict(os.environ) # Pythonスクリプトの環境変数をコピー
env['DB_HOST'] = 'localhost' # 環境変数を変更

cwd = pathlib.Path('/')

subprocess.run(['setup.sh'], cwd=cwd, env=env)
```

```perl
use strict;
use warnings;
use Cwd qw(chdir);  # chdir時に$ENV{PWD}をアップデートする
use autodie;

local $ENV{DB_HOST} = 'localhost';
my $orig_dir = Cwd::getcwd();

chdir "/";
system "setup.sh";
chdir $orig_dir;
```

### リダイレクトを使う

```python
import subprocess
import os.path

fi = open(os.path.expanduser('~/.bash_history'))
fo = open('p.txt', 'wb')
subprocess.run(['grep', 'python[23]'], stdin=fi, stdout=fo)
# p.txt に検索結果が出力される
```

```perl
# COREモジュール縛り
use strict;
use warnings;
use autodie;
use IPC::Open2;

open my $fh_history, '<', scalar glob '~/.bash_history';

my ($fh_out, $fh_in);
my $pid = open2($fh_out, $fh_in, 'grep', 'python[23]');
print {$fh_in} $_ while <$fh_history>;
close $fh_in;
close $fh_history;

open my $fh_txt, '>:raw', 'p.txt';
print {$fh_txt} $_ while <$fh_out>;
close $fh_txt;
close $fh_out;

waitpid $pid, 0;
my $return_code = $? >> 8;
```

```perl
# COREモジュール以外も使っていい場合
use strict;
use warnings;
use IPC::Run3 qw(run3);  # non-core

my $fi = scalar glob '~/.bash_history';
my $fo = 'p.txt';

run3 ['grep', 'python[23]'], $fi, $fo;
```

### パイプを使う

```python
p1 = subprocess.Popen(["cat", '.bash_history'], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["grep", "python"], stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close()
output = p2.communicate()[0] # history から 'python' を含む行を検索した結果
p2.returncode # grep の終了コード
```

```perl
use strict;
use warnings;
use autodie;
use IPC::Open2;

open my $fh_history, '<', scalar glob '~/.bash_history';

my ($fh_out, $fh_in);
my $pid = open2($fh_out, $fh_in, 'grep', 'python');

print {$fh_in} $_ while <$fh_history>;

close $fh_in;
close $fh_history;

waitpid $pid, 0;
my $returncode = $? >> 8;
my $output = do { local $/; <$fh_out> };

close $fh_out;
```

### spawn → wait （外部コマンドを起動し、終了を待つ）

```python
import subprocess

p1 = subprocess.Popen(['/path/to/heavy-sub-process1'])
p2 = subprocess.Popen(['/path/to/heavy-sub-process2'])

p1.wait()
p2.wait()
```

```perl
use strict;
use warnings;

my @cmds = (
    ['/path/to/heavy-sub-process1'],
    ['/path/to/heavy-sub-process2'],
);

my @pids;
for my $cmd (@cmds) {
    my $pid = fork;
    if (!defined $pid) {
        die "Can't fork: $!";
    }
    elsif (!$pid) {
        exec @{$cmd};
        exit 1;
    }
    else {
        push @pids, $pid;
    }
}

my @rcs;
for my $pid (@pids) {
    waitpid $pid, 0;
    push @rcs, $? >> 8;
}
```

### シェルを実行する

```python
subprocess.run('echo Hello > /dev/null', shell=True)
```

```perl
system 'echo Hello > /dev/null';  # ">" のようなメタキャラクタがあればshellが呼ばれる
```

## 時刻関係

```python
from datetime import datetime, timedelta # datetimeは日時を、timedeltaは経過時間を表す

epoch = datetime.now().timestamp()
# => 1540277405.379158 現在のUnix時刻（小数）

datetime.fromtimestamp(1540277405).strftime('%FT%T')
# => '2018-10-23T15:50:05'

start = datetime.now()
# 何か時間がかかる処理をする

duration = datetime.now() - start # datetime - datetime は timedelta
print(duration / timedelta(seconds=1)) # 経過時間を数値型にするには、別の timedeltaで割る
# => 42.680422 かかった秒数（小数）
print(duration.total_seconds()) # これでもOK
# => 42.680422 かかった秒数（小数）
```

```perl
use strict;
use warnings;
use v5.12;
use Time::Piece;
use Time::HiRes;

my $epoch = localtime->epoch;
# => 現在のUnix時刻（整数）

my $start = localtime;
# なにか時間がかかる処理をする

my $duration = localtime() - $start;  # Time::Piece - Time::Piece は Time::Seconds
say $duration->seconds;
# => かかった秒数（整数）

my $frac_epoch = Time::HiRes::time();
# => 現在のUnix時刻（小数）

my $frac_duration = Time::HiRes::time() - $frac_epoch;
say $frac_duration;
# => かかった秒数（小数）
```

## 文字列関係

### 文字列への式埋め込み

```python
message='世界!'
print(f"Hello {message}") # => Hello 世界!
```

```perl
use strict;
use warnings;

my $message = '世界!';
print "Hello ${message}\n";  # => Hello 世界!
```

```python
print(f"1 + 2 = {1 + 2}") # => 1 + 2 = 3
```

```perl
use strict;
use warnings;

# 文字列に埋め込めるようリファレンスをくぐらせる
print "1 + 2 = ${\(1 + 2)}\n";  # => 1 + 2 = 3
```

### ヒアドキュメント

```python
import textwrap

report = textwrap.dedent(f"""
    レポート
    日付: {date}
""")
```

```perl
use strict;
use warnings;
use v5.26;
use Time::Piece;

my $report = <<~EOF;  # 比較的新しいperlのdedent
    レポート
    日付: ${\(localtime->strftime("%Y年%m月%d日"))}
    EOF
```

## コマンドライン引数

```python
import sys
sys.argv # => ['a.py', 'input.txt', '-o', 'output.txt']
```

```perl
# プログラムファイル名自体は $0 に入っている。
@ARGV; # => ('input.txt', '-o', 'output.txt')
```

```python
import argparse

parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
                    help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
                    const=sum, default=max,
                    help='sum the integers (default: find the max)')

args = parser.parse_args()
print(args.accumulate(args.integers))
```

```perl
#!/usr/bin/perl
use v5.12;
use strict;
use warnings;
use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat permute);
use Pod::Usage qw(pod2usage);
use File::Basename qw(basename);
use List::Util qw(max sum);

# 無理やりpythonのargparseに寄せたので多少ぎこちないかも。

my %args = (
    accumulate => \&max,
);

GetOptions(
    'h|help' => sub { pod2usage(0) },
    sum => sub { $args{accumulate} = \&sum },
) or pod2usage(1);

for my $arg (@ARGV) {
    if ($arg =~ /\D/) {
        pod2usage(
            -exitval => 1,
            -msg => "argument N: invalid int value: $arg",
        );
    }
}

say $args{accumulate}->(@ARGV);

1;
__END__

=pod

=encoding UTF-8

=head1 NAME

test.pl -- blah blah

=head1 SYNOPSIS

test.pl [-h] [--sum] N [N ...]

Process some integers.

=head2 OPTIONS

=over 2

=item C<-h>

show this help message

=item C<--sum>

sum the integers (default: find the max)

=back

=cut
```

## 終了時の処理&シグナルをtrapする

```python
import atexit
import os

tf = '/path/to/tempfile'

@atexit.register
def cleanup():
    os.remove(tf)
```

```perl
use strict;
use warnings;
use autodie;

my $tf = '/path/to/tmp';
$SIG{$_} = \&cleanup for qw( HUP INT QUIT TERM );

sub cleanup {
    unlink $tf;
}
```

## HTTPリクエスト(curlやwgetの代替)

```python
# URLを、GETでリクエストし、レスポンスボディを出力する
import urllib.request
import urllib.parse

params = urllib.parse.urlencode({'spam': 1, 'eggs': 2, 'bacon': 0})
url = f"http://www.musi-cal.com/cgi-bin/query?{params}"
with urllib.request.urlopen(url) as f:
     print(f.read().decode('utf-8'))
```

```perl
# URLを、GETでリクエストし、レスポンスボディを出力する
use strict;
use warnings;
use v5.12;
use HTTP::Tiny;

my %params = (spam => 1, eggs => 2, bacon => 0);
my $url = 'http://www.musi-cal.com/cgi-bin/query';
my $response = HTTP::Tiny->new->get($url, \%params);
die "Failed" unless $response->{success};
say $response->{content};
```