JUNのブログ

JUNのブログ

活動記録や技術メモ

HHKB Professional HYBRID Type-S を買った

HHKB Professional HYBRID Type-S 英語配列/墨 を買いました。

1週間くらい使ったので感想的なものを書いておく。

キーボード歴

キーボード歴としては、

色んなノートパソコンのキーボード5年くらい → Thinkpadのキーボード3年くらい → HHKB Professional Type-S 英語配列 2年くらい → HHKB Professional HYBRID Type-S 英語配列

って感じです。

前は会社支給の HHKB Professional Type-S を2年くらい使ってました。ちょうど以下の記事を書いた後くらいから使い始めました。

jun-networks.hatenablog.com

で、そのキーボードを会社に返却したので自分用を買ったという感じです。

1週間使った感想

めっちゃ良いです。HHKBの打ち心地やキー配置が良いのは知っていましたが、相変わらず良いです。

HHKB Professional Type-S との違いですが、個人的には底面のラバーが改善され、入力した際に机に響かなくなっていて打ち心地と静音性が上がった様に思います。あと、今回色は墨を選んだのですが、カッコよくてとても良いです。

また、有線とBluetooth両方対応しているのも嬉しいですね。自分のメインマシンはLinuxBluetoothがあまり安定しないイメージがあるので、有線で繋いでいますが、iPadなどで文字入力作業をする場合なんかにはBluetoothで繋げられるのでとても良いですね。

こんな感じでHHKB, iPad, iSHを使えば開発も出来なくは無さそうです。

iSH Shell

iSH Shell

  • Theodore Dubois
  • Developer Tools
  • Free
apps.apple.com

自分は前HHKBを使っていた時に買ったパームレストがあるのでそれを使っていますが、もし新規にHHKBを買うならパームレストを買った方が良いと思います。HHKB自体にある程度高さがあるのでパームレストが無いと手首が痛くなります。また、個人的にはHHKB公式のパームレストが高さがちょうど良く触り心地も良いのでおすすめです。

www.pfu.fujitsu.com

自分は昔Amazonで買った安い布製のパームレストを使っていましたが、汗とかが染み込んで凄く汚くなって最後らへんは手が痒くなったりしたので公式パームレストじゃないにしても木製のものをおすすめします。

逆にちょっとだけ不満点があるとするならば、HHKB Professional Type-S で使えていた音量上げ下げとかのキーの挙動が不安定になっていることですね。自分はDIPスイッチの設定をMacモードで使っており、これによりUbuntuであればMacと同じ様にFnキーと他のキーを組み合わせて音量調整やマイクのミュートなどが出来ていました。しかし、今回のモデルではこの動作が少し不安定なように思えます。具体的には音量調整は機能したりしなかったりといった感じで、マイクミュートに関してはなぜか別のキーが割り当てられています。前のモデルでは上手く動いていたのですが、今回のモデルは今の所このあたりの機能は安定しないです。もしかしたら何か別途Ubuntu側で設定する必要があるのかも知れません。まぁ、今の所さほど困ってはいないので良いのですが。

てな感じで新しく買ったHHKBが良いというお話でした。自分のように1日の大半をモニター前でキーボックスを叩いているような人ならばキーボードはケチらず自分に合った良いものを使うと良いと思います。

ではでは〜〜👋👋

AtCoder緑になりました

5ヶ月くらい前ですが, 2021年7月31日にAtCoder緑になったのでその振り返り記事を今更ながら書きます.

f:id:JUN_NETWORKS:20211231232320p:plain
レーティング変動

自分のスペック

  • 高校数学は多少やったけど大体忘れてる
  • 42Tokyoでプログラミング勉強してる
  • 数学オリンピックとかとは無縁の人間
  • 言語はPython

やったこと

  • 毎朝典型90の問題を1問解く
  • 新しく知ったアルゴリズムやデータ構造などは手元にメモしてコピペで使えるようにライブラリ化する
  • ABCにはなるべく毎回参加する

毎朝典型90の問題を1問解く

これは書いてある通りで, 毎朝典型90の問題を解きました. ★2から★4の問題までを一日1問ずつやりました. これのおかげでABCで頻出のアルゴリズムや考え方を知ることが出来ました.

自分は競プロの精進をめっちゃやってるような人でも無いので, そもそも手札(選択肢)が無いので, 手札を増やすという意味でも典型90は素晴らしい教材だと思います.

今回緑レートに達成できたのは典型90のおかげだと思います.

新しく知ったアルゴリズムやデータ構造などは手元にメモしてコピペで使えるようにライブラリ化する

これはかなり役に立っています.

UnionFindやセグメント木, BFS, DFS, ダイクストラ など新たに知ったアルゴリズムやデータ構造はある程度汎用的に使えるようにライブラリ化してローカルのマークダウンファイルにアルゴリズムの説明と共に書いています.

ABCにはなるべく毎回参加する

ABCに参加して解けた問題, 解けなかった問題を振り返ると, 「成長したな」と思うこともあれば, 「もっと頑張らねば」と思うこともあり, ちょっと無理してでも毎回出るのはモチベーション的にも大事だと思います.

使ったアルゴリズムやデータ構造

自分の手元にある 競プロメモ.md の目次をコピペします.

