PHPとデータベースによるバックエンドプログラミング
前回の講義では、Webページの作り方として、JavaScriptを用いたブラウザで動くプログラミングについて説明を行った。今回の授業では、データを管理しているサーバ側(バックエンド)で使われるプログラミング言語 PHP についての紹介と、データを管理するためのプログラム言語 SQL について説明し、簡単な演習をレポート課題とする。
PHPとデータベースによるバックエンドプログラミング
- PHPとデータベースによるバックエンドプログラミング
- 以下のサンプル(sampleD.php~) PHP のファイルなので、ダウンロードしたファイルを開いてもこのままでは動きません。動作確認のページにて実行結果を確認してください。
- PHPによるHelloWorld
- PHPによるデータの受け取り
- データベースとは
- sampleG-itemlist.sql
- sampleG-userlist.sql
- sampleG-buylist.sql
- Paiza.io の itemlist,userlist,buylist の動作確認ページ – このページにてSQLの練習問題を答えてください
- PHPの中でSQLを使う
- 05/02 練習問題のレポート提出先はこちら
JavaScriptによるフロントエンドとPHPバックエンド入門
前回の講義では、インターネットの仕組みを復習し、そこで使われるプログラミング言語などを紹介した。
今回の授業では、インターネットのブラウザ側(フロントエンド)で使われるプログラム言語である JavaScript の基本について整理しなおし、簡単な穴埋め問題による演習を行う。
JavaScriptによるフロントエンドプログラミング
- JavaScriptによるフロントエンドプログラミング
- 以下のサンプル(sample3.html~sampleA.html)は、各HTMLファイルを開くとソースコードが表示されます。JavaScriptによるプログラムなので、自分のパソコンにダウンロードし、演習についてはダウンロードしたファイルを編集して、ブラウザで動作を確認してください。
- sample3.html
- sample4.html
- sample5.html
- sample6.html — 簡単な穴埋め問題
- sample7.html
- sample8.html
- sample9.html — 簡単な穴埋め問題
- sampleA.html
- sampleA.css
- 以下のサンプル(sampleB2.html~sampleC2.html)は、jquery が html ファイルと同じ場所に置いてある必要があり、ダウンロードしたファイルを開いてもこのままでは動きません。動作確認のページにて実行結果を確認してください。
- sampleB2.html 動作確認Webページ
- sampleC2.html 動作確認Webページ
- sampleC.json
- 無名関数の説明
- 練習問題 6 , 9 の提出先
- 2023/情報メディア工学(4/28)小テスト
Webページの生成とプログラム言語
前回の講義では、OSの仕組みとインターネット(Web)の仕組みについて、総括・復習をおこなった。
2回目の授業では、インターネットのWebページを作るために使われているHTMLやCSSやプログラム言語について解説を行う。
Webページの生成とプログラム言語
理解確認
- こちらの小テストに回答してください。
情報メディア工学・ガイダンス/2023
情報メディア工学では、前期では情報を扱うためのOSの仕組みなどを、実践を交えながら演習を中心に行う。後期は5年の人工知能の授業につながる内容として、情報の中のデータをどう処理するのかを議論する。
OSの役割と仕組み
組込み系システム
組込み系のシステムで、OSが無い場合(例えば Arduino でデバイスを制御する場合)には、ユーザプログラムはデバイスを操作するライブラリやI/Oポートを直接制御しながら、ハードウェアを制御する。ユーザプログラムは、デバイスを操作するライブラリを含むため、異なるシステムでは機械語をそのまま使うことはできない。(共通化が不十分)
組込み系システムでは、ハードウェアを操作する命令をすべてユーザプログラムが面倒を見る必要があるため、システムが複雑化するとプログラム開発が大変になってくる。また、ユーザプログラムが間違った制御方法を取れば、ハードウェアを壊すような処理を実行してしまうかもしれない。(資源保護ができない)
オペレーティングシステム経由でハード操作
コンピュータのハードウェアの違いは OS がすべて包み隠し、OSが管理する。OSは 特権モード で動作し、ハードウェアを直接制御する。ユーザプログラムはユーザモードで動作し、OSの機能を呼び出すシステムコールを経由し、デバイス毎のデバイスドライバを経由して、ハードウェアを操作する。ユーザモードのプログラムは、ハードウェアを直接操作するような命令を実行しようとすると、OSが命令を強制停止させる。(資源保護)
ユーザプログラムには、ハードウェアを直接操作する機械語が含まれていないので、ユーザプログラムの機械語を同じOSが動く他のコンピュータにコピーして動かすことができる。(資源の扱いを共通化)
(例) helloworld のプログラムがコンソールに出力
簡単な例として、helloworld.c のような簡単なコンソール出力プログラムが動いて、画面に文字が表示されるのは以下の図のようにOSを経由して文字を表示している。
古いコンピュータで、プログラムが動作するだけならば、仕組みはすごく簡単にみえる。ユーザプログラムはすべて特権モードで動くOS(狭義のOSとかカーネルと呼ぶことが多い)を経由してハードウェアを操作する。
GUI が使えるグラフィカルな OS の場合
GUI が使えるグラフィカルなOSの場合、GUI の操作を支援するプログラム(ウィンドウマネージャ)などを利用しながら、ユーザはOSを操作する。コンピュータを操作する場合は、こういうウィンドウマネージャなどがないと不便であり、カーネルとユーザ支援のウィンドウマネージャなどをまとめて広義のOSと呼ぶ場合も多い。
ユーザプログラムは、GUIを操作するためのライブラリを経由し、さらにカーネルを経由してディスプレィに結果が表示される。
ユーザモードのプログラムの実行単位プロセスでは、処理を実行するためのメモリなどは他の処理と分離されており、他のプロセスのメモリ領域などを間違ってアクセスすると「メモリエラー」といった例外などが発生し、処理が強制的に停止させられる。このように、プロセスが他に悪影響を及ぼさないように、OS はメモリを管理する。(OSの保護機能)
(例) helloworld の結果を端末ソフトで表示
以下のように、コンソールアプリの実行結果を表示するような、cmd.exe は、helloworld.exe と OS を経由しながら連動して動いている。
helloworld.exe の出力は、OS を経由しながら cmd.exe に伝わり、cmd.exe はその表示内容に応じて、テキストの文字やフォントに合わせてグラフィカルな画面に文字を表示しようとする。グラフィカルな出力は GUI のライブラリを経由しながら OS に送られ、グラフィックドライバが画面に文字を表示する。
インターネットとプログラム
次に、インターネットの仕組みを踏まえ、インターネットで使われるプログラム言語やデータについて3~4週をかけて演習を中心にしながら、今まで習ってきたことを総括する。
理解確認
授業アンケート前期科目
情報メディア工学
今年度、初めて開講した情報メディア工学だが、斉藤担当の前半についてアンケートの結果がでた。情報セキュリティの話を演習中心に講義を行い、レポートやFormsによる小テストにて評価を行ったが、課題などの提出が怪しい学生以外は好成績であった。学生からの評価も81.6ポイントとまずまずの評価であった。
情報制御基礎(学際)
学際科目の情報制御基礎については、複数の学科の学生の参加で、講義内容も色々と制限もあるが、基礎的な課題でのレポート点と期末テストで評価を行う中、期末テストでは評価が低い学生でも、きちんとレポート課題を提出している学生については合格点を出すことができた。
授業アンケートの評価でも 85.9 ポイントと高い評価が得られた。講義資料や過去試験問題などのWeb公開などについては、今後も続けていきたい。
プログラムのバージョン管理とオープンソース
プログラムを複数人で開発する場合のバージョン管理と、オープンソースプログラムを使う場合の注意を説明する。
バージョン管理システム
プログラムを学校や自宅のパソコンで開発する場合、そのソースコードはどのように持ち運び管理修正すべきだろうか?
最も原始的な方法は、常に全部を持ち歩く方法かもしれない。
- 同期方式 – 2つのディレクトリのファイルの古い日付のファイルを、新しい日付のファイルで上書きするようなディレクトリ同期ソフトを使って管理
- 圧縮保管 – ファイル全体だと容量も多いため、複数のファイルを1つのファイルにまとめて圧縮を行う tar コマンドを使うことも多い。(tar ball管理)
diffとpatch
プログラムの修正を記録し、必要最小限で修正箇所の情報を共有する方式に patch がある。これには、2つのファイルの差異を表示する diff コマンドの出力結果(通称patch)を用る。diff コマンドでは、変更のある場所の前後数行の差異を !(入替) +(追加) -(削除) の目印をつけて出力する。patch コマンドに diff の出力を与えると、!,+,- の情報を元に修正を加えることができる。(通称「patchをあてる」)
((( helloworld-old.c ))) #include <stdio.h> void main() { printf( "Hello World\n" ) ; } ((( helloworld.c ))) #include <stdio.h> int main( void ) { printf( "Hello World\n" ) ; return 0 ; } ((( diff の実行 ))) $ diff -c helloworld-old.c helloworld.c ((( 生成された patch 情報 ))) *** helloworld-old.c 2022-07-25 10:09:10.694442400 +0900 --- helloworld.c 2022-07-25 10:09:26.136433100 +0900 *************** *** 1,5 **** #include <stdio.h> ! void main() { printf( "Hello World\n" ) ; } --- 1,6 ---- #include <stdio.h> ! int main( void ) { printf( "Hello World\n" ) ; + return 0 ; }
インターネットの初期の頃には、他の人のプログラムに対して間違いを見つけると、作者に対してこのpatch(diff出力)をメールなどで送付し、プログラムの修正が行われた。
広く世界で使われている Web サーバ apache は、オープンソースで開発されてきた。当初はプログラム公開後に間違いや機能追加の情報(patch)が世界中のボランティア開発者から送られてきながら改良が加えられていった。このため、”a too many patches”「つぎはぎだらけ」という皮肉を込めて apache と名付けられたと言われている。
初期のバージョン管理システム
バージョン管理システムは、複数人で少しづつテキストファイルに修正を加えながら改良を行うような際に、誰がどのような修正を行ったかという修正履歴を管理するためのツール。unix などのプログラム管理では rcs (revision control system) が使われていたがその改良版として cvs (concurrent version system) が使われていた。現在は後に紹介する Git などを使うようになった。
- ci コマンド(check in) – ファイルをバージョン管理の対象として登録する。
- co コマンド(check out) – ファイルを編集対象とする(必要に応じて書き込みロックなども可能)。co されたファイルは、編集した人が ci して戻すまで ci することができない。
- 修正結果を ci する際には、新しい編集のバージョン番号などをつけて保存される。
- co コマンドでは、バージョン番号を指定してファイルを取り出すことも可能。
[Bさんの修正] /check out \check in ファイルver1.0-----→ver1.1------→ver1.2 \check out /check in [Aさんの修正]
集中管理型バージョン管理システム
rcs,cvs では、ファイルのバージョンは各ファイルを対象としているため、ファイルやディレクトリの移動や削除は管理が困難であった。これらの問題を解決するために、集中管理を行うサーバを基点として、対象ファイルのディレクトリ全体(ソースツリー)に対してバージョン番号を振って管理を行う。subversion はサーバに ssh などのネットワークコマンドを介して、保存・改変を行うことができる。
しかし、複数の人の修正のマージ作業の処理効率が悪く、処理速度が遅いため使われなくなっていった。同様のバージョン管理システムが企業により有償開発されていた(BitKeeperなど)が製品のライセンス問題が発生し、業を煮やした Linux 開発の Linus が Git のベースを開発・公開している。
分散型バージョン管理システム
Gitは、プログラムのソースコードなどの変更履歴を記録・追跡するための分散型バージョン管理システムである。Linus によって開発され、ほかの多くのプロジェクトで採用されている。(以下wikipedia記事を抜粋加筆)
Gitは分散型のソースコード管理システムであるため、リモートサーバ等にある中心リポジトリの完全なコピーを手元(ローカル環境)に作成して、そのローカルリポジトリを使って作業を行う。
一般的な開発スタイルでは、大雑把に言えば、以下のようなステップの繰り返しで作業が行なわれる:
- git clone – リモートサーバ等にある中心リポジトリをローカルに複製する。
- git commit – ローカルでコンテンツの修正・追加・削除を行い、ローカルリポジトリに変更履歴を記録する。
- 必要に応じて過去の状態の閲覧や復元などを行う。場合によってはこのステップを何度か繰り返す。
- git push – ローカルの変更内容を中心リポジトリに反映させる。
- git merge – git push の段階で、作業者ごとの変更内容が衝突することもある。Gitが自動で解決できる場合もあれば、手動での解決する。
- git pull – 更新された中心リポジトリ(他者の作業内容も統合されている)をローカルの複製にも反映する。これによりローカル環境のコードも最新の内容になるので、改めてステップ2の作業を行う。
ローカルリポジトリ(Aさん) ver1.0a1 ver1.0a2 ver1.1a1 修正--(git commit)--修正--(git commit) 修正--(git commit) /git clone \git push /git pull Bさんの修正 中心リポジトリver1.0-----------------ver1.1 も含まれる \git clone /git push 修正--(git commit)--修正--(git commit) 編集の衝突が発生すると ver1.0b1 ver1.0b2 git merge が必要かも ローカルリポジトリ(Bさん)
GitHub
Git での中心リポジトリを保存・管理(ホスティング)するためのソフトウェア開発のプラットフォーム。コードの管理には Git を利用し GitHub 社によって保守されている。2018年よりマイクロソフトの傘下企業となっている。
GitHub では単なるホスティングだけでなく、プルリクエストやWiki機能(ドキュメントの編集・閲覧機能)といった、開発をスムーズに行うための機能も豊富である。(個人的な例:github.com/tohrusaitoh/)
GitHub で管理されているリポジトリには、公開リポジトリと非公開リポジトリがあり、非公開リポジトリはその管理者からの招待をうけないとリポジトリ改変に参加できない。
企業でのプログラム開発で GitHub を内々で使っている事例なども多いが、間違って公開リポジトリと設定されていて企業の開発中のプログラムが漏洩してしまった…との事例もあるので、企業での利用では注意が必要。
オープンソースとライセンス
オープンソースプログラムは、プログラムのソースコードをインターネットで公開されたものである。しかし、元となったプログラムの開発者がその利用に対していくつかの制約を決めていることが多い。これらのオープンソースプログラムでのソフトウェア開発手法の概念として「伽藍とバザール」を紹介する。
伽藍とバザール
伽藍(がらん)とは、優美で壮大な寺院のことであり、その設計・開発は、優れた設計・優れた技術者により作られた完璧な実装を意味している。バザールは有象無象の人の集まりの中で作られていくものを意味している。
たとえば、伽藍方式の代表格である Microsoft の製品は、優秀なプロダクトだが、中身の設計情報などを普通の人は見ることはできない。このため潜在的なバグが見つかりにくいと言われている。
これに対しバザール方式では明確な方針が決められないまま、インターネットで公開されているプログラムをボランティアを中心とした開発者を中心に開発していく手法である。
代表格の Linux は、インターネット上にソースコードが公開され、誰もがソースコードに触れプログラムを改良してもいい(オープンソース)。その中で、新しい便利な機能を追加しインターネットに公開されれば、良いコードは生き残り、悪いコードは自然淘汰されていく。このオープンソースを支えているツールとしては、前に述べた git が有名。
オープンソース・ライセンス
ソースコードを公開している開発者の多くは、ソフトウェア開発が公開することで発展することを期待する一方で、乱用をふせぐために何らかの制約をつけていることが多い。最初の頃は、開発者に敬意を示す意味で、プログラムのソースコードに開発者の名前を残すこと、プログラムを起動した時に開発者の名前が参照できること…といった条件の場合もあったが、最近ではソフトウェアが広く普及・発展することを願って条件をつけることも多い。
こういったオープンライセンスの元となったのは、Emacs(エディタ),gcc(コンパイラ)の開発者のストールマンであり、「ユーザーが自由にソフトウェアを実行し、(コピーや配布により)共有し、研究し、そして修正するための権利に基づいたソフトウェアを開発し提供することにより、ユーザーにそのような自由な権利を与えた上でコンピュータやコンピューティングデバイスの制御をユーザーに与えること」を目標に掲げた GNU プロジェクトがある。linux を触る際のコマンドで、g で始まるプログラムの多くは GNU プロジェクトのソフトウェア。
GNU プロジェクトが掲げる GNU ライセンス(GPL)では、GPLが適用されていれば、改良したソフトウェアはインターネットに公開する義務を引き継ぐ。オープンソースライセンスとして公開の義務の範囲の違いにより、BSD ライセンス、Apacheライセンスなどがある。
コピーレフト型 | GNU ライセンス(GPL) | 改変したソースコードは公開義務, 組み合わせて利用では対応箇所の開示が必要。 |
準コピーレフト型 | LGPL, Mozilla Public License | 改変したソースコードは公開義務。 |
非コピーレフト型 | BSDライセンス Apacheライセンス |
ソースコードを改変しても公開しなくてもいい。 |
GPLライセンス違反
GPLライセンスのソフトウェアを組み込んで製品を開発した場合に、ソースコード開示を行わないとGPL違反となる。大企業でこういったGPL違反が発生すると、大きな風評被害による損害をもたらす場合がある。
最近のライセンスが関連する話題を1つ紹介:GitHub を使った AI プログラミング機能「Copilot」というサービスが提供されている。Copilot のプラグインをインストールした vscode(エディタ) では、編集している関数名や変数名などの情報と GitHub で公開されているプログラムの 学習結果を使って、関数名を数文字タイプするだけで関数名・引数・処理内容などの候補を表示してくれる。しかし、Copilot を使うと非オープンライセンスで開発していたプログラムにオープンソースの処理が紛れ込む可能性があり、非オープンソースプロジェクトが GPL で訴えられる可能性を心配し「Copilot は使うべきでない」という意見の開発者も出ている。
理解度確認
ライブラリと分割コンパイル
巨大なプログラムを作ってくると、プログラムのコンパイルに時間がかかる。こういった場合には、共有できる処理であればライブラリにまとめたり、分割コンパイルといった方法をとる。
ライブラリ
C言語でプログラムを作っている時、printf() や scanf() といった関数を使うが、こういった組み込み関数のプログラムはどこに書かれているのだろうか?
ソースプログラムがコンパイルする際には、コンパイラ(compiler)によるコンパイル処理(compiler)、リンカ(linker or linkage editor)によるリンク処理(link)が行われる。この時に、printf()やscanf() の機械語(組み込み関数などのライブラリの内容)が実行プログラム a.out の中に埋め込まれる。通常は、コンパイルとリンク処理は一連の処理として実行される。
helloworld.c ソースプログラム ↓ compiler $ gcc -c helloworld.c (コンパイルだけ行う) helloworld.o オブジェクトファイル(中間コード) ↓ linker $ gcc helloworld.o (リンク処理を行う) (+) ← libgcc.a ライブラリ(printf, scanf....) ↓ $ ./a.out a.out 実行プログラム
静的リンクライブラリと動的リンクライブラリ
しかし、printf() や scanf() のような組み込み関数の機械語が実行プログラムの中に単純に埋め込まれると、
- よく使われるprintf()やscanf()の処理は、沢山の実行プログラムの中に埋め込まれる。
そして、組み込み関数を使ったプログラムが複数実行されると、実行中のメモリに複数の組み込み関数の処理が配置されてメモリの無駄が発生する。 - その組み込み関数に間違いがあった場合、その組み込み関数を使った実行プログラムをすべて再コンパイルしないといけなくなる。
リンクされたプログラムの機械語が実行プログラムに埋め込まれる方式は、静的リンクライブラリと呼ぶ。
しかし、静的リンクライブラリの方式は、実行時の命令の領域のムダや、ライブラリに間違いがあった場合の再コンパイルの手間があるため、動的リンクライブラリ方式(共有ライブラリ方式)がとられる。
動的リンクライブラリでは、プログラム内には動的リンクを参照するための必要最小限の命令が埋め込まれ、命令の実体は OS が動的リンクライブラリとして管理する。
Linux では、静的リンクライブラリのファイルは、lib~.a といった名前で保存され、動的リンクライブラリは、lib~.so という名前で保存されている。 Windows であれば、拡張子が ~.DLL のファイルが動的リンクライブラリである。
OS にとって必須の動的リンクライブラリは /lib 配下に保存されるが、ユーザが独自にインストールするパッケージの場合 /lib のアクセス権限の都合で別の場所に保存されるかもしれない。この場合、その独自パッケージを起動する時に、動的リンクライブラリの保存場所を見つけ出す必要がある。Linux では 環境変数 LD_LIBRARY_PATH に自分が利用する動的リンクライブラリの保存場所を記載すれば、OS がプログラム起動時に動的リンクライブラリを見つけてくれる。
分割コンパイル
複数人でプログラムを開発する場合、1つのファイルを全員で編集するのは混乱してしまう。例えば、ちょうど情報構造論で説明している、リスト処理のようなプログラムであれば、List 構造の構造体、cons(),print() といったList 構造を操作する関数を作る人と、そのそれらの関数を利用するプログラムを書く人に分かれてプログラム開発をすれば混乱も減らせる。そして、それぞれ別のファイルになっている方が開発しやすい。
- list.h : ヘッダファイル – 構造体の宣言や関数のプロトタイプ宣言や変数のextern宣言などを記載
- list.c : リスト処理の cons,print などの処理内容を記載
- main.c : cons,print を使った処理を記載
#include “ヘッダファイル”
自作のヘッダファイルを読み込む場合は、#include “list.h“ のように記載する。
#include で、ヘッダファイルを < > で囲むと、/usr/include フォルダから探してくれる。” “ で囲むと、ソースプログラムと同じフォルダの中からヘッダファイルを探す。
プロトタイプ宣言と extern 宣言
ヘッダファイルは、list.c と main.c の両方で使われるデータ構造、関数、変数の宣言を記載する。関数は、引数の型や返り値の型を記載した struct List* cons( int , struct List*) ; といったプロトタイプ宣言を記載する。変数については、変数の型だけを宣言する extern struct List* stack ; といった extern 宣言を記載する。
// list.h ----------------------------- // リスト構造の宣言 struct List { int data ; struct List* next ; } ; // リスト操作の関数のプロトタイプ宣言 extern struct List* cons( int , struct List* ) ; extern void print( struct List* ) ; // stack の extern 宣言 extern struct List* stack ; // スタック操作関数のプロトタイプ宣言 extern void push( int ) ; extern int pop()
// list.c ----------------------------- #include <stdio.h> #include <stdlib.h> #include "list.hxx" // リストの要素を作る struct List* cons( int x , struct List* n ) { struct List* ans = (struct List*)malloc( sizeof( struct List ) ) ; if ( ans != NULL ) { ans->data = x ; ans->next = n ; } return ans ; } // 全要素の出力 void print( struct List* p ) { for( ; p != NULL ; p = p->next ) printf( "%d " , p->data ) ; printf( "\n" ) ; } // stack の実体 struct List* stack = NULL ; // スタックに x を保存 void push( int x ) { stack = cons( x , stack ) ; } // スタックの先頭を取り出す int pop() { int ans = stack->data ; struct List* d = stack ; stack = stack->next ; free( d ) ; return ans ; }
// main.c ----------------------------- #include <stdio.h> #include "list.hxx" int main() { struct List* top = cons( 1 , cons( 2 , cons( 3 , NULL ) ) ) ; print( top ) ; push( 11 ) ; push( 22 ) ; push( 33 ) ; printf( "%d\n" , pop() ) ; printf( "%d\n" , pop() ) ; printf( "%d\n" , pop() ) ; return 0 ; }
分割コンパイルの作業を確認するために、以下のコマンドを実行してみよう。
((( 一度にコンパイルする方法 ))) guest00@nitfcei:~$ cp /home0/Challenge/seg-compile/* . guest00@nitfcei:~$ gcc list.c main.c guest00@nitfcei:~$ ./a.out # 正しく実行できる。 ((( 失敗するコンパイル ))) guest00@nitfcei:~$ gcc list.c /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start': (.text+0x24): undefined reference to `main' collect2: error: ld returned 1 exit status # list.c の中に main() が無いからエラー guest00@nitfcei:~$ gcc main.c /usr/bin/ld: /tmp/ccxr4Fif.o: in function `main': main.c:(.text+0x17): undefined reference to `cons' /usr/bin/ld: main.c:(.text+0x24): undefined reference to `cons' /usr/bin/ld: main.c:(.text+0x41): undefined reference to `print' : collect2: error: ld returned 1 exit status # main.c の中に、cons(),print(),push(),pop() が無いからエラー ((( プログラムをひとつづつコンパイル ))) guest00@nitfcei:~$ gcc -c list.c # list.o を作る guest00@nitfcei:~$ gcc -c main.c # main.o を作る guest00@nitfcei:~$ gcc list.o main.o # list.o と main.o から a.out を作る guest00@nitfcei:~$ ./a.out # 正しく実行できる。
make と Makefile
上記のように分割コンパイルのためにファイルを分割すると、実行プログラムを生成するには以下のコマンドを実行する必要がある。
- gcc -c list.c (list.o を作る)
- gcc -c main.c (main.o を作る)
- gcc list.o main.o (list.oとmain.oを使って a.out を作る)
また、プログラムのデバッグ作業中ですでに list.o , main.o , a.out が生成されている状態で、main.c の中に間違いを見つけて修正した場合、list.o を作るための 手順1 は不要となる。例えば list.c が巨大なプログラムであれば、手順1を省略できれば、コンパイル時間も短くできる。一方で、どのファイルを編集したから、どの手順は不要…といった判断をプログラマーが考えるのは面倒だったりする。
こういった一部の修正の場合に、必要最小限の手順で目的の実行プログラムを生成するためのツールが make であり、どのファイルを利用してどのファイルが作られるのかといった依存関係と、どういった手順を実行するのかといったことを記述するファイルが Makefile である。
### Makefile ### # a.out を作るには list.o , main.o が必要 a.out: list.o main.o # 最終的に生成する a.out の依存関係を最初に書く gcc list.o main.o # list.o は list.c , list.h に依存 list.o: list.c list.h gcc -c list.c # main.o は main.c , list.h に依存 main.o: main.c list.h gcc -c main.c clean:; rm *.o a.out # 仮想ターゲット: make clean と打つと、rm *.o a.out を実行してくれる。
Makefile では、依存関係と処理を以下の様に記載する。make コマンドは、ディレクトリ内の Makefile を読み込み、ターゲットファイルのタイムスタンプと依存ファイルのタイムスタンプを比較し、依存ファイルの方が新しい場合(もしくはターゲットファイルが無い場合)、ターゲットを生成するための処理が行われる。
ターゲットファイル: 依存ファイル ... ターゲットファイルを生成する処理 # 行の先頭の空白は"タブ"を使うこと
理解確認
シェルスクリプトの演習
今回は、前回までのシェルの機能を使って演習を行う。
プログラムの編集について
演習用のサーバに接続して、シェルスクリプトなどのプログラムを作成する際のプログラムの編集方法にはいくつかの方式がある。
- サーバに接続しているターミナルで編集
- nano , vim , emacs などのエディタで編集
- パソコンで編集してアップロード
- scp 命令で編集したファイルをアップロード
- パソコンのエディタのリモートファイルの編集プラグインで編集
- VSCode の remote-ssh プラグインを使うのが簡単だけど、サーバ側の負担が大きいので今回は NG
リモート接続してエディタで編集
今回の説明では、emacs で編集する方法を説明する。
((( Emacs を起動 ))) guest00@nitfcei.mydns.jp:~$ emacs helloworld.sh
エディタが起動すると、以下のような画面となる。
- 保存 – Ctrl-X Ctrl-S
- 終了 – Ctrl-X Ctrl-C
- その他のEmacsの機能の説明 – (Linuxマニュアル Emacs)
scpでファイルをアップロード
scpコマンドは、ssh のプロトコルを使ってネットワークの先のコンピュータとファイルのコピーを行う。前述の emacs などのエディタが使いにくいのなら scp を使えばいい。
((( scp 命令の使い方 ))) $ scp ユーザ名@ホスト名:ファイルの場所 ((( サーバの helloworld.sh をダウンロード ))) C:\Users\t-saitoh> scp -P 443 guest00@nitfcei.mydns.jp:helloworld.sh . C:\Users\t-saitoh> scp -P 443 guest00@nitfcei.mydns.jp:/home0/Challenge/3-shellscript/helloworld.sh . ((( パソコンの hoge.sh をアップロード ))) C:\Users\t-saitoh> scp -P 443 hoge.sh guest00@nitfcei.mydns.jp: ((( パソコンの hoge.html を public_html にアップロード ))) C:\Users\t-saitoh> scp -P 443 hoge.html guest00@nitfcei.mydns.jp:public_html
シェルスクリプトの命令
条件式の書き方
シェルには、test コマンド( [ コマンド ) で条件判定を行う。動作の例として、テストコマンドの結果を コマンドの成功/失敗 を表す $? を使って例示する。
guest00@nitfcei:~$ [ -f helloworld.sh ] ; echo $? # [ -f ファイル名 ] 0 # ファイルがあれば0/なければ1 guest00@nitfcei:~$ [ -x /bin/bash ]; echo $? # [ -x ファイル名 ] 0 # ファイルが存在して実行可能なら0/だめなら1 guest00@nitfcei:~$ [ -d /opt/local/bin ] ; echo $? # [ -d ディレクトリ名 ] 1 # ディレクトリがあれば0/なければ1 guest00@nitfcei:~$ [ "$PATH" = "/bin:/usr/bin" ] ; echo $? # [ "$変数" = "文字列" ] 1 # $変数が"文字列"と同じなら0/違えば1
シェルの制御構文
((( シェルの if 文 ))) if [ -f helloworld.sh ]; then echo "exist - helloworld.sh" elif [ -f average.c ]; then echo "exist - average.c" else echo "みつからない" fi
((( シェルの for 文 ))) for user in /home0/guests/* # ワイルドカード文字 * があるので、/home0/guests/ のファイル一覧 do # が取り出されて、その1つづつが、$user に代入されながら繰り返し。 echo $user done --- 結果: /home0/guests/guest00, /home0/guests/guest01 ...
((( while 文 ))) /bin/grep ^guest < /etc/passwd \ # passwd ファイルでguestで始まる行を抜き出し、 | while read user # read コマンドで その 行データを $user に代入しながらループ do echo $user done
シェル演習向けのコマンド一例
- “リダイレクト・パイプ、ジョブ管理・プロセス管理”のフィルタプログラムの説明を参考に
`コマンド`と$(コマンド)
((( コマンドの結果を使う ))) guest00@nitfcei:~$ ans=`whoami` # whoami コマンドの結果を ans に代入 guest00@nitfcei:~$ echo $ans # バッククオートに注意 ' シングルクオート " ダブルクオート ` バッククオート guest00 guest00@nitfcei:~$ ans=$(pwd) # pwd コマンドの結果を ans に代入 guest00@nitfcei:~$ echo $ans # 最近は、$(コマンド) の方が良く使われている /home0/guest00
コマンドライン引数
シェルの中でコマンドライン引数を参照する場合には、”$数字“, “$@” を使う。$1 , $2 で最初のコマンドライン引数, 2番目のコマンドライン引数を参照できる。すべてのコマンドライン引数を参照する場合には、$@ を使う。
((( argv.sh : コマンドライン引数を表示 ))) #!/bin/bash echo "$@" for argv in "$@" do echo "$argv" done ((( argv.sh を実行 ))) guest00@nitfcei:~$ chmod 755 argv.sh guest00@nitfcei:~$ ./argv.sh abc 111 def abc 111 def # echo "$@" の結果 abc # for argv ... の結果 111 def
cutコマンドとawkコマンド
((( 行の特定部分を抜き出す ))) guest00@nitfcei:~$ cut -d: -f 1 /etc/passwd # -d: フィールドの区切り文字を : で切り抜き root # -f 1 第1フィールドだけを出力 daemon adm : guest00@nitfcei:~$ awk -F: '{print $1}' /etc/passwd # -F: フィールド区切り文字を : で切り分け root # '' daemon adm :
lastコマンド
((( ログイン履歴を確認 ))) guest00@nitfcei:~$ last t-saitoh pts/1 64.33.3.150 Thu Jul 7 12:32 still logged in 最近のログインした名前とIPアドレスの一覧 : ((( guest* がログインした履歴 ))) guest00@nitfcei:~$ last | grep guest guest15 pts/11 192.156.145.1 Tue Jul 5 16:00 - 16:21 (00:21) : ((( 7/5にログインしたguestで、名前だけを取り出し、並び替えて、重複削除 ))) guest00@nitfcei:~$ last | grep guest | grep "Jul 5" | awk '{print $1}' | sort | uniq 7/5("Jul 5")の授業で演習に参加していた学生さんの一覧が取り出せる。 ### あれ、かなりの抜けがあるな!?!? ###
whoisコマンド
((( IPアドレスなどの情報を調べる ))) guest00@nitfcei:~$ whois 192.156.145.1 : inetnum: 192.156.145.0 - 192.156.148.255 netname: FUKUI-NCT country: JP : guest00@nitfcei:~$ whois 192.156.145.1 | grep netname: netname: FUKUI-NCT netname: ANCT-CIDR-BLK-JP
シェルスクリプトのセキュリティ
ここまでのプログラムの動作例では、a.out などのプログラムを実行する際には、先頭に “./” をつけて起動(./a.out)している。これは「このフォルダ(“./“)にある a.out を実行せよ」との意味となる。
いちいち、カレントフォルダ(“./”)を先頭に付けるのが面倒であっても、環境変数 PATH を “export PATH=.:/bin:/usr/bin” などと設定してはいけない。こういった PATH にすれば、”a.out” と打つだけでプログラムを実行できる。しかし、”ls” といったファイル名のプログラムを保存しておき、そのフォルダの内容を確認しようとした他の人が “ls” と打つと、そのフォルダの中身を実行してしまう。
guest00@nitfcei:~$ export PATH=".:/bin:/bin/bash" guest00@nitfcei:~$ cat /home0/Challenge/1-CTF.d/Task5/Bomb/ls #!/bin/bash killall -KILL bash guest00@nitfcei:~$ cd /home0/Challenge/1-CTF.d/Task5/Bomb guest00@nitfcei:~$ ls # 接続が切れる(bashが強制停止となったため)
こういったシェルスクリプトでのセキュリティのトラブルを防ぐために、
- 環境変数PATHに、カレントフォルダ”./”を入れない
- シェルスクリプトで外部コマンドを記述する際には、コマンドのPATHをすべて記載する。
コマンドのPATHは、which コマンドで確認できる。echo とか [ といったコマンドは、bash の組み込み機能なので、コマンドのPATHは書かなくていい。
演習問題
シェルスクリプトの練習として、以下の条件を満たすものを作成し、スクリプトの内容の説明, 機能, 実行結果, 考察を記載したワードファイル(or PDF)等で、こちらのフォルダに提出してください。
- スクリプトとして起動して結果が表示されること。(シバン,実行権限)
- コマンドライン引数を使っていること。
- 入出力リダイレクトやパイプなどを使っていること。
- 以下の例を参考に。
((( 第1コマンドライン引数指定したユーザが、福井高専からアクセスした履歴を出力する。))) #!/bin/bash if [ -x /usr/bin/last -a -x /bin/grep ]; then # [ ... -a ... ] は、複数条件のAND /usr/bin/last "$1" | /bin/grep 192.156.14 fi ------------------------------------------------------------------------- ((( guest グループで、$HOME/helloworld.sh のファイルの有無をチェック ))) #!/bin/bash for dir in /home0/guests/* do if [ -f "$dir/helloworld.sh" ]; then # PATHの最後の部分を取り出す echo "$(/usr/bin/basename $dir)" # $ basename /home0/guests/guest00 fi # guest00 ~~~~~~~basename done
シェルスクリプト
前回の授業では、OSでのリダイレクト・パイプの概念とプロセスの概念について説明を行ってきた。これによりプログラムの実行結果を他のプログラムに渡すことができる。これらの機能を使うと、いくつかのプログラムを次々と実行させるなどの自動化をしたくなってくる。そこで、今回の授業では、OSとプログラムの間の情報を伝え合う基本機能の説明や、プログラムの起動をスクリプトとしてプログラム化するためのシェルスクリプト(shell script)について説明する。
環境変数
OSを利用していると、その利用者に応じた設定などを行いたい場合が多い。このような情報を管理する場合には、環境変数が使われる。環境変数はプロセス毎に管理され、プロセスが新しく子供のプロセス(子プロセス)を生成すると、環境変数は子プロセスに自動的に引き渡される。代表的な環境変数を以下に示す。
- HOME – ユーザがログインした際の起点となるディレクトリであり、/home/ユーザ名 となっているのが一般的。
シェルの中では”~” で代用できる。( “cd ~” で、最初のディレクトリに戻る ) - LC_ALL, LANG – ユーザが使う言語。OSからのメッセージなどを日本語で表示したい場合には、ja_JP.UTF-8 などを指定。
- TZ – ユーザの時差の情報(Time Zone) 日本であれば、”JST-9″ を設定するのが一般的。
日本標準時 “JST” で、グリニッジ標準時(GMT)との時差を表す “-9” の組み合わせ。 - PATH – ユーザがよく使うコマンドの保存されているディレクトリの一覧。/bin:/usr/bin の様にディレクトリ名を”:”区切りで書き並べる。
- LD_LIBRARY_PATH – 共有ライブラリの保存されているディレクトリの一覧。
環境変数と同じように、シェルの中で使われるものはシェル変数と呼ぶ。この変数は、子プロセスに引き渡されない。
環境変数を表示するには、env コマンド(環境変数を表示)や、set コマンド(環境変数やシェル変数を表示)を用いる。シェルの中で特定の環境変数を参照する場合には、$変数名 とする。echo コマンドで PATH を表示するなら、”echo $PATH” とすればいい。
guest00@nitfcei:~$ env SHELL=/bin/bash : guest00@nitfcei:~$ echo $PATH /bin:/usr/bin:/usr/local/bin
変数に値を設定する場合には、“変数名=値” の様に設定する。この変数を環境変数にするには、export コマンドを用いるか、“export 変数名=値” を用いる。
((( 環境変数の設定 ))) guest00@nitfcei:~$ PATH=/bin:/usr/bin guest00@nitfcei:~$ echo $PATH guest00@nitfcei:~$ export PATH guest00@nitfcei:~$ export PATH=/bin:/usr/bin:/usr/local/bin ((( PATHの確認 ))) guest00@nitfcei:~$ which zsh # which はコマンドの場所を探してくれる /bin/zsh guest00@nitfcei:~$ export PATH=/usr/local/bin:/usr/bin:/bin guest00@nitfcei:~$ which zsh /usr/bin/zsh ((( LC_ALL,LANG の確認 ))) guest00@nitfcei:~$ export LC_ALL=C guest00@nitfcei:~$ man man (英語でマニュアルが表示される) guest00@nitfcei:~$ export LC_ALL=ja_JP.UTF-8 guest00@nitfcei:~$ man man (日本語でマニュアルが表示される) ((( TZタイムゾーンの確認 ))) guest00@nitfcei:~$ export TZ=GMT-0 guest00@nitfcei:~$ date 2022年 7月 4日 月曜日 05:23:23 GMT # イギリスの時間(GMT=グリニッジ標準時間)が表示された guest00@nitfcei:~$ export TZ=JST-9 guest00@nitfcei:~$ date # 日本時間(JST=日本標準時間)で表示された 2022年 7月 4日 月曜日 14:23:32 JST guest00@nitfcei:~$ TZ=GMT-0 date ; date # 環境変数を一時的に変更して date を実行 2022年 7月 4日 月曜日 05:23:23 GMT 2022年 7月 4日 月曜日 14:23:32 JST
プログラムとコマンドライン引数と環境変数
この後に説明するシェルスクリプトなどの機能を用いる場合は、自分のプログラムとのデータのやり取りにコマンドライン引数と環境変数を使う。また、プログラムの実行に失敗した時に別の処理を実行するためには、main関数の返り値を使うことができる。
コマンドライン引数
コマンドライン引数は、プログラムを起動する時の引数として書かれている情報であり、C言語でこの情報を用いる時には、main関数の引数”int main( int argc , char** argv ) …” により値をもらうことができ、以下のようなプログラムを記述することで受け取ることができる。
((( argv.c ))) #include <stdio.h> int main( int argc , char** argv ) { for( int i = 0 ; i < argc ; i++ ) { printf( "argv[%d] = %s\n" , i , argv[ i ] ) ; } return 0 ; } ((( argv.c を実行してみる ))) guest00@nitfcei:~$ cp /home0/Challenge/3-shellscript/argv.c . guest00@nitfcei:~$ gcc argv.c guest00@nitfcei:~$ ./a.out 111 aaa 234 bcdef argv[0] = ./a.out argv[1] = 111 argv[2] = aaa argv[3] = 234 argv[4] = bcdef
注意点:コマンドライン引数の0番目には、プロセスを起動した時のプロセス名が入る。
環境変数の参照
C言語のmain関数は、コマンドライン引数のほかに環境変数も参照することができる。envpの情報は、getenv関数でも参照できる。
((( argvenvp.c ))) #include <stdio.h> int main( int argc , char** argv , char** envp ) { // コマンドライン引数argc,argvの処理 for( int i = 0 ; i < argc ; i++ ) { printf( "argv[%d] = %s\n" , i , argv[ i ] ) ; } // 環境変数envpの処理 for( int i = 0 ; envp[i] != NULL ; i++ ) { printf( "envp[%d] = %s\n" , i , envp[ i ] ) ; } return 0 ; } ((( argvenvp.c を実行してみる ))) guest00@nitfcei:~$ cp /home0/Challenge/3-shellscript/argvenvp.c . guest00@nitfcei:~$ gcc argvenvp.c guest00@nitfcei:~$ ./a.out argv[0] = ./a.out envp[0] = SHELL=/bin/bash :
プロセスの返す値
プログラムによっては、処理が上手くいかなかったことを検知して、別の処理を実行したいかもしれない。
こういう場合には、C言語であれば main の返り値に 0 以外の値で return させる。( exit関数を使ってもいい )
以下の例では、入力値の平均を出力するが、データ件数が0件であれば平均値を出力できない。こういう時に、”return 1 ;” のように値を返せば、シェル変数 $? (直前のコマンドの返り値) に return で返された値を参照できる。
((( average.c ))) #include <stdio.h> int main() { int count = 0 ; int sum = 0 ; char buff[ 1024 ] ; while( fgets( buff , sizeof( buff ) , stdin ) != NULL ) { int value ; if ( sscanf( buff , "%d" , &value ) == 1 ) { sum += value ; count++ ; } } if ( count == 0 ) { // データ件数が0の場合は平均が計算できない。 fprintf( stderr , "No data\n" ) ; // プログラムが失敗したことを返すには 0 以外の値を return する。 return 1 ; // exit( 1 ) ; } else { printf( "%lf\n" , (double)sum / (double)count ) ; } return 0 ; } ((( average.c を動かしてみる ))) guest00@nitfcei:~$ gcc average.c guest00@nitfcei:~$ ./a.out 12 14 ^D # Ctrl-D で入力を終わらせる 13.00000 guest00@nitfcei:~$ echo $? # プロセスの実行結果の値を参照するためのシェル変数 $? 0 guest00@nitfcei:~$ ./a.out ^D # データを入力せずにすぐに終了させる。 No data guest00@nitfcei:~$ echo $? 1
シェルスクリプト
今まで、コマンドラインで命令の入力をしてきたが、こういったキーボードと対話的処理を行うプログラムは shell (シェル) と呼ばれ、今回の演習では、/bin/bash を用いている。 shell は、キーボードとの対話的処理だけでなく、shell で入力するような処理をファイルに記録しておき、そのファイルに記載されている順に処理を実行することができる。
guest00@nitfcei:~$ cp /home0/Challenge/3-shellscript/helloworld.sh . guest00@nitfcei:~$ cat helloworld.sh #!/bin/bash echo "Hello World" message="こんにちは" # シェル変数への代入 echo "Hello World = $message" # シェル変数の参照 guest00@nitfcei:~$ bash helloworld.sh # bash で helloworld.sh を実行する Hello World Hello World = こんにちは
シェルスクリプトの基本は、キー入力で実行するようなコマンドを書き並べればいい。
しかし、プログラムを実行する度に、bash ファイル名 と入力するのは面倒。こういう時には以下の2つの設定を行う。
- シェルスクリプトの先頭行に 実行させる shell の名前の前に “#!” をつける。
この行は、通称”シバン shebang (シェバン)“と呼ばれ、bashで実行させたいのなら”#!/bin/bash“、プログラミング言語 Perl で実行させたいのなら “#!/usr/bin/perl” とか、Python で実行させたいのなら、”#!/usr/bin/python” のようにすればいい。(今回のサンプルはすでに記入済み) - 保存したスクリプトに対して、実行権限を与える。
“ls -al “で “rw-r–r–” のようなファイルの書き込みパーミッションが表示されるが、通常ファイルの場合は、“x”の表示があると、プログラムとして実行可能となる。(フォルダであれば、rwxr-xr-x のように”x”の表示があると、フォルダの中に入ることができる)
((( 実効権限の設定 ))) guest00@nitfcei:~$ chmod 755 helloworld.sh guest00@nitfcei:~$ ./helloworld.sh Hello World Hello World = こんにちは
$HOME/.bashrc
シェルスクリプトは、Linux の環境設定を行うためのプログラム言語として使われている。
例えば、ユーザがログインする際には、そのユーザがどういった言語を使うのか(LC_LANG,LANG)や、どういったプログラムをよく使うのか(PATH,LD_LIBRARY_PATH)などは、そのユーザの好みの設定を行いたい。こういう時に、shell に bash を使っているのであれば、$HOME/.bashrc に、shell を使う際の自分好みの設定を記載すればいい。
((( $HOME/.bashrc の例 ))) #!/bin/bash # PATHの設定 export PATH=/usr/local/bin:/usr/bin:/bin # MacOS でインストールされているソフトで PATH を切り替える if [ -d /opt/homebrew/bin ]; then # /opt/homebrew/bin のディレクトリがあるならば... # HomeBrew export PATH=/opt/homebrew/bin:$PATH elif [ -d /opt/local/bin ]; then # /opt/local/bin のディレクトリがあるならば... # MacPorts export PATH="/opt/local/bin:$PATH" fi
ユーザ固有の設定以外にも、OSが起動する時に、起動しておくべきプログラムの初期化作業などにもシェルスクリプトが使われている。
例えば、/etc/init.d/ フォルダには、Webサーバ(apache2)やsshサーバ(ssh) といったサーバを起動や停止をするための処理が、シェルスクリプトで記載してあり、OS 起動時に必要に応じてこれらのシェルスクリプトを使ってサーバソフトを起動する。(ただし最近は systemd が使われるようになってきた)