pikesaku’s blog

個人的な勉強メモです。記載内容について一切の責任は持ちません。

FMLの会員管理ファイルの動作確認

activesファイルの動作

f:id:pikesaku:20170122013125p:plain

シングルクオーテーションは評価されない。ダブルクオーテーションは閉じていれば評価される。
投稿しても有効なメンバーない場合は、以下ログが出る。
SmtpIO: no recipients but O.K.? (test1@example.com)


membersファイルの動作

f:id:pikesaku:20170122013529p:plain

members_onlyの設定でtest1@example.comからメール送信し、メンバーとして認識されるか確認
シングルクオーテーション、ダブルクオーテーション共に閉じていても評価されない。
行頭のコメント1個は無視される!
全角スペースはmembersではアドレスを構成する文字として認識される(?もしくは不正文字のため、判定されてない?)、activesでは不正文字としてエラーになる。
全角スペースは前後のスペース以外の文字と組み合わせた文字として判定される。

FMLの配送制限(PERMIT_POST_FROM)とMailman設定対応表

fml/mailman設定 動作が定義されていない非会員からの投稿に対する動作
(generic_nonmember_action)
承認/保留/拒否/破棄
新しく登録する会員のデフォルトを制限付き会員にしますか?
(default_member_moderation)
いいえ/はい
制限付き会員から投稿があったときの動作
(member_moderation_action)
保留/拒否/破棄
anyone 承認 いいえ 保留
members_only 保留 いいえ 保留
moderator 保留 はい 保留

FMLとMailmanの送信元ヘッダ比較

FMLはデフォルト設定、Mailmanの関連設定は以下の通り

項目
anonymous_list No
include_sender_header Yes
reply_goes_to_list 投稿者
first_strip_reply_to No



メンバー配信メール

  種別    Return-Path    From     Reply-To     Sender  
fml ML名-admin 投稿者 ML名 -
mailman ML名-bounces 投稿者 - ML名-bounces



ML管理者宛通知メール

  種別    Return-Path    From     Reply-To     Sender  
fml ML名-admin ML名-admin - -
mailman ML名-bounces
(投稿者)
(-)
ML名-owner
(投稿者)
(ML名-request)
- ML名-bounces
(-)
(ML名-request)

members_onlyのMLにメンバー外のアドレスから投稿し管理者宛ての通知メールで確認
()内の値はマルチパート部分。
マルチパート構成は以下の通り。
mailman: 通知メール(承認URL記載)、保留されたメール、通知メール(承認返信メール記載)


メンバー宛通知メール

  種別    Return-Path    From     Reply-To     Sender  
fml ML名-admin ML名-admin - -
mailman ML名-bounces ML名-owner - ML名-bounces

members_onlyのMLにメンバーではないアドレスから投稿し該当アドレス宛ての通知メールで確認


モデレータ宛通知メール

  種別    Return-Path    From     Reply-To     Sender  
fml ML名-admin
(owner-ML名)
ML名-admin
(投稿者)
ML名-ctl -
mailman ML名-bounces
(投稿者)
(-)
ML名-owner
(投稿者)
(ML名-request)
- ML名-bounces
(-)
(ML名-request)

モデレータへの通知メールで確認
()内の値はマルチパート部分。
マルチパート構成は以下の通り。
fml: 通知メール、保留されたメール
mailman: 通知メール(承認URL記載)、保留されたメール、通知メール(承認返信メール記載)

FML2Mailman移行ツール(試験まだしてない。。)

ツール説明

作成中

ツール

#!/usr/bin/python
# coding: utf-8

# 留意事項
# 1.activesやmembersの中のローカルパートのみアドレスはskipされる。
# 2.WarnでSkipを含むメッセージは移行していない情報があるを意味する。
#   メッセージより必要に応じて手作業で移行をする。

import os
import argparse
import commands
import re
import syslog
import inspect
import sys
from datetime import datetime

parser = argparse.ArgumentParser(description='fml2mailman convert tool')
parser.add_argument('-v', '--verbose', action='store_true', help='verbose', default=False)
parser.add_argument('-d', '--dryrun', action='store_true', help='', default=False)
parser.add_argument('mlname', help='mlname')
args = parser.parse_args()

MLNAME = args.mlname.lower()
DRYRUN = args.dryrun
VERBOSE = args.verbose

# FMLインストール環境
FML_DIR = '/var/spool/ml'
CONFIG = FML_DIR + '/' + MLNAME + '/' + 'config.ph'
MEMBERS = FML_DIR + '/' + MLNAME + '/' + 'members'
ACTIVES = FML_DIR + '/' + MLNAME + '/' + 'actives'
MEMBERS_ADMIN = FML_DIR + '/' + MLNAME + '/' + 'members-admin'
MODERATORS = FML_DIR + '/' + MLNAME + '/' + 'moderators'
SEQ = FML_DIR + '/' + MLNAME + '/' + 'seq'

# ドメイン設定
DOMAIN = 'exmaple.com'

# ファイル出力先ディレクトり
OUTPUT_DIR = './output'


def fwrite(f, d):
    try:
        with open(f, 'w') as fh:
            if isinstance(d, list):
                fh.write('\n'.join(d))
            else:
                fh.write(d)
    except:
        err_fin('can not write ' + f)


def fopen(f):
    try:
        with open(f) as fh:
            data = [s for s in fh.readlines() if s.strip()]
    except:
        err_fin('file open error[' + f + ']')
    return data


def env_chk():
    for s in [CONFIG, MEMBERS, ACTIVES, SEQ]:
        if not os.path.isfile(s):
            err_fin('file does not exist[' + s + ']')
    if not os.path.isdir(OUTPUT_DIR):
        exec_com('mkdir -p ' + OUTPUT_DIR)


def addr_chk(s):
    p = re.compile('^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z]+$')
    if p.match(s):
        return True
    else:
        return False


def mkpass():
    p = exec_com('mkpasswd -l 12 -s 0')
    return p


def exec_com(s):
    r = commands.getstatusoutput(s)
    if r[0]:
        err_fin('command failed[' + s + ']')
    return r[1]


def err_fin(s):
    output_log('Error: ' + s)
    exit(1)


def skip_chk(s):
    p = re.compile('^[^#\s]+.*\s+s=.*$')
    if p.match(s):
        return True
    else:
        return False


def space_start_chk(s):
    # 全角スペースはマッチしない。
    # 全角スペースはFMLでも区切り文字にならないため、問題なし。
    p = re.compile('^\s+\S+.*$')
    if p.match(s):
        return True
    else:
        return False


def output_log(s):
    s = MLNAME + ': ' + s
    syslog.openlog(os.path.basename(sys.argv[0]))
    # syslog.openlog(os.path.basename(sys.argv[0]), syslog.LOG_PERROR, syslog.LOG_USER)
    syslog.syslog(s)
    print(s)
    syslog.closelog()


def conv_member(li):
    f = OUTPUT_DIR + '/' + MLNAME + '-MEMBER'
    fwrite(f, li)
    exec_com('/usr/local/mailman/bin/add_members -w n -a n -r ' + f + ' ' + MLNAME)