- 競プロで使えるコード, テクニック集
    - 計算量
        - Python の処理速度の目安
        - リスト
        - collections.deque
        - set
        - 辞書
    - 設定
        - 入力高速化
        - 再帰呼び出し上限
    - 注意すべき点
        - float('inf') の生成は整数リテラルとは違い生成に時間がかかる
    - プロファイラ
    - データ構造
        - デキュー
        - 辞書型
            - defaultdict
            - Counter
        - UnionFind
        - Priority Queue
            - 使い方
            - 最大値の取り出し
        - セグメント木 (RMQ)
            - 遅延評価セグメント木 (RUQ)
            - 遅延評価セグメント木 (RAQ)
    - グラフ
        - 用語
        - グラフの表しかた
            - 隣接リスト (Adjacency list)
            - 隣接行列 (Adjacency matrix)
        - 探索
            - 幅優先探索 (BFS)
                - 応用
                    - 01BFS
            - 深さ優先探索 (DFS)
        - 最短経路探索 (重み付きグラフ)
            - ダイクストラ法
                - 特徴
                - 実装
            - ワーシャルフロイド法
                - 特徴
                - 実装
            - ベルマンフォード法
                - 特徴
                - アルゴリズム
            - ジョンソン法
        - 強連結成分分解 (SCC)
        - 辺に対する周遊性
            - 用語
            - オイラーグラフの判定
            - オイラー回路の求め方
        - 巡回セールスマン問題 (TSP)
        - 木の直径
        - 最小全域木
            - クラスカルのアルゴリズム
            - プリムのアルゴリズム
        - 最近共通祖先(LCA)
        - 木の表現方法
            - 左子右兄弟表現
            - 二分木
    - ソート
        - 複数のキーでソート
        - 複数のキー(1 つはリバース)
        - 比較関数を基にソート
        - トポロジカルソート
    - 探索
        - 二分探索
            - ライブラリを使う場合
        - bit 全探索
    - 文字
        - 全アルファベットのリスト
    - 数学
        - 素数で割った余りを求めよ
            - 割り算の余り
        - 倍数の性質
        - 総和
        - 順列・組み合わせ
        - 繰り返し二乗法(二分累乗法) (a^b % c を高速に求める)
        - 累積和
        - いもす法 (多次元対応累積和的なやつ)
        - 2 つの数の最大公約数 (GCD)
        - 最小公倍数 (LCM)
        - 素数判定
        - 素数列挙
        - 素因数分解
        - 約数取得
        - 平方数の判定

こんなに無くても緑コーダーになれるかもしれないですが, ある程度典型的な使い方を知っておくだけでもコンテスト中の選択肢が増えるので良いと思います.

その他

言語はPythonを使っています. 最初はC++とかを使おうかとも思ったのですが, 自分が最も慣れている言語がPythonだったのでPythonを使っています. 勿論提出時はPyPy3です.

完走した感想

緑レート達成した時に記事書けばよかったなーとおもってたんですけど, 友人と喋っていたら, 「別に今から書いてもよくね?」と言われ, 確かに〜 って感じだったので書きました.

AtCoder始めて3年経っての緑レートってことでとても嬉しいです. (まぁ1年単位で空白の期間があるわけですが)

自分は競プロ200問解くとか,C問題埋めとかは特にせずに緑コーダーになったわけですが, これは完全に典型90のおかげだと思います.

余談ですが, この記事を2021年中に公開しようと思って書いていたらいつの間にか日付が変わって2022年になっていました. あけましておめでとうございます.

ではでは〜〜👋👋

2021年振り返り

2021年も今日で終わりなので毎年恒例振り返り記事を書きます.

去年のやつ

jun-networks.hatenablog.com

大雑把な感想

今年1年は家に籠もってコード書いたり, アニメ見たり, ゲームしたり, って感じで本当に家から出ることが少なかったです. そのせいもあってか今年はなんだか体感時間が短かったような気がします. また, 生活リズムの乱れが酷く, 活動時間が深夜帯に寄っていたのも原因かもしれないです.

42Tokyoはオンラインで学習出来ていいのですが, 42Tokyoしかやってなくてなおかつオンラインだと家族以外との交流が殆ど無くて刺激が少ないですね. なんだかんだ言ってオフラインで交流するのが好きなのかも知れないです.

なんだか今年は何をやったか具体的には思いだしにくいような気がしてこの年末振り返り記事も書くのが乗り気じゃなかったのですが, 今年書かなかったら来年以降も書かなくなりそうだし, 自戒と反省の意味も込めて書いてみることにしました.

去年立てた目標について

去年の年末振り返り記事でこんなことを書いてました.

目標は高くすると年末に振り返った時に悲しくなるので2021年は今年の反省も踏まえ以下のように設定したいと思います.

  • 生産性の高い日々を送れるように努力する. 具体的には1ヶ月単位で目標を立てて, それを達成するような方針で生きる. (もしかしたら変わるかも)
  • Twitter, YouTube などを触らないようにし, メリハリのある生活をする.
  • 30日OS本, Nand2Tetris, アルゴリズムイントロダクションを完遂する.
  • 42Tokyoの退学システム(通称ブラックホール)に飲まれないようにする. 2021年は振り返った時に, 完遂したものが今年より多くなれるように頑張りたいと思います.

で結果としては

  • 生産性の高い日々を送れるように努力する. 具体的には1ヶ月単位で目標を立てて, それを達成するような方針で生きる. (もしかしたら変わるかも) : いまいち. 正直結構だらけていることが多かったような気がする.
  • Twitter, YouTube などを触らないようにし, メリハリのある生活をする.: メリハリは無い.
  • 30日OS本, Nand2Tetris, アルゴリズムイントロダクションを完遂する.: どの本も読み終わってない. なんなら30日OSは途中までやって放置中.
  • 42Tokyoの退学システム(通称ブラックホール)に飲まれないようにする.: これは大丈夫

って感じでズタボロですね...

というかこの年末振り返り記事, 書いてからはずっと放置してたので今見たら「こんな目標も立てたなぁ」って感じです. 目標を立てたならば定期的に見返しておくべきですね.

生産性はが高い日々を過ごすってやつは, 2021年前半は割と過ごせていた気がしますが, 後半はあまり良くなかった気がします. ずっと家にいてメリハリは無かったでしょう.

