ソフトウェアII 第7回(2024/01/25)

本日のメニュー

  • シェルスクリプト入門
  • プログラミング言語紹介

本日は気楽に聞いてください。おそらく最大でも1コマ分程度の時間で終わると思います。(その場合は適宜任意の質問タイムにしますので、試験勉強がんばってください。)

シェルスクリプト入門

今日はこれまで学んできたC言語から少し離れて、これまで「コンパイル」と「プログラムの実行」を担ってきたターミナルを少ししっかりと使うことを考えてみましょう。

コマンドラインシェル

ターミナルに入力されたユーザから文字列を解釈し、それを適切にOSに伝える役割を果たすのが「コマンドラインシェル」です。直感的にはユーザが入力したコマンドを順次実行していく様子を記述するプログラミング言語とみなすことができます。ターミナルから何らかの作業をしたいという観点に立った時、コマンドラインシェルを使うことは以下の点で有用です。

  • ファイルの操作やプログラムの実行を簡単に書くことができる
  • すでに存在する複数のプログラムを組み合わせて利用する

例えばファイルをあるディレクトリから別のディレクトリに移すという操作をしたい場合に、C言語でこれをわざわざ記述するのは現実的ではありません。例えばディレクトリAにあるhoge.txt というファイルをディレクトリBに移動したい場合、

#include <stdio.h>

int main(void){
    char old[] = "A/hoge.txt";
    char new[] = "B/hoge.txt";
    
    rename(old, new);
    return 0;
}

というコードをコンパイルして実行すれば確かに要件を満たしますが、コマンドmv を知っている上で、

mv A/hoge.txt B

とすればより簡単です。Unix系を扱う上では標準とされているコマンドは先人たちの需要に基づいて取り込まれてきたものですので、これらを知ってさっと使えることは非常に価値があります。これらに加えて、コマンドラインシェルにもC言語などと同様に制御構造が存在し、例えば「似たようなことを変数を変えてN回繰り返す」といった処理もコマンドラインシェルで記述することができます。

今日はこのようなUnix環境におけるコマンドラインシェルの中でも、特に現在の多くの環境で標準となっているBash を使ってみましょう。今時はzsh だろとか fish が良いよねとか先祖からの教えでtcshを使うことになっているとかを語れる方は今日は気楽に聞いてください。 Unix系環境を想定しているので、Windowsユーザの場合はWSLやCygwin、msys などを想定しています。Powershell は残念ながら互換性がないので、別扱いです。

コマンド

これまでもソフトウェア1/2を通して基本的なコマンドにはいくつか触れてきたと思います。少し復習しましょう。

  • ls: ファイル一覧表示
  • pwd: 現在のディレクトリを表示
  • cd: ディレクトリ の移動
  • cat: 引数で与えられたファイルを連結し標準出力へ(1ファイルだと中身の表示)
  • mkdir: ディレクトリ 作成
  • mv: ファイルの移動/リネーム
  • cp: ファイルのコピー
  • rm: ファイルの削除

上記はおもにファイル自体を取り扱うコマンドでした。

他に頻出のコマンドとしてecho があります。

  • echo: 引数の文字列をそのまま標準出力へ

もうちょっとコマンド

基本的なコマンドをいくつかざっくり紹介します。

  • head : ファイルの先頭から指定行数表示
  • tail : ファイルの末尾から指定行数表示
  • sed : ストリームエディタ。よく使うのは文字列置換。
  • nl : 行頭に行番号を付与
  • find: 指定条件を満たすファイルを探してくる
  • xargs: 受け取った入力をコマンドライン引数にする
  • sort : 指定列の条件に基づいてファイルの行を並べ替え
  • uniq : ファイル中ユニークな行を取り出してくる

上記は主にファイルを操作したりするコマンドですが、もちろん他にも基本として知っておくべきコマンドは数多く存在します。触っていくうちに慣れていきましょう。

素早くターミナルを操作してみる

ターミナルを操作する際に、矢印だけを使っていないでしょうか。bashで使えるショートカットを使ってみましょう。以下の表ではCtrlキーとの同時押しをC-xMetaキー(WindowsだとAlt / Macだと esc が標準)との同時押しをM-x と記載します。