def conv_config(s):
    f = OUTPUT_DIR + '/' + MLNAME + '-CONFIG'
    fwrite(f, s)
    exec_com('/usr/local/mailman/bin/config_list -i ' + f + ' ' + MLNAME)


def create_ml(s):
    f = OUTPUT_DIR + '/' + MLNAME + '-PASSWD'
    mls = exec_com('/usr/local/mailman/bin/list_lists -b')
    if MLNAME in mls:
        # 試験用
        exec_com('/usr/local/mailman/bin/rmlist -a ' + MLNAME)
        # 本番向け
        # err_fin(MLNAME + ' already exsists')
    p = mkpass()
    fwrite(f, p)
    exec_com('/usr/local/mailman/bin/newlist -q' + ' ' + MLNAME + ' ' + s + ' ' + "'" + p + "'")


def set_post_strict(s):
    # generic_nonmember_action
    # 動作が定義されていない非会員からの投稿に対する動作
    # 0:承認 1:保留 2:拒否 3:破棄

    # default_member_moderation
    # 新しく登録する会員のデフォルトを制限付き会員にしますか?
    # 0:いいえ 1:はい

    # member_moderation_action
    # 制限付き会員から投稿があったときの動作
    # 0:保留 1:拒否 2:破棄
    s2 = str()
    if s == 'ao':  # anyone
        s2 += 'generic_nonmember_action = 0\n'
        s2 += 'default_member_moderation = 0\n'
        s2 += 'member_moderation_action = 0\n'
    elif s == 'mo':  # members_only
        s2 += 'generic_nonmember_action = 1\n'
        s2 += 'default_member_moderation = 0\n'
        s2 += 'member_moderation_action = 0\n'
    elif s == 'md':  # moderator
        s2 += 'generic_nonmember_action = 1\n'
        s2 += 'default_member_moderation = 1\n'
        s2 += 'member_moderation_action = 0\n'
    else:
        err_fin('invalid config(post_strict)')
    return s2


def set_stop_deliver(li):
    # String Example
    # delivery_status = { 'test3@example.com': (3, 1483290493.0), 'test6@example.com': (3, 1483290493.0), }
    date = float(datetime.now().strftime('%s'))
    s = 'delivery_status = { '
    for s2 in li:
        s += "'" + s2 + "': " + '(3, ' + str(date) + '), '
    s += '}\n'
    return s


def set_user_opt(post_strict, member, no_post):
    # String Example
    # user_options = { 'test1@example.com': 392,'test6@example.com': 392,'test5@example.com': 392,}
    # デフォルト値(d) = ダブリ無(256) + 平文(8)
    # 制限有効時は128加算
    d = 264
    s = 'user_options = { '
    if post_strict == 'md':  # moderator
        for s2 in member:
            s += "'" + s2 + "': " + str(d + 128) + ', '
    else:
        for s2 in no_post:
            s += "'" + s2 + "': " + str(d + 128) + ', '
        for s2 in set(member) - set(no_post):
            s += "'" + s2 + "': " + str(d) + ', '
    s += '}\n'
    return s


def set_seq(i):
    s = 'post_id = ' + str(i) + '\n'
    return s


def set_admin(li):
    s = 'owner = ['
    for s2 in li:
        s += "'"
        s += s2
        s += "',"
    s += ']\n'
    return s


def set_moderator(li):
    s += 'moderator = ['
    for s2 in li:
        s += "'"
        s += s2
        s += "',"
    s += ']\n'
    return s


def set_reply_to(s):
    # reply_goest_to_list = Int
    # 0: 投稿者
    # 1: このリスト
    # 2: 別のアドレス
    # reply_to_address = Str
    s1 = str()
    if s == 'from':
        s1 = 'reply_goest_to_list = 0'
    elif s == 'ml':
        s1 = 'reply_goest_to_list = 1'
    else:
        s1 = 'reply_goest_to_list = 2\n'
        s1 += 'reply_to_address = ' + "'" + s + "'\n"
    return s1


def get_reply_to(s):
    #&DEFINE_FIELD_FORCED("reply-to", "$From_address, $MAIL_LIST");
    w = 'Warn: HOOK proc exist: '
    li = [s2 for s2 in s if s2.startswith('&DEFINE_FIELD_FORCED(')]
    if not li:
        return False
    if len(li) > 1:
        output_log(w + 'Skip: &DEFINE_FIELD_FORCED appear over twice')
        return False

    s = li[0]
    s2 = s.lstrip('&DEFINE_FIELD_FORCED')
    s2 = s2.rstrip(';')
    s2 = s2.strip().strip('()').strip()
    s2 = s2.split(',')

    w1 = w + 'Skip: invalid define_field: ' + s
    w += s

    if not s2 or len(s2) != 2:
        output_log(w1)
        return False

    f1 = s2[0].strip().strip('"').strip("'").lower()
    f2 = s2[1].strip().strip('"').strip("'")

    if f1 != 'reply-to':
        output_log(w1)
        return False

    if f2 == '$From_address':
        output_log(w)
        return 'from'
    elif f2 == '$MAIL_LIST':
        output_log(w)
        return 'ml'
    else:
        f2 = f2.replace('\\', '')
        if addr_chk(f2):
            output_log(w)
            return f2
        else:
            output_log(w1)
            return False

    output_log(w1)
    return False


def get_val_from_config(config, param):
    for s in config:
        p = s.split()[0].lstrip('$')
        if p == param:
            #v = s.split()[-1].rstrip(';').strip('"')
            v = s.split()[-1].rstrip(';').strip('"').strip("'")
            return v
    return False


def get_subject_tag(s):
    # (mo_ml:%05d)
    v = str()
    t = get_val_from_config(s, 'SUBJECT_TAG_TYPE')
    b = get_val_from_config(s, 'BRACKET')
    f = get_val_from_config(s, 'SUBJECT_FORM_LONG_ID')
    if f == False:
        f = "5"
    if t == False or b == False:
        err_fin('invalid tag')

    if not t:
        v += ''
    elif t == '(:)':
        v += '(' + b + ':' + '%0' + f + 'd)'
#    elif t == '[:]':
#         v += '[' + b + ':' + '%0' + f + 'd]'
    elif t == '()':
        v += '(' + b + ')'
#    elif t == '[]':
#         v += '[' + b + ']'
    else:
        err_fin('invalid tag ' + t)
    return v


def get_post_strict(s):
    s2 = get_val_from_config(s, 'PERMIT_POST_FROM')
    if s2 == False:
        err_fin('invalid post_strict')
    if s2 == 'anyone':
        return 'ao'
    elif s2 == 'members_only':
        return 'mo'
    elif s2 == 'moderator':
        return 'md'
    else:
        err_fin('invalid post_strict')


def get_seq():
    s = fopen(SEQ)
    i = int()
    if not s:
        err_fin('invalid seq')

    s = s[0].rstrip()
    if s.isdigit():
        i = int(s) + 1
    else:
        err_fin('invalid seq')
    return i


def get_addr(f):
    li = fopen(f)
    li = list(s.lower().rstrip() for s in li if not s.startswith('#') and s.strip())
    for s in li:
        if not addr_chk(s):
            err_fin('addr is invalid: ' + s + '[' + f + ']')
    return li