30日OS本を読むとかの, 積読消化の目標は30日OS本だけは途中まで良い感じに進んでたんですけど, 途中で2週間くらい免許合宿で空いたらいつの間にかやらなくなっていましたね. やらねば.

42Tokyoの退学システム(ブラックホール)に呑まれないようにするというのは達成出来ました. というか今年は42Tokyoの課題してるかだらけてるかって感じだったのでまぁこれは行けますね. あとこのブラックホールってシステムがかなり緩くなったので, それも大きいです. 今から入学する人とかは多分ブラックホールに怯える必要はあまりないと思います.

プログラミングの話

プログラミング関係の話でいうと, ちゃんと成長したんじゃないのかな? って気はしてます.

今年できるようになったこととしては

って感じでしょうか. まぁどれも42Tokyoで学んだことですね.

競プロ緑レート達成は記事書くべきでしたね. なんか記事書こうかなぁと思っていたら日が過ぎていて書くタイミングを逃しました.

他にも色々やったような気がするのですが, どうも思い出せないのでまぁこんなもんでしょう.

逆に今年足りなかったなぁというところに関しては

  • サービスを作るなどと言った技術を使って何かを作る経験が不足してる.
  • 42Tokyoの課題以外だとイマイチぱっとしない.

って感じですかね.

とは言ってもなんだか多少はやっていて, そんで多少は記事書いてるんで, そっち見ると意外と何かしらやっていたりもする.(HackDayとかにも出たしね)

jun-networks.hatenablog.com

目標に関しては, 毎年目標を立てるんですけど, 目標があると目標と現実とのギャップでなんだか辛くなるタイプっぽいので2022年はあまり目標立てずに気ままにやっていきたいですね.

来年どうすんの

来年は就活などもしつつ気ままにやりたいことをやりたいと思います.

目標とかは立てないです. やりたいことをやるのが良い気がします.

どうも自分は目標を書くと書いたことで満足しちゃうような気がするので目標は立てないし, 書かないです.

今年は記事の数が少なかったので, もっと軽率に書いた方が良かった気がします. AtCoder緑レートとかも軽率に適当でも良いからその時に勢いで記事を書いておくべきでしたね. そうすれば何をしたかわかりやすかったかも?

追記: 記事書きました.

jun-networks.hatenablog.com

そんで, 振り返り記事は月一で書いたほうが良いのかも知れないです. 文量は少なくていいから記事を書いて, その過程で今月を振り返るというのが大切な気がします. 書くぞー!

総括

今年も元気に生きれて偉い!

完走した感想

ノリと勢いで書いた歴代一適当な年末振り返り記事になってしもうた.

書く気が無いという気持ちがこの記事の適当さに現れてしまってるな.

ブログ記事はちゃんと書かなきゃーって思うと書き始めるのが億劫になってしまうから, 適当でいいかーくらいで書くほうがいいな. 実際緑レートの記事も「ちゃんと書かなきゃ」とか「書いた後にレート落ちたらださいなぁ」とかくっっっっそどうでもいいことを考えて書かなかった気がするし, そんなことうだうだ考えるよりかはノリと勢いでガーっと書いてしまったほうがいいじゃろな.

まぁ言うても個人ブログなんだし, この"完走した感想"のコーナーみたいに敬語とか書き言葉とか全部捨てていつものTwitterのノリで書いてしまう方が書いてて気持ちが良いかもしれんな. (読んでる方は読みづらくてしゃーないかもしれんけども)

てなわけで来年も頑張って良い感じに生きるんでよろしく〜

ではでは〜〜👋👋

Ubuntuマシンから WSL2 にデータを移行する

自作ゲーミングデスクトップPCを組んだので, そっちでも開発できるように開発環境を整えます.

やりたいこととしては以下のとおりです.

  • WSL2で開発したい.
  • 既存のUbuntuを搭載したノートパソコンに入っている作業中のプロジェクトとかのデータをWSL上のファイルシステムにコピーしたい.

OSは本当はWindows11が良かったのですが, まだ自分にはWindows11のアップデートの案内が来ていないのでとりあえずWindows10でやります.

WSL2でUbuntuを動かす

これは簡単です. 以下のマイクロソフトが出してる記事を見ながらやればおk.

docs.microsoft.com

一応Tipsとしては

# copy to clip clipboard
if hash clip.exe 2> /dev/null
then
        alias pbcopy='clip.exe'
else
        alias pbocpy='xsel --clipboard --input'
fi

これは dotfiles みたいなリポジトリ作って, 各OS共通の .zshrc を作ってる人向けですね. WSL2上でしか使わないなら特に必要ないです.

ちなみにこれだけだとVimのヤンクとかうまくWindows側のクリップボードにコピー出来ないのですが, そのあたりは以下のIssueのコメントとか見ると良いかもです.

github.com

既存のUbuntuマシンからファイルをWSLにコピーする.

こっちが今日の本題. 最初はext4(Linuxファイルシステム)の外付けHDDをNTFS(Windowsファイルシステム)に変換するソフトとか使おうかと思っていたが, でもそれだとWSLからのアクセスはどうすれば...とか思っていたが, 同じLANにつないであるマシンならUSBとか経由せずネットワーク上でコピーすればよくね? ってことに気づいた.

今回は rsync というコマンドを使って行う.

以下やり方を書きますがここでは以下のように想定して文章を書いてるので注意して読んでください.

  • ノートパソコン: 元々使っていたUbuntuマシンのこと
  • WSL2: 新しいWindowsパソコンのWSL2のこと

Ubuntu on ノートパソコン の設定

WSL2ではネットワークがWindowsのものと完全に分けられていて, 外部からWSL2にアクセスするのは難しいので今回はWSL2から外部のコンピュータ(Ubuntu on ノートパソコン)にアクセスする形にします.

