Unix

最新コメント

UNIX環境向け簡易通知システムの例

wakairo @wakairo

UNIXシステムの既存機構を活用したミニマルな通知システムの実装例です。 複数の通知元から任意の通知メッセージを受信し、それらをシェルプロンプトに表示できます。 個人利用の環境(趣味用途や開発用途など)への導入を想定しています。

  • コードの短さと処理の軽さが特徴です。
  • カスタマイズや環境依存部分の調整を前提としています。利用前にコードに一通り目を通してください。
  • 通知先(メッセージ受信側)のコードはBash用です。Bash以外のシェルではそのシェルに合わせた変更が必要です。

設計思想

本システムの設計の核は「付け加えるものは最小限に、UNIXそのままの仕様と挙動を活かす」ことです。

  • 本システムは、「ファイル」と「シェルのプロンプト更新」という既存機構のみを用いた通知機構です。そのため、常駐プロセスは不要です。
    • 通知状態を「ファイルの存在」に還元することで、通知の生成・可視化・永続化・消費を、仕様と実装の両面で、OSに委ねます。そのため、同期や状態管理のための複雑な機構は不要です。
    • プロンプト表示時にのみ通知を消費します。そのため、常時監視のためのリソース消費と実装複雑性は不要です。

以上の基本思想に合わせ、厳密な順序保証やマルチユーザ間の公平性よりも、単純性・可搬性・挙動の自明さを優先する設計とします。

システム仕様(外部仕様)

1. システム概要

ファイルシステムを介してユーザのシェル(Bash)のプロンプトに通知を表示する軽量な仕組みです。例えば、systemdユニットの実行失敗通知に利用できます。

2. 基本仕様

  • 通知メッセージ: 通知したい内容そのものを「ファイル名」とします(ファイルの中身は空で構いません)。
  • 通信路: /var/lib/notify_inbox/ ディレクトリを使用します。
  • 通知発行: 前述のディレクトリに、touchコマンドなどで、メッセージをファイル名とした空ファイルを作成します。
  • 永続性: メモリ(RAMディスク)ではなくディスクを使用するため、OS再起動後も通知は保持されます。
  • 表示タイミング: シェルプロンプトがユーザに表示される直前のタイミングでチェックされ、プロンプトと同時に表示されます。
  • ライフサイクル: 通知は一度だけ表示され、表示直後に自動削除されます。
  • 重複排除: 同じファイル名(同じメッセージ)はシステム上に1つしか存在できません。
  • 順序: 複数のメッセージがあるときは、アルファベット順(ロケール依存の辞書順)で表示されます。

3. 注意事項・制約

  • ファイル名(通知メッセージ)の制限:
    • メッセージの最大長はファイルシステムのファイル名の最大長(最大バイト数)に依存します。
    • /(スラッシュ)はファイル名に使えません。
    • .(ドット)から始まるファイル名については、通知と削除が行われるかどうかは、Bashのdotglobの設定に依存します。
    • ファイル名にスペースを含めても動作しますが、見やすさのため _- の使用を推奨します。
  • 特殊文字やマルチバイト文字を含む通知メッセージ:
    • 通知メッセージ(ファイル名)に記号やスペースを含む場合、適切なエスケープやクォートが必要です。通知元での記述仕様を確認してください。
    • 通知メッセージに非ASCII文字を使うときには、systemdのユニットファイルなど、メッセージの文字列を含むファイルの文字コードとUNIXシステムのロケールが整合するようにしてください。
    • 通知元と通知先でロケール(言語設定)が異なるせいで文字化けが発生するなど、ロケールによる不具合が起こる場合があります。必要に応じて、メッセージの文字種を問題が起きない文字(ASCII文字など)に限ったり、適切にロケールを設定したり(例: env LC_ALL=ja_JP.UTF-8 touch "/var/lib/notify_inbox/01_メッセージ")してください。
  • 複数のユーザで利用している環境での利用:
    • 通知ディレクトリは全ユーザ読み書き可能(0777)です。システム上の全ユーザが通知を見ることができます。
    • マルチユーザ環境の場合、このプロンプト表示システムを導入したユーザの中で「最初にシェルを操作した人」だけが通知を受け取り、ファイルが消去されます(早い者勝ち)。
  • セキュリティ:
    • この簡易システムは詳細なセキュリティの検討を経たものではありません。セキュリティの確保が必要な環境では使用しないでください。

