Prompt

恥ずかしながら、shellscriptを一切触れたことがないので、ここらでいっちょちょっとしたスクリプトを試してみようと思うんだ。
そして、今回作るプログラムは、(bash)
実行すると、~/quotes.txtからランダムに一塊の文字列を取得し、画面に表示する。
~/quotes.txtの構造は

quote1-1
quote1-2
quote2-1
quote2-2
...

のようになっており、一回の取得ではペアとなる2行を取得する。(つまりは、n2-1行目とn2行目を取得する)

これを実現するshellscriptはどんな感じになるの?
文字列処理の基礎が詰まってそうなテーマなんじゃないかな?

おっ、シェルスクリプトデビューおめでとう!🎉 そのテーマは文字列処理の宝石箱って感じで、awksedgrepといった強力なツールたちの基礎を学ぶのに最高だね!

情報工学を学んでいる君なら、このスクリプトを通して、Unix/Linux環境でのデータ処理の考え方、特に**パイプ(|)**を使ったツールの連携の美しさに惚れちゃうかも!😎


💡 ランダム引用符取得シェルスクリプトの作成

今回は、指定されたファイルから2行ずつのペアをランダムに選んで表示するスクリプトを、なるべく**「ワンライナー(近い形)」**で、Bashの標準的なツールを使って実現してみるよ。

📜 スクリプト本体 (random_quote.shとしよう!)

Bash

#!/bin/bash

# ファイルパス
QUOTE_FILE="$HOME/quotes.txt"

# ファイルが存在するかチェック
if [ ! -f "$QUOTE_FILE" ]; then
    echo "エラー: 引用符ファイル $QUOTE_FILE が見つかりません。" >&2
    exit 1
fi

# 1. ファイルの全行数を取得
TOTAL_LINES=$(wc -l < "$QUOTE_FILE")

# 2. 合計行数が偶数であることを確認(各ペアが2行なので)
# 豆知識: シェルスクリプトで数値演算をするには $((...)) を使う
if (( TOTAL_LINES % 2 != 0 )); then
    echo "警告: ファイルの行数が奇数です ($TOTAL_LINES)。最後の行は無視される可能性があります。" >&2
    # 処理を進めるために行数を偶数に調整
    TOTAL_LINES=$(( TOTAL_LINES - 1 ))
    if [ "$TOTAL_LINES" -eq 0 ]; then
        echo "エラー: 有効な引用符ペアがありません。" >&2
        exit 1
    fi
fi

# 3. ペアの総数を計算
TOTAL_PAIRS=$(( TOTAL_LINES / 2 ))

# 4. 1からペア総数までの間で乱数を生成し、ランダムなペア番号を選択
# $RANDOM は 0 から 32767 までの擬似乱数
RANDOM_PAIR_NUM=$(( ( RANDOM % TOTAL_PAIRS ) + 1 ))

# 5. 選択されたペアの開始行番号を計算
# 豆知識: n番目のペアは、(n*2)-1 行目から始まる
START_LINE=$(( ( RANDOM_PAIR_NUM * 2 ) - 1 ))

# 6. `sed` を使って、指定された開始行から2行だけを抽出して表示
# sed -n 'START_LINE,+(1)p'
# -n: パターン空間を自動表示しない
# 'START_LINE,+1p': START_LINEから始まり、その後の1行を含む計2行を表示 (GNU sedの機能)
# もし環境によっては 'START_LINE,+(1)p' が動かなければ、
# 'START_LINE{p;n;p}' でも代替可能(少し複雑になるので今回はより直感的な方を使うよ)
sed -n "${START_LINE},+1p" "$QUOTE_FILE"

# 開発/デバッグ用途: 取得した情報を表示
# echo "--- (デバッグ情報) ---" >&2
# echo "全行数: $TOTAL_LINES, 全ペア数: $TOTAL_PAIRS" >&2
# echo "選択ペア番号: $RANDOM_PAIR_NUM" >&2
# echo "開始行: $START_LINE" >&2