ショートカット 機能
C-a 行頭へ移動
C-e 行末へ移動
C-f 1文字進む
C-b 1文字戻る
M-f 1単語進む
M-b 1単語戻る
C-l 現在行を残して画面クリア
C-k 行末まで切り取り削除
C-u 行頭まで切り取り削除
C-y 上記で切り取ったものを貼り付ける
C-r コマンド履歴のインクリメンタルサーチ

これらのショートカットとファイル名などのTAB補完を効果的に用いることでタイピング速度はそれほどでなくても、かなり効率的に作業が進められるようになります。

連続する数字を出力する

例えば、整数引数を受け取って、それが3の倍数のときに「 3で割り切れるので、3で割り切れます」と出力する以下のC言語のプログラムがあったとします。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv){
    if (argc != 2){
	fprintf(stderr, "usage: %s integer\n", argv[0]);
	exit(1);
    }
    int num = atoi(argv[1]);
    if ( num % 3 == 0)
	printf("(Shinjiro): %dは 3で割り切れるので、3で割り切れます。\n", num);
    
    return 0;
}

上記のプログラムをコンパイルして、プログラムを実行可能にします。

gcc -o shinjiro shinjiro.c 

さてこのプログラムが正しく動きそうか、1から1000 ぐらいまで試したいとします。これまではこのような繰り返しはCのプログラムの方に書いていたかと思います。もしくはターミナルに変数を変えながら1000回Enterを叩くという人もいたかもしれません。しかし、よくあるシチュエーションとして実験用のプログラムを汎用的に作ったのち、それをテストする場合は、変化させるパラメータなどはシェルで制御する方が簡単なケースが多いです。

bashで連続する数字を出力する場合、いくつかの方法がありますが、ここではforループを使う方法を示します。

for i in {1..1000}; do ./shinjiro $i ; done

上記の構文はbash で繰り返しを書く際によく書く構文です。i は変数であり、in 以下の要素を順次代入します。変数名はiに限りません。実際に実行されるのは do から done に囲まれた部分で、for のところでセットされたi を $i という形で参照しています。なお、{1..100} という書き方はbash において、空白区切りで数字の列を出力するものです。

echo {1..10}

などとするとどのような出力が得られるでしょうか?この{..} の記述は便利で、例えばaからzまでの系列を得たい場合には

echo {a..z}

とすると空白区切りで出力できます。一つ注意点として、開始・終了の文字や数字と.. の間に空白を入れてはいけません。

スクリプトを書いてみる

シェルスクリプトというものを書いてみましょう。シェルスクリプトは文字通りコマンドラインシェルをベースにしたスクリプト言語です。スクリプトといっても一番最初は、一回の処理で使うコマンドを羅列するだけで立派なシェルスクリプトになります。以下の内容をhelloworld.sh という名前で保存してみましょう。

#!/bin/bash

# シェルスクリプトでは #以降がコメントになる
echo "Hello, World"
echo "Hello, Exam"
echo "Good bye Software2"

一行目にある#!/bin/bashシェバン とよばれ、OSにこのスクリプトはこの言語で実行してねということを指示するおまじないです。Pythonを書いたことのある人は#!/usr/bin/pythonといった書き方をしたことがあるかと思います。

最も基本的な実行法はbash で呼び出すことです。

# bash で 今書いたスクリプトを実行する
bash helloworld.sh

ただ、実際にはスクリプト単独で実行するケースが多いので、一工夫します。まずは現在のファイルのパーミッションを確認してみましょう。ファイルのパーミッションとは、今のユーザがそのファイルに対して何ができるのかの情報です。helloworld.sh があるディレクトリで以下を実行します。

# ls に -l オプションをつけて詳細情報を表示
ls -l

以下のような結果が得られるかと思います。

-rw-r--r--  1 dsk_saito  staff  79  1 19 09:53 helloworld.sh

先頭にあるr``w 等がファイルに対する権限を表して、このファイルは

  • 所有者(最初のブロック)は読み取りr と 書き込みw ができる
  • 同じグループのユーザ(2番目のブロック)は読み取りrのみできる
  • グループも異なる他のユーザ(3番目のブロック)は読み取りrのみできる

この状態ではこのシェルスクリプト単体で実行することができません。

以下のコマンドでスクリプトに実行権限を付与します。

chmod u+x helloworld.sh
ls -l 
# -rwxr--r--  1 dsk_saito  staff  79  1 19 09:53 helloworld.sh

