新しいシェルプログラミングの教科書
24.1 読んだ本
https://www.amazon.co.jp/dp/B077NC4N14/?coliid=I3H5RRXW6EUIXN&colid=3JNLB7VSWO00H&psc=0&ref_=list_c_wl_lv_ov_lig_dp_it
https://www.sbcr.jp/product/4797393101/
UNIX哲学
https://ja.wikipedia.org/wiki/UNIX哲学
chapter1
シェル(bash)
Linuxの標準シェルであるbashを利用して、cdなどのコマンドは実行される
シェルの起動
sshログイン、端末エミュレータを開くと自動でシェルが起動する
Linuxの標準シェルはbash, mac(catalina以降)はzsh.
ログインした時に自動で起動するシェルを、ログインシェルと呼ぶ。
利用可能なログインシェルは、/etc/shellsに記載されている
code:shell
current: ~/Desktop/study/shell_programing
cat /etc/shells
# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.
/bin/bash
/bin/csh
/bin/dash
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
zshを使っている時などでも、例えばbashと入力することでbashのシェルに変更できる
これは一時的などもので、恒久的に変えたい場合は少し設定を弄る必要がある。
戻したい時はexitで。
なぜシェルを使うのか
Linuxコマンド実行の流れ
コマンド入力 > シェルがlinuxカーネルにコマンドを実行, カーネルが結果を返す > シェルがユーザーに結果を出力
シェルは、入力されたコマンドを解釈しカーネルへと受け渡す役目を持つ
コマンド・インタプリター(コマンドを解釈するもの)と呼ぶ
linuxカーネルを直接操作しない理由
カーネルと、インターフェースであるシェルを分離させている
コマンドの流れ
1. コマンドAを入力し、実行
2. Aの出力結果を得る
3. Aの出力結果をもとに、次に実行するBを入力
4. コマンドBを実行
というように、実行と出力結果を得ることを繰り返してLinuxを操作する
シェルというコマンドインタープリターの機能をカーネルと分離させることで、コマンド入力の管理を容易にする
1つのプログラムには1つのことをやらせるという考えは、こういった部分でも実装されている。
シェルが異常終了した場合でも、カーネルへの影響をなくすためでもある
シェルはコマンド・インタプリターとしてユーザーとLinuxカーネルの仲立ちをする役割
lsコマンドの実行例
aと付くファイルを検索する、ls a*
1. ユーザーはコマンドの実行をシェルに入力
2. シェルはコマンドを解釈して展開
a*に該当するファイルをコマンドラインで組み立てる
lsコマンドは/bin/lsに存在する...などなど
3. シェルはLinuxカーネルへコマンドを実行させる
コマンドの実行について
シェルはコマンドを実行する際、forkシステムコールを呼び出す
システムコール
カーネルの機能を呼び出すための機構のこと
fork
プロセスをコピーし、自分の子プロセスを生成するシステムコール
結果として、プロセスIDだけが違う別のシェルが出来上がる
子プロセスはコマンドを実行するexecシステムコールにより入力したコマンド(lsなら、lsに)に変わる
親プロセスはwait システムコールにより子プロセスのコマンド終了を示すexitシステムコールを待つ
子プロセスのコマンドが終了すれば、親プロセスのシェルは次のコマンドを実行できるようになる
これがfork-execモデル。色々利点があるが、今回の本では省く
以下の2点が大事
プロセスには親子関係があること
コマンドはシェルのコピーから実行されること
chapter2
シェルスクリプトについて知る
code:sh
echo 'hello'
hello
直打ちでも可能だが、ファイルに記述してそちらから呼ぶことができる
コマンドラインを予め記述しておくファイルのことをシェルスクリプトと呼ぶ。
作ってみる
code:hello.sh
#!/bin/bash
echo 'hello!'
#!/bin/bash
シェバン(shebang)と呼ばれる、「これはbashで起動させる」という印
シェルスクリプトの実行に必要な権限を付与する
code:sh
# 何も付与しない場合
./hello.sh
zsh: permission denied: ./hello.sh
-rw-r--r-- 1 skoni staff 26 1 4 17:28 hello.sh
# 権限付与
chmod +x hello.sh
-rwxr-xr-x 1 skoni staff 26 1 4 17:28 hello.sh*
実行する
code:sh
./hello.sh
hello!
シェルから見た流れ
ベースは同じ。キーボードで入力したか、ファイルから取得したかの違い
ただし、シェルの役目は概念的に以下の2つを抑えておくと良い。
コマンド・インタープリターとしてのシェル
キーボードからコマンドを入力する
スクリプト言語としてのシェル
シェルスクリプトとしてファイルを実行する
シェルスクリプトについて
当然だが、ファイルに何行でもコマンドをかける
変数や制御構造(if, forなど)が利用できる
つまり、コマンドを組み合わせて自分好みのコマンドを作るのがシェルスクリプト
なぜシェルスクリプトを使う
作業の自動化ができる
複雑な処理は手順書としてまとめるのではなくスクリプトにしておけば、引き継ぎの時も楽
利用するシーンの例
大量のファイルを読み込み、テキストの抽出や置き換え、計算してレポートを提出する必要がある場合
当然そうできるようなスクリプトを用意しておくだけでよい
Linux自体も多くのシェルスクリプトの組み合わせでできている
欠点について
コマンドラインの文法そのままのため、書き方が特殊である
デバッグ環境が貧弱なため、大規模な開発はできない
プログラミング言語とシェル
インタプリタ形式
ソースコードを1行ずつ読み込み実行
Ruby, Python, PHPもそう
機械語に直さないというわけではなく、1行ずつ変換するみたいなイメージ
こちらの形式ではソースコードのことをスクリプトと呼ぶ
コンパイル形式
ソースコードをコンパイルしてマシン語の形で出し、CPUに直接実行させる
Cとか
実行前のコンパイル作業が必要
スクリプトという単語について
脚本、台本
なのでコンピューターに指示する一連の処理を書いたファイルをスクリプトと呼ぶようになった
C言語などはコンパイルを使ってマシン語に変換する必要があるので、ソースコードのことをスクリプトとは呼ばない。
Rubyなどで書いた.rbのファイルをRubyスクリプトと呼んだりする
シェルスクリプト
シェルが理解できるようなスクリプトを記述し、実行すること
ただしパイプやリダイレクトなどシェル独自が持つ強力な機能をそのまま利用できるのが強み。
chapter3
シェルスクリプトの構造について
code:hello.sh
#!/bin/bash
echo 'hello!'
ファイル名と拡張子
.shとしたが、仕様上は何でも良い。拡張子がなくても動作する。
慣習的に.shとする。こうしておくとファイル名を見るだけでシェルスクリプトだと認識できる
bashのシェルスクリプトと明示させるため、.bashなどとつける場合もある
プロジェクトによる
シバン / シェバン shebangについて
#!/bin/bash
#!を書いたのち、シェルのフルパスを記述する
#!bin/shとする場合もあるが、本書はbashのシェルスクリプトを扱うので/bin/bashとする
2行目からが実際に入力されるスクリプト本体
作ってみる
code:echo-pwd-ls.sh
#!/bin/bash
echo 'hello!'
pwd
ls
実行
code:terminal
./echo-pwd-ls.sh
hello!
/Users/skoni/Desktop/study/shell_programing/3_chapter
echo-pwd-ls.sh
改行について
改行が1つのコマンドを区切る役割を持つので、プログラミング言語のように改行を挟んではいけない
どうしても長くなってしまう場合は、バックスラッシュ\で対応できる
code:newline.sh
#!/bin/bash
echo \
'hello!' \
'this is my shell script.' \
'thank you.'
逆に1つの行でコマンドを複数書きたいなら;を使う。
code:sh
echo 'hello';pwd;ls
hello
/Users/skoni/Desktop/study/shell_programing/3_chapter
echo-pwd-ls.sh* newline.sh*
また空行は無視されるので、処理のまとまりごとに空行を入れて読みやすいコードを目指すと良い
シェバンの下に空行はよく使われる
改行コードについて
linuxはLFで、windowsはCR+LF
シェルスクリプトの改行コードをCR+LFにして実行すると実行できない場合がある
windowsでshを作ると発生しやすいので気をつけよう
コメント
#でできる
複数行のコメントアウトは存在しない。
シェルスクリプトの実行方法
指定のシェルで実行する
bash hello.sh
ファイルに+xなどで実行権限をつける必要はない
bashがファイルをスクリプトとして解釈して読む。
シェバンを描き、実行権限を付与して直接実行する
./hello.sh
シェバンに書いたコマンド(今回なら、/bin/bash)で実行される。通常はこっち
chapter4
変数
シェルスクリプトの場合、文字列や数値の値に名前をつけてメモリ上に格納する仕組みを持つ
code:cd-study.sh
#!/bin/bash
study_dir=/Users/skoni/Desktop/study
cd $study_dir
ls
pwd
code:実行
./cd-study.sh
docker_kihon_kihon dokushu_php nginx_guide
docker_kihon_kihon_6_chapter genba_ruby_on_rails shell_programing
/Users/skoni/Desktop/study
軽くルール
変数定義時にスペースなどを入れない (dir = /etcなどはダメ)
文字列として入れる場合は",'で括る
空文字も入れられる
変数を参照する場合だけ$をつける
定義時にはつけない。
存在しない変数が参照された場合、から文字となる
{}で変数を明確にできる
code:item.sh
#!/bin/bash
item=pen
echo I have many ${item}s
# I have many pens
環境変数
変数を定義している状態でコマンドを実行した時、現在のシェルからコマンドへ引き継がれる関数のこと
シェルの流れおさらい
コマンド実行時、forkシステムコールで子プロセスが作られる
この時に引き継がれる情報の1つが環境変数。
親プロセスのシェルで設定されていた値が子にコピーされるため、コマンドから参照ができる
code:terminal
# 変数定義と呼び出し
LANG=ja_JP,UTF-8
echo $LANG
ja_JP,UTF-8
環境変数の設定
自分で定義した変数は、exportコマンドで環境変数として扱うように設定できる
例えば、上のLANGはまだ単なる変数のまま。
code:terminal
TEST_PATH=~/Desktop
export TEST_PATH
code:main.sh
#!/bin/bash
TST=~/Desktop
export TST
./config.sh #exportすることで、プログラム内で他のシェルスクリプトを実行した時引き継ぐことができる
OSが用意するシェル変数などもある
知っておいた方がいいのは、PATH
シェルがコマンドを探すディレクトリを指定するための変数
:区切り技で繋いだ文字列として指定する
コマンドを配置するためのディレクトリを新規作成した場合などは、PATHに追加しておく必要がある。
これがいわゆる、パスを通す
位置パラメータ
$1, $2のように1から始まる数値を名前に持つ変数
code:sh
#!/bin/bash
echo arg1 : $1
echo arg2 : $2
bash args.sh aaa wawa
$1にはaaa, $2にはwawaが入る。
ちなみに$0には、実行時ファイルargs.shが入る。
特殊パラメータ
プロセスIDを取得したりできるものがある。作る時に見れば良い。
echo $$でプロセスIDが取得できるので、一意な名前をつける時によく使われる
配列
連想配列(キーと値の組を保持したデータ構造のこと)も使える。
code:sh
user=(id=5 name=miyake)
echo $userid
5
echo $username
miyake
chapter5 展開とクォーティング
パス名展開
*などの記号をパスやファイルの名前に置き換える機能のこと(ワイルドカードのことだよ)
code:.sh
# 任意の文字列 . c か hの1文字
ls *.ch
test.c test.h
chapter1で説明したように、展開処理はシェルが行なっておて、lsコマンドには展開後の値が渡される
つまり上の例なら、実質ls test.c test.hとしている結果になる
この辺りは軽く抑えて、知っておくくらいで良さそう。
ブレース展開
複数の文字列を一度に指定する記法
code:.sh
# こんな感じ
ls
test.exe test.sh test.txt
ls test.{txt,sh}
test.sh test.txt
色々な拡張子を探す時に便利そう
チルダ~展開
ユーザーのホームディレクトリを指定するための記法
ホームディレクトリ
ユーザーごとの設定ファイルやデータ保管用ディレクトリのことを一般的に指す。
パラメータ展開
変数名の前に$をつけると変数の値が表示されるが、それがパラメータ展開
:-展開
指定した変数が設定されていたら展開され、そうでない場合は指定した値で変数が設定される
code:sh
bash-3.2$ echo ${TEST:-sample}
sample
bash-3.2$ TEST=update_message # ここで指定
bash-3.2$ echo ${TEST:-sample}
update_message
デフォルト値を指定する際に使う
code:sh
#!/bin/bash
ls ${1:-/}
$1は位置パラメータ
コマンドから引数をもらったらそれを使い、そうでなかったら/をlsするというコード
code:sh
bash-3.2$ ./ls-root.sh ~/Desktop/study
docker_kihon_kihon dokushu_php nginx_guide
docker_kihon_kihon_6_chapter genba_ruby_on_rails shell_programing
# していなし
bash-3.2$ ./ls-root.sh
Applications System Volumes cores etc opt sbin usr
Library Users bin dev home private tmp var
後ろは変数でも良い
:=展開
変数名に空文字以外の値が設定されている場合、その値を出す
変数名が設定されていないもしくは空文字の場合、変数に値を代入した上で値に展開する
code:sh
bash-3.2$ echo ${name:=miyake} # 初定義 nameは設定されていないので値を代入
miyake
bash-3.2$ echo $name
miyake
bash-3.2$ name=okita # okitaという変数で定義する
bash-3.2$ echo ${name:=miyake} # nameにokitaが入っているので、okitaを出す
okita
bash-3.2$ echo $name # 格納はされていない
okita
# :-では変数に代入がされない。
bash-3.2$ unset name
bash-3.2$ echo ${name:-miyake}
miyake
bash-3.2$ echo $name
(空文字が出力)
:?展開
変数に値が設定されていない場合のエラー制御をするための記号
code:sh
bash-3.2$ unset name
bash-3.2$ echo ${name:?name is not defined.}
bash: name: name is not defined.
エラーメッセージは省略もできる
:+展開
:-とは逆に、変数に値が指定されているときに指定した値に展開する
code:sh
bash-3.2$ unset name
bash-3.2$ name=100
bash-3.2$ echo ${name:+number}
number
bash-3.2$ echo $name # 変数の中身は変わらない
100
文字列を切り出す
code:sh
bash-3.2$ name='I have a pen.'
bash-3.2$
bash-3.2$ echo ${name:1}
have a pen.
bash-3.2$ echo ${name:5}
e a pen.
bash-3.2$ echo ${name:2}
have a pen.
bash-3.2$ echo ${name:3}
ave a pen.
# マイナスも指定できる(末尾指定)
# その場合は:-と区別するために空白を入れること。
bash-3.2$ echo ${name: -3}
en.
配列も指定できる
code:sh
bash-3.2$ echo ${arr*} # *と@は同じ意味
one two three
bash-3.2$ echo ${arr@} # *と@は同じ意味
one two three
bash-3.2$ echo ${arr@:1}
two three
bash-3.2$ echo ${arr@:2}
three
bash-3.2$ echo ${arr@: -1}
three
bash-3.2$ echo ${arr@: -2}
two three
文字数の展開
code:sh
bash-3.2$ echo $name
I have a pen.
bash-3.2$ echo ${#name} # 13文字
13
パターン指定の切り出し
#は前方一致、%は後方一致。
code:sh
bash-3.2$ pref1=Aomori
bash-3.2$ pref2=Akita
bash-3.2$ echo ${pref1#Ao} # Aoにマッチしたので、Aoを切り取ったものを出力
mori
bash-3.2$ echo ${pref2#Ao}
Akita
拡張子を取り出す時に使う。
code:sh
# 前方最短一致
bash-3.2$ file=home.tar.gz
bash-3.2$ echo ${file#*.} # *(任意の文字列.)にマッチしたので、home.を切り取ったものを出力
tar.gz
# 前方最長一致 file. と file.tar. の2つが条件としてマッチするが、最長のfile.tar.を切り取る形になる
bash-3.2$ echo ${file##*.}
gz
# 後方最短一致
bash-3.2$ echo ${file%.*} # .* にマッチするのは.gzなのでそれを切り取った値が出る
home.tar
# 後方最長一致 拡張子抜きで出力ができるようになる
bash-3.2$ echo ${file%%.*} # .gz と .tar.gzがマッチする。最長の方を切り取る形になる
home
パスからファイル部分だけ、ファイル部分だけを取り出すものを作ってみる
code:sh
# こんな感じでできた
bash-3.2$ echo $test_path
/Users/skoni/skoni.png
bash-3.2$ echo ${test_path##*/} # / と /Users/ と /Users/skoni/ がマッチして、最長の最後が採用される
skoni.png
# 逆
bash-3.2$ echo ${test_path%/*} # /* にマッチする最初の/skoni.pngが消される形になる
/Users/skoni
後方一致が苦手かも
置換して展開する
${変数名/パターン/置換文字列 or ${変数名//パターン/置換文字列
code:sh
bash-3.2$ echo $file
home.tar.gz
bash-3.2$
bash-3.2$ echo ${file/./_} # $fileの . を _ に置換するよ
home_tar.gz
bash-3.2$ echo ${file//./_} # 完全に変える
home_tar_gz
# パターンを . から、 .* に変えた。ここに該当する文字を.txtに置換する
bash-3.2$ echo ${file/.*/.txt}
home.txt
前方一致と後方一致についておさらい
code:sh
file=home.tar.gz
# 変数名 一致方法 パターンという組み合わせ
# $fileの中の値 を 後方一致 .*と合う文字列で一致を探す
# .tar.gz と .gz が一致する。%1つは最短一致なので、.gz が該当し、そちらが抜き取られた値になる
echo ${file%.*}
home.tar
# $fileの中の値 を 前方一致 *.と合う文字列で一致を探す
# home.tar と home が一致する。 #1つは最短一致なので、homeが該当し、切り取られた値になる
echo ${file#*.}
tar.gz
コマンド置換
コマンドを実行した際の出力を文字列に展開できる
$(コマンド)と書く
code:sh
touch $(date +%Y-%m-%d).txt
ls
2024-01-11.txt
算術式評価
code:sh
bash-3.2$ declare -i num
bash-3.2$ num=2+10
bash-3.2$ echo $num
declare
明示的に関数を宣言する時に使う
-i: 変数を整数として扱う
-r: 読み取り専用として使う
-a: 配列として使う
-A: 連想配列として使う
特に指定しない場合、文字列型になる
declareで宣言しなくとも、かっこ2つで括ることで算術として扱うことができる
code:sh
bash-3.2$ ((a=1+2))
bash-3.2$ echo $a
3
また、変数同時を計算するときに$が不要になる
code:ari-true-false.sh
#!/bin/bash
((5 > 3))
echo $?
((5 < 3))
echo $?
code:sh
bash-3.2$ bash ari-true-false.sh
0
1
$?は直前に実行したコマンドの終了ステータス値
算術式展開
$(( ... ))と書く
code:sh
bash-3.2$ echo $((5+2))
7
プロセス置換
コマンドを実行した時の出力を別コマンドで使いたい場合、通常はパイプ|を使う
<()で呼ぶことができる
code:sh
# 通常
# 差分が3つあることがわかるが、いちいちファイルを出さなくてはいけないので大変
bash-3.2$ ls | grep .txt | grep 2024 > grep1.txt
bash-3.2$ ls | grep .txt > grep2.txt
bash-3.2$ diff grep1.txt grep2.txt
1a2,4
grep1.txt
grep2.txt
test.txt
# 1行で書く
# diff lsの結果その1 lsの結果その2
bash-3.2$ diff <(ls | grep txt) <(ls | grep txt | grep 2024)
2,4d1
< grep1.txt
< grep2.txt
< test.txt
履歴展開
直前のコマンドを呼ぶことができる
基本!から繋がるコマンドがこれに該当する
!
履歴置換の開始
指定したコマンドが最後どういった形で使用されたかを追える
!!: 直前のコマンドの実行
などなど
クォーティング
|など特別な意図を持つ記号を通常の文字として扱うこと
code:sh
bash-3.2$ echo \*
*
bash-3.2$ echo \$HOME
$HOME
引用符' " によるクォーティング
バックスラッシュ以外でも指定できる
echo '*'とか
シングルクォートとダブルクォートの違い
ダブル"の方が弱いクォーティングになる
code:sh
bash-3.2$ echo "$HOME"
/Users/skoni
bash-3.2$ echo '$HOME'
$HOME
迷った場合はシングルクォートを使えば良い。
chapter6 制御構造
いわゆる、if文とかその周辺の言語のこと
code:sh
if 条件; then
...
elif 条件; then
...
else
...
fi
書いてみる
code:sh
#!/bin/bash
if "$1" = yes ; then
echo YES
else
echo NO
fi
bash-3.2$ bash if_yes.sh yes
YES
bash-3.2$ bash if_yes.sh wawawa
NO
[
コマンドという扱いで覚えてしまって良い
bashはifの直後の条件の箇所に書いたコマンドを実行する
[がいい感じに$1=yesかどうかを判定してくれている役割を持っている。
引数に演算子をしていづると、文字列や数値の比較・ファイルの存在を判定してくれる
真なら0, 偽なら1を終了ステータスとして返す。
シェルスクリプトにはtrue/falseの概念はなく、ステータスで表すので注意する
コマンドとして扱うので、しっかり空白を開けて書かないとダメ
code:sh
"$1" = 'yes' #'yes'] と書くとyesの一部として扱われるので
echo $?
testコマンド
code:sh
test "$2" = 'no'
echo $?
# bash test.sh no
0
同じ挙動を見ることができるが、[がいらない
.shを作らなくても挙動の確認ができる
code:sh
bash-3.2$ answer=10
bash-3.2$ test "$answer" = '1011'
bash-3.2$ echo $? # 直前のプロセスのステータスを返す
1
数値比較などもできる。本参照
code:sh
# answerが10であること -a(AND) result.txtというファイルが-w(書き込み可能)であること
bash-3.2$ test "$answer" = '10' -a -w result.txt
bash-3.2$ echo $?
1
bash-3.2$ touch result.txt
bash-3.2$ echo $?
0
bash-3.2$ ls -la result.txt
-rw-r--r-- 1 skoni staff 0 1 14 20:29 result.txt
注意すること
[はスペースで開ける
;を書く.コマンドの終わりを表す記号のため。
改行して書いてもOK
code:sh
# 1行でかくと、 if "$answer" = '10' ; then
bash-3.2$ if "$answer" = '10'
then
echo YES
else
echo NO
fi
YES
条件に合致した際に何もさせない場合
:を書く
code:sh
if "$answer" = '10' ; then :; fi
&&
コマンドをAND演算する
コマンド1を実行し、終了ステータスが0であるときだけコマンド2を実行する形になる
code:sh
bash-3.2$ cd ~/Desktop && ls
221107_senkyocom illust_clip liko_200107 個人事業用
Laravel_kirThread illust_image study
||
コマンドをOR演算する
コマンド1を実行し、失敗(ステータスが0でない)した場合2を流す。
code:sh
bash-3.2$ -e "$file" # ファイルの存在確認
bash-3.2$ echo $?
1
bash-3.2$ -e "$file" || touch "$file" # 存在確認し、無いなら作る
bash-3.2$ echo $?
0
[[
通常、-aなどオプションのような形で条件式を書くが、=など直感的な記号で書けるようになる
for文
code:sh
# for 変数 in 単語リスト; do 繰り返す処理; done
for i in aaa bbb ccc
do
echo $i
done
bash-3.2$ for i in aaa bbb ccc; do echo $i; done
aaa
bbb
ccc
code:sh
#!/bin/bash
for file in *.txt
do
cp "$file" "${file}.bak"
done
bash-3.2$ ls
if-quote.sh if_cd.sh if_yes.sh result.txt sample.txt skoni_1.sh txt-bak.sh
bash-3.2$ bash txt-bak.sh # .txtのバックアップを作成する
bash-3.2$ ls
if-quote.sh if_yes.sh result.txt.bak sample.txt.bak txt-bak.sh
if_cd.sh result.txt sample.txt skoni_1.sh
breakとcontinueも使える。P109参照。
case文
1つの文字列に対して複数のパターンを指定して、それぞれのパターンにマッチした処理を実行させる
whileとuntil
他の言語同様存在する。使いたい時に文法は見ておけば良さそう。
chapter7 リダイレクトとパイプ
標準入出力について
Linuxでは、ディスクやキーボード、端末ディスプレイなどのハードウェアもファイルとして扱われる
カーネルがハードウェアを抽象化し、コマンドからファイルとして扱えるようにするため
/dev配下がそう。
psコマンド実行時の挙動について
code:sh
PID TTY TIME CMD
29012 pts/0 00:00:00 bash
29047 pts/0 00:00:00 ps
標準出力というファイルに結果を出力して出している
標準出力
その名前の通り、標準的なプログラムの出力先のこと
プログラムの結果は基本ここに書かれる(lsなどの結果もそう)
指定のない場合は端末ディスプレイに表示される
標準入出力
下記の3つを合わせた名称。
stdin
標準入力。 通常はキーボード
stdout
標準出力。通常は端末ディスプレイ
stderr
標準エラー出力。通常は端末ディスプレイ
シェルによって置き換えることで、必要に応じて入力元・出力元を指定できる。(リダイレクト)
リダイレクト
ここでは、標準入出力元を置き換えるシェルの機能のことを指す
code:sh
# 出力をファイルにリダイレクト
ps > ps_result.txt
# bashのエラーをリダイレクト 2>
bash-3.2$ ls /xxx 2> xxx_error.txt
bash-3.2$ cat xxx_error.txt
ls: /xxx: No such file or directory # xxx_error.txtの中身
# コマンドに入力
# tr: 文字列を比較して置換する
bash-3.2$ tr b B < word.txt
aBcd
my Book
数字の意味
ファイルディスクリプタ
プロセスからファイルを参照する際の識別子のこと
入力が0, 出力が1, エラーが2
n> file という形で書くと、ファイルディスクリプタn版の出力先を決めるという意味合いになる
省略すると1になる。
ファイルの上書きを防ぐ
同じファイルがあるとき、> sample.txtとして違う情報を更新してしまう時がある
noclobberの設定をbashですることで対応できる
逆に上書きできない時などはbashの設定を確認しよう
追記
>>とすれば元のファイルにどんどん書き加えることができる
まとめて書く
bash-3.2$ ls /usr /xxx > result.txt 2> error.txtという感じ
/usrにいった結果はresultに書かれて、/xxxにいったけどエラーになった結果はerrorに書かれる
同じファイルに書くこともできる。P121参照
&>>についても参照
その他リダイレクト記号については、P125を参照
/dev/nullとは
リダイレクトと組み合わせて使う
/dev/nullから読み込んでも何もデータは返されない
/dev/nullにデータを書き込んでもどこにも残ることなく、データが消える
code:sh
bash-3.2$ cat /dev/null
# なし
bash-3.2$ ls / > /dev/null
bash-3.2$ cat /dev/null
# 書き込んだのに、何もなし
不要なメッセージを取り除くために使う
code:sh
bash-3.2$ ls / /xxx
ls: /xxx: No such file or directory # /xxxのエラー文
/:
Applications System Volumes cores etc opt sbin usr
Library Users bin dev home private tmp var
bash-3.2$ ls / /xxx 2> /dev/null # エラー文をnullに投げて消す
/:
Applications System Volumes cores etc opt sbin usr
Library Users bin dev home private tmp var
ヒアドキュメント
コマンドの入力をファイルとして指定するのではなく、シェルスクリプト内に記述する場合に使う
用途がいまいち掴めないので、必要になったら見る
パイプライン
リダイレクトしてファイルを展開する代わりに、別のコマンドの入力として使うこと
code:sh
# lsコマンドの標準出力をlessコマンドで表示する
# ls > result.txt; less < result.txt;と同じ意味
bash-3.2$ ls | less
# lsして、pyが含まれるものを絞り込んで、 行数カウント
# 3なので3つのファイルがあるということになる
bash-3.2$ ls /usr/bin | grep 'py' | wc -l
3
標準エラー出力は渡されないので注意する。
コマンドのグループ化
date, echo, lsの結果全てを同じファイルに出したい場合
>>を使ってできるが、一括して処理することもできる
code:sh
# group1.sh
#!/bin/bash
{
date +%Y-%m-%d
echo '-------- /usr list --------'
ls /usr
} > result.txt
# ----------------------------------------
bash-3.2$ bash group1.sh && cat result.txt
2024-01-15
-------- /usr list --------
X11
X11R6
bin
lib
libexec
local
sbin
share
standalone
chapter8 関数
code:lsal.sh
#!/bin/bash
lsal()
{
pwd
ls -al
}
lsal # 定義した関数の呼び出し
bash-3.2$ bash lsal.sh
/Users/skoni/Desktop/study/shell_programing/8_chapter
total 8
drwxr-xr-x 3 skoni staff 96 1 16 23:08 .
drwxr-xr-x 11 skoni staff 352 1 16 23:07 ..
-rw-r--r-- 1 skoni staff 43 1 16 23:08 lsal.sh
関数は呼び出す前に定義しておくこと。
function lsal()とも、 function lsal とも、 lsal()とも書いて定義できる。
変数の有効範囲
指定がない限り、変数内で定義した変数もグローバル変数となる。
ローカルとしたい場合はlocal 変数名=値として明示する。
特に理由がなければ、ローカル変数として定義しておこう
関数の終了ステータス
return 1などを書いておくことで、ステータスを指定できる。
成功したか失敗したかを判断させられる
code:ls.sh
#!/bin/bash
lsal()
{
if -z "$1" ; then # lsalに渡される引数が、空文字なら
echo 'lsal: missing file operand' 1>&2
# 1>&2: P121 ファイルディスクリプタ1番を2番のコピーにする
# 標準出力1番が、標準エラー出力2番のコピーになる。(= 出力もエラーも同じ場所にリダイレクトされる)
# 書かなくても同じ結果にはなるが、明示的にしているのかも。
return 1
fi
# 渡される引数があれば、ls -alをそちらに向かって行う
ls -al "$1"
}
lsal
echo "return status = $?"
chapter9 組み込みコマンド
/usr/bin/などに存在するものではなく、シェル自体に組み込まれているコマンドのこと
: ヌルコマンド 常に0のステータスを返す
pwdとかcdもそう
bashのエイリアス
既存コマンドと同じ名前のエイリアスや関数を作成できる
同じ名前の場合はエイリアスの方が優先度が高い。
優先度を変えた場合はcommandというコマンドで確認できる
本来の機能なのか誰かが作ったものなのか分からなくなったら、typeコマンドで確認できる
set
3つの機能がある
設定されているシェル変数、関数などを一覧できる
シェルオプションを有効/無効化する
位置パラメーターの値を設定する(P165)
unset
変数を削除する
read
標準入力から1行分読み取り、変数に入れる
code:sh
bash-3.2$ read msg
hello! # 自分で打った
bash-3.2$ echo $msg
hello!
wait
バックグラウンドで動作するプロセスが終わるまで待つ
dockerの立ち上げに使えるかもしれない。
exec
引数で指定したコマンドを起動する
この方法で起動したコマンドは、新しいプロセスが作成されない
従来コマンドは実行時、親から子プロセスが作られ実行される
execの場合現在のシェルのプロセスが直接execシステムコールによってlsコマンドに変わる
元のシェルのプロセスは終了する
code:sh
exec ls
eval
引数で指定した文字列をシェルのコマンドラインとして解釈して実行する
code:eval.sh
#!/bin/bash
eval echo \$$1
#'$ $1(引数)' という値をechoする
code:sh
./eval.sh HOME
# $HOMEという変数をechoするという意味になる
evalを使って呼び出すこと前提のコマンド
ssh-agent
sshでログインする時のパスフレーズを毎回入力しなくてもいいようにするコマンド
単独で実行した場合は、そのための準備として実行すべきコマンドが出力される
code:sh
bash-3.2$ ssh-agent
SSH_AUTH_SOCK=...49; export SSH_AUTH_SOCK;
SSH_AGENT_PID=xxx; export SSH_AGENT_PID;
echo Agent pid xxx;
eval "${ssh-agent}"とは
$()の内側がssh-agentコマンドの出力に展開される
その後evalコマンドによって出力がそのままシェルスクリプトのコードとして解釈され実行される
環境変数などの設定が必要になった場合、設定が必要なコードを呼び出すことで実行するようにしている
なるほどな〜
chapter10 正規表現
メタ文字
正規表現に使用する記号のこと
grep
入力から指定した検索パターンにマッチする行を絞り込む
.任意の1文字
[]範囲内の文字
^行の先頭にマッチする
code:sh
bash-3.2$ cat example.txt
/test1/file_1
/test1/file_2
/test2/file_1
/test3/file_x
/test4/file_y
/test10/file_1
/test11/file_1
/work/test1/file_x
/work/test5/file_1
# /test1/という文字が先頭にヒット
bash-3.2$ grep '^/test1/' example.txt
/test1/file_1
/test1/file_2
とか色々。詳しくはそろそろ常識?マンガでわかる正規表現
grepコマンドについて
入力を使い分ける
code:sh
# 複数指定したり
grep bash /etc/passwd ~/.bashrc
/etc/passwd:_mbsetupuser:*:***:Setup User:/var/setup:/bin/bash
オプション
-i: 大文字小文字の違いを無視する
-e: 検索パターンの指定
code:sh
# biome という文字 OR root という文字をgrep
bash-3.2$ grep -e biome -e root /etc/passwd
-v: 結果を反転させる(マッチした以外のものを出力する)
-n: 行数を含めるなどなどたくさんある
sedコマンド
文字列置換に使用する
grepと同じくらい、シェルスクリプトを書くならよく使う。必要なら見る。P194~
chapter11 シェルスクリプトの実行方法
shebangの記述、実行権限の付与、bash ファイル名称など色々ある
shebangの役割
シェルスクリプト実行時、現在動作しているシェル(カレントシェル)はforkシステムコールによって子プロセスを生成
その子プロセス上でexecシステムコールによって実行する
execシステムコール
本来はlsなどのバイナリ形式のファイルを実行するための仕組み
ただし対象ファイルの先頭2バイトが#!だった場合、テキスト形式のファイルを実行するための処理を行う
#!の後ろから行末までに書かれている文字をコマンド名称とみなして実行する
元々実行しようとしていたファイル名が引数として指定された状態になる
code:test.sh
#!/bin/bash
...
execシステムコールによって、
/bin/bashコマンドが、引数としてtest.shが渡された状態で実行される
つまり、実質的に/bin/bash ./test.shと同じ意味
さらにshebangは#で始まるので、シェルスクリプトとして実行された場合はコメント文として扱われる
コマンド検索パス
/.test.shと入力するが、./について
実行ファイルの場所を明示的に指定するために追加している
指定したコマンドをシェルが実行するには、絶対パス/usr/bin...を知る必要がある
ただ相対パスでもいけるので、そちらで参照しても良い
奥深い階層にあった場合、冗長になる
カレントディレクトリに関係なく短い名前で簡潔に実行したいファイルを指定できる
それがコマンド検索パス
概要
シェルが実行ファイルを探すディレクトリのパスのこと
$PATH変数に設定した値がコマンド検索パスとして使用できる
例
code:sh
bash-3.2$ hello.sh
bash: hello.sh: command not found
$PATHで登録したディレクトリに/usr/bin/hello.shがあるかどうかを検索したりする
ない場合は当然not foundとなる
なのでスクリプトを実行したい場合は./など、パスの形で実行しなければならない
逆に、普段使いたいスクリプトを作った場合は
自分で.../binなどのファイルを適当に作る
コマンド検索パス$PATHにディレクトリを追加(206)
そのディレクトリに置いておくと、ファイル名だけで実行できるようになるということ
シェルスクリプトのパーミッション
chmod -x xxx.shとしているが、これはfork後にexecシステムコールがファイル実行時に実行権限をチェックするから
bash xxx.shとする場合
fork後execシステムコールがシェルスクリプトファイルを実行するわけではないので、権限を付与しなくても実行ができる。
ただしファイルの読み取り権限は必要。
bashコマンドがファイルを読むのに必要なので
またコマンドとする場合でも必要
ただshebangまでは読み取れる。その後/bin/bash xxx.shとなるのでそこで読めなくなる
sourceコマンド
シェルスクリプトを実行できる別のコマンド
code:sh
bash-3.2$ source hello.sh
hello!
chapter12
実際に作る
code:sum.sh
#!/bin/bash
readonly SCRIPT_NAME=${0##*/}
result=0
for number in "$@"
do
if ! $number =~ ^-?0-9+$ ; then
printf '%s\n' "${SCRIPT_NAME}: '$number': non-integer number" 1>&2
exit 1
fi
((result+=number))
done
printf '%s\n' "$result"
解説
readonly...
シェルスクリプトのファイル名を取り出して格納
$0にはコマンドとして指定した名前が格納される(P40~) エラーメッセージ用。
##
$0は./sum.shとか、/usr/bin/sum.shとか色々な場所に置かれていることが懸念される
最後の/までを削除している(P64~パラメータ展開)
新しいシェルプログラミングの教科書#659ea394eb72390000071d61
code:sh
# 前方最長一致 file.tar.gz
# file. と file.tar. の2つが条件としてマッチするが、最長のfile.tar.を切り取る形になる
bash-3.2$ echo ${file##*.}
gz
./とか/usr/bin/とかがマッチするが、最長の/usr/bin/がマッチして切り取られる
シェルスクリプト名は途中で変わらないため、値の上書きを防ぐため読み取り専用で宣言する
SCRIPT_NAME変数が変わらないようにということ。
for number in "$@"
$@: 引数全てが格納された配列
[[ ! $number =~ ^-?[0-9]+$ ]]
[[: パターンマッチの宣言
=~: 右辺の文字列を正規表現とみなすということ
^-?[0-9]+$
数字の連続または先頭にマイナス符号がついた数字の連続を許容する
^
否定もしくは、先頭を示す表現。今回は先頭
ちなみに否定の場合は[^a-c]という感じで書く(a-c以外)
?
手前の文字があってもなくてもマッチさせる.つまり-があってもなくても。
[0-9]: 0-9という数字の指定。
+
1回以上繰り返す文字とマッチする
10とか23とか。
$: 末尾を示す
printf '%s\n' "${SCRIPT_NAME}: '$number': non-integer number" 1>&2
printf 文字列 渡す文字
https://atmarkit.itmedia.co.jp/ait/articles/1907/05/news012.html
code:sh
# こんな感じ
bash-3.2$ printf "%sです\n" 100
100です
1>&2
(P121) ファイルディスクリプタ1番を2番のコピーにする
# 標準出力1番が、標準エラー出力2番のコピーになる。(= 出力もエラーも同じ場所にリダイレクトされる)
((result+=number))
カッコ二つで算術式評価が使える。P76
sumline.sh
while IFS= read -r number
IFS=
https://qiita.com/kawaz/items/00a4b1693bf4cf67e8ca
空白やタブなどの値も評価して考慮する形になる
read 変数名標準入力の内容を読み取り、変数名(number)に格納する
-r:1行ずつ
readで読めるまで続けるということ
chapter13[]
コマンドとして作られているシェルスクリプト
gunzip
コマンドのように見えるが、実際はgzip -dという操作をラップしている
cromバッチ
タスクスケジューラ
処理を定期的に行うためのデーモン
シェルスクリプトは定期的に呼ばれる仕組みをよく作るので、組み合わせて使われる
プログラム補完
tabキーで補完できるやつ。あれもシェルスクリプトで作られている
自分で作ったシェルスクリプトの補完ファイルを作ることも可能
保管ファイルの読み込み
ログインした時に自動的にファイルを読み込むようにするには、そういうスクリプトも作ってしまえば良い
P318~など
chapter14
テスト環境の紹介とか。