実装コード一式

Step 1. 共有ディレクトリの設定(管理者権限)

systemdの機能を使って、起動時にディレクトリを作成し、適切な権限を与えます。

作成ファイル: /etc/tmpfiles.d/notify_inbox.conf

# Type Path                   Mode UID  GID  Age Argument
d      /var/lib/notify_inbox  0777 root root -   -
  • 解説: 0777 に設定し、スティッキービット(t)を意図的に付けていません。これにより、rootが作成した通知ファイルを一般ユーザが削除できるようになります。

設定の反映:

sudo systemd-tmpfiles --create

Step 2. 通知先(受信側)の設定(一般ユーザ)

ユーザの .bashrc に、プロンプト表示時に通知をチェックし、 通知があれば表示する処理を追加します。

編集ファイル: ~/.bashrc

# --- 簡易通知システム 受信関数 ---
notify_in_prompt() {
    NOTIFY_PS1_PREFIX=''
    local inbox_dir="/var/lib/notify_inbox"

    # 1. ファイル一覧を取得
    local files=("$inbox_dir"/*)

    # 2. ディレクトリがない、または、ディレクトリがあっても
    #    ファイルがない場合は以後の処理をしない (このことが軽量化のポイント)
    # 該当する場合は、nullglobがオンでもオフでも早期リターンする
    # こちらの方が、shopt -p nullglobで前の状態を保存してevalで戻すよりも高速

    # nullglobがオンで、マッチするファイルがない場合、
    # 配列の要素数が0になるのでここでreturnして以後の処理はしない。
    if [ ${#files[@]} -eq 0 ]; then
        return
    fi

    # nullglobがオフで、マッチするファイルがない場合、
    # 配列の先頭要素に パターン文字列 "/var/lib/notify_inbox/*" がそのまま入る
    # 「そのパスにファイルが存在しない」かつ「文字列がパターンと一致する」ならファイル無し
    if [ ! -e "${files[0]}" ] && [ "${files[0]}" = "$inbox_dir/*" ]; then
        return
    fi    

    # パスを取り除き、ファイル名のみの配列を作成
    local names=("${files[@]##*/}")

    # 複数の通知がある場合、スペース区切りで連結される
    #
    # 区切りをスペースから変更したい場合は、デフォルトの IFS を、
    # local 変数として上書きすることで、結合文字を変更できる
    # ※ "${names[*]}" の展開時に、IFSの先頭の1文字が使われる
    #
    # 例1: 縦棒(パイプ)区切りにしたい場合 "Msg1|Msg2"
    # local IFS='|'
    # 例2: 区切り文字なしにしたい場合 "Msg1Msg2"
    # local IFS=''
    #
    # ここではスペース区切りで連結した上で
    #「赤背景・白文字」で目立たせて表示する
    # 色コード(\e[...m)は \001 と \002 で必ず囲む。
    # (\001 及び \002 は Readline に「非表示文字列」を知らせるマーカー)
    # これを忘れると、Bashがプロンプトの長さを計算できず、
    # 長いコマンド入力時や履歴を遡った時に表示崩れが発生する。
    # またANSI-C Quoting(シングルクォーテーション囲みの前に$を付ける)で
    # バックスラッシュエスケープを解釈させ実バイトを埋め込む。
    NOTIFY_PS1_PREFIX=$'\001\e[41;37m\002 '"${names[*]}"$' \001\e[0m\002'
    # シンプルにそのまま表示する場合
    # NOTIFY_PS1_PREFIX="${names[*]}"

    # 3. ファイルを一括削除 (表示済みとして消費)
    # ユーザがrmをエイリアスで定義していても、そのエイリアスは無視
    # -- を挟むことで - で始まるファイル名があったとしてもオプションとして解釈されない
    command rm -f -- "${files[@]}"
}

# 1. PROMPT_COMMAND に登録
# プロンプトが表示される「直前」に毎回実行され、変数 NOTIFY_PS1_PREFIX を更新する
#
# 注意: コードの複雑化を避け、可読性を優先するため、.bashrcの複数回読み込み(リロード)に対する
# ガード処理は省略しています。必要があれば環境に合わせて書き換えてください。
if [ -z ${PROMPT_COMMAND:+x} ]; then
    # PROMPT_COMMANDが定義されていない、または、空文字列のときは単純にセット
    PROMPT_COMMAND="notify_in_prompt"