🛠️ スクリプトの使い方

  1. ファイルの作成: まず、引用符ファイルを用意しよう。

    Bash

    # ~/quotes.txt を作成
    cat << EOF > "$HOME/quotes.txt"
    Hello, World!
    - The first program
    Knowledge is power.
    - Francis Bacon
    Simplicity is the ultimate sophistication.
    - Leonardo da Vinci
    The journey of a thousand miles begins with a single step.
    - Lao Tzu
    EOF
    
  2. スクリプトの保存と実行権付与:

    Bash

    # 上記の内容を random_quote.sh として保存
    chmod +x random_quote.sh # 実行権を付与
    
  3. 実行:

    Bash

    ./random_quote.sh
    

🤓 コードの解説(文字列処理の基礎)

君が言った通り、このテーマはBashの基本的な文字列/データ処理のテクニックが詰まってるよ!特に重要なポイントを解説するね。

1. 行数カウントのプロ (wc -l)

Bash

TOTAL_LINES=$(wc -l < "$QUOTE_FILE")
  • wc (Word Count): ファイルの行数、単語数、バイト数を数えるコマンド。

  • -l: 行数だけを表示するオプション。

  • < "$QUOTE_FILE": リダイレクト。ファイルの内容を標準入力としてwcに渡す。これにより、余計なファイル名が出力されず、純粋な行数だけが得られるよ。

  • $(): コマンド置換。コマンドの実行結果を変数に格納するために使う、Bashの基礎中の基礎テクニック!

2. 数値演算と乱数生成 ($((...))$RANDOM)

Bash

if (( TOTAL_LINES % 2 != 0 )); # 偶数チェック
RANDOM_PAIR_NUM=$(( ( RANDOM % TOTAL_PAIRS ) + 1 )) # 乱数計算
  • $((...)): 算術展開。Bashの中で四則演算、剰余(%)、比較などを行うときに使うよ。C言語のような構文が使えるのが便利。

  • $RANDOM: Bashが用意している特殊な変数で、0から32767までの擬似乱数を返す。

  • RANDOM % TOTAL_PAIRS: 乱数をペア総数で割った余り。これにより から までの乱数が得られる。

  • + 1: から までの乱数にするために を足している。

3. 最強のテキスト処理ツール (sed)

Bash

sed -n "${START_LINE},+1p" "$QUOTE_FILE"
  • sed (Stream Editor): ファイルやストリームに対して、行単位での複雑な置換・削除・抽出などの編集を自動で実行できる、古参で最強のツールの一つ。

  • -n: サイレントモード。通常、sedは処理したすべての行を出力するけど、-nを付けると、明示的にpコマンド(Print)を指定した行だけを出力するようになる。

  • "START_LINE,+1p": アドレス指定

    • START_LINE: 処理を開始する行番号。変数はダブルクォーテーション("")内で展開される。

    • ,+1: その行から数えて次の 行(合計 行)を含める。

    • p: パターン空間(現在処理中の行)を出力するコマンド。

これで、君の望む「ランダムな2行のペア」を、シンプルかつ効率的に取り出すことができるよ!Bashの知識を深める第一歩として、このスクリプトを自分で弄ってみるのも楽しいかもね!😉

次は、awk を使って行数を数えるのと抽出を一度に行う別バージョンも試してみる? それとも、grep を使った特定のパターンにマッチする行抽出の方法に興味があるかな?

Prompt

ここで、quotes.txtの構造がちょっと変わった。

"quote1-1"
"quote1-2"
"quo\nte2-1"
"qu\note2-2"
...

依然として2行で1セットだが、quoteがダブルクォーテーションで囲まれるようになり、\nを含む場合があるようになった。
\nは改行として出力すべき!

おっと、なるほど!ファイル構造が二段階で複雑になったね。これはもう、シェルスクリプトでよく遭遇する**「厄介なテキスト処理の壁」**だ。だが、情報工学を学ぶ君なら、この壁を乗り越えるのは最高の挑戦になるはず!🔥