なので, ノートパソコン側ではSSH接続できるようにSSHサーバーの起動と, 22番ポートを許可します.

$ sudo systemctl start ssh  # SSH起動
$ sufo ufw allow  from <Powershellなどで確認したIPアドレス> to any port 22  # 22番ポートへのアクセスを許可

これでノートパソコン側はWindowsマシンからの22番ポートの接続を許可するようになりました. (<Powershellなどで確認したIPアドレス>ipconfigPowershell に打つと確認できます)

WSL2 の設定

設定というか, こっちはやること簡単で, rsync 使ってノートパソコン側からWSL2にコピーしてくるだけです. 以下のコマンドをWSL2内で実行します.

rsync -avzhr <ノートパソコン側のユーザー名>@<ノートパソコンのIPアドレス>:<ノートパソコン側のコピーしたいディレクトリやファイル> <WSL2のどこにコピーしてほしいか>

各オプションは以下のような意味です.

  • a: ファイルを再帰的にコピーできる&ファイルのパーミッションやタイムスタンプなどを維持する.
  • v: 進捗状況の可視化
  • z: ファイル圧縮して転送
  • h: 人間が読める形で進捗表示する
  • r: 再帰的にコピーする.

例: LAN内のノートパソコン(IP 192.168.50.94)のwork_spaceディレクトリをWSL2にコピーする場合は

rsync -avzhr jun@192.168.50.94:/home/jun/Desktop/work_space ~/

をWSL2で実行すれば良い. そしたら WSL2 内に ~/work_space のような形でコピーされる.

後片付け

ノートパソコン側のポート開きっぱなしになっており, これはよろしくないのでちゃんと使い終わったら閉じましょう.

$ sudo ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 22                         ALLOW IN    192.168.50.232            
$ sudo ufw delete 1
Deleting:
 allow from 192.168.50.232 to any port 22
Proceed with operation (y|n)? y
Rule deleted

感想

調べてもいい感じの情報出てこなかったしメモ代わりに書いた.

久しぶりにWindows使ったが, 最近のWindowsは普通に開発できそうでいい感じ. VM使って環境構築してもいいけど, WSL2のほうが軽いし, 問題なければこれでやっていきたいね.

あとは, 自分のdotfilesが整備されてなさすぎて流石にきちんと綺麗にしたい.

github.com

では今日はこのへんで. ではでは~👋👋

おまけ

Powerline フォントのインストール

stackoverflow.com

ダイクストラ法試してみた

最近早水先生の講義を見ているのですが, そこでダイクストラ法の説明があり, 実装したくなったので実装した.

www.youtube.com

import heapq

INF = float("inf")

# 各駅間の道を (行き先, 重み) という要素の隣接リストの形で保持する.
graph = [
    [(1, 16), (2, 8), (3, 15)],
    [(0, 16), (2, 13), (4, 27), (6, 78)],
    [(0, 8), (1, 13), (3, 21), (4, 10), (5, 17)],
    [(0, 15), (2, 21), (5, 33)],
    [(2, 10), (1, 27), (6, 15)],
    [(2, 17), (3, 33), (6, 52)],
    [(1, 78), (4, 15), (5, 52)],
]
# 英語バージョン
"""
stations = [
    "Tokyo",
    "Shibuya",
    "Shinagawa",
    "Sinjuku",
    "Shinyokohama",
    "Yokohama",
    "Odawara",
]
"""
# 日本語バージョン
stations = [
    "東京",
    "渋谷",
    "品川",
    "新宿",
    "新横浜",
    "横浜",
    "小田原",
]


def dijkstra(s, n):  # (始点, ノード数)
    dist = [INF] * n
    hq = [(0, s)]  # (distance, node)
    heapq.heapify(hq)
    dist[s] = 0
    prevs = [-1] * n  # 前の駅(経路復元用)
    seen = [False] * n  # ノードが確定済みかどうか
    while hq:
        cost, v = heapq.heappop(hq)  # ノードをpopする
        seen[v] = True
        for to, cost in graph[v]:  # ノードvに隣接しているノードに対して
            if seen[to] == False and dist[v] + cost < dist[to]:
                print(f"{stations[to]}のコストを更新! {dist[to]} -> {dist[v]+cost}")
                dist[to] = dist[v] + cost
                prevs[to] = v
                heapq.heappush(hq, (dist[to], to))
    return dist, prevs


# (出発, 到着)
start, end = input().split()

if start not in set(stations) or end not in set(stations):
    print("指定された駅は無いよ!")
    exit(1)

dist, prevs = dijkstra(stations.index(start), len(stations))
print(f"{start}から{end}の所要時間は {dist[stations.index(end)]}")

# 経路復元
def restore_path(prevs, v):
    if prevs[v] != -1:
        restore_path(prevs, prevs[v])
    print(f"{stations[v]}", end="->" if v != stations.index(end) else "\n")

restore_path(prevs, stations.index(end))

実行すると

$ python3 dijkstra.py
東京 小田原
渋谷のコストを更新! inf -> 16
品川のコストを更新! inf -> 8
新宿のコストを更新! inf -> 15
新横浜のコストを更新! inf -> 18
横浜のコストを更新! inf -> 25
小田原のコストを更新! inf -> 94
小田原のコストを更新! 94 -> 33
東京から小田原の所要時間は 33
東京->品川->新横浜->小田原

とこんな感じで講義動画と同じような経路更新が出来ている.

f:id:JUN_NETWORKS:20210801130052p:plain
離散数学入門#5: 最短経路問題:ダイクストラ法とワーシャル–フロイド法」より

他の経路も試してみる