elif declare -p PROMPT_COMMAND 2>/dev/null | grep -q '^declare \-[a-z]*a'; then
    # PROMPT_COMMANDが配列の時は先頭要素として追加
    PROMPT_COMMAND=("notify_in_prompt" "${PROMPT_COMMAND[@]}")
else
    # PROMPT_COMMANDが空でない文字列の時はその前に追加
    PROMPT_COMMAND="notify_in_prompt; $PROMPT_COMMAND"
fi

# 2. PS1 の先頭に変数を埋め込み
# 必ず 'シングルクォート' で囲む。
# ダブルクォートだと「読み込み時」に展開されてしまうが、
# シングルクォートなら「表示のたび」にその時点の変数が参照される。
PS1='${NOTIFY_PS1_PREFIX}'"$PS1"

設定の反映:

exec $SHELL -l

Step 3. 通知元(送信側)の設定例

systemdユニットファイルに通知コマンドを仕込みます。

例: バックアップサービスのユニット (/etc/systemd/system/backup.service)

[Unit]
Description=Daily Backup Service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup_script.sh

# --- 通知機能の追加 ---
# 処理が失敗した場合のみ通知ファイルを置く
# ファイル名: "[!Backup_Failed]"
ExecStopPost=/bin/bash -c 'if [ "$EXIT_STATUS" != "0" ]; then touch "/var/lib/notify_inbox/[!Backup_Failed]"; fi'

解説:

  • ExecStopPost: サービス終了時(成功・失敗問わず)に実行されます。
  • $EXIT_STATUS: systemdが終了時に設定する環境変数の一つです。0 以外ならエラーと判断しています。確実な失敗通知には OnFailure= の利用も検討してください。
  • touch "...": ファイルを作るだけです。このファイル名がそのまま通知メッセージになります。

Step 4. 動作確認

実際に通知が表示されるかテストします。

  1. テスト用通知を作成(手動):

    # 任意の場所で実行
    touch "/var/lib/notify_inbox/[!Test_Message]"
    
  2. プロンプトを表示(Enterキーを押す): ターミナルで Enter を押したタイミングで、プロンプトの前に以下のように表示されます。

    [!Test_Message] user@hostname:~$

  3. 消去確認: もう一度 Enter を押し、通知が消えることを確認します。

  4. systemd経由のテスト(失敗シミュレーション): ExecStart=/bin/false と書いたダミーのユニットを作成して systemctl start し、プロンプトにエラーが出るか確認してください。

0
Raw
https://www.techtips.page/ja/comments/1113

【systemd Unitファイル】設定項目の説明が見つからない理由と公式manページの階層構造

wakairo @wakairo

systemdのUnitファイルを読んだり書いたりしていると、 特定の設定項目について、どこに正確な説明があるのか分からず困ることがあります。

実はUnitファイルの設定項目は、ユニット種別ごとに1ページにまとまっているわけではなく、 複数のmanページに階層的に分散して記述されています。

本記事では、この階層構造を踏まえて公式マニュアルの読み方を整理します。

なお、この記事はsystemdのUnitファイルを編集した経験がある人を想定読者としています。

systemd Unitファイルの公式仕様はmanページにある

systemdのUnitファイルの仕様や設定項目は、公式にはmanコマンドで表示されるマニュアルに集約されています。

例えば、Unitファイル全体の共通仕様は次のコマンドで確認できます。

man systemd.unit

Web版は、systemdの開発主体が以下のサイトで公開しています。

https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html

注意:systemdのバージョンによって、ユニット種別や設定項目に違いがあります。 このリンク先は最新バージョンのマニュアルを指しているため、お手元の環境と違いがある場合はmanコマンドを優先してください。

マニュアルの読み方のポイント

systemdのmanページは情報量が多く、設定項目がどのページにあるのか分かりにくい構成になっています。 そこで「設定項目がどの層に属するか」を意識して読むと見通しが良くなります。

マニュアルの「3層構造」

Unitファイルの設定項目などは、概念的には次の3層に分けられます。

  • 第一層:全Unit共通の仕様(systemd.unit)と記法(systemd.syntax)
  • 第二層:複数Unit種別で共有される設定(systemd.exec、systemd.kill、systemd.resource-control)
  • 第三層:Unit種別固有の設定(systemd.service、systemd.timer、systemd.socketなど)