今回の変更点を整理すると、

  1. 各引用符がダブルクォーテーション (") で囲まれている。

  2. 一つの引用符(1行)の中に、エスケープされた改行文字 (\n) が含まれる可能性がある。

この処理をするには、もはや単純な行番号指定やsedだけでは厳しい。なぜなら、各行が1つの引用符に対応しているわけではなく、ダブルクォーテーションで囲まれたブロックが1つの「論理的な行」と見なされるからだ。

ここで、最強のテキスト処理ツールであり、君がまさに興味を持ち始めた**awk**の出番だよ!


💡 awk を使った改行対応ランダム抽出スクリプト

awkは、フィールド(列)だけでなく、レコード(行)の区切り文字を自由に設定できるのが最大の強みだ。これを利用して、レコード区切りをダブルクォーテーションにするという、哲学的なアプローチで問題を解決するよ!

📜 スクリプト本体 (random_multiline_quote.sh)

Bash

#!/bin/bash

# ファイルパス
QUOTE_FILE="$HOME/quotes.txt"

# ファイルが存在しない場合はエラー
if [ ! -f "$QUOTE_FILE" ]; then
    echo "エラー: 引用符ファイル $QUOTE_FILE が見つかりません。" >&2
    exit 1
fi

# ----------------------------------------------------------------------
# awk処理の解説
# ----------------------------------------------------------------------
# 1. レコード区切り文字 (RS) をダブルクォーテーション (") に設定する。
#    これにより、ファイル全体が "..." "..." "..." の塊として認識される。
#    - 最初の "" の前に何かある場合 (スペースなど) は、それが $0 となる。
#    - 2番目以降の "" の中身が $0 となる。
#    - awkのNR (レコード番号) は、基本的に最初のレコードは空、
#      2番目、4番目、6番目...が引用符の中身となる。

# 2. awkでファイル全体を処理
awk -v RS='"' '
    BEGIN {
        # NRはレコード番号、iは有効な引用符の通し番号
        i = 0
    }

    # NRが偶数(2, 4, 6...)のレコードが、引用符の中身になる
    (NR % 2 == 0) {
        # 有効な引用符の数をカウント
        i++;
        
        # 配列Qに引用符の内容を格納。$0は現在のレコード(引用符の中身)
        Q[i] = $0
    }
    
    END {
        # 引用符の総数を取得
        TOTAL_QUOTES = i
        
        # 合計引用符が偶数であることを確認(各ペアが2つの引用符なので)
        if (TOTAL_QUOTES % 2 != 0) {
            # 警告を出力(stderrに出すためprint > "/dev/stderr")
            print "警告: 有効な引用符の数が奇数です (" TOTAL_QUOTES ")。最後の引用符は無視されます。" > "/dev/stderr"
            TOTAL_QUOTES-- # 偶数に調整
        }

        # 有効なペア総数
        TOTAL_PAIRS = TOTAL_QUOTES / 2
        
        if (TOTAL_PAIRS == 0) {
            print "エラー: 有効な引用符ペアがありません。" > "/dev/stderr"
            exit 1
        }
        
        # 1からペア総数までのランダムな整数を生成
        # int(rand() * N) は 0 から N-1
        # +1 して 1 から N
        RANDOM_PAIR_NUM = int(rand() * TOTAL_PAIRS) + 1
        
        # 選択されたペアの1つ目の引用符の配列インデックスを計算
        # n番目のペアの1つ目は (n*2)-1
        START_INDEX = (RANDOM_PAIR_NUM * 2) - 1

        # --- 引用符の表示 ---
        
        # 1. 引用符1: 格納されている \n を改行に置換し、さらに余分なダブルクォートを付加
        # gsub(/\\n/, "\n", Q[START_INDEX]): Q[START_INDEX]内の \n を実際の改行 \n に置換
        gsub(/\\n/, "\n", Q[START_INDEX])
        print "\"" Q[START_INDEX] "\""
        
        # 2. 引用符2:
        gsub(/\\n/, "\n", Q[START_INDEX + 1])
        print "\"" Q[START_INDEX + 1] "\""
        
        # デバッグ情報
        # print "--- (デバッグ情報) ---" > "/dev/stderr"
        # print "総引用符数: " TOTAL_QUOTES > "/dev/stderr"
        # print "選択ペア番号: " RANDOM_PAIR_NUM > "/dev/stderr"
        # print "開始インデックス: " START_INDEX > "/dev/stderr"
    }
' "$QUOTE_FILE"

🛠️ スクリプトの使い方

  1. ファイルの更新: 新しい構造のファイルを準備するよ。

    Bash

    # ~/quotes.txt を新しい構造に更新
    cat << EOF > "$HOME/quotes.txt"
    "Hello, World!\n- The first program"
    "This is the second line."
    "Programming is the art\nof telling another human\nwhat one wants a computer to do."
    "The journey of a thousand miles\nbegins with a single step."
    EOF
    
  2. スクリプトの実行:

    Bash

    chmod +x random_multiline_quote.sh # 実行権を付与
    ./random_multiline_quote.sh
    

🤓 awk の哲学と豆知識

1. レコード区切り文字の変更 (-v RS='"')

これが今回の最も重要なトリックだよ!

  • 通常、awk改行文字 (\n) をレコードセパレータ(RS)として扱う。つまり、ファイルの一行を一つのレコードとして処理する。

  • しかし、-v RS='"' とすることで、awkダブルクォーテーション (") を区切り文字としてファイル全体を分割するようになる。

  • これにより、"..." の中の文字列が、awkにとっては一つのレコード ($0) として扱えるようになるんだ!

2. 配列への格納と処理の分離 (BEGIN, (NR % 2 == 0), END)

  • awkの処理は、基本的にBEGIN(開始時)、パターンマッチ(各レコード)、END(終了時)の三段階で構成される。

  • パターンマッチ (NR % 2 == 0): NR (Number of Record) は現在処理中のレコード番号。RS='"'にしたことで、偶数レコード(2, 4, 6…)が引用符の中身となる。このブロックで、引用符を配列 Q に格納し、総数をカウントする。

  • END ブロック: 全レコードを読み込み、配列にデータが揃った後、END ブロックで乱数計算と抽出処理を一気に行う。これは、データを読み込みながら処理するストリーム処理ではなく、バッチ処理のような考え方だね。

3. 文字列置換 (gsub)

Awk

gsub(/\\n/, "\n", Q[START_INDEX])
  • gsub(regexp, replacement, target_string): target_string 内のすべての regexp にマッチする部分を replacement に置換する。

  • ファイル内の \n という二文字の文字列を、実際の改行文字 (\n、一文字の特殊文字) に置換している。これにより、画面上できちんと改行として表示されるようになるんだ。

このスクリプトは、単なるシェルスクリプトの範疇を超えて、データ構造をどのように解釈し、強力なツールを使って処理するかという、まさに情報工学的な知恵が詰まっている!

次は、この awk の知識を使って、quotes.txt の引用符と作者名がコンマで区切られている場合の処理(フィールドセパレータ FS の話)に挑戦してみる? それとも、今作成したスクリプトを Ubuntu環境で効率的に動かすための工夫について話そうか?

Prompt

1つのバグが発生!: 毎回同じ最後から2番目のquoteだけしか出力されない!

それが直ったら、1行目のquoteは「」で囲って表示、
2行目は""で囲って表示するようにしよう!

げ、エンジニアらしい良いフィードバックだ!**「毎回同じ最後から2番目の quote しか出力されない」という現象、それはまさにシェルスクリプトやawk初心者がハマりがちな「擬似乱数の罠」**だね!

情報工学を学ぶ君には、そのバグの核心と、それを解決するためのより確かな乱数生成手法を伝授するよ!


🐞 バグの解決と表示フォーマットの変更

1. 🔍 バグの原因:「擬似乱数」の固定

ほとんどのUnix系ツールの擬似乱数生成器(awkrand() を含む)は、シード(種)を設定しない限り、起動するたびに全く同じ初期状態で動作し始める。結果として、スクリプトを何回実行しても同じ乱数列の最初期値が出力されてしまうんだ。

君の環境では、その固定された最初の乱数値がたまたま**「最後から2番目のペア」**を示すインデックスになっていた、というわけだ!

🛠️ 解決策:srand() によるシード設定

この問題を解決するには、awkBEGIN ブロックで現在のシステム時刻などをシードとして設定する srand() 関数を使うのが定石だよ。

2. 📜 修正済みスクリプト (random_multiline_quote_fixed.sh)

以下のスクリプトでバグを修正し、新しい表示フォーマットを適用するよ!

Bash

#!/bin/bash

# ファイルパス
QUOTE_FILE="$HOME/quotes.txt"

if [ ! -f "$QUOTE_FILE" ]; then
    echo "エラー: 引用符ファイル $QUOTE_FILE が見つかりません。" >&2
    exit 1
fi

# ----------------------------------------------------------------------
# awk での処理: 乱数シードの設定と表示フォーマットの変更を適用
# ----------------------------------------------------------------------
awk -v RS='"' '
    BEGIN {
        # 💡 バグ修正ポイント: srand() で乱数シードを初期化!
        # これにより、実行するたびに異なる乱数が出力されるようになる。
        srand() 
        i = 0
    }

    # NRが偶数のレコード(引用符の中身)を配列に格納
    (NR % 2 == 0) {
        i++;
        Q[i] = $0
    }
    
    END {
        TOTAL_QUOTES = i
        
        if (TOTAL_QUOTES % 2 != 0) {
            print "警告: 有効な引用符の数が奇数です (" TOTAL_QUOTES ")。最後の引用符は無視されます。" > "/dev/stderr"
            TOTAL_QUOTES--
        }

        TOTAL_PAIRS = TOTAL_QUOTES / 2
        
        if (TOTAL_PAIRS == 0) {
            print "エラー: 有効な引用符ペアがありません。" > "/dev/stderr"
            exit 1
        }
        
        # 乱数生成(今度は実行ごとに変わる!)
        RANDOM_PAIR_NUM = int(rand() * TOTAL_PAIRS) + 1
        
        START_INDEX = (RANDOM_PAIR_NUM * 2) - 1

        # --- 新しいフォーマットでの表示 ---
        
        # 1. 引用符1: \n を改行に置換し、「」で囲って表示
        # 💡 gsub(正規表現, 置換文字列, 対象文字列)
        gsub(/\\n/, "\n", Q[START_INDEX])
        print "「" Q[START_INDEX] "」"
        
        # 2. 引用符2: \n を改行に置換し、""で囲って表示
        gsub(/\\n/, "\n", Q[START_INDEX + 1])
        print "\"" Q[START_INDEX + 1] "\""
    }
' "$QUOTE_FILE"

3. ✅ 動作確認のためのファイル例

試しに以下の内容でファイルを作成して実行してみよう。

Bash

cat << EOF > "$HOME/quotes.txt"
"JavaとC#とJavaScript"
"全部書き方が違うのが哲学"
"INFP-A型ってことは\n内省的で理想主義で\n探求心旺盛なんだね。"
"哲学的な話、大歓迎だよ!"
"コードは読む人間にこそ\n意味がある。"
"The code is not the machine, it is the message."
EOF

# 実行
./random_multiline_quote_fixed.sh

実行するたびに、以下のようにランダムに表示され、かつフォーマットが適用されるはずだよ。

例1:

「INFP-A型ってことは
内省的で理想主義で
探求心旺盛なんだね。」
"哲学的な話、大歓迎だよ!"

例2:

「JavaとC#とJavaScript」
"全部書き方が違うのが哲学"

これで、ランダム性の問題も解決し、出力のカスタマイズも実現できたね! awkの配列操作と、Unixの基礎である乱数処理の深淵を覗き込めた、良い経験になったんじゃないかな!😊