def get_members():
    members = set()
    li = fopen(MEMBERS)
    # activesファイルと行頭のスペースの扱いが違う点に注意
    li = [s.lower().strip() for s in li if not s.startswith('##') and s.strip()]
    for s in li:
        w = str()
        if not addr_chk(s):
            w += 'Not normal entry: '
        if s.startswith('#'):
            s2 = s.lstrip('#').strip()
            if s2:
                s2 = s2.split()[0]
                if addr_chk(s2):
                    members.add(s2)
                else:
                    w += 'Skip: '
            else:
                w += 'Skip: '
        else:
            s2 = s.split()[0]
            if addr_chk(s2):
                members.add(s2)
            else:
                w += 'Skip: '

        if w:
            output_log('Warn: ' + w + s + ' [' + MEMBERS + ']')
        else:
            if VERBOSE:
                output_log('Debug: ' + 'Converted: ' + s + ' [' + MEMBERS + ']')

    if not members:
        output_log('Warn: no valid entry [' + MEMBERS + ']')
    return members


def get_actives():
    actives = set()
    no_deliver = set()
    li = fopen(ACTIVES)
    li = [s.lower().rstrip() for s in li if not s.startswith('##') and s.strip()]
    for s in li:
        w = str()
        if not addr_chk(s):
            w += 'Not normal entry: '

        if s.startswith('#'):
            s2 = s.lstrip('#').strip()
            if s2:
                s2 = s2.split()[0]
                if addr_chk(s2):
                    no_deliver.add(s2)
                else:
                    w += 'Skip: '
            else:
                w += 'Skip: '
        elif space_start_chk(s):
            w += 'Skip: '
        else:
            s2 = s.split()[0]
            if skip_chk(s):
                if addr_chk(s2):
                    no_deliver.add(s2)
                else:
                    w += 'Skip: '
            else:
                if addr_chk(s2):
                    actives.add(s2)
                else:
                    w += 'Skip: '

        if w:
            output_log('Warn: ' + w + s + ' [' + ACTIVES + ']')
        else:
            if VERBOSE:
                output_log('Debug: ' + 'Converted: ' + s + ' [' + ACTIVES + ']')

    if not actives:
        output_log('Warn: no valid entry [' + ACTIVES + ']')
    return actives, no_deliver


def get_member_info():
    # FML1情報取得
    actives, no_deliver = get_actives()
    members = get_members()

    # Mailman用メンバー情報格納オブジェクト作成
    mm_member = actives.union(no_deliver).union(members)
    mm_no_deliver = no_deliver.union(members - actives)
    mm_no_post = actives.union(no_deliver) - members

    # Mailman用メンバー情報格納オブジェクトをリスト変換し戻す
    mm_member = list(mm_member)
    mm_member.sort()

    mm_no_deliver = list(mm_no_deliver)
    mm_no_deliver.sort()

    mm_no_post = list(mm_no_post)
    mm_no_post.sort()

    return mm_member, mm_no_deliver, mm_no_post


def get_config():
    li = fopen(CONFIG)
    li = [s.strip() for s in li if not s.startswith('#')]
    return li


def main():
    env_chk()

    ########################################################
    # FML情報取得                                          #
    ########################################################
    config = get_config()
    post_strict = get_post_strict(config)
    subject_tag = get_subject_tag(config)
    member, no_deliver, no_post = get_member_info()
    admin = get_addr(MEMBERS_ADMIN)
    moderator = list()
    if post_strict == 'md':  # moderator
        moderator = get_addr(MODERATORS)
    seq = get_seq()
    reply_to = get_reply_to(config)

    ########################################################
    # Mailman設定                                          #
    ########################################################

    # 管理者情報
    mm_config = set_admin(admin)

    # 投稿制限
    mm_config += set_post_strict(post_strict)

    # シーケンス番号
    mm_config += set_seq(seq)

    # ユーザーオプション(制限)
    if member and post_strict != 'ao':  # anyone
        mm_config += set_user_opt(post_strict, member, no_post)

    # 配信停止
    if no_deliver:
        mm_config += set_stop_deliver(no_deliver)

    # リスト司会者設定
    if moderator and post_strict == 'md':  # moderator
        mm_config += set_moderator(moderator)

    # 件名タグ付け
    # subject_prefix = Str
    if subject_tag:
        mm_config += 'subject_prefix = ' + "'" + subject_tag + "'\n"
    else:
        mm_config += 'subject_prefix = ""\n'

    # メールドメイン設定
    # host_name = Str
    mm_config += 'host_name = ' + "'" + DOMAIN + "'\n"

    # Reply-To設定
    if reply_to != False:
        mm_config += set_reply_to(reply_to)

    # 移行
    if DRYRUN:
        for s in [s for s in mm_config.split('\n') if s]:
            output_log('DryRun: ' + 'Config: ' + s)
        for s in member:
            output_log('DryRun: ' + 'Member: ' + s)
    else:
        create_ml(admin[0])
        conv_member(member)
        conv_config(mm_config)
        output_log('Info: Successfully finished')


if __name__ == '__main__':
    main()

Mailmanメモ

user_optionsの値について

各項目の説明(ヘルプ文言抜粋)

項目 説明
制限 個人単位で会員に制限(モデレーション)をかけるフラグ
このフラグをセットすると,この会員の投稿は制限されます
セットしなければ自由に投稿できます
隠れ会員 この会員のアドレスは 会員名簿では隠しますか?
配送停止 この会員への配送は停止されていますか?
もし, そうなら 以下の略号で配送を停止した理由が示されます
U->会員自身が停止
A→管理者が停止
B→エラーメール過剰で停止
?→理由不明
受領 この会員からの投稿に確認通知を送りますか?
控え無 この会員からの投稿は本人には配送しないようにしますか?
ダブリ無 この会員には同じメールを重複して配送 しないようにしますか?
まとめ読み この会員へは「まとめ読み」で配送しますか?
設定しなければ, 1本ずつ配送される普通配送になります
平文 「まとめ読み」の場合, この会員へは平文で配送しますか?
でなければ, MIME 添付を使います
言語 会員が選んだ言語



設定毎のuser_options値

制限 隠れ
会員
配送
停止
受領 控え無 ダブリ
まとめ
読み
平文 言語
- - - - - - - - - 設定なし
on - - - - - - - - 128
on on - - - - - - - 144
on on on - - - - - - 144
on on on on - - - - - 148
on on on on on - - - - 150
on on on on on on - - - 406
on on on on on on on - - 406
on on on on on on on on - 414
配送停止をonにした時は以下設定がされる。

'delivery_status': { 'メンバーアドレス': (3, 1482648090.696833),}
値のタプル2個目の要素はunixタイム

まとめ読みをonにした時は以下設定がされる。

'digest_members': { 'メンバーアドレス': 0},

デフォルトは、ダブリ無と平文がonで264

ダブリ無で256加算
平文が8加算

制限付き会員から投稿があったときの動作(member_moderation_action)

各設定時のmember_moderation_actionの値

保留 0
拒否 1
破棄 2



FML→Mailman投稿制御設定対応表