$ python3 dijkstra.py
渋谷 新宿
東京のコストを更新! inf -> 16
品川のコストを更新! inf -> 13
新横浜のコストを更新! inf -> 27
小田原のコストを更新! inf -> 78
新宿のコストを更新! inf -> 34
新横浜のコストを更新! 27 -> 23
横浜のコストを更新! inf -> 30
新宿のコストを更新! 34 -> 31
小田原のコストを更新! 78 -> 38
渋谷から新宿の所要時間は 31
渋谷->東京->新宿

$ python3 dijkstra.py
渋谷 横浜
東京のコストを更新! inf -> 16
品川のコストを更新! inf -> 13
新横浜のコストを更新! inf -> 27
小田原のコストを更新! inf -> 78
新宿のコストを更新! inf -> 34
新横浜のコストを更新! 27 -> 23
横浜のコストを更新! inf -> 30
新宿のコストを更新! 34 -> 31
小田原のコストを更新! 78 -> 38
渋谷から横浜の所要時間は 30
渋谷->品川->横浜

$ python3 dijkstra.py
小田原 東京
渋谷のコストを更新! inf -> 78
新横浜のコストを更新! inf -> 15
横浜のコストを更新! inf -> 52
品川のコストを更新! inf -> 25
渋谷のコストを更新! 78 -> 42
東京のコストを更新! inf -> 33
渋谷のコストを更新! 42 -> 38
新宿のコストを更新! inf -> 46
横浜のコストを更新! 52 -> 42
小田原から東京の所要時間は 33
小田原->新横浜->品川->東京

正しく求められているっぽい.

感想

早水先生の講義わかり易すぎてビビる.

乗り換えアプリとかも基本的にはこういう感じで作られているのかなぁと思った.

最小値を取り出すところはPriotiryQueueを使うと最小値を計算時間(O(log N))で取得出来て良い感じ.

参考にしたサイト

Linux環境でも放送大学の授業を受けたい!

放送大学の授業はテレビ,ラジオ,ブラウザで視聴することが可能なのですが, このブラウザ視聴の対応OSにLinuxが入っていません.

www.ouj.ac.jp

Linux(Ubuntu 20.04) の Chrome から放送大学の授業科目動画のサンプル(授業の第一回目)を視聴しようとするとエラーメッセージが表示されてページにアクセス出来ません.

f:id:JUN_NETWORKS:20210722014401p:plain
Ubuntu20.04のGoogle Chromeから視聴ページにアクセスするとエラーが発生する

www.ouj.ac.jp

解決方法

以下のページに書いてある通り推奨環境はPCの場合はWindowsMacです.

www.ouj.ac.jp

じゃあどうやって放送大学側はLinuxChromeからのリクエストを識別しているかと考えれば, まぁ普通はHTTPの User-Agent だろうなという予想はつくわけです. なのでChrome側のUser-AgentWindowsChromeの文字列に書き換えてメッセージを送ってみます.

f:id:JUN_NETWORKS:20210722014759p:plain
`User-Agent` をWindowsChromeに書き換えることで視聴できる

User-Agentの書き換え方はChrome公式が紹介しているのでそれを参考にしました.

developer.chrome.com

で, 毎回毎回こんな感じでChromeデベロッパーツールを開いてUser-Agentを書き換えるのはめんどいのでUser-Agentドメインごとに切り替えるChrome拡張を入れます.

chrome.google.com

User-Agent Switcher and Manager のオプションを開いて, 以下の内容を貼り付ければ放送大学ドメインではWindowsChromeUser-Agentでリクエストを送信するようになります.

{
  "ouj.ac.jp": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
}

これで放送大学の授業をLinux環境のChromeから視聴出来るようになりました!

感想

Linux環境のChromeから放送大学の授業を視聴できなくて, 調べても情報が無かったので備忘録として書いておきました.

放送大学さん, Linux環境からも普通に授業が視聴できるようにしてくれると嬉しいです.

シェル実装の課題を完走した

42Tokyoで minishell という課題があり, 完走したので感想書きます.

minishellについて

minishellとはbashっぽいシェルをC言語で実装する課題.

実装する機能は以下の通り

  • readlineを使った入力と履歴の実装.
  • 絶対パス もしくは $PATH 環境変数を使った実行ファイルの実行 (/bin/lsls)
  • ビルトインコマンド
    • echo (-nオプションのみ対応)
    • cd (相対か絶対パス)
    • pwd (オプションなし)
    • export (オプションなし)
    • unset (オプションなし)
    • env (オプションなし)
    • exit (オプションなし)
  • シングルクオートとダブルクオート
  • 入出力リダイレクション (<, >, >>) と heredocument (<<)
  • パイプ (|)
  • 環境変数$?
  • ctrl-C(SIGINT), ctrl-\(SIGQUIT), ctrl-D(EOF)

っていう感じでbashっぽい小さなシェル(minishell)を作るという課題です.

この課題はチーム課題で, torusさん とチームを組んで取り組みました.

実装

実装したコードはここにあります.

github.com

動いている様子

gyazo.com

シェル全体の構成

全体を超大雑把に分けると以下のようなコンポーネントに分解できます.

  1. 入力処理(readline): JUN
  2. Lexer & Parser: torus
  3. CommandExecution: JUN

JUNと書いてあるのが自分が主に担当した部分で, torusと書いてあるのがtorusさんが主に担当した部分です.

大まかな設計は The Architecture of Open Source Applications という本の中にあるbashの章の中のbashコンポーネントアーキテクチャ図を参考にしてコンポーネントを分け, その後それぞれに担当を決めました.

ここから先はこれらコンポーネントそれぞれについての説明を書くのですが, 自分が担当した部分に関しては詳細に書けるのですが, 自分が担当していない部分に関しては少し大雑把な説明になるかもしれないです.

入力処理

入力処理はreadlineを使って良いとのことなので, 使いました.

