はじめに
各シェルの違いを調べていた所 ksh93 はスクリプト言語として機能が想像以上に強化されていることに気づきました。ksh88 は POSIX シェル標準化のベースにもなったシェルで ksh93 は ksh88 の正統な後継シェルです。そのわりに ksh93 の情報は少ないように思えます。そこでこの記事では ksh93 がスクリプト言語としてどのような機能を持っていたのかをまとめてみました。
なお、この記事は ksh93 でのプログラミングを薦めているわけではありません。単に私がシェルスクリプトによるプログラミング環境を改善するためにシェルスクリプトの理解を深めその可能性を知るために調べているだけです。プログラミングをする上では他の言語の方が圧倒的に生産性は上です。
KornShell (ksh) とは
KornShell (ksh) は Unix を開発した AT&T でデビッド・コーンによって開発されたシェルで Bourne シェルの後継として AT&T の標準シェルとなったシェルです(歴史)。後期の Unix でも ksh がシステムシェルとして採用されていました。シェルとしての機能だけでなくスクリプト言語としての機能もかなり強化されており、ksh93 で拡張された機能の一部は bash、mksh、zsh にも取り入れられています、現在 bash の拡張機能と思われているものの多くは実は ksh93 がオリジナルです。
しかしながら ksh93 の情報は bash や zsh に比べるとかなり少ないように思えます。その理由はひとえにクローズドだったからでしょう。ksh93 は ksh88 から引き続きプロプライエタリソフトウェアとして開発が行われ 2005 年にようやくオープンソースになりました(Debian でパッケージが提供されたのは 2009 年)。一方 bash は Linux が採用した GNU プロジェクトの一つで初版の 1989 年からずっとオープンソースです。Linux とインターネットが大きく普及したのは 1990 年台後半~2000 年台前半頃ですが、その当時 ksh を使っていた人は高額な商用 Unix を仕事で使っていた人ぐらいなわけで、bash の方が広まり ksh93 の情報が少ないのは当然と言えます。
日本語の ksh93 の書籍も少ない(ない?)です。2001 年にオライリーからでている「入門 Kornシェル」は 1st Edition で ksh88 を対象としているので注意してください。この本は訳者まえがきによると 1994 年にインターナショナル・トムソン・パブリッシング・ジャパンから発行され絶版となった「Korn Shell入門」の復刻版らしいので内容はほぼ同じだと思います。
KornShell ファミリー
- AT&T 公式
- AT&T 非公式
- クローン(ソースコードを共有していない)
- pdksh - ksh88 のパブリックドメイン実装版。1999 年に開発が終了
- OpenBSD ksh - 2003 年頃に pdksh をフォークしてコードのクリーンアップ、バグ修正を行う
- mksh - pdksh のフォーク。OpenBSD ksh の成果を引き継ぎ 2004 年以降は独自に機能強化。現在も開発は続いている
参考
ksh93 の機能
データ型(属性)
正確には型というより属性ですが typeset
コマンドで変数を整数型などにすることができます。export
や readonly
も実は変数につけられる属性として扱われています。typeset
(delare
) コマンドによる属性の追加は ksh88 の時代から存在し bash、mksh、yash、zsh など多くのシェルで導入されています。
整数型
整数属性をつけると数値または算術式しか入れられなくなります。文字列は入らないと思うかもしれませんが算術式になっていれば入ってしまうので注意が必要です。
$ typeset -i i=123
$ echo "$i"
123
$ typeset -i i=1+2 # $((1+2)) と同じ扱い
$ echo "$i"
3
$ foo=123
$ typeset -i i="foo" # 文字列のように見えて変数 foo 参照する
$ echo "$i"
123
-l
を追加すると long integer になります。また typeset -li
には integer
というエイリアス(ksh93u+m ではシェルビルトインコマンドに変更された)が定義されており、integer 型のように定義することができます。
integer i=123
浮動小数点型
浮動小数点にも対応しています。こちらも算術式を入れることができます。出力形式は固定小数点形式(-F
)と指数形式(-E
)を選ぶことができます。
$ typeset -F f=0.000012345
$ echo "$f"
0.0000123450000
$ typeset -E e=0.000012345
$ echo "$e"
1.2345e-05
こちらも -l
を追加すると long float となり。また typeset -lE
には float
というエイリアスが定義されているため、float 型のように定義することができます。
float f=1.23
バイナリ型
バイナリデータを変数に入れることができます。データは base64 でエンコーディングされます。base64 を使うのはメモリ効率と \0 を含めたバイナリデータを安全に扱うためだと思われますが、あまり扱いやすい形式とは言えないですね。配列に 1 バイトずつ格納されていた方が良かった気がします。
$ typeset -b bytes
$ read -r -n 100 bytes <<<"あいうえお"
$ echo "$bytes"
44GC44GE44GG44GI44GKCg==
$ printf '%B' bytes
あいうえお
$ bytes=$(echo "かきくけこ" | base64)
44GL44GN44GP44GR44GTCg==
$ printf '%B' bytes
かきくけこ
バイナリデータの処理については Manipulating Binary Data Using The Korn Shell の解説が詳しいです。
真偽値型
ksh93 ではなく破棄された ksh2020 ですが、true / false しか入れることができない真偽値型が登場していました。
$ bool flag
$ flag=true
$ echo "$flag"
true
$ flag=false
$ flag=1
ksh: flag: invalid value 1
true / false は算術式として評価すると数値として扱うことができます。真が 1
であることに注意してください。
$ bool flag=true
$ echo $((flag)) # => 1
真偽値型は ksh93u+ および ksh93u+m では使えませんが列挙型を使って自分で定義することができます。列挙型の説明は次項を参照してください。
$ enum _Bool=(false true)
$ alias bool=_Bool
列挙型
指定した値しかいれることができない列挙型を定義することができます。前項の真偽値型はこれを使って実装されています。
$ enum fruits=(apple orange banana)
$ fruits fruit
$ fruit=dog
ksh: fruit: invalid value dog
$ fruit=banana
$ echo $fruit
banana
$ echo $((fruit))
2
連想配列 (Associative arrays)
連想配列は bash、zsh でも実装されています。yash や mksh でも実装の予定があるようです。連想配列は typeset -A
もしくは変数の割当で [キー]=値
形式で指定することで作成することができます。
typeset -A assoc=([foo]=1 [bar]=2 [baz]=3)
assoc=([foo]=1 [bar]=2 [baz]=3)
echo "${assoc[bar]}" # => 2
複合変数 (Compound variables)
構造体といえば伝わるかと思います。連想配列とは違ってそれぞれが個別の変数であり、それぞれに型を定義することもできます。一方キー一覧を取得するのは簡単にはできません。
$ typeset -C comp=(integer int=123; float f=1.23; bar="str")
$ typeset -p comp
typeset -C comp=(bar=str;typeset -l -E f=1.23;typeset -l -i int=123)
# echo "${comp.f}" # => 1.23
typeset -C
にも compound
というエイリアスが定義されています。上記の定義を compound
を使い複数行で書くとこのようになります。
compound comp=(
integer int=123
float f=1.23
bar="str"
)
ネストしたデータ構造
配列、連想配列、複合変数はそれぞれネストすることができ、複雑なデータ構造を作ることができます。
$ VAR=(
a
(
1
(
[FOO]=foo
[BAR]=(A=1 B=2)
[BAZ]=baz
)
3
)
c
)
$ typeset -p VAR
typeset -A VAR=([0]=a [1]=([0]=1 [1]=([BAR]=(A=1;B=2) [BAZ]=baz [FOO]=foo) [2]=3) [2]=c)
$ echo "${VAR[1][1][BAR].B}"
2
参照型 (Variable name references)
別の変数を参照する変数を定義することができます。これを利用すると間接参照を行うことができ、関数に配列や複合変数の中身を渡す代わりに変数名を渡すとことで効率的に関数を呼び出すことができます。
value=123
typeset -n ref=value
ref=456
echo "$value" # => 456
typeset -n
には nameref
というエイリアスが定義されています。これを使った間接参照のコード例です。POSIX シェルで必要な eval
を無くすことができます。
inc() {
nameref _v=$1
_v=$((_v + 1))
}
value=1
inc value
echo "$value" # => 2
ディシプリン関数 (Discipline functions)
ディシプリン関数は変数の参照や代入時に呼び出される関数で JavaScript の Getter/Setter に相当するものです。AIX の日本語ドキュメントでは制御手順関数と訳されています。例えば一部のシェルには参照するたびにランダムな値を返す RANDOM
変数がありますが、これと似たような感じで現在の UNIX 時間を返す UNIXTIME
変数を実装することができます。
UNIXTIME.get() {
.sh.value=$(date +%s)
}
echo "$UNIXTIME" # => 1628774715
代入できる値にバリデーションをかけたりすることもできます。
num.set() {
[[ "${.sh.value}" =~ ^[0-9]+$ ]] && return
echo "Not a number" >&2
# .sh.fun 関数名 num.set
# .sh.name 変数名 num
# _ 前の値
unset .sh.value # 値を変更しない
}
num="str" # => Not a number
他に unset
時に呼び出される .unset
ディシプリン関数も定義することができます。
複合変数を使って定義することもできます。
compound SERIAL=(
integer _value=0
get() {
.sh.value=$((++_._value))
}
)
echo "$SERIAL" # => 1
echo "$SERIAL" # => 2
独自データ型(クラス定義)
typeset -T
を使うと独自のデータ型を定義することがます。データ型に紐付いた変数や関数を中で定義することができ、インスタンスを作ることもできます。記述方法はシェルスクリプトとは思えないほど違和感がないです。
# この alias は不要ですがそれっぽくなるかなと思って定義してみました
alias class="typeset -T"
class Klass=(
integer value=0
# コンストラクタ相当のディシプリン関数
create() {
echo "created"
}
value() {
# _ が this 相当です
echo "${_.value}"
}
add() {
_.value=$((_.value + $1))
}
)
Klass obj=(value=10) # => created
obj.add 20
obj.value # => 30
継承っぽいことをすることもできます(参照)。
名前空間
以下のような名前空間を定義することができます。
var=1
namespace my {
var=2
foo() {
echo "$var"
}
}
echo "$var" # => 1
echo "${.my.var}" # => 2
.my.foo # => 2
HTML エスケープ・拡張正規表現⇔シェルパターン変換・シェルエスケープ
printf
コマンドが拡張され、HTML エスケープを行うことができます。
$ printf %H "<html>"
<html>
また拡張正規表現とシェルパターンの相互変換も行うことができます。
$ printf %P "^abc([0-9])+"
abc+([0-9])*
$ printf %R "abc+([0-9])*"
^abc([0-9])+
他のシェルでもおなじみですが、シェルエスケープを行うこともできます。
$ printf %q "a\"b'c d"
$'a"b\'c d'
ファイルのランダムアクセス
一般的にシェルスクリプトはシーケンシャルアクセスしかできませんが、ksh93 ではランダムアクセスを行うことができます。
seq -f %09g 10 > data.dat
# 以下の内容のファイルを作る
# 000000001
# 000000002
# :
# 000000010
#!/bin/ksh
{
<#((50))
read -n 10 line
echo "$line"
<#((CUR+10))
read -n 10 line
echo "$line"
<#((0))
read -n 10 line
echo "$line"
echo "===="
<#"000000005" 000000005 が見つかるまで飛ばして
<##"000000008" 000000008 が見つかるまで出力する
} < data.dat
000000006
000000008
000000001
====
000000005
000000006
000000007
これに加えてバイナリデータ型を利用すれば、バイナリファイルの読み書きもできるでしょう。
コプロセス
簡単に言えば対話型プログラムをリモート操作したりするのに使います。例えば bc
コマンドを引数なしで実行すると一行の入力ごとに答えを返すプログラムとして対話的に使うことができます。人が使うように 1 行入力してその答えをもらい、その答えを元に次の計算を行う。というようなことを実行することができます。使い方は簡単で bc
コマンドを |&
と特殊な形でバックグラウンドで起動し、あとは &p
に出力し &p
から入力するというのを繰り返すだけです。この機能は元々 ksh88 の頃からあって pdksh や mksh でも使うことができます。また同様の機能が bash や zsh にもありますがそれぞれ書き方は異なります。
#!/bin/ksh
bc |&
{
echo 100 + 200
read ans
echo "$ans * 2"
read ans
echo quit
} <&p >&p
echo "$ans"
ネットワーク通信(tcp、udp、sctp)対応
シェルだけでネットワーク通信ができます。実は意外と多くのシェルでネットワーク通信に対応しています(bash、yash、zsh 等)。
#!/bin/ksh
{
printf '%s\r\n' 'GET / HTTP/1.0' 'Host: www.example.com' '' >&3
cat <&3
} 3<>/dev/tcp/example.com/80
まあ tcp 通信ができるからといって、http 通信やる時に便利かと言うとそういった上位層のプロトコル用のライブラリはないわけで(探せばあるかもしれませんが)普通は他の言語を使ったほうが良いでしょう。
数学関数
多数の数学関数を算術式で使用することができます。
echo $(( round(12.5) )) # => 13
man ksh
より
abs acos acosh asin asinh atan atan2 atanh cbrt ceil copysign cos cosh erf erfc exp exp2
expm1 fabs fdim finite floor fma fmax fmin fmod hypot ilogb int isfinite sinf isnan
isnormal issubnormal issubordered iszero j0 j1 jn lgamma log log10 log2 logb nearbyint
pow remainder rint round scanb signbit sin sinh sqrt tan tanh tgamma trunc y0 y1 yn
また、ユーザー定義の算術式用の関数を定義することができます。これはシェル関数とは独立しており特殊な書き方をする必要があります。
シェル関数と違って引数を定義することができます
function .sh.math.mul v1 v2 {
.sh.value=$((v1 * v2))
}
echo $(( mul(2, 3) )) # => 6
引数には配列を使うこともできます。
function .sh.math.mul v {
.sh.value=$((v[0] * v[1]))
}
ary=(2 3)
echo $((mul(ary))) # => 6
メッセージの国際化
メッセージカタログを用意して、出力する文字列を $"..."
で括ることで
echo $"Hello World" # => こんにちは世界
と表示させることができる・・・はずですが、メッセージカタログを作るのが面倒なので試していません。POSIX コマンドの getcat を使うようです。ちなみに bash も同様の機能を持っています。
国際化の手順はこちら Localizing Korn Shell Scripts
コンパイラ
バイナリファイルへのコンパイラが実装されています(ただ圧縮して詰め込んだだけではありません)。
さいごに
さていかがだったでしょうか?私も調べていて驚いたのですが思ったより多くのスクリプト言語としての機能が搭載されていることがわかりました。当時考えられていた ksh200X の計画 には、名前空間、オブジェクトの継承、バイナリオブジェクト、マルチスレッド対応などが検討されていたようです。
ksh93 で実装されたこれらの機能はシェルスクリプトに必要なものなのか?と思うかもしれません。よくシェルスクリプトはプログラミング言語ではない、シェルスクリプトらしい使い方をするべきだ、などと言われたりしますが、ksh93 もまた Unix で生まれたシェルであり、これらの機能はシェル開発者であるデビッド・コーンがシェルスクリプトに必要だと思って実装したものです。シェルスクリプトがどういうもので、どういう使い方をするかなんて、いくつもある考え方の一つでしかないということがわかります。
とは言えシェルスクリプトに向き不向きがあるのも事実です、私個人としてはシェルスクリプトよりもプログラミングに適した生産性の高い言語はいくつもあるので、普通はそちらを使うべきだと思いますが、それでもシェルの適用範囲(コマンドの連携させることが得意)はあります。そして多くの環境で OS インストール直後から使えるスクリプト言語でもあるので、できないよりはできたほうが良いなという考えです。POSIX シェルの範囲でもやれないことはありませんが ksh、bash、zsh などの拡張機能が使えれば簡単に実装できたり実行速度を上げたりする場合もいくつかあります。
シェルスクリプトにもたびたび型が欲しいという話題がでることがあるようです。それを聞いて私が思うのは欲しいのは本当に型なんですか?今どんな問題で悩んでいて型があることで何が解決するんですか?ということです。もしシェルスクリプトに型が欲しいと言ってる人がいれば型もクラスもある ksh を紹介してあげてください。そうすれば型の次に必要なものは何かを考えることでしょう。おそらくそれは PowerShell と巨大なクラスライブラリになると思います。そしてそれを使うのかと聞いたら使わないと答えそうです。そうは言ってもシェルスクリプトに型を入れるという考えは滅びたりせず何度でも蘇るのでしょう。型の力こそ人類の夢だから!です。