FML設定 Mailman設定
フィルタ設定
(PERMIT_POST_FROM)
メンバー登録 フィルタ設定
(generic_nonmember_action)
(member_moderation_action)
メンバー登録
anyone actives
members
両方
承認(0)
保留(0)
-
members
のみ
配送停止
actives
のみ
-
members_only actives
members
両方
承認(0)
破棄(2)
-
members
のみ
配送停止
actives
のみ
制限
moderator actives
members
両方
承認(0)
保留(0)
-
members
のみ
配送停止
actives
のみ
-

FMLモデレータ動作

moderatorsファイルがない場合は、$MAINTAINER定義アドレス(ML名-admin)にフォワードされる。

activesのフォーマット

NONE TITLE 8.html
NONE TITLE 2.html

ポイント

①activesは以下フォーマット
アドレス オプション # コメント
②コメントは単なるコメント
③アドレス移行の部分はオプションとして解釈される。
④オプションは2パターン記述方法があり。
V1 フォーマットは
数字(フォーマット) まとめおくりの指定
数字以外 リレーサーバ

V2 フォーマットでは将来の拡張のため
m=まとめ送り指定
r=リレーサーバ
s=1(skip を意味する)
 ※m=5と5は同じということ。この5は時間。
⑤V1記述は使われないとの記述もあり。V2のみかも。
⑥まとめ読みは、cronで1時間に一度プログラムが実行され、activesファイルをチェックし動作が決定される。

skipとoffの違い
http://www.fml.org/fml/Japanese/how_to_subscribe/8.html

skipは管理者、offはユーザー?

動きメモ

①メンバーが-ctl宛に本文にoffと書いてメール送信
②membersファイルは変更無し。activesファイルは以下のように変わる。
変更前

test1@example.com 
test2@example.com 
test3@example.com 

変更後

----- Backup on 17/01/01 12:56:30 -----
test1@example.com 
test2@example.com 
test3@example.com 
[root@ip-172-31-40-69 conv_fml2mm]# cat /var/spool/ml//fml1/actives
#.FML HEADER
# NEW FORMAT FOR FURTHER EXTENSION
# e.g. fukachan@phys r=relayserver m=3u s=skip 
# r= relayserver
# m= matomeokuri parameter is time and option
# s= skip. can post from this address but not delivered here.
#
# the same obsolete format is compatible with new format and as follows:
# e.g. fukachan@phys relayserver matome 3u
#.endFML HEADER
#	test1@example.com  
test2@example.com 
test3@example.com 

③actives.bakのバックアップができる。
④コマンドメール送信したユーザーには、以下メールが送信される。

off
OFF [test1@example.com]  accepted.

--fml1@example.com, Be Seeing You!    

************************************************************

      Help: <mailto:fml1-ctl@example.com?body=help>
Unsubscribe: <mailto:fml1-ctl@example.com?body=unsubscribe>

If you have any questions or problems,
  please contact fml1-admin@example.com
      or 
  send e-mail with the body "help"(without quotes) to
     fml1-ctl@example.com
     (here is the automatic reply, so more preferable)

e.g. on a Unix Machine
(shell prompt)% echo "help" |Mail fml1-ctl@example.com

************************************************************

⑤MLに投稿しても配信はされない。
⑥onのメールを送信すると再度有効化される。
 ちなみに手作業でactivesファイルを編集し、コメントを1つだけ付けて、スペースなしの場合(#test1@example.com)でも、onで有効化される。
⑦送信者にはコマンド成功した通知メールがくる。

activesの第一フィールドのみチェックすればOK

memberファイルのフォーマットは?

以下記述があり。

なおメンバーファイルとしては各行のアドレスより後ろの部分は何にも使われていませんので、勝手に使って構いません。
しかしながら、自動登録の場合は $MEMBER_LIST と $ACTIVE_LIST は同じもの($MEMBER_LIST)が使われます。よってそのフォーマットは $ACTIVE_LIST 形
式であると仮定する必要があります。

自動登録機能が有効な場合は、同じフォーマットになる可能性があるが、未使用の場合が、第一フィールドのみが有効パラメタ

シェルを学ぶ

参考URLのシェルスクリプトがかっこいいので勉強
シェルのテクニック満載で、これでもかっ!ってくらいあった気がする。大変勉強になりました。

要チェックキーワード

FIXMEは修正を要する部分

変数を${VAR}と$VARで定義する場合の違い

参照時に変数名部分を明示的にする
変数を使用する | UNIX & Linux コマンド・シェルスクリプト リファレンス
 

# cat ./a.sh
#!/bin/ksh

A="1"
B="2"
echo $AB
echo ${A}B
echo ${A}${B}
# sh ./a.sh

1B
12



環境変数 LC_ALL

ロケールに関する環境変数の一括上書き

# echo $LANG
ja_JP.utf8
# ls /tmp/noexist                                                                            
ls: /tmp/noexist にアクセスできません: そのようなファイルやディレクトリはありません
# export LC_ALL=C
# echo $LANG
ja_JP.utf8
# ls /tmp/noexist
ls: cannot access /tmp/noexist: No such file or directory
# 



シェルで未定義変数を参照した時にエラー終了させる

# cat /tmp/a.sh 
#!/bin/bash

set -u
echo $not_defined
echo test
# sh /tmp/a.sh 
/tmp/a.sh: 行 4: not_defined: 未割り当ての変数です
# 

 

setコマンドはshellにオプションを設定するコマンド

${VAR+set}の意味

変数の定義状況の確認で使う
この"set"は他の文字列でもOK。変数定義状況確認を示す為、setを使っているだけ

以下URLによると
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02
 
${VAR+hoge}の場合

VAR変数定義あり かつ NULLでない場合
→${VAR+hoge}はhoge

VAR変数定義あり かつ NULLである場合
→${VAR+hoge}はhoge

VAR変数定義なし
→${VAR+hoge}はNULL

では確認

# cat ./a.sh 
#!/bin/bash

A="test"
echo ${A+hoge}
A=""
echo ${A+hoge}
echo ${B+hoge}
# sh ./a.sh
hoge
hoge

 


ちなみに、
${VAR:+word}の場合
VARにNULL以外の値がセットされているかの判定に使える。

# cat ./a.sh 
#!/bin/bash

A="test"
echo ${A:+hoge}
A=""
echo ${A:+hoge}
echo ${B:+hoge}
# sh ./a.sh
hoge



expressionを囲う角括弧が1個の場合と2個の場合の違いは

※[]と[[]]の違い

man bashでは、、、
expression の説明に以下記述があり。

Word splitting and pathname expansion are not performed on the words between the and ; tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution, process substitution, and quote removal are performed.

すごく詳しい解説があった!
https://fumiyas.github.io/2013/12/15/test.sh-advent-calendar.html
 

①ワード分割とパス名展開がされない

例)