正直 readline が便利すぎてあまり入力処理について説明することは無いです. 強いて言うならシグナルハンドリングは少し気をつける必要がありますが, The GNU Readline Libraryを使ったを見ればなんとかなります.

実はこの入力処理についてはちょっとした小話があるのですが, それはまた後ほど...

Lexer & Parser

Lexer (字句解析)

Lexerとは字句解析機であり, 入力文字列を解釈し, 字句(ひとつの意味のある文字列単位)単位で取り出せるみたいな感じです. 字句はToken, 字句解析機はTokenizerとも呼ぶみたいです.

例えば入力が echo Hello I am "JUN" 'san' | cat なら以下のように取り出せるイメージです.

Token Type
echo EXPANDABLE
Hello EXPANDABLE
am EXPANDABLE
JUN EXPANDABLE_QUOTED
san NON_EXPANDABLE
| PIPE
cat EXPANDABLE

ja.wikipedia.org

Parser (構文解析)

Lexerで字句単位に文字列を取り出せるようになったのでここでは取り出した字句を元に構文解析を行い抽象構文木(AST)を作成します.

シェルが受け付ける文字列の文脈自由文法(課題に必要な部分のみ)は バッカス・ナウア記法(BNF) を用いて以下のように表される.

command_line ::=
      "\n"
    | sequencial_commands delimiter "\n"
    | sequencial_commands "\n"

delimiter ::=
      ";"

sequencial_commands ::=
      piped_commands delimiter sequencial_commands
    | piped_commands

piped_commands ::=
      command "|" piped_commands
    | command

command ::=
      arguments

arguments ::=
      redirection
    | redirection arguments
    | string
    | string arguments

string ::=
      expandable <no_space> string
    | expandable
    | not_expandable <no_space> string
    | not_expandable
    | expandable_quoted <no_space> string
    | expandable_quoted

redirection ::=
      "<" string
    | ">" string
    | ">>" string
    | "<<" string

この文法に基づいて再帰下降構文解析を行いASTを生成する.

f:id:JUN_NETWORKS:20210701232046p:plain
abc def | file < infile ; xyz ; のパース結果

CommandExecution

Lexer, Parser の処理が完了し, ASTが生成できたら今度はそれを元に実際にリダイレクションの処理やコマンドの実行などを行います.

今回は "Lexer & Parser" と "CommandExecution" を完全に分けて実装したので, ASTをそのまま使って実行するのではなくCommandExecution側で受け取りたい構造体(t_command_invocation)というのを予め定義しておき, ASTとt_command_invocationを変換する関数を最終的に作ることによって同時並行でそれぞれの担当箇所を実装できると共に, どちらか一方に変更があっても変換関数を修正すれば良いだけになります.

t_command_invocation は具体的には以下のような構造体です.

typedef struct s_command_invocation
{
    t_cmd_redirection           *output_redirections;
    struct s_command_invocation    *piped_command;
    t_cmd_redirection           *input_redirections;
    const char                    **exec_and_args;
    pid_t                       pid;
}   t_command_invocation;
  • t_cmd_redirection はリダイレクションに関する情報を持つ片方向連結リストの構造体です. 具体的には以下の情報を持っています.

    • ファイルパス (heredocの場合は入力された文字列)
    • リダイレクションの種類(追記かどうか, heredocかどうかなど)
    • リダイレクションで指定されたファイルディスクリプタ
    • 次のリダイレクションへのポインタ
  • piped_command はパイプの次のコマンドへのポインタです.

  • exec_and_args は実行するプログラムに渡す文字列配列です. int main(int argc, char **argv) と書いたら受け取れる argv の部分です.

  • pid はコマンド(プログラム)が子プロセスで実行された時のプロセスIDです. fork() して作成した子プロセスを wait() して終了ステータスを取得するために使います. あと, fork()して作成した子プロセスを正しくwait()してあげないとゾンビプロセスになってしまうのでこれを避けるためにも子プロセスのpidは必要です.

リダイレクションの処理

リダイレクションの処理は基本的には dup2() などを使って標準入出力のfd(2>3<みたいなfd指定の場合は指定のfd)をファイルのfdに置き換えれば出来ます.

気をつけるべき点としては, bashでは複数リダイレクションが存在する時, そのfdのリダイレクションの中で最後リダイレクションのみが書き込みや読み込みが行われることです.

bashecho hello > file1 > file2 > file3 と実行するとfile1file2はファイルは作成されるが, 何も書き込まれておらず, file3にのみ"hello"が書き込まれています. ちなみにzshの場合だと全てのファイルに"hello" が書き込まれるという違いがあります.

入力の場合も同様で, cat < file1 < file2 の場合, 実際にcatコマンドの標準入力に繋がれるのは file2 のみです.

heredocument

heredocument はbash特有の概念ではなく, いろんな言語に実装されている.

en.wikipedia.org

要は複数行文字列リテラルということみたいです. bashにおいては << と区切り文字の組み合わせで始まり, もう一度区切り文字が出現するまでの文字列をコマンドの標準入力に流し込むというものになっています.

f:id:JUN_NETWORKS:20210701040100p:plain
heredocumentの例

f:id:JUN_NETWORKS:20210701040157p:plain
複数heredocumentの例

f:id:JUN_NETWORKS:20210701040746p:plain
パイプとheredocumentを組み合わせた例

まぁ大体雰囲気はこんな感じです.

で, 実装の方なんですが, heredocumentの入力タイミングはParserの後, ASTをt_command_invocationに変換する関数内で行うようにしました. つまり, cat << EOF はparserが処理して, そこから先入力される文字列はParserではなくCommandExecution側で入力を受け付けて, 入力された文字列を構造体のメンバに保存します.

fd周りについてですが, ファイルのリダイレクションだとopen()すればよいのですが, heredocument はファイルを入力としているわけではないので, リダイレクションとは分けて考えるべきです.