このコマンドは所有者(ユーザ: u)に実行権限(x)を付与する(+)という意味になります。

この状態で以下のように実行ができます。

./helloworld.sh
Let's try
  • 上記の手順でhelloworld.sh の写経と実行をしてみましょう。

シェルスクリプトにおける変数

シェルスクリプトにおける変数は基本的に全て文字列を扱います。幸か不幸か型はありません。文字列ですがダブルコーテーションで囲む必要はありません。シェルスクリプトにおける変数の最大の注意点は以下です。

  • 変数を代入する際は空白を開けずに=で代入する。$ はつけない
  • 変数を使うとき(参照するとき)は$をつける

例をみてみましょう。

#!/bin/bash

teacher=situ
comment="Good afternoon"

echo $comment, ${teacher}

変数参照は$をつける他にも、${} で囲むことも可能です。

計算する

シェルスクリプトにおける変数は基本的に文字列なので、数字も文字列として解釈されます。たとえば以下の例を実行してみましょう

#!/bin/bash

x=10

echo $x
echo $x+2

ただし、数字を扱いたい場合もあるので、その場合は$(()) という変数展開を使います。

#!/bin/bash

x=10

echo $x

echo $((x+2))

$(( )) は内部で演算を行ってくれる便利な変数展開です。これを使う場合は中のx は$が不要です。なお$(()) 内部は空白に対して寛容です。注意点としてbash の場合は整数演算のみです。zshと呼ばれるbashより高機能なシェル(bashの構文はほぼ使える)では、実数の演算も受け付けます。

変数展開をもう少し詳しく

bashの変数は様々な形で展開することができます。以下に例を示します。一つずつみていきましょう。こちらの変数展開では{} で変数を囲みつつ必要な演算をかきます。

#!/bin/bash

var="ThisSampleisBoring.jpg"