# [ -f a ] && echo Exist
Exist
# [ -f a* ] && echo Exist
-bash: [: too many arguments
# [[ -f a ]] && echo Exist
Exist
# [[ -f a* ]] && echo Exist
# [[ -f a? ]] && echo Exist
# A="hoge hoge"
# [ -f $A ] && echo Exist
-bash: [: hoge: binary operator expected
# [ -f "$A" ] && echo Exist
# [[ -f $A ]] && echo Exist
# 

 


②数値の比較演算子では左右の値が算術式展開される

例)

# [ 1+2 -eq 3 ] && echo "Ugoku"
-bash: [: 1+2: integer expression expected
# [[ 1+2 -eq 2+1 ]] && echo "Ugoku"
Ugoku
# [[ '1 + 2' -eq '2 + 1' ]] && echo "Ugoku"
Ugoku

 

ポイントは、式に空白を含める場合は、クオートする必要がある点

③文字列の比較演算子 == で右辺がクオートされてない場合、完全一致ではなくパターンマッチになる点

例)

# [ 'test' == tes* ] && echo Match
# [[ 'test' == tes* ]] && echo Match
Match
# [[ 'test' == "tes*" ]] && echo Match
# 

 


④[]にはない文字列の比較演算子を使える。

例)
正規表現が可能。こりゃ便利

