この処理Pythonでどう書く? の二番煎じです。
実際perlだったらどう書くかなと考えながら練習がてら。できる限りCOREモジュール縛り。
この処理Perlでどう書く?
標準出力・標準エラー出力
# 標準出力にメッセージ
echo 'HELLO'
# 標準エラーにメッセージ
echo 'ERROR!' >&2
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";
ファイル関係
パス操作
name="$(basename "/foo/bar/baz.txt")" # => baz.txt ファイル名
name="$(dirname "/foo/bar/baz.txt")" # => /foo/bar/ 親ディレクトリパス
fullpath="/foo/bar/${name}" # => /foo/bar/baz.txt パスの連結
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
チルダや環境変数が含まれるパスを扱う
import pathlib
bad = pathlib.Path("~/Donwloads/report.txt") # ~ は自動展開されない!
f = pathlib.Path("~/Donwloads/report.txt").expanduser()
g = pathlib.Path("${WORK_DIR}/input.txt").expandvars()
use strict;
use warnings;
my $bad = "~/Downloads/report.txt";
# globを使う。ワイルドカードに注意。
my $f = glob '~/Downloads/report.txt'; # スカラコンテキストでアクセスする。
# %ENV から環境変数を取れる。
my $g = "$ENV{WORK_DIR}/input.txt";
ファイルの読み書き
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のメソッドを使って開くこともできる。
# ...
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
)
with path.open('rb') as f:
count = sum(1 for line in f)
use strict;
use warnings;
use autodie;
open my $fh, '<:raw', $path;
my $count = () = <$fh>; # リストコンテキストでファイルハンドルを取り、それにスカラコンテキストでアクセス。
ファイルの列挙
import pathlib
dir = pathlib.Path('/tmp')
for file in dir.glob('tmp.*'):
# ファイルを処理する
# fileは文字列ではなく、pathlib.Pathであることに注意
use strict;
use warnings;
for my $file (glob '/tmp/tmp.*') {
# ファイルを処理する
# $fileは文字列
}
ファイルの情報(存在確認・作成日時)
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 # アクセス日時
use strict;
use warnings;
use File::stat;
my $f = '/bin/bash';
my $sb = stat($f); # 特殊ファイルハンドル "_" がキャッシュされる。
-e _; # 存在確認
-f _; # ファイル?
-d _; # ディレクトリ?
$sb->ctime; # 作成日時
$sb->mtime; # 更新日時
$sb->atime; # アクセス日時
移動・削除
import pathlib
path_from = pathlib.Path('.bash_history')
path_to = pathlib.Path('/tmp/.bash_history')
path_from.rename(path_to) # 移動
path_from.unlink() # 削除
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; # 削除
コピー
import shutil
import pathlib
path_from = pathlib.Path('.bash_history')
path_to = pathlib.Path('/tmp/.bash_history')
shutil.copy2(path_from, path_to) # コピー
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;
外部コマンド
単純に実行する
import subprocess
subprocess.run(['sl', '-h'], check=True)
use strict;
use warnings;
use autodie qw(system);
system qw(sl -h);
外部のコマンドを実行し、標準出力を受け取る
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'
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;
}
環境変数やカレントディレクトリを変更する
env = dict(os.environ) # Pythonスクリプトの環境変数をコピー
env['DB_HOST'] = 'localhost' # 環境変数を変更
cwd = pathlib.Path('/')
subprocess.run(['setup.sh'], cwd=cwd, env=env)
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;
リダイレクトを使う
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 に検索結果が出力される
# 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;
# 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;
パイプを使う
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 の終了コード
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 (外部コマンドを起動し、終了を待つ)
import subprocess
p1 = subprocess.Popen(['/path/to/heavy-sub-process1'])
p2 = subprocess.Popen(['/path/to/heavy-sub-process2'])
p1.wait()
p2.wait()
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;
}
シェルを実行する
subprocess.run('echo Hello > /dev/null', shell=True)
system 'echo Hello > /dev/null'; # ">" のようなメタキャラクタがあればshellが呼ばれる
時刻関係
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 かかった秒数(小数)
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;
# => かかった秒数(小数)
文字列関係
文字列への式埋め込み
message='世界!'
print(f"Hello {message}") # => Hello 世界!
use strict;
use warnings;
my $message = '世界!';
print "Hello ${message}\n"; # => Hello 世界!
print(f"1 + 2 = {1 + 2}") # => 1 + 2 = 3
use strict;
use warnings;
# 文字列に埋め込めるようリファレンスをくぐらせる
print "1 + 2 = ${\(1 + 2)}\n"; # => 1 + 2 = 3
ヒアドキュメント
import textwrap
report = textwrap.dedent(f"""
レポート
日付: {date}
""")
use strict;
use warnings;
use v5.26;
use Time::Piece;
my $report = <<~EOF; # 比較的新しいperlのdedent
レポート
日付: ${\(localtime->strftime("%Y年%m月%d日"))}
EOF
コマンドライン引数
import sys
sys.argv # => ['a.py', 'input.txt', '-o', 'output.txt']
# プログラムファイル名自体は $0 に入っている。
@ARGV; # => ('input.txt', '-o', 'output.txt')
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))
#!/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する
import atexit
import os
tf = '/path/to/tempfile'
@atexit.register
def cleanup():
os.remove(tf)
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の代替)
# 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'))
# 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};