echo ${var}
echo ${#var} # 文字数を出力
echo ${var:0:4} # 0番目から4文字取り出し
echo ${var:12:6} # 12番目から6文字取り出し
echo ${var%.jpg} # 末尾から.jpg を取り除く
echo ${var#This} # 先頭からThis を取り除く
echo ${var%s*} # 末尾からsで始まる任意長の文字列を最短一致で取り除く
echo ${var%%s*} # 末尾からsで始まる任意長の文字列を最長一致で取り除く
echo ${var#*s} # 先頭からsで終わる任意長の文字列を最短一致で取り除く
echo ${var##*s}  # 先頭からsで終わる任意長の文字列を最長一致で取り除く
echo ${var/Sample/Lecture} # Sample を Lecture で置換
echo ${var/is/as} # is を asで置換(1回)
echo ${var//is/as}  # is を as で置換(全て)

上の変数展開を用いることで、文字列の抽出や置換が容易に実現できます。例えばファイルの拡張子部分を除いて別のファイルを作るなどの場合に用いることができます。

Let's try

ここまでのシェルスクリプトを写経して実行してみましょう。

コマンドの結果を代入する

コマンドの結果を代入する方法は二つあります。一つは$() で囲む方法です。

#!/bin/bash

# printf コマンドで0埋めで4桁表示した結果をvar に代入
var=$(printf %04d 5)
echo $var

もう一つはバックコーテーション` で囲む方法です。

#!/bin/bash

# printf コマンドで0埋めで4桁表示した結果をvar に代入
var=`printf %04d 5`
echo $var

for ループをもう一度

forループを使うケースはいくつかあります。以下に例を示します。

#!/bin/bash

# 決まった回数(1から10まで)実行
for n in {1..10}; do 
    echo "Hello, world ($i)"
done

ちょっと便利なコマンドにseq があります。

seq 1 10 # 1から10まで表示
seq 0 2 10 # 0から10まで2個飛ばしで表示

これを利用してforループを書くと以下のようになります

#!/bin/bash

for i in $(seq 0 2 10); do
    echo "Hello, world ($i)"
done

ディレクトリ以下にあるファイルそれぞれに操作をしたい場合をやってみましょう。下準備としてディレクトリ の下に100個ファイルを作ります。各ファイルにはランダムな数字が入るとします。シェルでは環境変数$RANDOM が、毎回異なる乱数を出してくれるのでこれを利用します。

#!/bin/bash

# 0 から99までファイルを作る
# ファイル名は00.txt , 01.txt, ....

for i in $(seq 0 99);do
    var=$(printf %02d $i)
    echo $RANDOM > $var.txt
done

ディレクトリを見てみると100個ファイルができていることがわかります。

続いてこれらのファイルに、データ0 を追記したいとします。

#!/bin/bash

# * でそのディレクトリ以下のファイル全てを指定する。拡張子で制約することもできる
# >> で追記する
for i in *.txt ; do
    echo 0 >> $i
done

最後に拡張子.txt を除いてみましょう。

#!/bin/bash

for i in *.txt ; do
    mv -v $i ${i%.txt}
done

if 文と演算子

例えば、決められたファイル名を並べて、該当するファイルが存在するときだけls するようなシチュエーションを考えましょう。シェルスクリプトにもif文による制御が存在します。

#!/bin/bash

for i in {a.txt,b.txt,c.txt};do
    if [ -e $i ];then
	ls -l $i
    fi
done

シェルスクリプトのif 文は非常に空白にうるさいのですが、条件文を空白を開けて [ と ] で囲みます。-e はその名前のファイルが存在するかどうかをチェックする演算子です。他にも以下の表のような演算子があります。

ファイル演算子 役割
-d file ディレクトリかどうか
-f file ファイルかどうか
-e file ファイルが存在するかどうか
-L file シンボリックリンクかどうか
-r file 読み取り可能か
-w file 書き込み可能か
-x file 実行可能か

引数を取り出す

シェルスクリプトでは引数は$1、$2 といった形で取り出します。$0 は自身のプログラム名です。$# で引数の個数を取り出せます。

#!/bin/bash

if [ $# != 2 ]; then
    echo "usage: $0 arg1 arg2"
    exit 1;
fi


echo "0: $0"
echo "1: $1"
echo "2: $2"

Let's try

ここまでのスクリプトを写経してそれぞれ実行してみましょう

まとめ

シェルスクリプトの機能をかなりざっくり説明しました。シェルスクリプトの基本機能の範囲でC言語 / Pythonで書くには複雑になることを場合によっては極少量の記述で実現可能になります(いちいちPython書くな的なシチュエーションですね)。またUNIXには先人たちの所産であるコマンド群が存在するので、それらと組み合わせることで作業の効率化を実現できます。

一方、実行速度はあくまでコマンドを一つずつ実行するのと同様ですので、お世辞にもものすごく速いとはいえません。ファイル操作等であっても本当に速さが要求される場合は、C言語で専用の処理を書く必要性もありますが、普段使いでシェルスクリプトを使いこなすことで、大幅な効率化が実現できます。

今回紹介しなかった機能として

  • ファイルリダイレクト・パイプの詳細
  • プロセスのファアグラウンド・バックグラウンド処理
  • 配列
  • プロセス置換

などがあります。興味がある人はキーワードをもとにぜひ春休みの期間に少しターミナルに慣れ親しみつつ進めるとよいと思います。

プログラミング言語紹介

ここでは、いくつかのプログラミング言語をごくごく簡単に紹介していきます。キーワードをヒントに次に学ぶ参考にしてください。後半につれ、如実にバテていくため、必要な部分は講義後追記します。

アセンブリ

CPUに与える機械語プログラムは純粋なビット列ですが、さすがにそのままでは人間が直感的に理解するのは難しいため、ビット列に対応する文字列命令で記述したもの。C言語よりもさらに機械語に近いと考えると良い(低水準 と言われる)。

C言語から当該CPUのアセンブリを出力したい場合は以下のようにコンパイルする。

// helloworld.c
#include <stdio.h>

int main(){
    printf("Hello, World\n");
    return 0;
}
gcc -S helloworld.c

-Sオプションをつけることで、このCPUのアセンブリが出力される。M1 mac だと以下のような感じ。

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 12, 0	sdk_version 13, 1
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #32
	stp	x29, x30, [sp, #16]             ; 16-byte Folded Spill
	add	x29, sp, #16
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	mov	w8, #0
	str	w8, [sp, #8]                    ; 4-byte Folded Spill
	stur	wzr, [x29, #-4]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	ldr	w0, [sp, #8]                    ; 4-byte Folded Reload
	ldp	x29, x30, [sp, #16]             ; 16-byte Folded Reload
	add	sp, sp, #32
	ret
	.cfi_endproc
                                        ; -- End function
	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"Hello, World\n"

.subsections_via_symbols

用途としては、コンパイラ自体の開発や、コードの高速化における解析などが挙げられる。CPU自体の研究をする場合にも触れる可能性が大きい。 なお、C言語でも、一部をアセンブリで記述する方法があり、予約後 asm を使ってCのソースコードに直接アセンブリを記述できる機能がある(インラインアセンブラ)と呼ばれる。

Fortran

C言語の大先輩。世界最初の高水準言語として1950年代に登場。FORTRAN と書くとFOTRAN 77 と呼ばれる70年代の仕様、Fortran と書くと、90年代以降の仕様を指す。Fortran 90以降は比較的柔軟にかけるようになった。数式記述が直感的に数値計算分野ではいまだに用いられる。行列計算のライブラリなどはC言語で使われるものでも、Fortran で書かれたものがある(BLASやLAPACKなど)。

余談ですがこの言語、配列のインデックスが1スタートなので、「1からプログラミングを教えてください」と言われた場合、こちら系の言語を教えるのかなと思ったりします。「0からプログラミングを教えてください」と言われたらひとまずC言語を教えます。

! test.f90
! コメントは! で 
program main
  integer :: a = 10
  real :: d = 3.34
  real v(5)

  print *, a
  print *, d

  do i=1,5
    v(i) = i
  end do

  do i=1,5
    print *,v(i)
  end do 
end program main

C++

C言語にオブジェクト指向の概念を入れたもの。というのが基本的な説明ですが、言語仕様の拡張により柔軟な記述力とC言語由来の高速性を兼ね備えています。以下は、入門書によくあるiostream を使ったhello worldと、何となくC++っぽい要素を入れたものです。

#include <iostream> // .h が要らない
#include <cstdio> // c*** で c言語標準もほぼ使える

int main(){
    // std は名前空間と呼ばれる
    std::cout << "Hello, world" << std::endl;
    // cstdio を読んでいるので、もちろん以下も可能

    printf("Hello, world\n");

    // c++ では 代入以外に、初期化子という記法が使える
    int i = 10;
    int j(10);

    std::cout << i << " " << j << std::endl;

    return 0;
}

コンパイラはg++です。

g++ helloworld.cpp

以下、段々簡潔に....

Java

Cのちにできた、オブジェクト指向のプログラミング言語。JVMという仮想マシンで動く中間コードを生成し、実行マシンへの依存が少ないため30億のデバイスで動く(客先では動かないかもしれない)。CやC++に比べると、ポインタを直接触らせないなど、安全性もかなり配慮されている。

Perl

スクリプト言語の古参と言えばこれ。100人いれば100通りの書き方があると言われるあたり、Pythonとは根本的に思想がことなる。文字列操作にかなり強く、2024年現在、40代から60代の人は実務で世話になっているケースも多い。

Ruby

まつもとゆきひろ氏開発のスクリプト言語。齋藤の感覚では島根といえば鷹の爪とRuby と思っている。Perlからの影響を受けつつ、簡潔な文法で人気も高い。Ruby on Rails など、Web系での実績も多い。機械学習系には若干弱いか。

Python

現在、かなり広く用いられているスクリプト言語。強力なライブラリ群が開発されており、人工知能ブームも相まって、かなり使用者が多い。速度面では遅いが、コンパイラ言語でライブラリ部分を組み込むなどでカバーしている。

その他

ちょっとバテましたので、名前をとりあえず.....

  • なでしこ
  • MATLAB
  • Octave
  • Julia
  • PHP
  • Javascript
  • Go
  • D
  • Rust
  • C#
  • Objective-C
  • Swift
  • Haskell
  • ...

講義内演習

時間があれば、簡単なC言語のプログラムをちょっとずつC++にしていくお話をしようと思います。

アンケート

UTAS上で工学部が出している授業評価アンケートがありますので、そちらに回答をお願いします。成績には一切影響しません。

またソフトウェア2について、別途、より詳細なアンケートをとろうと思います。こちらも成績には一切影響しません。試験で忙しいと思うので、落ち着いた頃にかSlackかITC-LMSにて連絡します。こちらもぜひ忌憚なくコメントをお願いします。

フリーディスカッション

適当な時間、講義全体に対して自由に質問コメントを受け付けます。