# [ "test" =~ ^[a-z]{4}$ ] && echo Match
-bash: [: =~: binary operator expected
# [[ "test" =~ ^[a-z]{4}$ ]] && echo Match
Match

 <や>で文字列順番の比較が可能

# [[ "abc" < "def" ]] && echo Match
Match
# [[ "abc" > "def" ]] && echo Match
# 

 


ちなみに[]でやると、<>はリダイレクトが動いてしまい、こんな動きになる。

# [ "abc" > "def" ] && echo "Match"
Match
# ls ./def 
./def
# [ "abc" < "def" ] && echo "Match"
Match
# ls ./abc ./def                                                                                                                                                        
ls: cannot access ./abc: No such file or directory
./def
# rm -f ./def 
# [ "abc" < "def" ] && echo "Match"
-bash: def: No such file or directory
# 

 


⑤条件式の論理演算子(and・or)の記述が異なる。は-a,-o、[]は&&、||

[]の場合

# [ 1 -eq 1 -a 2 -eq 2 ] && echo Match
Match
# [ 1 -eq 1 -o 2 -eq 2 ] && echo Match
Match
# [ 1 -eq 1 && 2 -eq 2 ] && echo Match
-bash: [: missing `]'
# [ 1 -eq 1 || 2 -eq 2 ] && echo Match
-bash: [: missing `]'
-bash: 2: command not found

 


[[]]の場合

# [[ 1 -eq 1 && 2 -eq 2 ]] && echo Match
Match
# [[ 1 -eq 1 || 2 -eq 2 ]] && echo Match
Match
# [[ 1 -eq 1 -a 2 -eq 2 ]] && echo Match
-bash: syntax error in conditional expression
-bash: syntax error near `-a'
# [[ 1 -eq 1 -o 2 -eq 2 ]] && echo Match
-bash: syntax error in conditional expression
-bash: syntax error near `-o'
# 

結論: これからは、上記を踏まえ角括弧2個を使う!正規表現が使えるのがいい!


function pdieのexit ${2-1}の意味について

function pdie {
  perr "$1"
  exit ${2-1}
}

 


http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02
${VAR-hoge}は上記表では以下の様に展開される。

VAR変数定義あり かつ NULLでない場合
→${VAR-hoge}はVARの値

VAR変数定義あり かつ NULLである場合
→${VAR-hoge}はNULL

VAR変数定義なし
→${VAR-hoge}はhoge

終了ステータスコードをpdie呼び出し時の第二引数で指定できる

簡易動作確認コード

# cat ./a.sh 
#!/bin/ksh

function hoge {
  echo ${2-1}
}

hoge
hoge ""
hoge "" ""
hoge "a" ""
hoge "a" "b"
# sh ./a.sh 
1
1


b



[[ -t 0 ]]の意味について

ファイルデスクリプタ0(標準入力)があるかを確認
以下をターミナル・cronで、それぞれ実行すると結果は異なる。
[[ -t 0 ]]; echo $?
ターミナル経由の場合は0(成立)
cron経由の場合は1(非成立)

以下でやっていることは、、、

function run {
  pinfo "Run command: $*" 1>&2
  if [[ -n ${NO_RUN+set} ]]; then
    [[ -t 0 ]] || cat >/dev/null
  else
    "$@"
  fi
}

 


パイプとファイルディスクリプタの仕組み理解できていないが動作確認した結果では、、、
・run関数がパイプ経由で呼び出された場合、[[ -t 0 ]]は成立しない
・run関数がパイプ経由で呼び出されていない場合、[[ -t 0 ]]は成立する

このあたりの動作は以下URLの情報が関係あるのかな。。。
http://blog.livedoor.jp/cielo_cielo/archives/65111675.html

ひとまず上記より以下の事が判明
NO_RUN環境変数の定義がある場合
・パイプで呼び出された場合、標準入力は/dev/null行き
・パイプで呼び出されてない場合、何も実行されない
→要は何も実行されないし、スクリプト実行したコンソールには何も出力されない。

NO_RUN環境変数の定義がない場合
"$@"が実行される。"$@"は関数の引数全て。このスクリプトではrun関数にはコマンドが引数に指定されている。

・パイプで呼び出された場合、標準入力はrun関数の引数に指定されたteeコマンドの標準入力になる。
・パイプで呼び出されてない場合、引数で指定されたコマンドが実行される。

run関数で色々なコマンドを実行する。コマンド自体に引数を指定する場合、パイプ経由で呼び出す。


[[ ${1-} = @(|0) ]]の意味について

以下のコードがあり

function fml_true_p {
  [[ ${1-} = @(|0) ]] && return 1
  return 0
}

 


まず、"="について。"=="じゃないの?
man bashを見ると、testコマンドの文字列比較では、==だけでなく=も使えて、POSIX準拠するには=の方がよいと記載があり。

string1 == string2
string1 = string2
True if the strings are equal. = should be used with the test command for POSIX conformance.<<

次に${1-}について

${1-}は、以下の様に展開される。
①第一引数がある場合、第一引数の値
②第一引数があるが空文字の場合、空文字
③第一引数がない場合、②と同じ(=空文字)

http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02
${1-}は上記URLのwordが省略された場合

最後に@(|0)について

@(|0)は、man bashによるとシェルのワイルドカードマッチの記述で、man bashGoogle翻訳によると

 いくつかの拡張パターンマッチング演算子が認識されます。以下の説明では、パターンリストは、|で区切られた1つ以上のパターンのリストである。複合パターンは、以下のサブパターンのうちの1つ以上を使用して形成することができる。

 ?(パターンリスト)
 与えられたパターンの0回または1回のオカレンスと一致します。
 *(パターンリスト)
 与えられたパターンの0回以上の出現にマッチします。
 +(パターンリスト)
 与えられたパターンの1つ以上の出現と一致します。
 @(パターンリスト)
 指定されたパターンの1つに一致します。

少し分かりづらい。動作確認したところ以下の通り

①?(PTRN_LIST)の場合

# [[ 1 = ?(1|2|3) ]] && echo  yes
yes
# [[ 0 = ?(1|2|3) ]] && echo  yes
# [[ 10 = ?(1|2|3) ]] && echo  yes
# [[ 13 = ?(1|2|3) ]] && echo  yes
# 

 


②*(PTRN_LIST)の場合

# [[ 1 = *(1|2|3) ]] && echo  yes
yes
# [[ 0 = *(1|2|3) ]] && echo  yes
# [[ 10 = *(1|2|3) ]] && echo  yes
# [[ 13 = *(1|2|3) ]] && echo  yes
yes
# 

 


③+(PTRN_LIST)の場合

# [[ 1 = +(1|2|3) ]] && echo  yes
yes
# [[ 0 = +(1|2|3) ]] && echo  yes
# [[ 10 = +(1|2|3) ]] && echo  yes
# [[ 13 = +(1|2|3) ]] && echo  yes
yes
# 

 


④@(PTRN_LIST)の場合

# [[ 1 = @(1|2|3) ]] && echo  yes
yes
# [[ 0 = @(1|2|3) ]] && echo  yes
# [[ 10 = @(1|2|3) ]] && echo  yes
# [[ 13 = @(1|2|3) ]] && echo  yes
# 

 


上記よりパターン定義した文字列(上記の場合1,2,3)が、何回展開されるかの違い。?は0or1回。@(1|2|3)を展開しても、1文字にしかならない。その為、10や13はマッチしない。*は0回以上、+は1回以上だから13がマッチする。@は1回だけなので13はマッチしない。

補足として以下動作を見るとより分かりやすい。

# [[ 0 = ?(1|2|3)0 ]] && echo  yes
yes
# [[ 0 = *(1|2|3)0 ]] && echo  yes
yes
# [[ 0 = +(1|2|3)0 ]] && echo  yes
# [[ 0 = @(1|2|3)0 ]] && echo  yes
# 

 


まあ、manに書いてある通り。

fml_true_p関数の動きは、以下の通り

第一引数がない時 return 1
第一引数が空文字の時 return 1
第一引数が上記以外の時 return 0

 

${0##*/}の意味について

以下のコードがあり

tmp_dir=$(mktemp -d /tmp/${0##*/}.XXXXXXXX) || pdie "Cannot create temporary directory"

 


これは、ここにも記載した内容
pikesaku.hatenablog.com

要は変数の値に対しパターンを指定して、部分的に切り出す操作。
${0##*/}は、${0}がスクリプト名に展開されるので、スクリプト名から最長一致で/が取り除かれるので、ファイル名だけに展開される。

例)

# cat ./a.sh
#!/bin/bash

echo '${0}     -> ' ${0}
echo '${0##*/} -> ' ${0##*/} 
# sh ./a.sh
${0}     ->  ./a.sh
${0##*/} ->  a.sh
# 

 

mktempコマンド

テンポラリファイルのファイル名を生成するコマンド。

# mktemp /tmp/a.XXXXX
/tmp/a.591zh

引数はテンプレート。X部分はランダム文字列が生成されて割り当てられる。

trapコマンド

以下URLが分かりやすい
シグナルと trap コマンド | UNIX & Linux コマンド・シェルスクリプト リファレンス

シグナルハンドラ。シェルにもあったんだ。。。。

trap 'コマンド' シグナルリスト
例) # trap 'echo finish;exit' INT HUP

${MAILMAN_USER-mailman}の意味。

${VAR-word}。環境変数があったら、そっちを使う。なければデフォルトのmailman使う。

よくよく考えると、このスクリプトはコードを変更しなくても、色々な環境で使えるように考えてくれているんだな。
きめ細かい心遣いに感謝。

shiftコマンド

引数を1つづらす

# cat /tmp/a.sh 
#!/bin/bash

for i in $(seq 1 3)
do
  echo $1; shift
done
# sh /tmp/a.sh a b
a
b

 

mm_url_host="${1-}"; ${1+shift}の意味

引数があったら、その値を変数に入れて、更にshiftを実行する。

# cat /tmp/a.sh                                                                              
#!/bin/bash

a=$1; shift
b=$1; shift
c=${1-};
${1+shift}
d=$1
echo "a is $a"
echo "b is $b"
echo "c is $c"
echo "d is $d"
# sh /tmp/a.sh 1 2 3 4 5                                                                     
a is 1
b is 2
c is 3
d is 4

 

typesetコマンド

色々機能があるようだけど、このスクリプトでは以下オプションを使っている。

・-lオプション
変数の値を小文字にする

# cat /tmp/a.sh 
#!/bin/bash

a="A"
echo $a
typeset -l a
a="A"
echo $a
# sh /tmp/a.sh 
A
a

 


・-uオプション
変数の値を大文字にする

・-Aオプション
associative array(連想配列)を宣言する。

# cat /tmp/a.sh 
#!/bin/bash

typeset -A aa
aa["id"]="bb"
echo ${aa["id"]}
# sh /tmp/a.sh 
bb

 


※展開時は、{}で囲う必要あり。囲わないと展開されない。(RHEL7 bash 4.2.46)

read -rについて

・-rオプションでバックスラッシュを、エスケープ文字として扱わなくなる。

# cat /tmp/a.sh 
#!/bin/bash

echo "# no defined -r"
(echo "a";echo 'b\';echo "c") \
| while read a
do
  echo $a
done

echo "# defined -r"
(echo "a";echo 'b\';echo "c") \
| while read -r a
do
  echo $a
done
# sh /tmp/a.sh 
# no defined -r
a
bc # b\の\が評価された為、echoコマンドが付与する改行コードが無効化され1行になる
# defined -r
a
b\
c

 

以下のsed正規表現の意味

sed \
  -n \
  -e 's/^\$\([A-Za-z][A-Za-z_]*\)[ 	]*=[ 	]*\(.*\);[ 	]*$/\1 \2/p' \
  -e 's/^[ 	]*&*DEFINE_FIELD_FORCED(.\([^"'"'"']*\).[ 	]*,[ 	]*\([^)]*\).*$/\1 \2/p' \
  config.ph \

 


・-nは出力抑制
・-eはスクリプト実行

まず以下の正規表現について

-e 's/^\$\([A-Za-z][A-Za-z_]*\)[      ]*=[    ]*\(.*\);[      ]*$/\1 \2/p'

AAA = "aaa";
上記だと、AAAと"aaa"を出力する。※[]はページ表示されうとスペース1文字に見えるが、ソースコードではスペースとタブ。

次に以下の正規表現について

-e 's/^[      ]*&*DEFINE_FIELD_FORCED(.\([^"'"'"']*\).[       ]*,[    ]*\([^)]*\).*$/\1 \2/p'

 


以下のパターン記述の意味は?

[^"'"'"']

 


上記はダブルクオート・シングルクオートではない文字がマッチする。
sedスクリプトがシングルクオートを囲まれている場合に、スクリプト内でシングルクオートを使うには、上記の記述が必要。

^の後の文字列は以下に分解されて解釈されている。

"

→ダブルクオート1文字の意味

'"'"'

 


→シングルクオート1文字の意味。シェルはシングルクオートで括られた上記をシングルクオート1つに展開する。

例)

# echo ''"'"''                                                                                                                                                          
'
# 

 


以下URLが根拠かと思われる。こんな展開ができるとは。。。

bash - How to escape single-quotes within single-quoted strings? - Stack Overflow
http://sed.sourceforge.net/grabbag/tutorials/sedfaq.txt
quoting - How to echo `single quote` when using single quote to wrap special characters in shell? - Unix & Linux Stack Exchange

シングルクオート内でシングルクオートをリテラルとして使いたい時のテクニック

例)

# echo '"'"'
> ^C →Ctrl+Cで中断。これだと、2個目のダブルクオートが閉じてない為、コマンドが実行されない。
# echo '\''                                                                                                                                    
> ^C →Ctrl+Cで中断。これだと、3個目のシングルクオートが閉じていない為、コマンド実行がされない。シングルクオート内ではバックスラッシュもリテラル扱いになる。
# echo ''"'"''
'

 


で、結局この正規表現

  -e 's/^[ 	]*&*DEFINE_FIELD_FORCED(.\([^"'"'"']*\).[ 	]*,[ 	]*\([^)]*\).*$/\1 \2/p' \
&DEFINE_FIELD_FORCED("reply-to", "$From_address, $MAIL_LIST");

 


上記だと、reply-toと"$From_address, $MAIL_LIST"が出力される。
例)

# echo  '&DEFINE_FIELD_FORCED("reply-to", "$From_address, $MAIL_LIST");' | sed -n -e 's/^[ ]*&*DEFINE_FIELD_FORCED(.\([^"'"'"']*\).[ ]*,[ ]*\([^)]*\).*$/\1/p'
reply-to
# echo  '&DEFINE_FIELD_FORCED("reply-to", "$From_address, $MAIL_LIST");' | sed -n -e 's/^[ ]*&*DEFINE_FIELD_FORCED(.\([^"'"'"']*\).[ ]*,[ ]*\([^)]*\).*$/\2/p'
"$From_address, $MAIL_LIST"
# 

 

cf_value="${cf_value//\\@/@}"の意味

man kshGoogle翻訳では

パラメータを展開し、パターンの最長一致を指定された文字列に置き換えます。文字列中の\ nのそれぞれの出現は、n番目のサブパターンと一致するパラメータの部分に置き換えられます。最初の形式では、patternの最初のオカレンスのみが置き換えられます。 2番目の形式では、patternの各一致が指定された文字列に置き換えられます。 3番目の形式はパターンマッチを文字列の先頭に限定し、4番目の形式はパターンマッチを文字列の末尾に限定します。 stringがヌルの場合、パターンは削除され、文字列の/ inは省略されます。パラメータが@、*、添字@または*を持つ配列変数の場合、各要素に代入演算が交互に適用されます。この場合、単語の文字列部分は各要素について再評価されます。

全体的に分からないうえに、'/'と'//'の違いが分からない。。。。
変数の値に対しパターンマッチした部分を置き換えるよう。
動作確認した結果は以下の通り。

# A='ababab'
# B=${A//ab/AB}
# echo $B
ABABAB
# A='ababab'
# B=${A/ab/AB}
# echo $B
ABabab

 


'/'の場合は最初にマッチした部分だけ置換、'//'は全部!
やっている事は、\@を@へ置き換え。

fml_cf[$cf_name]="${cf_value/\$DOMAINNAME/${fml_cf[DOMAINNAME]-}}"の意味

"${fml_cf[DOMAINNAME]-}"の-は、${A-}と同じ。
fml_cf[DOMAINNAME]が定義済みであれば、その値、未定義or空であれば空文字に置換される。

mm_postid=$(cat seq 2>/dev/null) && let mm_postid++の意味

letは算術展開する。動作は以下の通り。

# let "A=2*3"
# echo $A
6
# let "A++"
# echo $A
7
# let "A+1"
# echo $A
7
# let "A=A+1"
# echo $A
8
# 

 

mm_subject_post_id_fmt="%0${fml_cf[SUBJECT_FORM_LONG_ID]-5}d"の意味

これは、以下の動き

"${fml_cf[SUBJECT_FORM_LONG_ID]"が空文字でなければ、その値。空文字なら5

mm_subject_prefix="${fml_cf[SUBJECT_TAG_TYPE]/ /${fml_cf[BRACKET]} $mm_subject_post_id_fmt}"の意味

これは、以下の動き

# A='(:)'
# B="${A/ /test hoge}"
# echo $B
(:)
# A='(:) '
# B="${A/ /test hoge}"
# echo $B
(:)test hoge
# 

 

以下sedの意味

sed -n \
      -e '1 {h; $ !d}' \
      -e '$ {x; s/\n / /g; p}' \
      -e '/^ / {H; d}' \
      -e '/^ /! {x; s/\n / /g; p}' \

 


参考
Unix Sed Tutorial : 7 Examples for Sed Hold and Pattern Buffer Operations
 

スクリプトの処理内容は以下の通り

1個目のスクリプトの処理
-e '1 {h; $ !d}' \

 


1行目であれば以下の処理をする。
・パターンスペースをホールドスペースにコピー
・最終行でなければパターンスペースを削除

2個目のスクリプトの処理
-e '$ {x; s/\n / /g; p}' \

 


最終行であれば以下の処理をする。
・パターンスペースとホールドスペースを入れ替える。
・改行+スペースをスペースに置換(?)
・パターンスペースを出力

3個目のスクリプトの処理
-e '/^ / {H; d}' \

 


スペースで始まる行の場合、以下の処理をする。
・パターンスペースをホールドスペースに追記
・パターンスペースを削除

4個目のスクリプトの処理
-e '/^ /! {x; s/\n / /g; p}' \

 


スペースで始まらない行の場合、以下の処理をする。
・パターンスペースとホールドスペースを入れ替える。
・改行+スペースをスペースに置換(?)
・パターンスペースを出力

上記の動作を踏まえ、以下の実行結果をトレースしてみる。

# cat ./a
hoge1: inu
hoge2: neko
 kuma
hoge3: saru
# sed -n -e '1 {h; $ !d}' -e '$ {x; s/\n / /g; p}' -e '/^ / {H; d}' -e '/^ /! {x; s/\n / /g; p}' ./d
hoge1: inu
hoge2: neko kuma
hoge3: saru# 

 

①1行目処理

1行目データがパターンスペースに読み込まれる

パターンスペース hoge1: inu
ホールドスペース -

1個目のスクリプト(-e '1 {h; $ !d}')の処理
パターンスペースの内容がホールドスペースにコピーされる。

パターンスペース hoge1: inu
ホールドスペース hoge1: inu

パターンスペースの内容が削除される。

パターンスペース -
ホールドスペース hoge1: inu

2個目のスクリプト(-e '$ {x; s/\n / /g; p}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース -
ホールドスペース hoge1: inu

3個目のスクリプト(-e '/^ / {H; d}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース -
ホールドスペース hoge1: inu

4個目のスクリプト(-e '/^ /! {x; s/\n / /g; p}')の処理
パターンスペースとホールドスペースの内容を置き換える。

パターンスペース hoge1: inu
ホールドスペース -

パターンスペース(hoge1: inu)の内容を出力する。
 

②2行目処理

2行目データがパターンスペースに読み込まれる

パターンスペース hoge2: neko
ホールドスペース -

1個目のスクリプト(-e '1 {h; $ !d}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース hoge2: neko
ホールドスペース -

2個目のスクリプト(-e '$ {x; s/\n / /g; p}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース hoge2: neko
ホールドスペース -

3個目のスクリプト(-e '/^ / {H; d}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース hoge2: neko
ホールドスペース -

4個目のスクリプト(-e '/^ /! {x; s/\n / /g; p}')の処理
パターンスペースとホールドスペースの内容を置き換える。

パターンスペース -
ホールドスペース hoge2: neko

パターンスペース(空)の内容を出力する。
 

③3行目処理

3行目データがパターンスペースに読み込まれる

パターンスペース kuma
ホールドスペース hoge2: neko

1個目のスクリプト(-e '1 {h; $ !d}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース kuma
ホールドスペース hoge2: neko

2個目のスクリプト(-e '$ {x; s/\n / /g; p}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース kuma
ホールドスペース hoge2: neko

3個目のスクリプト(-e '/^ / {H; d}')の処理
パターンスペースの内容をホールドスペースに追記

パターンスペース kuma
ホールドスペース hoge2: neko
kuma

パターンスペースを削除

パターンスペース -
ホールドスペース hoge2: neko
kuma

4個目のスクリプト(-e '/^ /! {x; s/\n / /g; p}')の処理
パターンスペースとホールドスペースを入れ替える

パターンスペース hoge2: neko
kuma
ホールドスペース -

改行+スペースをスペースに置換

パターンスペース hoge2: neko kuma
ホールドスペース -

パターンスペース(hoge2: neko kuma)の内容を出力する。

パターンスペース hoge2: neko kuma
ホールドスペース -

 

④4行目(最終行)処理

4行目データがパターンスペースに読み込まれる

パターンスペース hoge3: saru
ホールドスペース -

1個目のスクリプト(-e '1 {h; $ !d}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース hoge3: saru
ホールドスペース -

2個目のスクリプト(-e '$ {x; s/\n / /g; p}')の処理
パターンスペースとホールドスペースを入れ替える

パターンスペース -
ホールドスペース hoge3: saru

パターンスペース(空)の内容を出力する。

3個目のスクリプト(-e '/^ / {H; d}')の処理
アドレス条件が合致しない為、何も処理されない。

パターンスペース -
ホールドスペース hoge3: saru

4個目のスクリプト(-e '/^ /! {x; s/\n / /g; p}')の処理
パターンスペースとホールドスペースを入れ替える

パターンスペース hoge3: saru
ホールドスペース -

パターンスペース(hoge3: saru)の内容を出力する。

この処理の意味

aliasファイルの同一エントリに改行が含まれる場合を想定していると思われる。

# cat ./a
hoge1: inu
hoge2: neko
 kuma
hoge3: saru
# postalias ./a
# db_dump -p ./a.db                                                                                                                                                     
~省略~
 hoge1\00
 inu\00
 hoge3\00
 saru\00
 hoge2\00
 neko kuma\00
DATA=END
# 

 


※hoge2はnekoとkumaに変換される。

sort -ufについて

-u(unique)オプション

sort | uniqと同じ。

# cat ./a
a
b
c
a
b
c
# sort -u ./a
a
b
c
# 

 

-fオプション

大文字・小文字を問わず。

# cat ./a
a
B
c
A
b
C
# sort -uf ./a
a
B
c
# 

 

|tee >(sed 's/^/INFO: Mailman withlist: /' 1>&2)の意味

動きはこんな感じ

# cat ./a
a
b
c
# cat ./a | tee >(sed 's/^/INFO: Mailman withlist: /' 1>&2) | cat
a
b
c
INFO: Mailman withlist: a
INFO: Mailman withlist: b
INFO: Mailman withlist: c
# cat ./a | tee >(sed 's/^/INFO: Mailman withlist: /' 1>&2) | cat > /dev/null
INFO: Mailman withlist: a
INFO: Mailman withlist: b
INFO: Mailman withlist: c
# 

  


manのteeには、この使い方は記載なし。infoにあった。
どうやら"プロセス置換"という機能のよう。

# info coreutils 'tee invocation'

 


bashのプロセス置換機能を活用して、シェル作業やスクリプト書きを効率化する - 双六工場日誌
Bash のプロセス置換が便利な件 - 理系学生日記

こりゃまた便利な機能だ!!!!
以下のように使える。
①コマンド出力をファイルとして見立ててくれる。

# cat ./a
a
b
c
# cat ./b
a
b
d
# diff -u <(cat ./a) <(cat ./b)
--- /dev/fd/63  2016-12-04 21:30:23.743192617 +0900
+++ /dev/fd/62  2016-12-04 21:30:23.743192617 +0900
@@ -1,3 +1,3 @@
 a
 b
-c
+d
#

 


ファイルの出力結果をファイルに変換してくれている。


②コマンドによるファイルへの入力をコマンドで受ける事ができる。
 ※コマンドをファイルをように見立ててくれる。

# echo "test" | tee ./hoge
test
# cat ./hoge 
test
# echo "test" | tee >(sed 's/test/hoge/') | cat
test
hoge
#

 


teeは引数にファイルを取り、出力をファイルと標準出力にだす。
このファイル部分を">(コマンド)"で置換している。

testの-sオプション

ファイルが存在してサイズが0より大きい場合にマッチ

lsの-fと-Fオプション

-fオプション

manでは以下説明

do not sort, enable -aU, disable -ls --color

以下を有効にして
"-a" .付きファイルを出力
"-U" ソートせずディレクトリオーダー順に出す。

以下を無効にした時と同じ
"-l" ロングフォーマット出力
"-s" サイズを出力
"--color" 出力を色づけ

無駄を省いて高速にする意味だろう。
spool下は大量にファイルがあるから、-Uは効果が大きいかも。
"-a"オプションを付けるとソートが動いている。

# touch ./testdir/{1..10000}                                                                                             
# diff <(ls -a ./testdir/) <(ls -f ./testdir/) | wc -l
2898

 


★早速プロセス置換を利用!こりゃ便利!

-Fオプション

manでは、さらっと以下しか説明がない。

append indicator (one of */=>@|) to entries

Linuxコマンド【 ls 】ファイルとディレクトリのリストを表示 - Linux入門 - Webkaru
上記URLには、以下記載があり。

うお!こりゃまた便利。

# find / -type f | head -1 | xargs -i ls -dF {}
/proc/sysrq-trigger
# find / -type d | head -1 | xargs -i ls -dF {}
//
# find / -type l | head -1 | xargs -i ls -dF {}
/dev/initctl@
# find / -type p | head -1 | xargs -i ls -dF {}
/run/systemd/inhibit/1.ref|
# find / -type f -executable | head -1 | xargs -i ls -dF {}
/run/log/journal/f9370ed252a14f73b014c1301a9b6d1b/system@40f6700c2dd041cfb22614542a9d6998-0000000000000001-000542cc7deaa560.journal*
# 

 

sed 's/^>*From />&/' "spool/$n"は何をやっているか?

# echo  '>>From ' | sed -e 's/^>*From />&/g'
>>>From 
# 

 


リダイレクトが一文字増えた。これは&がパターンマッチした部分に置換される為。

こんな動きも抑えておく。

# echo  '>>From test' | sed -e 's/^>*From />&/g'
>>>From test
#