しかし, 最初どういう風に実装されているか全然わからなかったのでbashのソースコードを見に行きました. そうするとどうやら親プロセスから子プロセスに対してheredocument用のpipeを繋いで文字列を渡しているようです.

先程紹介した "パイプとheredocumentを組み合わせた例" を例に図にすると以下のような感じです.

f:id:JUN_NETWORKS:20210701170325p:plain

パイプラインのコマンド間のパイプ以外にも, シェルのプロセスと子プロセス間のheredocument用のパイプを作成し, そのpipeに対してheredocumentで入力された文字列をwrite()することで実現出来ます.

プログラムの実行

今回自分はシェルのパイプラインの実装はループで子プロセスを生成していくやり方にしました. echo hello | wc -l | cat を例として考えると以下の様になります.

f:id:JUN_NETWORKS:20210701171159p:plain

シェルのパイプライン処理の実装方法はいくつかあるのですが, 複数コマンドが同時に動くという点(並列実行)に気をつけましょう.

例えば bashcat | ls とすると, catの実行が完了する前にlsの出力が先に出現します. これはcatの実行が完了してからlsを実行する逐次的な実行ではなく, catls を並列に実行させているからです.

f:id:JUN_NETWORKS:20210701174911p:plain
`cat` と `ls`が同時に実行され `cat` が入力待ちになっている

ビルトインコマンド

ビルトインコマンドというのはシェル側で実装してシェルに組み込まれているコマンドです.

ビルトインコマンドの実装自体は基本的にはbashのビルトインコマンドのヘルプとかを読みながら関数として実装すれば良いです.

で, 問題となるのはコマンドそのものというよりかはビルトインコマンドの実行方法です. ビルトインコマンドは外部のプログラムを呼び出すわけではないので execvp() などはしません.

ビルトインコマンドは単体で実行される場合とパイプラインとして実行される場合で実行方法が異なります.

ビルトインコマンド単体で実行される場合はfork()せずにシェルのプロセス上で対象の関数を呼び出します.

その際にリダイレクションが含まれている場合は標準入出力をdup() などを使って保存しておき, 後に復元する必要があります. こうしないとその後標準入出力が使えなくなります.

例えば echo hello > output.txtが入力された時, もしechoが外部コマンドならSTDOUT_FILENOopen("output.txt", O_WRONLY | O_CREAT)で出力先ファイルのfdを使ってdup2(fd, STDOUT_FILENO) のように置き換えれば良いだけです. なぜなら外部コマンドを実行しているプロセスはシェルのプロセスではなく, シェルのプロセスからfork()された子プロセスであり, 子プロセスのfdがどうなろうと親プロセスには影響しないからです.

しかし, シェルのプロセスで実行するビルトインコマンドは話が違います. そのまま愚直にdup2(fd, STDOUT_FILENO)などとしようものなら標準出力はcloseされ, その後シェルが標準出力に書き込む手段が無くなります. なのでこのような悲惨な事態を避けるために dup(STDOUT_FILENO) のようにリダイレクション用のfdとは別に標準入出力のfdを複製して保持しておきます. そしてリダイレクションの処理を行っている時に複製した標準入出力のfdとリダイレクションで指定されたfd(2> みたいなやつ)が被ったらもう一度dup(複製した標準入出力のfd)をしてからファイルのfdに置き換えることで標準入出力のfdを失わないようにしながらリダイレクションを処理する必要があります.

そしてリダイレクションなどの処理が終わり, ビルトインコマンド関数の実行が完了したらリダイレクション用に開けていたfdを閉じて, 標準入出力のfdを元のfdに戻して完了です.

このようにビルトインコマンド単体の場合はシェルと同じプロセスで実行されるためリダイレクション周りなどは注意を払う必要が有ります.

次にパイプラインの中にビルトインコマンドがある場合, 例えば echo hello | cat のような場合です.

この場合, 外部コマンドを実行する時と同じ様にfork()して子プロセスを生成します. 違いはexecvp()で外部のプログラムを読み込んでメモリを別のプログラムに置き換えることはせず, ビルトインコマンドに対応した関数を呼び出して終了する点です.

ここまでの話で説明したとおりビルトインコマンドは単体の場合はシェルのプロセスで, パイプライン内の場合はシェルの子プロセスで実行されます. そして子プロセスの環境変数やfdの変更, メモリの操作, カレントディレクトリの変更 は親プロセスに影響しません.つまりどういうことが起きるかというと, 以下のようなことが起きます.

f:id:JUN_NETWORKS:20210701215039p:plain
bashでの実行例

パイプライン内で export した環境変数が反映されていませんね. これは export環境変数を追加したけれど, コマンドがパイプライン内にあるのでexportは子プロセスで実行され, 親プロセスであるシェルのプロセス内の環境変数には反映されていないのです.

cdでも同様にパイプライン内で実行するとシステムコールchdir()が子プロセス内で実行されるためシェルのcwdは変わりません.

f:id:JUN_NETWORKS:20210702031526p:plain
bashでの実行例

こういうのを知るとなぜ exportunset, cdなどがビルトインコマンドとしてシェルに組み込まれているかがわかりますね.

その他

環境変数の保持方法

初めのころは, 環境変数は文字列配列environをそのまま保持していて, 環境変数を追加する時は文字列配列のメモリ領域を確保し直し(課題の制約でrealloc()は使えないので), environにもともと入っていたデータを新しい配列にコピーしその後最後の要素に新しい環境変数の文字列("key=value"形式)を入れるようにしていました.

しかし, この方法はおすすめできません. なぜなら環境変数の取得や追加, 削除などの操作に多くの手間が必要になりバグの原因になりますし拡張性が低いです. なので良い子は連結リストやハッシュテーブルなどの扱いやすいデータ構造で環境変数を保持しておき, コマンドを実行する時に文字列配列に変換する関数を作りましょう.

