1章:シェルスクリプトの世界へようこそ
新しいプログラミング言語を学ぶとき、まるで未開のジャングルに足を踏み入れたようなワクワク感が広がります。プログラミング言語の中でも、シェルスクリプトは魔法のようにコマンドと共に動作し、時には少し奇妙なテキストの世界へと導いてくれます。ディレクトリの中を歩き回り、ファイルと会話し、時にはエラーメッセージと格闘しながら、シェルスクリプトの世界を冒険しましょう! いきなり茶番に付き合ってもらってありがとうございます。はじめましてjackのもちくんです。 突然なんですが、みなさんシェルスクリプト(ShellScript)書いてますか?体感なんですが、コマンドやShellScriptを避けている人が一定数いる気がします。そんな人に「ShellScriptだけでこんなことまでできるんだ。意外と使える場面多くね。」と感じてもらえれば幸いです。 ※基本的にbashベースで進めるので、zshやfishとの細かい差異は適宜読み替えてください ※コマンドのオプションについての説明は省きます。 (コマンドオプションに関してはGoogle先生よりChatGPT先生の方が優秀です。)
2章:あなた誰?
そもそもShellScriptってなんやねん! Wikipediaを見てみましょう。 Wikipedia引用
A shell script is a computer program designed to be run by a Unix shell, a command-line interpreter.[1] The various dialects of shell scripts are considered to be scripting languages. Typical operations performed by shell scripts include file manipulation, program execution, and printing text. A script which sets up the environment, runs the program, and does any necessary cleanup or logging, is called a wrapper.
つまり
- シェルスクリプトはUnixシェルで動作するよう設計されたコンピュータプログラム
- 典型的な操作は、ファイル操作、プログラム実行、テキスト出力など
簡単に言うと、普段実行するcd、ls、touchみたいなコマンドを並べたものです。 リダイレクトやパイプライン処理を組み合わせて複雑な処理を行うこともできます。
3章:あなた何ができるの
わりとなんでもできます。まずはちょっと面白い系を紹介します。
- matrix.sh → ターミナルにmatrix風に文字列を表示
- fireworks.sh → ターミナルで花火が見れる
- cursor.sh → ターミナル上でカーソルが反射する
matrix.shの中身はこんな感じ
#!/bin/bash
echo -e "\e[1;40m"
clear
while :
do
echo $LINES $COLUMNS $(( $RANDOM % $COLUMNS)) $(( $RANDOM % 72 ))
sleep 0.05
done | gawk '
{
letters="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()";
c=$4;
letter=substr(letters,c,1);
a[$3]=0;
for (x in a)
{
o=a[x];a[x]=a[x]+1;
printf "\033[%s;%sH\033[2;32m%s",o,x,letter;
printf "\033[%s;%sH\033[1;37m%s\033[0;0H",a[x],x,letter;
if (a[x] >= $1)
{
a[x]=0;
}
}
}
'
実行結果はこんな感じ。友達に見せてかっこいいと言ってもらいましょう!!
4章:案外使えるやん
ターミナルでちょっと洒落た遊びができることはわかったけど、実用的な使い方あるの? あります!今回はShellScriptでWebサイトをスクレイピングして、欲しい情報をSlackに通知するアプリを作ってみましょう。作りながら考えてたことをつらつら書いたので少し長くなってしまいましたがご容赦ください。それでは行きます。
4.1 Webサイトのソースを取得する
まずはWebサイトのソースを取得します。 今回はIT関連のニュースを知るためにnews.yahoo.co.jp/categories/itのニュースを取得します。 Webサイトのソースを取得するのにcurlコマンドを使います。
$ curl -Ls https://news.yahoo.co.jp/categories/it
formatされてないHTMLが表示されるはずです。
4.2 HTMLをParseする
PythonやJSでhtmlをパースして、欲しい情報を取得することができますが、今回はコマンドでやっていきましょう。 html parse command で検索するとgo言語で開発されたpupというものを見つけました。これを使います。外部ライブラリに頼るなと言われそうですが一旦無視します。 まずは環境を整えます。以下にならって実行します。
$ sudo apt update
$ sudo apt install -y golang-go
(いろいろ出力される)
$ go version
go version go1.18.1 linux/amd64
$ go install github.com/ericchiang/pup@latest
(いろいろ出力される)
$ $HOME/go/bin/pup --version
0.4.0
次にインストールしたパーサーの実力を見てみましょう。 Documentを見てみるとCSS Selectorみたいに使えるみたい。 sectionタグにidがついてるのでこれを取得してみます。
$ curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup "section#uamods-topics"
<section class="sc-lbpTpA cDHbWL topics" id="uamods-topics">
<h2 class="sc-cnqyBG QeUDQ">
トピックス
</h2>
...(省略)
</div>
<p class="sc-hBovCB cEBBjy">
<a href="https://news.yahoo.co.jp/pickup/6483735" data-cl-params="_cl_vmodule:tpc_it;_cl_link:img;" class="sc-cjhlnM iSOnrC">
<span style="background-image:url(https://news-pctr.c.yimg.jp/t/news-topics/images/tpc/2023/12/3/8f21a39a501c7cad6c97fdfe66714d60dc96fb0259fc177d0066728b5d152f2d.jpg?w=440&h=440&pri=l&up=0)" class="sc-lOIgb dra-DRB">
</span>
国家資格 申請手続きデジタル化へ
</a>
</p>
</div>
</section>
おお!うまく動いてる!! 欲しい情報はタイトルとリンクなので、この2つを取得してみる。
$ curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup "section#uamods-topics > div > div > div > ul > li > a"
<a href="https://news.yahoo.co.jp/pickup/6483735" data-cl-params="_cl_vmodule:tpc_it;_cl_link:title;_cl_position:1;" data-ual-gotocontent="true" class="sc-dtLLSn dpehyt">
国家資格 申請手続きデジタル化へ
...(省略)
<a href="https://news.yahoo.co.jp/pickup/6483558" data-cl-params="_cl_vmodule:tpc_it;_cl_link:title;_cl_position:8;" data-ual-gotocontent="true" class="sc-dtLLSn dpehyt">
「honto」紙書籍の通販終了へ
</a>
$ curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup "section#uamods-topics > div > div > div > ul > li > a text{}"
国家資格 申請手続きデジタル化へ
政府運営サイトに誤記 500件超
広大病院のシステム障害が復旧
生成AI「国際指針」G7が閣僚声明
子の性的画像 AIと実物の区別困難
全銀ネット障害 補償対象は8000件
あんスタ運営 画像の加工巡り提訴
「honto」紙書籍の通販終了へ
$ curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup "section#uamods-topics > div > div > div > ul > li > attr{href}"
https://news.yahoo.co.jp/pickup/6483735
https://news.yahoo.co.jp/pickup/6483689
https://news.yahoo.co.jp/pickup/6483658
https://news.yahoo.co.jp/pickup/6483618
https://news.yahoo.co.jp/pickup/6483607
https://news.yahoo.co.jp/pickup/6483573
https://news.yahoo.co.jp/pickup/6483569
https://news.yahoo.co.jp/pickup/6483558
思ったより簡単に取得できたぞ!!(なんだかんだ沼ると思ってた)
4.3 Slackになにかメッセージを送る
slack通はWebhookにひっかけて通知します。 (Webhook URLの取得方法、SlackBotの導入方法については省略します。公式サイトはわかりにくいので注意しましょう。)
$ curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Hello, World!"}' \
$WEBHOOK_URL
ok
$
ターミナルにokが表示されて、ちゃんとSlackにも通知されました!
4.4 整理する
ここまでやったことを一つのファイルにまとめてみましょう。 以下 news_notifier.shというファイルを編集していきます。
#!/bin/bash
SLACK_WEBHOOK_URL="取得したWEBHOOK_URL"
curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a text{}'
sleep 1
curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a attr{href}'
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Hello, World!"}' "$SLACK_WEBHOOK_URL"
実行権限を与えて実行します。
$ chmod +x news_notifier.sh
$ ./news_notifier.sh
スマホゲーム利用 中学生で急増か
国家資格 申請手続きデジタル化へ
政府運営サイトに誤記 500件超
広大病院のシステム障害が復旧
生成AI「国際指針」G7が閣僚声明
子の性的画像 AIと実物の区別困難
全銀ネット障害 補償対象は8000件
あんスタ運営 画像の加工巡り提訴
https://news.yahoo.co.jp/pickup/6483806
https://news.yahoo.co.jp/pickup/6483735
https://news.yahoo.co.jp/pickup/6483689
https://news.yahoo.co.jp/pickup/6483658
https://news.yahoo.co.jp/pickup/6483618
https://news.yahoo.co.jp/pickup/6483607
https://news.yahoo.co.jp/pickup/6483573
https://news.yahoo.co.jp/pickup/6483569
ok
$
ニュースのタイトルとリンクがターミナルに表示されて、SlackにもHello, World!と通知が届きました。 できれば2回アクセスしたくないので、結果を変数に保存しておく。
#!/bin/bash
SLACK_WEBHOOK_URL="取得したWEBHOOK_URL"
tmp=$(curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a')
echo $tmp | $HOME/go/bin/pup "a text{}"
echo $tmp | $HOME/go/bin/pup "a attr{href}"
# curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Hello, World!"}' "$SLACK_WEBHOOK_URL"
$ ./news_notifier.sh
スマホゲーム利用 中学生で急増か
国家資格 申請手続きデジタル化へ
政府運営サイトに誤記 500件超
広大病院のシステム障害が復旧
生成AI「国際指針」G7が閣僚声明
子の性的画像 AIと実物の区別困難
全銀ネット障害 補償対象は8000件
あんスタ運営 画像の加工巡り提訴
https://news.yahoo.co.jp/pickup/6483806
https://news.yahoo.co.jp/pickup/6483735
https://news.yahoo.co.jp/pickup/6483689
https://news.yahoo.co.jp/pickup/6483658
https://news.yahoo.co.jp/pickup/6483618
https://news.yahoo.co.jp/pickup/6483607
https://news.yahoo.co.jp/pickup/6483573
https://news.yahoo.co.jp/pickup/6483569
ok
$
うん、変な改行がいっぱいだね。困ったなあと思ってpupにいい感じのがないかなと探してるとjsonにformatできるみたい!jsonにformatできるなら、jqコマンド使えるしjsonにしてみようか。
4.5 使い慣れたコマンドに助けてもらう
頑張れば、pupでもできそうだけど、わざわざソースコードを読む気にもなれないので、jsonに変換して、そこからjqコマンドを使う方針に変更。
$ curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a json{}'
[
{
"children": [
{
"children": [
{
"aria-label": "NEW",
"class": "sc-eirqVv XiyIJ",
"role": "img",
"tag": "span",
"type": "NEW"
}
],
"class": "sc-WZYut jLeSfx",
"tag": "span"
}
],
"class": "sc-dtLLSn dpehyt",
"data-cl-params": "_cl_vmodule:tpc_it;_cl_link:title;_cl_position:1;",
"data-ual-gotocontent": "true",
"href": "https://news.yahoo.co.jp/pickup/6483806",
"tag": "a",
"text": "スマホゲーム利用 中学生で急増か"
},
...(省略)
{
"class": "sc-dtLLSn dpehyt",
"data-cl-params": "_cl_vmodule:tpc_it;_cl_link:title;_cl_position:8;",
"data-ual-gotocontent": "true",
"href": "https://news.yahoo.co.jp/pickup/6483569",
"tag": "a",
"text": "あんスタ運営 画像の加工巡り提訴"
}
]
$
いいね。ここまでくれば、jqコマンドで頑張れそうだな。 どんどんいくぞ。jqコマンドでhrefとtextのフィールドを取り出してみる。
$ curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a json{}' \
| jq ".[].text"
"スマホゲーム利用 中学生で急増か"
"国家資格 申請手続きデジタル化へ"
"政府運営サイトに誤記 500件超"
"広大病院のシステム障害が復旧"
"生成AI「国際指針」G7が閣僚声明"
"子の性的画像 AIと実物の区別困難"
"全銀ネット障害 補償対象は8000件"
"あんスタ運営 画像の加工巡り提訴"
$
$ curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a json{}' \
| jq ".[].href"
"https://news.yahoo.co.jp/pickup/6483806"
"https://news.yahoo.co.jp/pickup/6483735"
"https://news.yahoo.co.jp/pickup/6483689"
"https://news.yahoo.co.jp/pickup/6483658"
"https://news.yahoo.co.jp/pickup/6483618"
"https://news.yahoo.co.jp/pickup/6483607"
"https://news.yahoo.co.jp/pickup/6483573"
"https://news.yahoo.co.jp/pickup/6483569"
$
$
これでニュースタイトルとそのリンクが取得できたので、news_notifier.shを変更しましょう。
#!/bin/bash
SLACK_WEBHOOK_URL="取得したWEBHOOK_URL"
tmp_json=$(curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a json{}')
titles=$(echo $tmp_json | jq ".[].text")
links=$(echo $tmp_json | jq ".[].href}")
echo $titles
echo $links
$ ./news_notifier.sh
"スマホゲーム利用 中学生で急増か" "国家資格 申請手続きデジタル化へ" "政府運営サイトに誤記 500件超" "広大病院のシステム障害が復旧" "生成AI「国際指針」G7が閣僚声明" "子の性的画像 AIと実物の区別困難" "全銀ネット障害 補償対象は8000件" "あんスタ運営 画像の加工巡り提訴"
"https://news.yahoo.co.jp/pickup/6483806" "https://news.yahoo.co.jp/pickup/6483735" "https://news.yahoo.co.jp/pickup/6483689" "https://news.yahoo.co.jp/pickup/6483658" "https://news.yahoo.co.jp/pickup/6483618" "https://news.yahoo.co.jp/pickup/6483607" "https://news.yahoo.co.jp/pickup/6483573" "https://news.yahoo.co.jp/pickup/6483569"
ok
$
あら、一行になっちゃった。しかもダブルクウォートで囲まれた文字列内に空白文字がある… このパターンはシェルスクリプト始めたばかりだとどう処理していいかわからないが(zshとかfishなら上手な扱いができるのあも知れないけど自分は知らないし)、いつも正規表現に頼ってるので今回もそうします。
$ curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a json{}' \
| jq ".[].text" \
| grep -o '"[^"]*"' \
| sed 's/"//g'
スマホゲーム利用 中学生で急増か
国家資格 申請手続きデジタル化へ
政府運営サイトに誤記 500件超
広大病院のシステム障害が復旧
生成AI「国際指針」G7が閣僚声明
子の性的画像 AIと実物の区別困難
全銀ネット障害 補償対象は8000件
あんスタ運営 画像の加工巡り提訴
$
これで一行ずつ扱えるね。次は繰り返し処理をするプログラムを書こう。
4.6 繰り返し処理
プログラミングといえば、これは外せないですね。そう繰り返し処理です。 もちろんShellScriptにも繰り返し処理はあってwhile文で書けます。以下繰り返し処理を追加したコードです。
#!/bin/bash
SLACK_WEBHOOK_URL="取得したWEBHOOK_URL"
tmp_json=$(curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a json{}')
titles_raw=$(echo $tmp_json | jq ".[].text")
links_raw=$(echo $tmp_json | jq ".[].href}")
# ここ大きくプログラム追加
i=0
while IFS= read -r title; do
i=`expr $i + 1`
echo $i : $title
done < <(echo $titles_raw | grep -o '"[^"]*"' | sed 's/"//g')
# curl -X POST -H 'Content-type: application/json' \
# --data '{"text":"Hello, World!"}' "$SLACK_WEBHOOK_URL"
$ ./news_notifier.sh
1 : スマホゲーム利用 中学生で急増か
2 : 国家資格 申請手続きデジタル化へ
3 : 政府運営サイトに誤記 500件超
4 : 広大病院のシステム障害が復旧
5 : 生成AI「国際指針」G7が閣僚声明
6 : 子の性的画像 AIと実物の区別困難
7 : 全銀ネット障害 補償対象は8000件
8 : あんスタ運営 画像の加工巡り提訴
$
すこし解説。
IFS= read -r title
読み込み。これはreadを使います。Pythonで言うならinput関数みたいなものです。読み込んだ値をtitleという変数に格納します。IFS= という意味の分からないものは区切り文字です。IFS=’,’ とすればカンマで区切って入力を受け付けます。ここでは何も指定していないので、行全体を読み込みます。
while ...; do
# なんか処理
done < <(...)
これ、意外と知らない人いるんですが、便利なので覚えてください。詳しく知りたい人はProcess Substitution で調べてください。まあ、while文に対して前からリダイレクトするとサブシェルがどうとかでwhile文の中の処理がスコープの外側に影響しなくて困ること多々なのでwhile文は後ろから入力をあげるくらいに思っていていいと思います。
4.7 ラストスパート
書くのも疲れたし、ここからは慣れの問題な気がするので一気に進みます。 まずpasteコマンドで2つの入力をつなげて、titles_rawとlinks_rawを同時にwhileに渡してあげます。区切り文字でくっつけて分けてもいいですが、またsedやらなんやらではがすのが面倒なので、改行でくっつけます(伝われ!)。
#!/bin/bash
SLACK_WEBHOOK_URL="取得したWEBHOOK_URL"
tmp_json=$(curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a json{}')
titles_raw=$(echo $tmp_json | jq ".[].text")
links_raw=$(echo $tmp_json | jq ".[].href}")
i=0
while IFS= read -r title && IFS= read -r link; do
i=`expr $i + 1`
echo $i : $title, $link
done < <(paste -d "\n" \
<(echo $titles_raw | grep -o '"[^"]*"' | sed 's/"//g') \
<(echo $links_raw | grep -o '"[^"]*"' | sed 's/"//g'))
# curl -X POST -H 'Content-type: application/json' \
# --data '{"text":"Hello, World!"}' "$SLACK_WEBHOOK_URL"
$ ./news_notifier.sh
1 : スマホゲーム利用 中学生で急増か, https://news.yahoo.co.jp/pickup/6483806
2 : 国家資格 申請手続きデジタル化へ, https://news.yahoo.co.jp/pickup/6483735
3 : 政府運営サイトに誤記 500件超, https://news.yahoo.co.jp/pickup/6483689
4 : 広大病院のシステム障害が復旧, https://news.yahoo.co.jp/pickup/6483658
5 : 生成AI「国際指針」G7が閣僚声明, https://news.yahoo.co.jp/pickup/6483618
6 : 子の性的画像 AIと実物の区別困難, https://news.yahoo.co.jp/pickup/6483607
7 : 全銀ネット障害 補償対象は8000件, https://news.yahoo.co.jp/pickup/6483573
8 : あんスタ運営 画像の加工巡り提訴, https://news.yahoo.co.jp/pickup/6483569
$
このままタイトルとリンクでもいいですが、せっかくなのでハイパーリンクとして表示させたいです。Slackでハイパーリンク表示は<https://hogehoge.co.jp|このテキストにリンクがつく> のように書けばいいらしい。これを踏まえてプログラムを修正する。
#!/bin/bash
SLACK_WEBHOOK_URL="取得したWEBHOOK_URL"
tmp_json=$(curl -Ls https://news.yahoo.co.jp/categories/it \
| $HOME/go/bin/pup 'section#uamods-topics > div > div > div > ul > li > a json{}')
titles_raw=$(echo $tmp_json | jq ".[].text")
links_raw=$(echo $tmp_json | jq ".[].href")
news=""
while IFS= read -r title && IFS= read -r link; do
tmp="- <$link|$title>\n"
echo $tmp
news="$news$tmp"
done < <(paste -d "\n" \
<(echo $titles_raw | grep -o '"[^"]*"' | sed 's/"//g') \
<(echo $links_raw | grep -o '"[^"]*"' | sed 's/"//g'))
POST_DATA=$(cat << EOS
{
"text": "今日のITニュース\n$news"
}
EOS
)
curl -X POST -H 'Content-type: application/json' \
--data "$POST_DATA" "$SLACK_WEBHOOK_URL"
$ ./news_notifier.sh
- <https://news.yahoo.co.jp/pickup/6483806|スマホゲーム利用 中学生で急増か>\n
- <https://news.yahoo.co.jp/pickup/6483735|国家資格 申請手続きデジタル化へ>\n
- <https://news.yahoo.co.jp/pickup/6483689|政府運営サイトに誤記 500件超>\n
- <https://news.yahoo.co.jp/pickup/6483658|広大病院のシステム障害が復旧>\n
- <https://news.yahoo.co.jp/pickup/6483618|生成AI「国際指針」G7が閣僚声明>\n
- <https://news.yahoo.co.jp/pickup/6483607|子の性的画像 AIと実物の区別困難>\n
- <https://news.yahoo.co.jp/pickup/6483573|全銀ネット障害 補償対象は8000件>\n
- <https://news.yahoo.co.jp/pickup/6483569|あんスタ運営 画像の加工巡り提訴>\n
ok
$
やりました!ちゃんと通知されました!!! 長かったですがこれにてYahooNewsをSlackに通知するプログラムは完成です。ここまでお付き合いありがとうございました!今回はこれ以上のことはしませんが、たった30行足らずでYahoo Newsを取得してSlackに通知するプログラムができました。外部のコマンドといえばcurlとpup だけです。それになんといってもめんどくさい環境構築もほとんど必要ありませんでした。ファイルも一つなのでGitHub Actionsなどを使えばcronなどで定期実行することも簡単にできるでしょう。いいこと尽くしですね!笑 ただShellScriptは拡張性が高くないです。なので、できるだけその場限りの処理にとどめておくべきですが、単純なファイル操作や、IOフォーマットは得意です。連結や正規表現などの単純な文字列操作にPythonやJSの出る幕はありません。 また、今回golangで作られたpupというコマンドを用いましたが、複雑な処理だけ別の言語で書く方法もあります。いい感じに組み合わせて最短最速で望みの魔法を作り出しましょう!
5章:最後に
ここまでシェルスクリプトの魔法の一端を垣間見ました。echo、grep、curlなどの呪文を使って、はじまりの草原を歩き回りました。しかし、これはまだ序章にすぎません。 次なる章では、もっと高度なテクニックや魔方陣に挑戦していくことでしょう。変数や条件分岐、パイプライン、リダイレクトなど、さまざまな呪文をマスターし、冒険のスキルを向上させていきましょう。 ときにはバグという厄介な魔物との遭遇もあるでしょう。しかし、それがあるからこそ確実に呪文を使えるようになるのです。そういった戦いのなかで、より強力な呪文を手に入れることもできるでしょう。 冒険は終わりません。新たな魔法やテクニックを学び、シェルスクリプトの世界で更なる発見をするために、冒険は続きます。プログラムの世界の冒険は果てしないものです。さあ、次なるページを開き、新たな魔法陣を解読していきましょう。冒険の先にはまだまだワクワクが待っています!
P.S. 最後まで茶番に付き合ってくれてありがとうございます。