マニュアルの索引:systemd.directives

「設定項目名は分かるが、どのマニュアルに載っているか分からない」ときは、索引を活用します。

man systemd.directivesコマンドで表示される systemd.directivesは索引ページに相当し、 全ての設定項目名と、それが記載されているページが一覧になっています。

このページを開いて項目名で検索するとどのページに説明があるかが分かります。

(Tips)「Additional options…」の誘導を鍵にページをたどる

systemdのマニュアルでは、ページ冒頭付近に

Additional options are listed in …

という文が書かれていることがあります。
これは「このページに載っていない設定項目は、別のどのページに書かれているか」を示す案内です。
設定が見つからない場合は、ページ内でこの一文を検索し、リンクされているページを確認すると効率的です。

各層の代表的なページの概略

第一層:全Unit共通の仕様(systemd.unit)と記法(systemd.syntax)

systemd.unit(共通の仕様)
  • 対象:全てのUnit
  • 主な内容:ユニットの説明文、依存関係、起動・有効化の条件
  • 設定項目の例:Description=Requires=After=ConditionPathExists=
  • 主なセクション:[Unit][Install]

参考:Specifiers(%i%h)についても、systemd.unitのページの当該項目に説明があります。

systemd.syntax(共通の記法)
  • 対象:全てのUnit
  • 主な内容:基本的な構文、エスケープ処理、コメントの書き方

参考:systemd.timerで使う時間関連(48hrWed *-1)の書き方はsystemd.timeのページです。

第二層:複数Unit種別で共有される設定

第二層の設定の特徴は、「記述するセクション」と「説明が書かれているmanページ」が一致しない点です。

例えばUser=Environment=[Service]セクションに記述しますが、 説明はsystemd.serviceではなくsystemd.execのページにあります。

対して、第三層の設定(例:ExecStart=)は、 「記述するセクション([Service])」と「説明ページ(systemd.service)」が一致しています。

注意:以下の第二層の説明において、対象として例示しているユニットはsystemdのバージョンによっては違いがあります。

systemd.exec(実行環境全般)
  • 対象:プロセスの実行に関わるユニット。例:service、socket、swap、mount
  • 主な内容:実行ユーザ、環境変数、標準出力、作業ディレクトリ
  • 設定項目の例:User=Environment=StandardOutput=WorkingDirectory=
systemd.kill(停止時の制御)
  • 対象:プロセスを持つユニット。例:scope、service、socket、swap、mount
  • 主な内容:kill方法、タイムアウト時の挙動
  • 設定項目の例:KillMode=KillSignal=
systemd.resource-control(リソース制御)
  • 対象:リソースを使用するユニット。例:slice、scope、service、socket、swap、mount
  • 主な内容:CPU、メモリ、I/Oなどの制限
  • 設定項目の例:CPUQuota=MemoryMax=IOWeight=TasksMax=

第三層:Unit種別固有の設定

systemd.service(serviceユニット固有)
  • 主な内容:起動方式、再起動、プロセス種別
  • 設定項目の例:ExecStart=Restart=Type=PIDFile=
  • 主なセクション:[Service]
systemd.socket(socketユニット固有)
  • 主な内容:待ち受けストリーム、モード、ユーザ・グループ
  • 設定項目の例:ListenStream=SocketMode=SocketUser=
  • 主なセクション:[Socket]

など

例:serviceユニットの書き方を調べる場合

.serviceファイルについて読み書きする際は、以下の手順でマニュアルを参照します。

全体像を把握するとき

  • 第一層(systemd.unit、systemd.syntax)で全Unitの共通事項を把握する
  • 第二層(systemd.exec、systemd.kill、systemd.resource-control)で利用可能な共通設定を確認する
  • 第三層(systemd.service)でserviceユニット固有の設定を確認する

設定項目名だけ分かっているとき

既存の.serviceを読む場合などで、特定の設定項目について調べたいときは、 systemd.directivesを開いてその設定項目名で検索します。 該当ページが分かれば、そこから詳細な仕様を確認できます。

0
Raw
https://www.techtips.page/ja/comments/1111

man pageが読むのに適したWebサイト

wakairo @wakairo
0
Raw
https://www.techtips.page/ja/comments/1110

一時的な環境変数設定はenvコマンドでも出来る

linux Linx使い @linux