今回自分は連結リストで環境変数を保持するようにしました. 速度ガー! って思うかもしれませんが, 環境変数の数なんて大して大きくなることは無いはずなので問題ないと判断しました. (問題が起きそうなら別のデータ構造に変えればいいだけ)

typedef struct s_var {
    const char            *key;
    const char            *value;
    bool               is_shell_var;
    struct s_var       *next;
}                       t_var;

入力処理に関しての小話

入力処理に関してはreadlineを使ったから苦労してない的なことを書きましたが, 実は自分のチームが課題を始めたころはreadlineは使えずTermcapを使って実装する必要がありました. (Termcapの部分はtorusさんがやってくれました)

en.wikipedia.org

このtermcapがかなり曲者のようでして, 入力処理の実装はかなり苦労していました. しかし, 自分達が課題を提出直前に課題変更があり, readlineが使えるようになりTermcapを使う必要が無くなったのと, 課題要件にheredocの追加が行われ, 話し合った結果Termcapによる入力処理の実装は破棄し, readlineによる入力処理を実装することにしました.

課題変更前はTermcapによる入力処理と履歴機能と文字列編集の実装のためにロープ木)による文字列編集が実装されていました.

GitHub - torus/42-minishell at termcapありのバージョン からTermcapを使ったバージョンのコードが見れます. (heredocumentは実装されていません)

豆知識

ここではminishellをやる上で役に立つかもしれない豆知識を書いておきます. (自分用の備忘録でもある)

bashのコードリーディング

bashのコードは1996年頃に書かれていて, 現代のC言語のコードとは書き方が一部異なっており, それを知らないととても読みづらいです.

具体的には

  • インデントのサイズはスペース8
  • 古いC言語の規格では引数の型は関数ヘッダーの次の行に書く必要がある.

en.wikipedia.org

この書き方をK&Rスタイルと言うらしいです.

なのでbashのコードを読む時はエディタのインデントサイズを8にして, 古いC言語の書き方を頭の片隅に置いておくとスムーズにコードが読めると思います.

GNUフラグパーサー

echo -n -n helloecho -nnnn hello と書いた時に echo -n hello と同じ挙動をするのはGNUのフラグパーサーライブラリであるgetoptの仕様によるものである.

en.wikipedia.org

www.gnu.org

完走した感想

いやー, 長かった. 4月に課題に取り組み始めてから提出するまで約3ヶ月かかりました. まぁ3ヶ月の間全てminishellやっていたというわけではなく競プロやったりフラクタルの実装したりちょくちょく寄り道したりしてましたが, それでも大変でした.

コミット数も42の課題では初めて1000コミット超えました. まぁPRやrevertなどのコミットがいっぱい入っているので全てのコミットがコードの実装を含んでいるというわけでもないですが.

f:id:JUN_NETWORKS:20210702032135p:plain

この課題をするまでシェルというものが自分で実装できるものというイメージは全くありませんでしたが, 今回実装してみてシェルの内部での処理やUNIXプログラミングについて少し詳しくなり, シェルくらい頑張れば実装できるっしょっていう気持ちになりました.

また, 今回シェルを作ったことでこの課題に取り組む前と後でシェルに対する意識が大きく変わったように思えます. 課題に取り組む前は, シェルはブラックボックスって感じで, とりあえずなんか知らんけどコマンドを入力したら実行してくれるって感じでしたが, 今では内部でどのような処理が行われているかがわかっているためシェルを触っている時にシェルの実装側からの視点というものがわかるようになった気がします.

この記事ではあまり深堀りしてませんでしたが, cdを含むいくつかのビルトインコマンドはbashのhelpコマンドでは出てこない挙動などがあり, それを知るためにbashソースコードを読んだりもしました. コミットが25年前(1996年)とかが普通にあり, 自分より年上のコードを読むのは新鮮でした. 25年前ともなると現在のC言語の文法やインデントスタイルなどが違っていたりして読むのが大変でした.

今回始めてUNIXプログラミングと呼ばれるものを行っったのですが,「詳解UNIXプログラミング」という本がとてもとても役に立ちました.

UNIXとはなんぞやという説明からUNIXにおけるファイルの扱い, シグナルやプロセス等々...UNIXプログラミングを行う上で必要な知識がここに書かれています. この本のおかげでminishellなんとかクリア出来たと言っても過言では無いくらいに役に立ちました. (自分の中では勝手に聖書と呼んでいます)

また, 今回一緒にチームを組んだtorusさんはキャリアの長いプログラマーの方で, 多くのことを教えて頂き大変勉強になりました. 豆知識のセクションに書いてある知識はtorusさんから教えてもらったものだったりします.

あと, 提出する直前で課題が大幅に変更されたのは堪えました. 特にtorusさんはその時点で既にTermcapに関わる入出力関係のコードを数週間掛けて千行以上書いていました. しかし, Termcapでの入出力をheredocに対応させる(複数行入力対応)こととreadlineの実装に切り替えるという2つの選択肢を考えた時に, ここまで書いてきた千行以上のコードを破棄し, readlineでの実装に舵を切ったのは凄いなぁと思いました. もし自分だったら惜しくて既存のコードに固執してしまうかもしれません.

f:id:JUN_NETWORKS:20210702013616p:plain
Termcapの削除に関するPR

github.com

今回の課題は今までの課題で一番重く, 実装量も多いため記事内で全てを解説しませんでした. 具体的なコードが見たい人はGitHubリポジトリを見てください.

github.com

とても大変でしたが, やりがいと学びのある良い課題でした.

といったところで今日はこのへんで. ではでは〜〜👋👋

参考リンク

bash関係

シェル関係

パーサー関係

readline

Termcap

その他