あるコマンドの実行のためだけに一時的に環境変数を設定するには、 bashやzshでは実行したいコマンドの直前で環境変数設定をすればOKです。

具体的には、下例のように、タイムゾーンに対応する環境変数を設定(TZ=UTC)して、dateコマンドを実行することが出来ます。

$ date
Thu Jan 16 21:08:10 JST 2025
$ TZ=UTC date
Thu Jan 16 12:08:13 UTC 2025

ところが、この機能は一部の古いシェルや軽量シェルには備わっていないらしいです。 そのようなシェルでは、シェルに依存しないenvコマンドを以下のように利用して同じことが可能、という小ネタでした。

$ env TZ=UTC date
Thu Jan 16 12:08:35 UTC 2025
0
Raw
https://www.techtips.page/ja/comments/743
❤️2
😄1

コメントを利用してコマンドを再利用する方法

kenicode SatoKen @kenicode

同じコマンドをパパッと使い回す方法として便利なときがあるかもしれませんね。

それから、.bash_historyを後から見たり検索したりするなら、こういうコメントが残っていると実は便利なのかも。

0
Raw
https://www.techtips.page/ja/comments/297

コメントを利用してコマンドを再利用する方法

wakairo @wakairo
最終更新

よく使うコマンドはaliasやシェルスクリプトの形で保存し再利用するのが王道かと思いますが、
「#」」を使ったコメントをコマンドの後ろに付けることで、コマンドを手軽に再利用する方法もあります。

例えば、以下のようにコマンドの後ろにコメントを付けておきます。

cd ~/foo/bar/baz #cfbb

すると今後は、このコマンドをそのまま再実行したいときには、C-rのあと#cfbbと入力してEnterで再実行できます。

ちなみに、コマンド全体が同じではなく、引数などの部分をそのときそのときで変えたい場合には、 先ほどの操作から続けて、C-eで行末に移動し、M-bM-fで単語単位で移動して、M-dC-wで単語単位で削除して、新たな内容を入力、といった形で対応することも出来はします。 もちろん、こういった操作がややこしい感じになるなら、素直にaliasかシェルスクリプトの活用の方が良いのではないかと思いますので、このコメントを利用した方法が活躍する場面は、全く同じコマンドを繰り返す場合だと思います。

0
Raw
https://www.techtips.page/ja/comments/296
💡1

パスワード入力でミスしたらC-uでたいていやり直せる

linux Linx使い @linux
最終更新

C-u、つまり、Ctrlキーを押しながらuキーを押す操作は、Bashやその背後にあるreadlineライブラリで、「カーソルから行頭までの切り取り」に割り当てられている。そのせいなのか、Unixシェルでパスワード入力中に失敗したときに、C-uを押してからパスワードを正しく再入力すれば、処理が通ることが多い、という小ネタです。

参考

unix-line-discard (C-u) ポイントから行頭までをキルします。 キルされたテキストはキルリング (kill-ring) に入ります。

https://ja.manpages.org/bash より

unix-line-discard (C-u) Kill backward from point to the beginning of the line. The killed text is saved on the kill-ring.

https://manpages.org/readline/3 より

2
Raw
https://www.techtips.page/ja/comments/289
😄1
🔄1
🔧1

全選択はC-x hか、もしくは、メニューから

wakairo @wakairo

emacsで全選択するにはC-x h

emacsでは全選択はときどきしかしないため、このキーバインドはなかなか覚えないなと思っていたら、
メニューバーを使うやり方の記事を見つけました。

確かにメニューバーに覚えておいてもらうのも1つの手だなと思いました。

0
Raw
https://www.techtips.page/ja/comments/67

タブを区別して表示する

wakairo @wakairo

ちなみに、タブが混入していたときに、一括して空白に置き換えるにはM-x untabify。 emacsで見ると綺麗にインデントされているが実は空白とタブがグチャグチャになっているようなファイルを空白に統一するときにとても便利です。

参考

https://flex.phys.tohoku.ac.jp/texi/emacs-jp/emacs-jp_104.html

1
Raw
https://www.techtips.page/ja/comments/66

タブを区別して表示する

wakairo @wakairo

ちなみに、インデントで空白を使いタブを使わないのであれば、以下の設定を.emacsに入れます。

(setq-default indent-tabs-mode nil)

なお、この設定をしていてもC-q TABでタブ文字が入力できます。

1
Raw
https://www.techtips.page/ja/comments/65