Quantcast
Channel: Bashタグが付けられた新着記事 - Qiita
Viewing all 2757 articles
Browse latest View live

UNIXの基礎(後編)

$
0
0

この記事について

UNIX系のパソコンについて基本的なことをまとめています。この記事を読んでちゃんと理解すればUNIXについての理解がかなり深まると思います。前編は以下のリンクからどうぞ。

UNIXの基礎(前編)
https://qiita.com/minnsou/items/6b6b74fcd79bd00ace1a

環境設定

環境の設定方法

UNIXで使われるシェル(sh, csh, bashなど)はカスタマイズができるようになっている。コマンドのカスタマイズの仕方は大きく分けて2つある。
1. 設定ファイルによる方法
設定ファイルとはコマンドの設定を記述するファイルで、一般に設定ファイルを使うコマンドは、起動されると最初に設定ファイルの内容を読み込み、その記述によって動作を変える。設定ファイルは例えばshなら.profileだし、bashなら.bash_profile.bashrcを使う。これはシェルによって異なる。
2. 環境変数による方法
環境変数は変数に値を設定して使う。コマンドは起動されると必要に応じて環境変数の値を調べ、設定されている値によって動作を変える。環境変数は設定ファイルの中で設定するのが普通。

対話的シェルと非対話的シェル

シェルの起動には対話的非対話的の2種類がある。対話的というのは「人間が要求を出すとそれをコンピュータが処理する」というような作業を繰り返し行うような形態のこと。非対話的というのは処理の内容をすべてあらかじめファイルか何かに記述しておき、コンピュータがその内容を読み出して処理するような形態のこと。
cshの設定ファイルである.cshrcのように非対話的シェルでも読み込まれるような設定ファイルでは、メッセージを表示したり端末を設定する場合には通常エラーになるので注意。cshならシェル変数$promptを使って場合分けをする必要がある。

シェル変数と環境変数

シェルの設定ファイルではシェル変数および環境変数、エイリアスなどの設定を行う。
シェル変数はシェルが固有にもつ変数。この変数の中には特別な意味を持った変数があり、そういった変数を設定することでシェルの動作を変更できる。
環境変数はシェルではなく、UNIXで使われるすべてのプロセスがそれぞれ保有する変数。あるプロセスから別のプロセスが作られた時に、新しく作られたプロセスは元のプロセスで設定されていた環境変数をそのまま受け継ぐようになっている。

シェル変数と環境変数は、「環境変数は子プロセスに渡されるが、シェル変数は渡されない(環境変数はカーネルが管理しているので、子プロセスが生成されたとき、つまりシェルからコマンドが起動されたときにカーネルがシェルの環境変数のコピーを子プロセスに渡せるが、シェル変数はシェルが管理するのでカーネルは感知しない)」という点で異なる。シェル変数と環境変数が混乱する時は「シェル変数はシェル自身の動作を設定するために使い、環境変数はシェルが起動するコマンドの動作を設定するために使う」と覚えると良い。

シェル変数はsh、bash、zshだとvariable=valueで設定する。=の両端にスペースは入れない。シェル変数の例としては、

  • プロンプト(PS1$PS2>PS4#が基本の設定。なお、MacのPS1のデフォルトは\h:\W \u \$で、\hはホスト名、\Wがカレントディレクトリ名、\uはユーザ名を表す。)
  • historyに保存するコマンドの数(HISTSIZE
  • ヒストリ置換で用いられる文字(histchars、これはデフォルトで!^だけど変数には何も入っていないので、histchars=%みたいに設定すればこの文字は変えられる)

などがある。

環境変数の例としてはPATH, TERM, HOME, USER, SHELLなど(bashは小文字ではだめ)で、一般に大文字である。どれもecho $PATHとかで見れる。自分が定義したシェル変数aを環境変数にしたい時は、export aとする。ただしこのようにした環境変数も、そのシェルからログアウトすると消えてしまうので、永続的に残したい時には.bashrc等に書いておく必要がある。

printenvコマンドやexportコマンドで全ての環境変数が見れるし、man environでそのシステムで使われている代表的な環境変数についての説明が見れる。シェル変数を全部見るのはdeclareコマンド(ただし定義されている関数も見える)。

エイリアス

コマンドに別名をつける機能。この機能により、次のようなことができる。
1. タイピングの手間を減らす(alias md=mkdirとか)
2. 新しいコマンドを作る(独自のコマンドを作るとか)
3. タイプミスを先取りする(alias mroe=moreとか)
4. コマンドの意味を変える(alias ls='ls -F'とか)
5. シェルスクリプトではできないことを行う(現在使っているシェルのシェル変数や環境変数を設定するのはシェルスクリプトでは無意味、なぜならそのシェルスクリプトを実行する時には別のシェルが起動されるから)

端末の設定

端末の設定も、シェルの設定ファイルで設定される項目の一つ。端末の設定の最も重要なコマンドはsttyコマンドである。sttyコマンドでは、端末の割り当てキーや出力のモードなどの設定を行うことができる。例えばstty erase ^Hというコマンドで一文字削除のキーがCtrl-Hになる。以下はsttyで変更できるキーのうち、よく使うものを書き出した。

機能デフォルトのキー意味          
erase^? (Del)1文字削除
kill^U1行削除
werase^W1単語削除
susp^Zサスペンド(ジョブ制御)
intr^C実行の停止
eof^Dファイルの終了(シェルの場合ログアウト)

(UNIXでの)正規表現

正規表現とは、ある規則に基づいて文字列の集合を表す方法の1つである。今後この記事で使われる正規表現は、UNIXで使われる正規表現であって、オートマトン理論や言語理論で使われる正規表現とは少し異なることに注意。

例えば^a.*tion$という文字列では、「行の最初(^)にaがあり、任意の1文字(.)の0回以上の繰り返し(*)があり、tionが続いて、行の終わり($)がある」という意味になる。^*のように、特別な意味を持つ文字のことをメタキャラクタという。

正規表現は1種類ではなく、ツールによって実現している機能が制限されていたり、表現方法が微妙に異なる。ここでは、大きく2つに分けて紹介する。

  1. grepedvisedless系の正規表現
  2. egrepawk系の正規表現

なお、シェルのファイル名置換で使われる*(任意の文字列)や?(任意の1文字)のようなメタキャラクタとは記号の意味が少し異なることに注意。ps | grep *tionのようにコマンド入力してしまうと、*がシェルによって先にファイル名置換されてしまうps | grep '*.tion'のように''で囲む必要がある。

1. grepedviless系の正規表現

  • \mメタキャラクタmの意味をなくす
  • ^行の先頭
  • $行の終わり
  • .任意の一文字
  • [c1c2...cn]並べた文字のうちの任意の1文字(例えば[abc]ならabc
  • [^c1c2...cn]並べた文字以外の任意の1文字(例えば[^abs]ならabs以外の任意の1文字)
  • r*正規表現rの0回以上の繰り返し
  • \(r\)タグ付き正規表現r(これは正規表現をグループ化するもので、この後ろに*をつけると0回以上の繰り返しが表せる)
  • \NN番目のタグ付き正規表現がマッチした文字列を表す

例えば、

  • a*cは、「c」「ac」「aac」などにマッチする。
  • \([ab]c\)\1は、「acac」または「bcbc」にマッチする。「\([ab]c\)」で「ac」をマッチした時は「\1」は「ac」を表すので、「acbc」などにはマッチしない。

2. egrepawk系の正規表現

基本的には上記の正規表現と同じだが、言語理論で定義されている正規表現の形式が使えるようになった。

  • r+正規表現rの1回以上の繰り返し
  • r?正規表現rが1つある、またはない
  • r1 | r2正規表現r1またはr2(orの意味)
  • (r)正規表現rのグループ化(これによりタグ付き正規表現はegrep等では使えなくなった)

例えば、

  • ab+cは、「abc」「abbc」「abbbc」などにマッチする
  • (a|b)cは、「ac」または「bc」にマッチする

検索と置換

検索

基本的にファイル内の単語の検索にはviとかemacsの検索やgrepを使い、ファイル名の検索findコマンドを使う。

viではコマンドモードで/keywordで順方向検索、?keywordで逆方向検索をする。emacsはCtrl-S keywordで順方向検索、Ctrl-R keywordで逆方向検索をする。

grepegrepfgrepは、あるデータを受け取り、その中から指定された文字列と一致する行を抜き出して出力するコマンド。grep keyword filesというのが基本の形で、ファイル名filesが指定されていない時は、標準入力からデータを入力する。grepはいわゆる「grepファミリー」の中で最古参のもの。egrepは基本はgrepだが、扱える正規表現が拡張されている。fgrepは複数の文字列を同時に検索できるが、正規表現は使えない。

findコマンドはファイルそのものの検索を行う。findコマンドの標準的な使い方は以下。

find ディレクトリ 検索条件

例えばfind . -name '*.py'とすればカレントワーキングディレクトリ以下で名前の最後が.pyとなっているファイルを全部探すという意味になる。他にもfind . -name '*.py' -atime +15とすれば16日以上アクセスされていないファイルを検索してくれる。-atimeは最終アクセス時刻(access time)を表す。

もう少し実用的な例としてはfind . -name '*~' -exec rm {} \; -printがある。これは~で終わるファイル名を削除(rm {})して、表示をする(-print)というもの。-execオプションは、エスケープされたセミコロン(\;)までのUNIXコマンドを実行するもので、{}には条件に適合したパス名が入るようになっている。

置換

viでの置換は、viのもう1つの顔であるexコマンドの置換機能と同じである。edsedも似たような置換コマンドを使う。基本的には次のような書式をしていて、patternにマッチする文字列をstringに置換する。

range s/pattern/string/g

rangeには置換範囲を指定する。例えば%で全体、start,endstartからendまでの行を表す。patternstringには正規表現が使える。最後の/gは1行中にある全ての置換該当箇所を変更することを意味する。ここらへんの詳細は、sedコマンドを紹介するところで詳しく扱う。なお、emacsの置換はM-%(M-x query-replace)で可能。

フィルタ

フィルタとは、標準入力から入力したデータを加工して、その結果を標準入力へ出力するようなプログラムのこと。大きく分けて2つあり、1つ目はフィルタ自身にプログラムを与えてフィルタ機能を指定できるもの(sedawkperlなど)、2つ目ははじめから機能が決まっているもの(headtailなど)である。

フィルタの例

headコマンドとtailコマンドを用いて新しい機能を持つフィルタを例として作る。belly n mでn行目からm行目までを取り出すフィルタとする。bellyの中身は以下。

#!/bin/bashfrom=$1to=$2arg=$(($to-$from+1))head -$to | tail -$arg

これでcat -n foo.txt | ./belly 12 14というコマンドでfoo.txtの12行目から14行目を取り出すことができる。

teeコマンド

引数として指定したファイルと標準出力の両方に、標準入力からの入力を出力するフィルタ。
例えば make | tee bar.txtとすればmakeコマンドの結果をbar.txtに出力しつつ、画面にも出力できる。

sortコマンド

標準入力からの入力を行単位でソートして標準出力に出すフィルタ。
例えば ls -l | sort -nr +4だと4番目のフィールド(ファイルの大きさ)を数値として比較し降順に並べる。
-nは数値として比較する、-rは指定された方法で逆順で表示、-fは大文字と小文字を区別しないという意味。

uniqコマンド

標準入力からの入力の隣接した行が同じ内容であれば重複とみなし、その重複行を除いて標準出力に出すフィルタ。
例えばOLDディレクトリにファイルaaとbb、ccがありNEWディレクトリにファイルaa1とbb、ccがあるとする。
(ls OLD; ls NEW) | sortとすると

aa
aa1
bb
bb
cc
cc

と表示されるが、(ls OLD; ls NEW) | sort | uniqとすると

aa
aa1
bb
cc

と表示される。かぶっているbbとccが消える。

trコマンド

標準入力からの入力を文字単位で置換して標準出力に出すフィルタ。
tr old-string new-stringという形で使い、old-stringに現れる文字をnew-stringに対応する文字で置き換える。tr ab XYならaをXに、bをYに置き換える。tr a-z A-Zなら全てのアルファベットが大文字で置換されて表示される。
例えばls | tr a-z A-Zとすれば、ファイル名が全て大文字で表示される。
なお、-dオプションを指定すると、指定した文字を削除するフィルタになるので、cat file | tr -d '\015'とすると制御文字のCRを消したものができる(windowsは行末がCR LFでUNIXはLFだけなので上記のコマンドで改行が直る)。

expandコマンド

標準入力からの入力のタブを相当する数の空白に変換して標準出力に出すフィルタ。
unexpandは逆に連続した空白をタブに置き換えるフィルタ。

foldコマンド

標準入力からの入力を、各行の長さが一定文字数となるように強制改行してから標準出力に出すフィルタ。
例えばexpand file | fold -65というコマンドで行幅を65文字にする。foldコマンドはタブを解釈しないのでこの例のようにexpandで前処理する。

prコマンド

標準入力からの入力をラインプリンタ向けの出力形式に変換して標準出力に出力するフィルタ。具体的には、各ページ番号のヘッダとして、ファイルの名前や時刻、ページ番号を追加することや文章をマルチカラム化するといった処理が可能になる。

sedコマンド

sed(stream editor)はフィルタとして用いることを目的として設計されたエディタ。
sed commandというのが基本的な形。commandは「どのアドレスにマッチする行に、どのような関数を適用するか」を表したもの。[ address1 [, address2 ] ] functionという形式である。アドレスを1つ指定するとその行のみ、アドレスを2つ指定するとその2つのアドレスの間の全ての行に対して、アドレスを指定しないと全ての行に対して関数functionを適用する。

アドレスの指定にはいくつかの方法がある。数字で行数を表したり、正規表現も使える。例えば10pなら、10行目にp関数を適用する。/^a/,/^b/pなら、aで始まる行からbで始まる行までにp関数を適用する。

各関数については以下。

  • p関数は処理中の行の内容を出力するために使う関数。sedは標準ではコマンドで処理した結果を出力するが、-nオプションを指定した時は自動的な行の出力を行わないようになるので、p関数で明示した行だけが表示されるようになる。例えばcat -n /usr/share/dict/words |sed -n '100, 105p'ってやると100行目から105行目までの各行が表示される。
  • d関数は入力から不要な行を削除するために使う関数。例えばsed '/^BEGIN/,/^END/d'というコマンドだと行頭でBEGINから始まる行から、行頭でENDで始まる行までを削除する。 q関数はアドレスにマッチした時点でsedの実行を終了する関数でsed -e '100, 105p' -e '105q'のように使う。-eは複数の関数を処理したい時に使うオプション。この例では105行目の時点でsedの実行が終了する。
  • s関数は正規表現を利用した文字列置換を行う関数。s/regular-expression/replacement/flagsという形で表現する。処理中の行に関して検索パターンregular-expressionに指定した正規表現にマッチする最初の文字列を、置換パターンreplacementに指定した文字列で置換する。flagsにはs関数の動作を変更するためのフラグを指定する。例えばgなら行内で正規表現にマッチする文字列を全て置換する(デフォルトでは最初にマッチする文字列のみしか置換しない)。pならマッチした行を出力する。
  • y関数はtrコマンドと同じく、入力単位で置換する関数。例えばy/aiueo/AIUEO/とすればaiueoの文字が全て大文字になる。

その他にも分岐や中間保存の結果などできるが、詳細は省略する。

awk(オーク)コマンド

awkはパターンに従って入力行を処理するための言語。入力の各行がスペースやタブのような区切り文字で分割されている表のような形式のファイルの処理を得意としている。cに少し近い。awkを実行するにはawk programという引数としてprogramを指定する方法と、awk -f program-fileとしてプログラムをファイルprogram-fileに書き込んでおいてそれを-fオプションで指定する方法がある。
programの中身は

pattern1 { action1 }
pattern2 { action2 }
...

であり、入力に対するパターンpatternとパターンにマッチした際に実行するアクションactionを対にして記述する。patternが省略された時は、全ての入力行に大してアクションactionが行われる。{ action }が省略された時は、patternにマッチした行を出力する。

正規表現によるパターンは/囲んで指定する。awk '/^From:/'とすればFrom:で始まる行を全て出力する。なお、コマンドラインでawkプログラムを指定する場合には、awkに与えるプログラムが1つの引数になるように'で囲むのを忘れないようにする。
awkは入力行を区切り文字によって自動的にフィールドに分割する。例えばls -lなら$1にモード(-rw-r--r--とかのやつ)、$2にハードリンクの数、$3は所有者名、$4にグループ名、$5にはファイルの大きさなどのように入る(ls -lの表示順になっている)。

例えばls -l | awk '$5 > 1000{ print "size of file", $9, "is", $5;}'とすると

size of file filename1 is 34694
size of file filename2 is 1011
size of file filename3 is 56203

のようにファイルのサイズが1000より大きいものだけ表示される。なお、actionにはprint以外にも変数の代入や演算、ifforなどの制御文も入れることができるが詳しくは省略する。

圧縮とアーカイブ

圧縮

コンピュータ科学の分野では、何らかの形式で表現された情報をその内容を失うことなくその分量(データ量)を減らすための仕組みが古くから考えられてきた。このような仕組みのことをデータ圧縮、あるいは単に圧縮という。圧縮されたファイルを元に戻すことを伸長という。

compressコマンドは圧縮率はそこまで高くないけど、処理速度も遅くなく、ほとんどのUNIXシステムに標準で付属している。圧縮すると.Zという拡張子が付く。伸長にはuncompressコマンドを使う。
gzipコマンドだと、.gzという拡張子が付く。これはGNUソフトウェアの一種。gzipコマンドで圧縮されたファイルを伸張する時にはgunzipコマンドを使う。なおzcatというコマンドを使えばcompressコマンドで圧縮されたファイルもgzipコマンドで圧縮されたファイルも伸長できる場合がある。bzip2という圧縮コマンドもある。伸長にはbunzip2コマンドを使う。

アーカイブ

複数のファイルをまとめて1つのファイルにすることをアーカイブするという。アーカイブするためのコマンドにはtar, shar, cpio, zipなどがある。

tartape archiveの略で、元々は磁気テープにアーカイブファイルを作るためのコマンドだった。しかし、使用できるメディアが磁気テープに限られているわけではない。このコマンドはtar key filename...という形で使用する。keyにはctxruという5つのコマンド文字のうち1つのみと、必要な場合はオプションが含まれる。

  • cはtarファイルを作る
  • tはtarファイルの中身を調べる
  • xはtarファイルからファイルを取り出す
  • ruはtarファイルの末尾にファイルを追加する

よく使うオプションは以下。

  • vオプションはverbose(おしゃべりな)の意味で、これを指定するとアーカイブの処理対象となるファイル名を順次出力する
  • fオプションは引数を1つ取り、作成するtarファイルの名前を指定する

ただしtarにおけるオプションには「-」という文字を付けずに、コマンド文字や他のオプションと繋げて指定する。例えばtar cvf srcfiles.tar ./srcで、./srcを元にscrfiles.tarというtarファイルを作る。

GNU tarもあり、元々のtarコマンドを拡張している。これはgtarというコマンド名の時もあるし、tarというコマンド名になっている時もある。sharcpiozipコマンドなど他にもたくさんあるが、ここでは省略する。

時刻

現在の時刻を知る

時刻を見るにはdateコマンドを使う。-uオプションでグリニッジ標準時になる。

カレンダーを見る

カレンダーを見るにはcalコマンド。例えばcal 11 2019で2019年11月のカレンダーが見れる。なお、引数が1つの場合には、年であると解釈されるので注意。

アラーム時計を使う

leaveコマンドはアラーム時計として使うことができる。leave 1920とすれば19時20分にアラームがセットされ、ビープ音とともにメッセージが出てくる。leave +30という指定の仕方(30分後にメッセージを出す)もあり、現在からの相対時間でアラームを設定することも可能。いらない時はkillコマンドを使う。

インターバルを取る

sleepコマンドでは、文字通り何もしないで休むコマンドである。引数に与えられた秒数だけ何もしないで休み、終了するコマンド。例えばsleep 300; echo "Tea is just ready." &というコマンドを実行すると、5分後にechoコマンドが実行されてメッセージが表示される。

指定した時刻にコマンドを実行する

自分の決めた時刻にコマンドを実行する方法のひとつとして、atコマンドがある。これは指定した時間に指定されたシェルスクリプトを実行するもの。at time [filename]という書式でtimeに起動する時間、filenameに実行するシェルスクリプト名を入れる。filenameを指定しないときは標準入力からシェルスクリプトを読み込んでそれを実行する。例えばat 6am 6 scriptというコマンドなら6月6日の午前6時にscriptが実行される。ただしこのコマンドはデフォルトでshを使う。またこのスクリプトの標準出力や標準エラー出力はメールで来るので、それが嫌ならリダイレクション(> result.outとか)をする。

atコマンドで実行を指定したシェルスクリプトの状況はatqコマンドで知ることができる。atコマンドで指定したシェルスクリプトの実行を取り消すにはatrmコマンドを使う。

定期的に仕事を行う

UNIXには定期的にコマンドを起動するような仕組みが用意されている。この仕組みを使うと毎日不要なファイルを自動的に削除する、というようなことが可能になる。この仕組みはcronというデーモンによって実現されている。また、定期的に実行されるコマンドの設定を行うファイルをcrontabファイルという。cronデーモンはcrontabファイルの内容を参照して、コマンドの定期的な実行を行う。

crontabファイルの設定にはcrontabコマンドを使う。crontabファイルを新たに作成する場合や、その内容を変更する場合にはcrontab -eとする。そうするとエディタが起動され、そのユーザのcrontabファイルが読み込まれる。

crontabファイルには、1行に1つの設定をかく。一般ユーザが使うcrontabファイルでは、各行は空白で区切られた以下のような6つの要素で構成される。

min hour day mon day-of-week command

最初の4つは、分(min)、時(hour)、日(day)、月(mon)の指定である。day-of-weekは曜日の指定で、0が日曜日、1が月曜日のように指定する。

例えば

0 0 1 1 * find $HOME -name '*core' -atime +7 exec rm {} \;

という行があると、1月1日の0時0分にfind $HOME -name '*core' -atime +7 exec rm {} \;というコマンドが実行される。コマンドを指定する最初の5つのフィールドで数字の代わりに*を指定すると、そのフィールドの数字がいくつの時でもコマンドが実行されるようになる。crontabファイルの内容の表示には-lオプションをつけて実行する。crontabファイルを削除する時にはcrontab -rで削除する。

コマンドの実行時間を測る

コマンドの実行時間を測るにはtimeコマンドを使う。time commandの形式。

bashプログラミング

シェルスクリプト

一括で実行するようにコマンドをあらかじめファイルに格納したものをシェルスクリプトという。やや古めかしい言い方だが、実行を開始する前に処理方法を決めておくという意味でバッチと呼ぶこともある。また何らかの目的のために処理が組まれているという点からシェルプログラムと呼ばれることもある。

シェルスクリプトを実行するときに毎回bash script_nameと打つのは面倒なので、ファイルの先頭に#!/bin/bashと入れておくとコマンドとして実行できるようになる。つまり./file_nameと打つだけでこれが実行できるようになる(ただしchmod +x file_nameのようにファイルのモードを実行可能にしておく必要がある)。このようにコマンドとして実行できるシェルスクリプトをシェルコマンドと呼ぶ。

シェルコマンド実行時の挙動

まずbashはコマンド名がfile_nameということを知ると、コマンドサーチパスからこのファイルを探す(ただし./file_nameの時は既にパスを指定しているのでパスから探すことはしない)。そしてそのファイルが実行可能モードの時、ファイルの1行目をみる。

  1. 行頭が#!の時は、そのファイルの実行をカーネルに要求する。そして#!の後の絶対パス名で指定されるファイルをプログラムとして起動する。例えば`#!/bin/cshとか#!/usr/bin/python`とか。テキストファイルをその入力とする。絶対パスの後に書かれているものは引数とみなす。
  2. それ以外の場合はbashを起動して、残りのテキストファイルをその入力とする

なお#の扱いは対話的にシェルを使っている時と、非対話的異なるので注意。シェルスクリプトで#を使っている時はコメントとして認識されてしまうので注意。例えばrm important#とコマンドラインから実行させるのとシェルスクリプトで実行させるのでは、消えるファイルが異なってしまう。シェルスクリプトでimportant#を消したい時には、important\#のようにエスケープする。

bashとかを起動させた時、起動させるbashのカレントワーキングディレクトリと起動させたカレントワーキングディレクトリは一致している。相対パスも使ってシェルコマンドを実行させることもできる。

変数の詳細

bashにおいて、変数では配列も使用することができる。例えばnames=(hatter duchess alice)という配列を宣言する。これでecho ${names[0]}のように値を参照することができる。

シェルスクリプトを書く時には、ある変数が定義されていない時にはデフォルト値を使用したり、配列の数に応じた処理をしたい時がある。そのためにbashには、変数が定義されているかどうかを調べたり、配列の数を調べる方法が用意されている。基本的なものを以下にあげる。

指定意味
${variable}変数varibleの値(変数の値$aの直後に文字zをつけたい時は変数azと解釈されないようにこの表記)
${#variable}配列variableの要素数
$#シェルコマンドを起動した時の引数の数
$0シェルコマンドのファイル名
$numシェルコマンドを起動した時のnum番目の引数
$*シェルコマンドを起動した時の全ての引数(配列ではなく文字列)
${variable:-word}variableで指定される変数が定義されていればその値、定義されていなければwordの値
${variable:=word}variableで指定される変数が定義されていればその値、定義されていなければwordの値として、さらにその値をvariableにセットする
${variable:?word}variableで指定される変数が定義されていればその値、定義されていなければwordの値を表示して終了
${variable:+word}variableで指定される変数が定義されていればその値、定義されていなければ空文字列

なお、標準入力から変数に入力するにはreadコマンドを使う。

シェルスクリプトの実際の例はscript.shというファイルに書いた。

script.sh
#!/bin/bashecho"this result is produced by $0"echo'$#' is $#echo'$*' is $*echo'$$' is $$'(process id)'echo${word:-abcg}echo'$1' is ${word:=$1}

これを./script.sh hello bashとコマンドラインで実行すると以下のようになる。
スクリーンショット 2020-04-04 15.26.09.png
$$はそのシェルのプロセスIDを表す。これを使う典型例としてシェルスクリプトで使う一時ファイルを作るという例がある。tmp=tmp.$$のように代入しておけば、プロセスIDはそのプロセスが終了するまでは絶対に被らないという性質を用いて安全に一時ファイルが作れる。

I/Oリダイレクション

プロセスの入力や出力はデスクリプタと呼ばれる0から始まる番号を使って指定する。0は標準入力、1は標準出力、2は標準エラー出力を表す。num>fileという形でデスクリプタnumへの出力をfileに出力することが可能。リダイレクションの>の後に&とデスクリプタnumをつけて>&numとすると、numのファイルにその出力がリダイレクトされる。分かりづらいので例を挙げる。

以下のoutput.shは標準出力にstdoutという文字列を、標準エラー出力にstderrという文字列を出すスクリプトである。

output.sh
#!/bin/bashecho"stdout">&1
echo"stderr">&2

これはhttps://qiita.com/laikuaut/items/e1cc312ffc7ec2c872fcからいただきました。

  1. ./output.sh > aで、aにstdoutの文字列が入る。これは./output.sh 1> aと同じで1が省略されているだけ。
  2. ./output.sh 2> aで、aにstderrの文字列が入る。
  3. ./output.sh 1> a 2> bで、stdoutの文字列がaに、stderrの文字列がbに入る。
  4. ./output.sh >& aで、aに両方の文字列が入る。>&の間にスペースを入れるとエラーとなる。
  5. ./output.sh > a 2>&1では、aに両方の文字列が入る。
  6. ./output.sh 2>&1 > aでは、aにstdoutの文字列しか入らない。これは最初の2>&1の時点でstderrという文字列を標準出力に出力してしまうからである。そのあとで> aをするので標準出力だけがファイルに書き込まれる。

制御構造について

プログラミングでは、条件判断をした上で、次にどの部分を実行するか決めたり、ある条件を満たすまで処理を繰り返すということが必要な場合がある。このような実行を制御するための構文として、ここでは

  1. if
  2. for
  3. while
  4. case
  5. 関数
  6. 制御構造を支えるその他のコマンド

を取り上げる。

1. if

ifの文法は以下。

if commandlist1
then commandlist2
else commandlist3
fi

コマンドリスト(commandlist)とは、コマンドを;や改行などで区切って並べたもの。ifコマンドではcommandlist1の実行結果(つまり最後のコマンドの終了ステータス)が正常終了(0)の時は条件が成立したとしてcommandlist2を実行する。そうでなければcommandlist3を実行する。fiはifを逆にしたもの。elifも存在する。

2. for

forの文法は以下。

for variable in list
do commandlist
done

for文では、リストlistの先頭のワードを変数variableの値としてセットして、doからdoneまでのコマンドリストcommandlistを実行する。次にリストの2番目のワードをvariableにセットして...を繰り返す。continueコマンドやbreakコマンドもある。

リストは複数の表現方法がある。for i in a1.py a2.pyのように列挙しても良いし、for i in *.pyのようにファイル名置換を用いても良い。bashではIFSという区切り文字を表す環境変数に:を設定してfor i in $PATHとすることで、パスのひとつひとつを変数iに代入することができる。

具体例としては以下のif_for.sh

if_for.sh
#!/bin/bashfor i in u.c t.c
do
    echo$i
    cc -c$iif[$?!= 0 ]# ここはif test $? != 0でも同じthen break
    fi
done

ccはコンパイルするコマンド。コンパイルに失敗する(u.cとかt.cというファイル名は適当に決めた名前なのでコンパイルできない)ので$?には0以外の値が入る。[コマンドは[ exp ]という形式で、expの式を計算して結果が真なら正常終了し(終了ステータスとして0を返す)、そうでないなら異常終了する(終了ステータスとして1を返す)コマンド。[コマンドと同じものにtestコマンドがある。今回はu.cというファイルは(多分)存在しないのでちゃんとforループが止まれば大丈夫。

expには様々な表現があるので一部を示す。

  • -a filenameファイルfilenameが存在する
  • -d filenameファイルfilenameが存在してディレクトリである
  • str1 = str2文字列str1と文字列str2が等しい
  • str1 != str2文字列str1と文字列str2が等しくない
  • num1 -eq num2数として比較した時に、整数num1 = 整数num2(equal)である
  • num1 -gt num2数として比較した時に、整数num1 > 整数num2(greater than)である

3. while

whileの文法は以下。

while commandlist1
do commandlist2
done

これはcommandlist1が正常終了する(終了ステータスとして0を返す)までcommandlist2を繰り返すというもの。具体例としては以下のwhile.sh

while.sh
while true
do
sleep 10
w
done

これは10秒ごとにwコマンドを実行するというもの。wコマンドは誰がログインしているか、そして何をしているかを表示するコマンド。whileの後のコマンドは、それが正常終了すれば(つまり$?が0のとき)ループに入る。trueコマンドは正常終了するだけのコマンドなので永遠にループする。falseコマンドもあり、これは異常終了するだけのコマンド。

4. case

caseの文法は以下。

case string in
pattern1 ) commandlist1 ;;
pattern2 ) commandlist2 ;;
...
esac

まず文字列stringpattern1とマッチするかを調べて、マッチしたらcommandlist1を実行する。マッチしない時はpattern2に移る。これを繰り返すというのがcase文である。最後のesaccaseを逆から記述したもの。具体例としては以下のcase.sh

case.sh
#!/bin/bashfor word in*do
    case$wordin*.out)echo$word is out file;;*.sh)echo$word is sh file;;*)echo$word is other file;;esacdone

例えばあるディレクトリにaa.outbb.shcc.txtというファイルがある時、これを実行すると

aa.out is out file
bb.sh is sh file
cc.txt is txt file

と表示される。

5. 関数

関数はname () { commandlist ; }という形式で定義できる。この定義を実行した後では、nameをコマンドとして実行すると、対応するcommandlistが実行される。具体例は以下で、func.shに書いた。

func.sh
go (){echo"file name = $1"if test!-r$1# !は否定の意味then echo$1 is not found
else echo$1 is found
fi}
go foo.txt
go bar.txt

foo.txtというファイルが存在し、bar.txtというファイルが存在しないディレクトリで./func_script.shで実行すると

file name =  foo.txt
foo.txt is found
file name =  bar.txt
bar.txt is not found

となる。

6. 制御構造を支えるその他のコマンド

execコマンドは、現在execコマンドを実行しているシェル自体の実行を終了して、指定されたファイルを実行する。例としてexec /bin/shというコマンドなら、これに成功すればshに移るし、失敗すればそのままのシェルで続行される。

evalコマンドは、コマンドラインcommandlineを引数として与えてそれを実行させる。例えばeval echo helloならhelloだけが表示される。これが使われるのは
1. あるコマンドが出力した文字列自体をコマンドとして実行するとき
2. 入れ子になった変数を評価するとき
である。1の例としてはeval $SHELL --versionとかで、これでバージョンがわかる。2の例としてはeval '$'$tempで、これで変数tempが指す変数の中身を参照できる。

exitコマンドは、そのシェルの実行を終了させるもの。exit exprとすると式exprの値を終了ステータスとして終了する。正常終了ならexit 1で、異常終了ならexit 0とするというのが普通である。式exprは省略可能。

sourceコマンドは、cshやbash、zshなら存在する。shの場合は.を使う。bash shell_scriptの場合は新たにbashのプロセスを起動してshell_scriptを実行するが、source.で実行するとそのシェルで実行する。以下のsource_dot.shをsourcebashでそれぞれ実行すれば、終了後のカレントディレクトリが異なることがわかる。ただしzshは.sourceの代わりとすることはできなくなった。

source_dot.sh
echo 1 \$PWD is $PWDecho 2 ls
ls
echo 3 change dir
cd ..
echo 4 \$PWD is $PWDecho 5 ls
ls

gotoコマンドは、goto labelという行があると、label:と書かれた行に移動するコマンド。

シグナル処理

bashとかshではtrapコマンドにより、シェルスクリプト中に受け取ったシグナルに対する処理を指定できる。trap command numならシグナル番号numで指定されたシグナルが発生した場合にコマンドcommandを実行する。trap '' numならシグナル番号numで指定されたシグナルを受け付けない(''を実行する)ようにする。trap numならシグナル番号numで指定されたシグナルの処理をもとに戻す。具体的な例として以下のloop.shを紹介する。

loop.sh
trap"echo 'you tried to kill me!'" TERM
trap"echo 'you hit CTRL-C!'" INT
while true;do
      sleep 5
      ps
done

これをbash loop.shとかで起動すると、5秒毎にpsコマンドを実行し続ける。このプロセスはCtrl-Cでは止めることができない。Ctrl-CではINTというシグナルが送られるが、echoコマンドを実行した後に、またwhile文に戻ってしまうためである。Ctrl-Zで一度プロセスを停止して、psコマンドでプロセスID(PID)を調べてkillコマンドを打ってもこのプロセスは生きている(fgコマンドでフォアグラウンドに戻ってもyou tried to kill me!が表示されてまたpsコマンドが実行され続ける)。これはkillコマンドではTERMというシグナルが送られるからである。このプロセスを終了したい時は、kill -KILLコマンドを使えば良い。

ログアウトの時に送られるシグナル

UNIXの設定によっては、ログアウトするとその端末から実行している全てのプロセスにHUP(HangUP)シグナルを送ることがある。HUPシグナルに対する処理を特にしていないプロセスはHUPシグナルを受け取ると終了する。ログアウト後も実行させたいプロセスはnohupの後にコマンドをつけて起動する。例えばnohup python aa.py &としておけば、ログアウトしたあともpython aa.pyは動き続ける。

複雑な置換

変数nに変数名が格納されているとき、その値を取り出すために${$n}と書いても置換が行われない。これは$nの変数置換後には、もう変数置換を行わないからである。その変数の値を取り出すにはevalなどを利用してもう一度評価させる必要がある。具体的には以下。
スクリーンショット 2020-04-04 17.20.45.png

シェルは置換(エイリアス置換、ヒストリ置換、変数置換、コマンド置換、ファイル名置換など)が全て終わった後でコマンドラインを引数に分解する。そのため、変数の値が空文字列であったり空白を含む場合であると、n番目の引数と取得するといった時に添字がずれる可能性があるので注意。

組み込みコマンド

シェル自身が実行するコマンドを組み込みコマンドという。組み込みコマンドを内部コマンドと呼び、それ以外のコマンドを外部コマンドということもある。あるシェルのプロセスの状態を参照したり、状態を変更することは基本的に他のプロセスからはできない。よってシェルのプロセスのカレントワーキングディレクトリを変更するcdとかは組み込みコマンドである。killコマンドのように組み込みコマンドと外部コマンドの両方が存在する場合もあるが、外部コマンドのkillはシェルが記憶しているジョブ番号を利用できないという制約がある。

bashを支えるコマンド

basenameコマンドとdirnameコマンド
basenameコマンドはパス名からディレクトリと拡張子を取り去ったものを出力する。拡張子を指定しなければ、パス名からディレクトリを取り除いたものを出す。
dirnameコマンドはパス名からファイル名を取り去ったものを出力する。実行例を見る方がわかりやすい。
スクリーンショット 2020-04-04 23.33.26.png

exprコマンド
引数の式を計算し、計算結果を標準出力に出力するコマンド。expr expという形式。式expには様々な形式があり、その一部を示す。

指定意味
exp1 & exp2exp1と式exp2のどちらかが空や0なら0を出力し、そうでないとexp2を出力する
exp1 = exp2exp1exp2が整数で一致すれば1を出力し、そうでなければ0を出力する(どちらかが整数でなければ文字列の比較を行う)
exp1 + exp2加算した結果を出力する(他にも-や*、/、%などが使える)
length str文字列strの長さを出す
substr string exp1 exp2文字列stringexp1番目からexp2番目の部分文字列を出力する

ただしbashには$(( exp ))という構文が組み込まれているので、このコマンドを使わなくても良い。shではよく使うコマンドである。
スクリーンショット 2020-04-04 17.38.37.png

シェルについて

UNIXでは様々なシェルが使われる。最も代表的なシェルはshやcshだが、現在ではこれらのシェルが使われることはあまりない。なぜなら、これらをベースに色々と機能を強化した便利なシェルがあるからである。代表的なシェルを簡単に紹介する。

  • tcshは、cshを元に機能を拡張したシェル。特にコマンドライン編集などの対話的作業のための機能の拡張が充実している。
  • kshは、shの機能を強化したシェル。David Korn氏によって開発されたので頭文字のkを取っている。
  • bashは、GNUプロジェクトで開発されているシェル。shの構文をベースにしている。
  • zshは「決定版のシェル」を目指して作られたシェル。およそ現存するほとんど全ての機能を取り込んでいる野心的なシェル。shの構文をベースにしている。

便利な機能

比較的新しいシェルには、shやcshにない様々な機能がある。代表的な機能について、以下のようなものがある。

  • 数値の範囲を指定するファイル名置換
  • スペルの自動修正機能
  • コマンドの定期的な実行機能
  • 高度なリダイレクション
  • コマンドの定期的な実行
  • 時間を指定したコマンドの実行
  • ログインとログアウトの監視機能

シェルはいっぱいあるので、自分にあうシェルを探そう。

終わりに

これでまとめは一旦終わりです。以上の内容はUNIX super textの上巻だけなので、機会があれば下巻もやるかも。なんかミス等あればご指摘いただけると幸いです。


【bash】Gitのブランチ名を「$」前に表示させる

$
0
0

はじめに

Gitを使っていて毎回ブランチを確認する際に

$ git branch

を使っていましたが、割とこのコマンドを使うことが多く、もっと楽に確認できる方法がないかなと探していた際にブランチ名を常に表示させておく方法があるとのことで、今回はその設定方法について書いています。


xcodeを使って入れたGitのバージョン

$ git --version
git version 2.20.1 (Apple Git-117)


他の記事(Homebrewを使ってインストールした記事)では、HomebrewでインストールしたGitを参照しているようですが、自分の実行コマンドのパスはxcodeをインストールした際のGitを参照しています。

Homebrewでインストールした場合のパス

$ which git
/usr/local/bin/git
xcodeでインストールした際のパス

$ which git
/usr/bin/git


Homebrewでインストールした場合は、/usr/local/etc/bash_completion.d/のフォルダ内にgit-completion.bashgit-prompt.shの2つのファイルが存在するらしいんだけど、xcodeでインストールしてそのままの場合は、そのようなファイルはなかった。

$ ls /usr/local/etc/bash_completion.d/
brew        brew-services   npm     tmux

やったこと

①以下のコマンドからHomebrewでインストールしたパッケージを表示してもgitは見当たらないことを確認。

$ brew list

②Homebrewを使ってGitを上書き

$ brew install git 
 
$ which git
/usr/local/bin/git

$ ls /usr/local/etc/bash_completion.d/
brew            git-completion.bash npm
brew-services       git-prompt.sh       tmux

無事二つのファイルが追加されています。

③.bash_profileに以下を記述

$ vim ~/.bashrc
source /usr/local/etc/bash_completion.d/git-prompt.sh
source /usr/local/etc/bash_completion.d/git-completion.bash
GIT_PS1_SHOWDIRTYSTATE=true

export PS1='\[\e[30;47m\] \t \[\e[37;46m\]\[\e[30m\] \W \[\e[36;49m\]$(__git_ps1 "\[\e[37;102m\] \[\e[30m\] %s \[\e[0;92m\]")\[\e[49m\]\[\e[m\] \$ '

パスを通して完了!

$ source ~/.bash_profile

スクリーンショット 2020-04-05 17.18.10.png

参考にした記事

シンボリックリンクとハードリンクの違いって?

$
0
0

シンボリックリンクとハードリンクの違いや使い分けについてご存知でしょうか?
自分はよく知らなかったので、関連する記事をいろいろ探してみました。

図として直感的にわかりやすかったのはこちらの記事。
それぞれの特徴についてもわかりやすく書かれています。
https://eng-entrance.com/linux-permission-link

シンボリックリンク

シンボリックリンクはショートカットというイメージがわかりやすいのではないでしょうか?
original_fnameというファイルを参照するシンボリックリンクsymkink_fnameを作成することを考えてみましょう。

./original_fname
./symlink_fname

ここで

cat ./symlink_fname

を実行すると

cat ./original_fname

と同等の操作を意味するはずです。
つまり、

rm ./symlink_fname

rm ./original_fname

と同等になってしまい、元のファイルoriginal_fnameを削除することになってしまいます。

これに関して、シンボリックリンク削除時の注意点が例えばこの記事に書かれています。
https://mimirswell.ggnet.co.jp/blog-165
要はrmコマンドでもリンクを削除できるけど、危ないからunlinkを使おうね、という話です。

ハードリンク

これに対し、ハードリンクはどうでしょうか。

./original_fname
./hardlink_fname

それぞれのパスは同じ実体ファイルを指し示すことになります。
この状態で

open ./hardkink_fname

を実行すると、

open <実体ファイル>

の意味になります。
面白いのは、実体ファイルへの参照カウンタのようなものがあり、

rm ./hardkink_fname

を実行すると、./hardkink_fnameというパスは削除されてしまいますが、実体ファイルは削除されません。

なぜなら、今考えている状況だと元々
./original_fname
./hardlink_fname
の2つの「参照」が存在していたため、「参照」を1つ削除してもまだ1つ残っているからです。

そして、「参照」の数が0になると、実体も削除されるらしいです。

inodeと実体ファイル

今までイメージを掴むために「実体」や「参照」という言葉を使ってきましたが、きちんと説明するにはinodeに触れる必要があります。

わかりやすいのはこちらの記事でしょうか。
https://qiita.com/katsuo5/items/fc57eaa9330d318ee342

ハードリンクの使い道

一般的に使われるのはシンボリックリンクの方らしいですね。
では逆に、ハードリンクはいつ使われるのでしょうか?

こちらの質問に対する回答を読むと、なるほど!と思えます。
https://q.hatena.ne.jp/1265361495

IMAPサーバで、送信先が複数人のメールを参照するのに使われているとのこと。
コピーせずに済む&適切に削除が行われる、ということですね。

...勉強になりました!

# bash で __git_ps1 を使うときの注意点

$
0
0

__git_ps1 と 改行の食い合わせを気をつけないとお腹壊す

僕は昔から PS1によく

.bashrc
export PS1="\n [\W]\n"

などのように ダブルコーテーションと \n を使うけど これが行けない。
シングルコーテーション と 改行は '$'\n を であるべきだった。

.bashrc
source ~/.git-prompt.sh
export GIT_PS1_SHOWUPSTREAM=1
export GIT_PS1_SHOWUNTRACKEDFILES=1
export GIT_PS1_SHOWDIRTYSTATE=1
export PS1=''$'\n [ \w ]  $(__git_ps1)  '$'\n$ '

理由は、追っかけてないのでわからない。

POSIX準拠のシェルスクリプトでバイナリデータを扱う

$
0
0

はじめに

シェルスクリプトでバイナリデータを扱う方法は以下のようにすでにいくつか記事があります。

本当は別の記事を書いてバイナリデータを扱う方法については省略しようと思っていたのですが、改めてやってみると色々と問題が出てきて話が長くなりそうなので別でまとめることにしました。

基本方針

POSIX 準拠の機能だけで実装する。外部コマンドにはなるべく頼らず速度よりも可搬性を重視、存在する全ての POSIX 準拠シェルで実際に動作するようにする。Bourne Shell は POSIX 準拠ではなく古くて殆ど使われてないと思うため考慮しない。

シェルスクリプトでバイナリを扱う方法

まずPOSIX 準拠の範囲においてシェルスクリプト単体ではバイナリデータを扱えません。問題なのは NULL 文字 (\0) で zsh では変数に NULL 文字を入れられますが、その他のシェルでは入れることが出来ません。そこで POSIX で標準化されている odコマンドを使用してシェルスクリプトと相性がいい 8 進数に変換します。本当は odコマンドも使いたくなかったのですがこれだけはどうしようもないようです。

$cat data.txt
Hello World

$od-t o1 -v data.txt
0000000 110 145 154 154 157 040 127 157 162 154 144 012
0000014

od コマンドの互換性問題

さて私は POSIX 準拠にこだわっていますが、なにも縛りプレイをしたいのではなく「実際に存在する多くの環境で動くようにする」ために POSIX 準拠にしています。しかしながら世の中には POSIX 準拠のコマンドだけを使用していても対応できない場合もあります。(そういった事があるため、外部コマンドも使用したくないのです。)BusyBox ベースの OS がその代表例でインストールするコマンド(アプレット)を選択できるため OpenWrt など一部の環境では odコマンドがインストールされていないことがあります。そういった環境でも代わりに hexdumpがインストールされていたりするのでその場合は対応が可能です。また BusyBox の古いバージョンでは出力のアドレス部分を非表示にする od -A nや 8 進数表示のための -t o1に非対応だったり od -bにバグがあったりしました。POSIX に準拠していなかったりバグが原因だったりするので、切り捨ててもいいと思うのですが、動くコマンドを使った関数を定義すればいいだけなので対応するのはさほど難しくありません。調べるのが面倒なぐらいです。

if od-A n -t o1 -v /dev/null >/dev/null 2>&1;then
  od_command(){od-A n -t o1 -v;}elif hexdump -e'16/1 " %03o" "\n"'-v /dev/null >/dev/null 2>&1;then
  od_command(){ hexdump -e'16/1 " %03o" "\n"'-v;}elif od-b-v /dev/null >/dev/null 2>&1;then
  od_command(){od-b-v | while IFS=" "read-r a o;do echo"$o";done;}# od コマンドに互換性がなく hexdump も入ってない場合の対応。そういう環境が# 存在するかは不明だが最低限の機能だけを使用しているためどこでも動くことが期待できる# -A n に相当するアドレス部の除去はシェルスクリプトで行っているelse
  echo"od command not found">&2
  exit 1
fi

これで可能な限り多くの環境でほぼ同じ形式で 8 進数出力を行う od_command関数を手に入れました。(正確には桁揃えのためのスペースの数が異なりますがあとのコードでその違いが吸収されるようなコードを書いています。)

シェルの8進数対応問題

バイナリデータとして操作する場合、数値として何かしらの計算を行うことが多いのではないでしょうか?算術式展開を使用すると odコマンドの出力結果を 8 進数として扱うことが出来ます。算術式展開はシェルの機能で POSIX 準拠シェルであればどのシェルでも使えます。単純な整数の計算程度で exprbcといった外部コマンドを使用する必要はありません。(外部コマンド呼び出しは時間がかかるため、安易に使ってしまうと遅くなってしまいます。)

たとえば 8進数で 110の場合 n=$((0110))と頭に 0をつけることで 72という数値を得られます。ただしこの書き方に対応していないシェルが存在します。(set -o posixしていない)mksh と (emulate -R shしていない)zsh と 古い pdksh です。POSIX 準拠モードに変更すれば動きますが、どんなモードでも動くようにしたいので変更しません。これらのシェルでは代わりに n=$((8#110))という表記に対応しているので、n=$((0110))に対応しているかどうかで頭につける文字を変更します。

# 頭 0 に対応していないシェルでは $((010)) は 8 ではなく 10 と解釈されます[$((010))-eq 8 ]&&OCT="0"||OCT="8#"oct="110"n=$((${OCT}${oct}))# 全てのシェルで n = 72 です

仮実装

上記のコードを利用して、バイナリデータとして読み取って計算するコードを実装します。計算の内容は特に意味がないですが ファイルの内容を一バイトずつ XORしていくだけのプログラムです。

xor.sh
#!/bin/sh[$((010))-eq 8 ]&&OCT="0"||OCT="8#"if od-A n -t o1 -v /dev/null >/dev/null 2>&1;then
  od_command(){od-A n -t o1 -v;}elif hexdump -e'16/1 " %03o" "\n"'-v /dev/null >/dev/null 2>&1;then
  od_command(){ hexdump -e'16/1 " %03o" "\n"'-v;}elif od-b-v /dev/null >/dev/null 2>&1;then
  od_command(){od-b-v | while IFS=" "read-r a o;do echo"$o";done;}else
  echo"od command not found">&2
  exit 1
fi# 110 145 154 154 157 040 127 157 162 154 144 012 という並びを1バイト1行に変換
od_serialize(){while IFS=read-r line;do
    eval"set -- $line"# set することで桁揃えのスペースの違いを吸収している# eval は zsh の SH_WORD_SPLIT 対策と# IFS にスペースが入っていない場合への対応のためfor oct;do
      echo"$oct"done
  done}

od_command | od_serialize | {value=0
  while IFS=read-r oct;do
    value=$(($value^${OCT}${oct}))done
  printf'%2x\n'"$value"}

od_serialize関数はデータを処理しやすくするために 1 バイト 1行 の形に整形しています。

ここまでの内容で終わっていれば記事にするつもりもなかったのですが・・・

ksh が異様に遅い

軽くベンチマークしていて速いと定評のある ksh でなぜか異様に遅いことが判明しました。Linux 上ではそこまでひどくないのですが、WSL1 上だと特に酷いです。

# data.dat は 100KB のファイル$ time sh xor.sh < data.dat
WSL1ash (busybox)bashdashkshmkshyashzsh
real3.256s2.793s1.379s11.368s1.589s4.455s3.856s
user1.266s2.609s0.594s2.172s1.141s1.906s1.984s
sys5.094s3.031s2.109s11.016s1.969s7.234s5.141s
Linuxash (busybox)bashdashkshmkshyashzsh
real0.692s1.071s0.371s2.429s0.670s1.220s0.969s
user0.762s1.521s0.420s1.306s0.865s1.435s1.202s
sys0.556s0.393s0.328s1.557s0.291s0.749s0.479s

こんなものなんでは?と思うかもしれませんが ksh は通常、他のシェルよりも速いことが多いので異常です。かかってる時間を見てみると sys の割合が多いです。WSL1 では sys(カーネル)に相当する部分が遅いことがしばしばあるので、このコードは他に比べてカーネルが処理する部分が多いのでしょう。

そこで色々実験してみると以下の形が遅いことがわかりました。

# oct.txt は10万行のファイル$ time ksh -c'cat oct.txt | while IFS= read -r oct; do :; done'
real    0m7.936s
user    0m1.063s
sys     0m6.969s

# この形式であれば速い$ time ksh -c'while IFS= read -r oct; do :; done < oct.txt'
real    0m0.249s
user    0m0.234s
sys     0m0.016s

ただしパイプを使ったからと言って必ずしも遅くなるとは限らず、ちゃんとした理由までつかめてないのですが、どうもパイプを使うなどしてサブシェルで readすると極端に遅くなる気がします。パフォーマンスは重視してないとはいえ、この差はちょっと見過ごしたくないのでパフォーマンス改善することにしました。

パフォーマンス改善

処理しやすくするために od_serializeで1バイト1行の形式にしていましたが、これによって 1 行が 16 行のデータなってしまっていたので、これをやめます。パイプ(標準入出力)で渡していたデータが少なくなるので他のシェルでも効果が期待できます。

#!/bin/sh[$((010))-eq 8 ]&&OCT="0"||OCT="8#"if od-A n -t o1 -v /dev/null >/dev/null 2>&1;then
  od_command(){od-A n -t o1 -v;}elif hexdump -e'16/1 " %03o" "\n"'-v /dev/null >/dev/null 2>&1;then
  od_command(){ hexdump -e'16/1 " %03o" "\n"'-v;}elif od-b-v /dev/null >/dev/null 2>&1;then
  od_command(){od-b-v | while IFS=" "read-r o o;do echo"$o";done;}else
  echo"od command not found">&2
  exit 1
fi

od_command | {value=0
  while IFS=read-r line;do
    eval"set -- $line"for oct;do
      value=$(($value^${OCT}${oct}))done
  done
  printf'%2x\n'"$value"}
WSL1ash (busybox)bashdashkshmkshyashzsh
real3.256s2.793s1.379s11.368s1.589s4.455s3.856s
2.780s1.676s1.125s0.599s1.343s3.850s1.675s
user1.266s2.609s0.594s2.172s1.141s1.906s1.984s
0.516s0.844s0.094s0.422s0.594s0.703s0.406s
sys5.094s3.031s2.109s11.016s1.969s7.234s5.141s
2.438s0.953s1.188s0.188s0.938s3.438s1.500s
Linuxash (busybox)bashdashkshmkshyashzsh
real0.692s1.071s0.371s2.429s0.670s1.220s0.969s
0.594s0.755s0.328s0.417s0.506s0.853s0.487s
user0.762s1.521s0.420s1.306s0.865s1.435s1.202s
0.414s0.666s0.223s0.383s0.417s0.564s0.396s
sys0.556s0.393s0.328s1.557s0.291s0.749s0.479s
0.200s0.128s0.148s0.060s0.129s0.316s0.119s

上の段が修正前。下の段が修正後です。全体的に改善されていますが ksh が劇的に改善されました。WSL 1 上では約 18 倍の速度になっています。思えばデータの行数が 1/16 になっているわけで、ある意味自然な数字とも言えます。ちなみにこのコードで od_command |のパイプ先をサブシェルに変更すると大きく速度が低下します。

od_command | {・・・
}# ↓ このように書き換える
od_command | (・・・
)

やはり ksh は他のシェルに比べてサブシェル間での標準入出力のやり取りが苦手なのかもしれません。

メンテナンス性改善

さてパフォーマンスは改善されましたが、od_command関数の出力を解釈する処理をそのまま埋め込んだので、メンテナンス性は悪くなりました。どうにか改善できないでしょうか?

外部コマンドとのやり取りでは標準入出力でデータを渡すしかありませんが、シェル内ではシェル関数を使ってデータを渡すことも出来ます。関数呼び出しのオーバーヘッドがありますがそこまで速度は低下しません。そこでコールバック関数を使う方法に書き換えます。

xor.sh
#!/bin/sh[$((010))-eq 8 ]&&OCT="0"||OCT="8#"if od-A n -t o1 -v /dev/null >/dev/null 2>&1;then
  od_command(){od-A n -t o1 -v;}elif hexdump -e'16/1 " %03o" "\n"'-v /dev/null >/dev/null 2>&1;then
  od_command(){ hexdump -e'16/1 " %03o" "\n"'-v;}elif od-b-v /dev/null >/dev/null 2>&1;then
  od_command(){od-b-v | while IFS=" "read-r o o;do echo"$o";done;}else
  echo"od command not found">&2
  exit 1
fi

read_as_binary(){
  od_command | {while IFS=read-r line;do
      eval"$1$line"done"$2"}}# --- ここより上は汎用コード ---

callback(){for i;do
    value=$(($value^${OCT}${i}))done}

finished(){printf'%02x\n'"$value"}value=0
read_as_binary callback finished
# この場所でのvalueの値がどうなるかはシェル依存# od_command | のパイプ先がサブシェルとなるシェルでは 0 のままだが# ksh などでは計算結果になっている場合があるので注意

read_as_binary関数は汎用的な関数なので共通コードとして再利用できます。callback関数はある程度のバイトの塊(通常 16バイト)を引数にして呼び出されます。すべての処理が終わったら finished関数が呼び出されます。本質的な処理を行っている callback関数と finished関数を書き換えるだけで他の処理を行うことが出来ます。

再度パフォーマンスチェックです。

WSL1ash (busybox)bashdashkshmkshyashzsh
real2.780s1.676s1.125s0.599s1.343s3.850s1.675s
2.758s1.743s1.092s0.590s1.349s3.815s1.748s
user0.516s0.844s0.094s0.422s0.594s0.703s0.406s
0.500s0.859s0.188s0.438s0.469s0.750s0.484s
sys2.438s0.953s1.188s0.188s0.938s3.438s1.500s
2.516s1.172s1.156s0.203s1.141s3.266s1.547s
Linuxash (busybox)bashdashkshmkshyashzsh
real0.594s0.755s0.328s0.417s0.506s0.853s0.487s
0.596s0.793s0.316s0.435s0.512s0.885s0.536s
user0.414s0.666s0.223s0.383s0.417s0.564s0.396s
0.370s0.681s0.242s0.432s0.448s0.564s0.381s
sys0.200s0.128s0.148s0.060s0.129s0.316s0.119s
0.246s0.148s0.102s0.029s0.107s0.349s0.182s

このようにコールバック関数を使っても殆どの場合、誤差程度しか違いがありません。ただし zsh は少し注意が必要です。evalや シェル関数呼び出しが他のシェルよりも遅いため、回数によっては影響が大きくなる場合があります。しかし共通コードとして関数にしているため特定のシェル向けのチューニングを行うのも簡単でしょう。例えば zsh 専用の read_as_binary関数です。

read_as_binary(){
  od_command | {while IFS=read-r line;do
      IFS=read-r line2
      IFS=read-r line3
      IFS=read-r line4
      # ${=line} という書き方は zsh 専用で SH_WORD_SPLIT を ON にした場合と同じように引数を処理する# 関数呼び出しが遅いので 16 × 4 = 64 個の引数にまとめてから呼び出している"$1"${=line}${=line2}${=line3}${=line4}done"$2"}}

これで WSL1 上の zsh で 1.748s だったのが 1.505s に改善されました。zsh は変数に NULL 文字を入れられるわけで odコマンドを使わない実装もあるかもしれません。他にも手はあると思いますが極端に遅くなければ問題ないと思ってるので深追いはしないこととします。

さいごに

それにしてもたかだか 100KB 程度でこんなに遅いわけで、やっぱりシェルスクリプトでバイナリデータを扱うものではないですね。使うならば小さいデータに使うことになると思います。使える場面は少ないと思いますが一つ例を。/proc/1/cmdlineというファイルにはプロセスID 1 を実行したときのコマンド名と引数が以下のような形式で取得できるのですが

$ cat /proc/1/cmdline
/lib/systemd/systemd --system--deserialize 58

実はこの引数は NULL 文字区切りなのです。

$ cat /proc/1/cmdline | hexdump -C
00000000  2f 6c 69 62 2f 73 79 73  74 65 6d 64 2f 73 79 73  |/lib/systemd/sys|
00000010  74 65 6d 64 00 2d 2d 73  79 73 74 65 6d 00 2d 2d  |temd.--system.--|
00000020  64 65 73 65 72 69 61 6c  69 7a 65 00 35 38 00     |deserialize.58.|
0000002f

こういったデータとしては短いけれど、バイナリデータとして扱わなければいけない。というものをシェルスクリプトで使いたいという時には使えると思います。

Ubuntu18.04で.bashrcの編集内容が保存されないときの対処法

$
0
0

環境

・ホストOS: windows10 home
・WSL Ubuntu18.04 lts
・Vscode ver1.43.2

問題

.bashrcに環境変数を設定しても、エディタを閉じると設定が保存されず、次回起動時に source ~/.bashrc で設定を読み込まないといけない。

対処法

以下の手順で解決した。

① .bash_profileを開く

 $ vi ~/.bash_profile

② 以下の設定を追記する

if [ -f ~/.bashrc ] ; then
    . ~/.bashrc
fi

これでエディタの起動時に.bashrcの内容が読み込まれるようになる。

POSIX準拠のシェルスクリプトでハッシュ値を計算する(FNV-1 / FNV-1a実装)

$
0
0

はじめに

POSIX 準拠のシェルスクリプトでハッシュ値を計算したかったため FNV-1 / FNV-1a を実装しました。FNV-1 / FNV-1a は乗算と XOR だけを使ったシンプルなハッシュ関数です(参考 Fowler–Noll–Vo hash function)。 一応断っておくとシェルスクリプトでの実装は計算速度が遅いので、特に理由がないなら md5sumsha1sum等を使用したほうが良いです(補足1参照)。なぜ実装したかと言うとこれらのコマンドは私が欲しかった用途には適していなかったからです。一応 POSIX 準拠でもハッシュを求めるコマンドとして cksumがあり、今回実装した FNV-1 / FNV-1a も 32 bit なので衝突率も大差ないらしく、使えるには使えるのですがハッシュ値を 1 つ計算するたびに外部コマンドを呼びださなくてはなりません(補足2参照)。今回欲しかったのはファイルに対してハッシュ値を求めるのではなく、大量の文字列に対して個別にハッシュ値を計算したかったので、そのような場合にはコマンド呼び出しで時間がかかってしまいます。(またなるべく外部コマンドには依存したくないという理由もありました。)

  • 補足1 今回の目的はセキュリティ目的ではなくハッシュの衝突率はさほど重要ではありません。これらが重要な場合は MD5 や SHA1 でも能力不足です。
  • 補足2 cksumコマンド等は引数で複数のファイルを指定できるので、それぞれの文字列をそれぞれのテンポラリファイルに書き出して処理させればコマンド呼び出しは一回ですみますが、ファイル書き込みで遅くなるかもしれないし、流石にハッシュ値計算程度で大量にファイルを作成するのはなぁという感じです。

Go 版の実装

まず自分で実装したコードが正しいかの検証用として Go ではライブラリがあるようなのでそれを使って実装しています。

fnv.go
packagemainimport("os""fmt""hash/fnv""io/ioutil")funcfnv1(b[]byte)uint32{h:=fnv.New32()h.Write(b)returnh.Sum32()}funcfnv1a(b[]byte)uint32{h:=fnv.New32a()h.Write(b)returnh.Sum32()}funcmain(){data,err:=ioutil.ReadAll(os.Stdin)iferr!=nil{fmt.Println("err",err)os.Exit(1)}fmt.Printf("%x\n",fnv1(data))fmt.Printf("%x\n",fnv1a(data))}
$cat data.dat | hexdump -C00000000  48 65 6c 6c 6f 20 57 6f  72 6c 64 0a              |Hello World.|
0000000c

$go build fnv.go
$fnv < data.dat
12a9a437
d8ea85d7
#上がfnv1、下がfnv1a

バイナリデータの扱い

シェルスクリプトからバイナリデータを扱う方法については長くなったので別記事にまとめました。以下の部分に関する説明は省略します。

[$((010))-eq 8 ]&&OCT="0"||OCT="8#"if od-A n -t o1 -v /dev/null >/dev/null 2>&1;then
  od_command(){od-A n -t o1 -v;}elif hexdump -e'16/1 " %03o" "\n"'-v /dev/null >/dev/null 2>&1;then
  od_command(){ hexdump -e'16/1 " %03o" "\n"'-v;}elif od-b-v /dev/null >/dev/null 2>&1;then
  od_command(){od-b-v | while IFS=" "read-r o o;do echo"$o";done;}else
  echo"od command not found">&2
  exit 1
fi

read_as_binary(){
  od_command | {while IFS=read-r line;do
      eval"$1$line"done"$2"}}

シェルスクリプト実装(手抜き版)

ひとまずだいたいのシェルで動作する一番単純な実装です。シェルスクリプトでは計算結果が 32 bit とは限らないので 0xFFFFFFFFでマスクしている所が違う程度でアルゴリズムは同じです。乗算と XOR (そして追加の AND )からなる実にシンプルなアルゴリズムですね。

# 詳細は「バイナリデータの扱い」参照[$((010))-eq 8 ]&&OCT="0"HEX="0x"||OCT="8#"HEX="16#"

fnv1(){for oct;do
    hash=$(((($hash*$FNV_PRIME_32)&${HEX}FFFFFFFF)^${OCT}$oct));done}

fnv1a(){for oct;do
    hash=$(((($hash^${OCT}$oct)*$FNV_PRIME_32)&${HEX}FFFFFFFF ))done}

finished(){printf"%8x\n"$hash}FNV_OFFSET_BASIS_32=2166136261
FNV_PRIME_32=16777619

hash=$FNV_OFFSET_BASIS_32
read_as_binary fnv1 finished < data.dat
# => 12a9a437hash=$FNV_OFFSET_BASIS_32
read_as_binary fnv1a finished < data.dat
# => d8ea85d7# 注意 zsh を除く シェルは NULL 文字 (\0) を変数に入れられないのでデータ(data.dat)に# NULL 文字が含まれていないと保証できない場合はデータを変数に入れることは出来ません# data.dat を 2 回読み込んでるのはそのためです

符号付き32bit整数対応

アルゴリズム的には上記のコードで良いのですが、動作しないシェルがあります。mksh と 32bit 環境の dash、posh、pdksh です。これらのシェルでは d8ea85d7ではなく ffffffffd8ea85d7と出力されてしまいます。

これらのシェルで数値が符号付き32bit整数として扱われているため上位ビットが 1 の場合にマイナス値と認識されこの様な結果となっています。対応は簡単でマイナスであれば(下位)32bit を2つに分けて出力するだけです。

finished(){if["$hash"-lt 0 ];then
    printf"%4x%4x\n"$((($hash>>16)&${HEX}FFFF ))$(($hash&${HEX}FFFF ))else
    printf"%8x\n"$hashfi}

古い ksh のバグ対応

debian 3.1/4.0 時代の ksh 93q/93r の話なので通常は対応する必要はないでしょう。なぜか ${HEX}FFFFFFFF書くと xFFFFFFFF: parameter not setなどとエラーが表示されてしまいます。いずれも固定値なので 10 進数表記にすることにします。

ksh の浮動小数点演算の精度問題

さて実はここからが問題です。ksh だけ全く異なる計算結果になりました。計算途中の値を詳しく見ていくと途中で計算した値がわずかにずれています。(ハッシュ値の性質上、途中で少し値が異なれば、その後は全く違ったものとなります。)次のコードはそれを再現する簡単なコードです。

# 値が変わっている
ksh -c'echo $((111111111111111111))'
111111111111111104

# 変数に入れればいけるか思いきや計算するとだめ
ksh -c'a=111111111111111111; echo $((a))'
111111111111111111
ksh -c'a=111111111111111111; echo $((a+0))' 
111111111111111104

# typeset -iで整数型にしてもだめ$ ksh -c'typeset -i a=123456789012345; typeset i=10; typeset -i a=$((a*i)); echo $a'-2147483648$ mksh -c'typeset -i a=123456789012345; typeset i=10; typeset -i a=$((a*i)); echo $a'
1015724730

ですがこれ WSL1 だけなんですよね・・・。Linux や Windows 上の Docker などでは再現しません。詳しくないのですがおそらくFPUの浮動小数点数の精度のモード(変更可能)が違っているのだと思います。

インテル(R) C++ コンパイラ Linux* と Windows* システムでの浮動小数点ユニット (FPU) 精度の違い

Linux* 上では、デフォルトの FPU 精度は 64 ビットです。これは、-pc80 オプションと同じです。Windows* 上では、デフォルトの FPU 精度は 53 ビットで、対応するオプションは -Qpc64 です。この違いにより、同じプログラムまたはアルゴリズムを Linux と Windows 上で実行すると、結果が異なる場合があります。
これらの処理は OS レベルで行われ、コンパイラが処理することはありません。

数値が 53 bit 以内に収まっているなら誤差は発生しないので計算式を工夫し、53 bit を超えないようにします。53bit を超える可能性があるのは以下の部分です。

  • fnv1: $hash * $FNV_PRIME_32
  • fnv1a: ($hash ^ ${OCT}$oct) * $FNV_PRIME_32)

FNV_PRIME_32の値が 16777619と大きな数値になっているため、被乗数が大きな数値(例 0xFFFFFFFF)だと計算結果が 0x16777618E98889E7(16桁)となり、53 bitで表せる最大値の 0x1fffffffffffff(14桁) を超えてしまいます。そのため上位 16 bitと下位 16 bitに分けて計算します。0xFFFF * 16777619であれば、最大 0x1000092FE6D(11桁)にしかならないので 53 bit に収まります。

シェルスクリプト実装(完成版)

これで終わりなので最終的なコードとしてまとめます。(余談 途中変なところで継続行にしてるのはシンタックスハイライトが <<でおかしくなるため)

[$((010))-eq 8 ]&&OCT="0"||OCT="8#"if od-A n -t o1 -v /dev/null >/dev/null 2>&1;then
  od_command(){od-A n -t o1 -v;}elif hexdump -e'16/1 " %03o" "\n"'-v /dev/null >/dev/null 2>&1;then
  od_command(){ hexdump -e'16/1 " %03o" "\n"'-v;}elif od-b-v /dev/null >/dev/null 2>&1;then
  od_command(){od-b-v | while IFS=" "read-r o o;do echo"$o";done;}else
  echo"od command not found">&2
  exit 1
fi

read_as_binary(){
  od_command | {while IFS=read-r line;do
      eval"$1$line"done"$2"}}

fnv1(){for oct;do
    hash=$((((((($hash>>16)*$FNV_PRIME_32)&65535)<< \ 16)+(($hash&65535)*$FNV_PRIME_32))&4294967295))hash=$(($hash^${OCT}$oct))done}

fnv1a(){for oct;do
    hash=$(($hash^${OCT}$oct))hash=$((((((($hash>>16)*$FNV_PRIME_32)&65535)<< \16)+(($hash&65535)*$FNV_PRIME_32))&4294967295))done}

finished(){if[$hash-lt 0 ];then
    printf"%4x%4x\n"$((($hash>>16)&65535))$(($hash&65535))else
    printf"%8x\n"$hashfi}FNV_OFFSET_BASIS_32=2166136261
FNV_PRIME_32=16777619

hash=$FNV_OFFSET_BASIS_32
read_as_binary fnv1 finished < data.dat
# => 12a9a437hash=$FNV_OFFSET_BASIS_32
read_as_binary fnv1a finished < data.dat
# => d8ea85d7

さいごに

今回ハッシュアルゴリズムとして FNV-1 / FNV-1a を選んだ理由はシェルスクリプトでもシンプルに実装できて計算コストも低いだろうと思ってのことだったのですが浮動小数点演算の精度問題で少し想定外なコードになってしまいました。実は FNV-1a を使ったコードはすでに使用していてるのですが記事にまとめているうちに気づいたので、つまり今使っている実装はバグが有るということです。クリティカルな所ではないのでそんなに問題はないのが救いですが、そのうちバグを修正するか他のハッシュアルゴリズムに変更すると思います。ElfHashなんかアルゴリズム的に今回の問題は発生しなさそうなんで良いんじゃないかなーって思っています。

Github(Git) 入門

$
0
0

はじめに

春ですね,ご入社,ご入学おめでとうございます.この時期にGithub入門する人も多いと思います.この記事はせっかく社内ドキュメント作るのなら共有しちゃおうというものです.特有の言い回しなどはかみ砕いて説明するので,多少の相違は了承ください.

なぜ必要? クラウドストレージと何が違うの?

更新の時系列がわからないディレクトリがあなたのパソコンにはありませんか? 版を管理することをバージョン管理といいますが,ソフトウェア開発,デザイン,文筆などあらゆることでバージョン管理は煩わしいものです.更にチームになるとこのようなファイルがクラウドストレージにあったら,チームメンバーは混乱してしまいます.個人でもチームでもこんなことに時間をさきたくないはずです.
スクリーンショット 2020-04-05 17.50.42.png
この煩わしさからGitは開放させてくれます.Gitは時系列順に版をスナップショットとして管理するバージョン管理システムです.Gitにcommitすることによって版が作成され更新されます.もとに戻したければ戻せますし,別バージョンを作りたければbranchを切ることによってできます.

スクリーンショット 2020-04-06 21.27.47.png

GitとGithub

Githubはバージョン管理システム(ソフトウェア)であるGitをクラウドサービスとして使えるようにしたもので,現在はMicrosoftによって運営されています.同等のサービスやソフトウェアとしてBitbucketやGitLabなどのプロジェクトがあります.

Bitbucket
GitLab

Gitはサーバークライアントとローカルクライアントが存在し,サーバーの方をサーバーに,ローカルをパソコンにインストールすることで使用します.Gitをサーバーにインストールすることによってバージョン管理サーバーとして,チーム内で共有することができるわけです.

スクリーンショット 2020-04-06 21.20.56.png

gitをローカルで使用する場合まず,

zsh
$ git --version#でバージョン情報を表示します.#=== 以下戻り値 ====-git version 2.21.0 (Apple Git-122.2)#インストールされている場合はこのようなバージョン情報が表示されています.-git: command not found
#not foundが表示される場合はgitをインストールする必要があります.

not found の場合例えばmacでしたら,
$ brew install git
$ sudo apt install git
などを使いインストールします.これは個人の環境で違います.
インストールが終わったら,

zsh
$ git config --global user.email <your e-mail address>
$ git config --global user.name <your user name>

注意

このアドレスと名前はgithub上で世界に公開されます.

GithubとGitの使い方

gitはレポジトリ(リポジトリ)という単位でプロジェクトを管理します.レポジトリとはプロジェクトのディレクトリーのこととの認識で構いません.このレポジトリにはリモートとローカルが存在します.ローカルは自分のPC内にあるもので,リモートはそれをサーバーにアップロードした状態のことを言います.
プロジェクトのルートレポジトリにある.gitという隠しファイル(サブディレクトリ)に更新情報やリモートレポジトリの情報が記載されています.

Gitのフロートしては,clone して add して commit して pushします.
スクリーンショット 2020-04-06 21.42.22.png

1.レポジトリを作る

まずプロジェクトを始めるにあたってリモートレポジトリであるGithubにレポジトリを作成します.本来はレポジトリを作る場合,

zsh
$ mkdir hoge        #ディレクトリの作成$ git init          #gitの初期化(.gitの作成)$ touch readme.md   #説明書の作成$ touch LICENSE     #Licenseファイルの作成

を実行する必要がありますが,Githubではレポジトリを作ると自動で,git initを実行してreadme.mdやLICENSE,.gitを作成してくれます.
スクリーンショット 2020-04-06 21.44.57.png
試しにhogeレポジトリを作ります.

2.clone

リモートレポジトリにあるレポジトリをローカルにダウンロードすることをcloneといいます.cloneするには任意の場所で,

$ git clone <hoge url or git>

を実行します.ダウンロードされたhogeは一見readme.mdとLICENSEだけですが,きちんと.gitが含まれています.

スクリーンショット 2020-04-06 22.01.12.png

Githubではこのcloneコマンドの他に,右上の緑色のボタンからzip形式でcloneすることもできます.プロジェクトメンバーで開発に参加していない人でも最新のzipを手軽にダウンロードすることが可能です.

スクリーンショット 2020-04-06 21.53.05.png

3.add

cloneした状態に新しいファイルを作成したならばaddが必要です.addはあたらしいファイルを管理リストに追加することです.これでファイルの変更を追従できるようになります.

zsh
$ git add -A

どのファイルが追加されていないかなどは

zsh
$ git status

で確認できます.
スクリーンショット 2020-04-06 22.03.44.png
追従していないファイルが表示されている.
スクリーンショット 2020-04-06 22.04.44.png
addをすることで追従される.

4.commit

既存のファイルを編集したときや,新しいファイルをaddしたときにはcommitをします.commitをすることで新しい版(バージョン)が作成されます.
どこまでやったらcommitするかは自由ですが,戻せない変更をする場合は,する前にかならずcommitします.セーブポイントできちんとセーブをしておけば,ラスボスで負けても再挑戦ができるわけです.

commitには何を変更したかcommit messageを添えます.

zsh
$ git commit -am"example commit"

またcommitした変更はコミット履歴という形で履歴に残ります.

zsh
$ git log

スクリーンショット 2020-04-06 22.06.20.png

5. push

commitはローカルストレージのプロジェクトディレクトリ内の.gitに記録されます.この情報をスナップショットと一緒にアップロードすることをpushといいます.
pushを行うことでリモートレポジトリを更新し最新にします.チームメンバーはgit pullをすることで,常に最新版を使うことができます.

zsh
$ git push origin master #masterブランチにpush$ git pull

チーム開発の場合,大抵はmasterブランチにはpushをせずにそれぞれのバージョンのブランチにpushし,それをmergeすることでmasterを最新に保ちます.これはバグなどの影響を最小限に留めるためです.

スクリーンショット 2020-04-06 21.48.12.png
pushするまえ
スクリーンショット 2020-04-06 22.07.45.png
pushしたあと

GUIソフト

Sourcetreeなどを使えばterminalではなく視覚的なソフトからでもGitを操作することができます.
スクリーンショット 2020-04-06 22.23.08.png


以上,いちばん基本的なGit及びGithubの使い方でした.

参考

Git --distributed-is-the-new-centralized


HTTPリクエストと(Postgre)SQLを自動化するスクリプトを書いてみた

$
0
0

ローカル環境において、手動でHTTPリクエストや(Postgre)SQLを実行するのが辛くなってきたため、それらを自動化するBashスクリプトを書いてみました。
今回はこのスクリプトを解説いたします。

GitHubリポジトリはこちらです

想定している環境

  • ローカル環境
  • DBサーバ(PostgreSQL)がDocker上に存在

必要なライブラリのチェック

スクリプトではdocker、psql、jq、curlを使用しているため、これらがインストールされているかをチェックします。
各ライブラリの名前を、for文のinに列挙しています。
whichコマンドでエラーが出力された場合、usage()を呼び出し、標準出力は捨てています。

function usage(){echo"please install docker, psql, jq, and curl"exit 1
}# check requirementsfor libName in docker psql jq curl;do
  which $libName>$DEVNULL|| usage
done

(テスト用)既存のPostgreSQLコンテナの削除

スクリプトのテスト用に作成したDBコンテナがあれば削除します(実際には既存のDBコンテナが立っているはずなので、この処理はコメントアウトされるでしょう)。
docker ps -aで、指定の名前のコンテナが存在するかを0or1で取得します。空白が入るためsedで除去しています。
コンテナが存在した場合、停止と削除を実行します。

# remove existing containerexist=$(docker ps -a | grep$NAME | wc-l | sed's/ //g')if[[$exist= 1 ]];then
  docker stop $NAME>$DEVNULL&& docker rm$NAME>$DEVNULLecho"existing container $NAME removed"fi

(テスト用)PostgreSQLコンテナの起動、疎通が確認できるまで待機

スクリプトのテスト用に、PostgreSQLのコンテナを起動します(実際には既にDBコンテナが立っているはずなので、この処理はコメントアウトされるでしょう)。
起動直後にpsqlを実行してしまうと接続できずにエラーとなるため、疎通が確認できるまで待つようにしています。
ここではpsql -c \lを実行し、エラーが出力された場合はwhileをループするようにしています。

function healthcheck(){PGPASSWORD=$PASSWORD psql -h$HOST-U$USER-c'\l'>$DEVNULL 2>&1 ||return 1
  return 0
}# startup postgres
docker run -it-d-p$PORT:$PORT--name$NAME-ePOSTGRES_PASSWORD=$PASSWORD postgres:latest >$DEVNULL# wait for container running upwhile true;do
  sleep 0.1 && healthcheck ||continue
  break
done

(テスト用)データベース、テーブルの作成、レコードのinsertとselect

テスト用PostgreSQLコンテナ内にDB、Tableを作成し、レコードをinsert, selectします(実際には既存のDBコンテナが立っているはずなので、この処理はコメントアウトされるでしょう)。

# (temporary) preparation postgresPGPASSWORD=$PASSWORD psql -h$HOST-U$USER-c"create database $DB"PGPASSWORD=$PASSWORD psql -h$HOST-U$USER-d$DB<<EOF
  create table fruit (id serial PRIMARY KEY, name text, price integer);
  insert into fruit (name, price) values ('apple', 100), ('orange', 200), ('lemon', 300);
EOF

# select all records before updatesfor i in{1..3};do
PGPASSWORD=$PASSWORD psql -h$HOST-U$USER-d$DB<<EOF
select name, price from fruit where id = ${i};
EOF
done

HTTPリクエストで取得したResponseを元にSQLを実行

curlでデータを取得します。ここではBitBankのPublic APIを利用しています。
取得したdataはヒアドキュメント内で参照され、psqlが実行されます。

# fetch data by HTTP Request# following response should be returned# {#   "success": 0,#   "data": {#     "code": 10000#   }# }data=$(curl -s$URL | jq '.data | .code')echo$data# update record with the dataPGPASSWORD=$PASSWORD psql -h$HOST-U$USER-d$DB<<EOF
  update fruit set price = ${data} where name = 'apple';
  update fruit set price = ${data} where name = 'orange';
EOF

レコードが更新されたことを確認

取得したdataによってレコードが更新されていることを確認します。

# select all records after updatesfor i in{1..3};do
PGPASSWORD=$PASSWORD psql -h$HOST-U$USER-d$DB<<EOF
select name, price from fruit where id = ${i};
EOF
done

スクリプト全体

上記をまとめると、以下となります。
実際には下記のコードをアレンジしてご使用頂くかと思います。

#!/bin/bashreadonly NAME='test_postgres'readonly PASSWORD='password'readonly HOST='0.0.0.0'readonly PORT='5432'readonly USER='postgres'readonly DB='temp'readonly DEVNULL='/dev/null'readonly URL='https://public.bitbank.cc'function healthcheck(){PGPASSWORD=$PASSWORD psql -h$HOST-U$USER-c'\l'>$DEVNULL 2>&1 ||return 1
  return 0
}function usage(){echo"please install docker, psql, jq, and curl"exit 1
}# check requirementsfor libName in docker psql jq curl;do
  which $libName>$DEVNULL|| usage
done# remove existing containerexist=$(docker ps -a | grep$NAME | wc-l | sed's/ //g')if[[$exist= 1 ]];then
  docker stop $NAME>$DEVNULL&& docker rm$NAME>$DEVNULLecho"existing container $NAME removed"fi# startup postgres
docker run -it-d-p$PORT:$PORT--name$NAME-ePOSTGRES_PASSWORD=$PASSWORD postgres:latest >$DEVNULL# wait for container running upwhile true;do
  sleep 0.1 && healthcheck ||continue
  break
done# (temporary) preparation postgresPGPASSWORD=$PASSWORD psql -h$HOST-U$USER-c"create database $DB"PGPASSWORD=$PASSWORD psql -h$HOST-U$USER-d$DB<<EOF
  create table fruit (id serial PRIMARY KEY, name text, price integer);
  insert into fruit (name, price) values ('apple', 100), ('orange', 200), ('lemon', 300);
EOF

# select all records before updatesfor i in{1..3};do
PGPASSWORD=$PASSWORD psql -h$HOST-U$USER-d$DB<<EOF
select name, price from fruit where id = ${i};
EOF
done# fetch data by HTTP Request# following response should be returned# {#   "success": 0,#   "data": {#     "code": 10000#   }# }data=$(curl -s$URL | jq '.data | .code')echo$data# update record with the dataPGPASSWORD=$PASSWORD psql -h$HOST-U$USER-d$DB<<EOF
  update fruit set price = ${data} where name = 'apple';
  update fruit set price = ${data} where name = 'orange';
EOF

# select all records after updatesfor i in{1..3};do
PGPASSWORD=$PASSWORD psql -h$HOST-U$USER-d$DB<<EOF
select name, price from fruit where id = ${i};
EOF
done

さいごに

久々にシェルスクリプトを書いたのですが、楽しいですね。
こんな書き方があるよ、といったコメント等あれば是非お願いいたします。

【Git】Gitで現在のbranchを環境変数にセットする方法

$
0
0

方法

現在developブランチにいるとして下記を実行します。

$ export CURRENT_BRANCH="$(git branch --contains | cut-b 3-)"$ echo$CURRENT_BRANCH
develop

developブランチが$CURRENT_BRANCHにセットされました。

何をやっているか

$ git branch --contains* develop

特定のコミットが所属するブランチを探す --contains を使えば、「現在のコミットの所属するブランチ」に絞り込むことができるので、今いるブランチが分かります。(ただし、今のコミットを含むブランチが複数あるときはあんまり意味ないです。当たり前ですが。)

引用

--containsは自動的にHEADを参照します。

これで現在のブランチを出力します。
ただこのままだとカレントブランチを表す*が含まれたままです。

| cut-b 3-

これでパイプで渡された標準出力の3バイト以降を出力します。
* developではd行こうが3バイト目なのでd以降が出力されます。

最後に$()で囲った処理を実行してexoprt CURRENT_BRANCHCURRENT_BRANCHにセットします。

$ export CURRENT_BRANCH="$(git branch --contains | cut-b 3-)"

参考

シェルスクリプトでUNIXTIMEの取得と日時との相互変換(POSIX準拠dateコマンドのみ使用)

$
0
0

はじめに

参考としてシェルスクリプトで UNIXTIME と日時を取得・変換する "一般的な" コマンドです。

# 現在のUNIXTIMEを出力$ date +%s
1586250991

# 指定した日時のUNIXTIMEを出力$ date +%s --date'2020-01-02 03:04:05'# Linux$ date-j-f"%Y-%m-%d %H:%M:%S"'2020-01-02 03:04:05' +%s # macOS
1577901845

# UNIXTIMEで日時を指定して指定したフォーマットで出力$ date--date @1577901845 +"%Y-%m-%d %H:%M:%S"# Linux$ date-r 1577901845 +"%Y-%m-%d %H:%M:%S"# macOS
2020-01-02 03:04:05

何を言わんとしているかはわかると思いますが dateコマンド自体は POSIX で規定されていますが、よく使いそうなこれらのオプションは POSIX 準拠ではありません。UNIXTIME を出力する +%sは macOS でも使えますが、これも POSIX 準拠ではありません。(ちょっと忘れてしまいましたが実際に動かない環境があります。古い Solaris?)

UNIXTIME は(一秒単位ですが)シリアル値として手軽に使えるのでこれが使えないとなるとちょっと不便です。そこで dateコマンドのうちPOSIX 準拠のオプションのみを使って実装した UNIXTIME 出力関数と、UNIXTIME と日時との相互変換関数を実装しました。

実装

特に解説するようなこともないのでいきなり実装です。

# 現在の UNIXTIME の取得
unixtime(){
  datetime2unixtime "$(date-u +'%Y-%m-%d %H:%M:%S')"}# 日時(%Y-%m-%d %H:%M:%S 形式)-> UNIXTIME
datetime2unixtime(){set--"${1%% *}""${1##* }"set--"${1%%-*}""${1#*-}""${2%%:*}""${2#*:}"set--"$1""${2%%-*}""${2#*-}""$3""${4%%:*}""${4#*:}"set--"$1""${2#0}""${3#0}""${4#0}""${5#0}""${6#0}"["$2"-lt 3 ]&&set--$(($1-1))$(($2+12))"$3""$4""$5""$6"set--$(((365*$1)+($1/4)-($1/100)+($1/400)))"$2""$3""$4""$5""$6"set--"$1"$(((306*($2+1)/10)-428))"$3""$4""$5""$6"set--$((($1+$2+$3-719163)*86400+$4*3600+$5*60+$6))echo"$1"}# UNIXTIME -> 日時(%Y-%m-%d %H:%M:%S 形式)
unixtime2datetime(){set--$(($1%86400))$(($1/86400+719468)) 146097 36524 1461
  set--"$1""$2"$(($2-(($2+2+3*$2/$3)/$5)+($2-$2/$3)/$4-(($2+1)/$3)))set--"$1""$2"$(($3/365))set--"$@"$(($2-((365*$3)+($3/4)-($3/100)+($3/400))))set--"$@"$((($4-($4+20)/50)/30))set--"$@"$((12*$3+$5+2))set--"$1"$(($6/12))$(($6%12+1))$(($4-(30*$5+3*($5+4)/5-2)+1))set--"$2""$3""$4"$(($1/3600))$(($1%3600))set--"$1""$2""$3""$4"$(($5/60))$(($5%60))printf"%04d-%02d-%02d %02d:%02d:%02d\n""$@"}# 使用例
unixtime # => 現在の UNIXTIMEdate +%s # 同等のコマンド (Linux)

datetime2unixtime "2020-04-07 01:23:45"# => 1586222625date-u +%s --date"2020-04-07 01:23:45"# 同等のコマンド (Linux)

unixtime2datetime "1586222625"# => 2020-04-07 01:23:45date-u--date @1586222625 +"%Y-%m-%d %H:%M:%S"# 同等のコマンド (Linux)

一見してなんじゃこりゃ?と思うかもしれませんが、たくさんの setは変数を使用しないようにしているためです。シェルスクリプトは POSIX 準拠の範囲ではグローバル変数しかないので使用している変数名がぶつからないように代わりに位置パラメータを使っています。

計算式についてはネットで検索して見つけたものを(書き方の変更程度で)使用しているだけなので解説はできません。date2unixtimeは有名なフェアフィールドの公式らしいです。参考にした場所は忘れてしまいましたが検索すればいくつも出てくると思います。unixtime2dateこちらのコードを参考にしました。

タイムゾーンや ISO 8601、RFC 3339 の対応は意図的にやっていません。これらに対応するのは難しくないはずなので必要な人がやればよいというスタンスです。スクリプトに組み込んで再利用しやすいようにコアのみをシェル関数として実装しています。(コードは修正の有無に関係なく自由に使ってもらって構いません。参照元とかも明記しなくて良いです。)

テスト

こういうのは計算式を見た所で正しいかなんて判断のしようがないので dateコマンドの結果と比較してテストします。時・分・秒に関しては計算は難しくないので軽く済ませて、日付についてはエポック日の 1970-01-01から 1日(86400秒)単位で 3000 年まで一致するかをテストしています。(テストコードは Linux用です。POSIX 準拠にはしていません。)

i=0 t=5025 # 1970-01-01 01:23:45while[$t-lt 32503647600 ];do# 3000-01-01 00:00:00a=$(unixtime2datetime "$t")b=$(date-u +"%Y-%m-%d %H:%M:%S"--date @$t)c=$(datetime2unixtime "$b")if["$a"="$b"]&&["$c"="$t"];then[$(( i %1000))-eq 0 ]&&date-u--date @$tt=$((t+86400))i=$((i+1))else
    echo"error $a$b : $c$t"exit 1
  fi
done

【VS Code】Bashをフォルダーを指定して起動する方法

$
0
0

Bashをフォルダーを指定して起動する方法

VS Codeで、ターミナルの設定をBashにしたいと思いました。
Bash起動時のフォルダーを開いているソースと同じフォルダーにしたかったのですが、Bashの起動オプションには該当するオプションが見つかりませんでした。Googleで探してみたのですが、ショートカットを使う方法しか見つかりませんでした。
試行錯誤の結果、コマンドプロンプト(cmd.exe)を使って指定できましたので、ご紹介いたします。

前提条件

  • Windows環境の方法となります。(Windows 10でのみ確認)
  • フォルダツリーが以下の場合の設定です。
    c:\VSCode
    ├─Git
    │  └─bin
    │    └─bash.exe
    └─VSCode.exe

実行コマンド

  • コマンドプロントを"/c"オプションを使用してフォルダーを変更後、Bashを起動しています。
  • コマンドプロンプトのオプションに関しては以下のサイトを参考にさせていただきました。
    Programming Field:Cmd - DOS コマンド一覧
  %windir%\system32\cmd.exe /c cd /d "フォルダー名" & c:\VSCode\Git\bin\bash.exe

Settings.json を変更

  • settings.jsonの場所
    ページ内に Settings.json の詳細がありますので興味のある方は読んでみてください。

Git Bash の設定例

settings.json
"terminal.integrated.shell.windows":"${env:windir}\\system32\\cmd.exe","terminal.integrated.shellArgs.windows":["/c","cd /d ${fileDirname}","& c:\\VSCode\\Git\\bin\\bash.exe"],

curlで -bash: -o: command not found

$
0
0

curlコマンドにはファイル保存する-oオプションがありますが、urlに&があるとshellのバックグラウンドプロセス実行として解釈されてしまい意図通りに動かない。(バックグラウンド処理されるだけなので標準出力に結果は表示される)

動かない例
$ curl https://www.googleapis.com/books/v1/volumes?q=search+%E6%8A%80%E8%A1%93&maxResults=10 -o result.json
[1] 2950
-bash: -o: command not found

&をエスケープするかurlをクォーテーションで囲むことで解決できる

動く例
$ curl "https://www.googleapis.com/books/v1/volumes?q=search+%E6%8A%80%E8%A1%93&maxResults=10" -o result.json

MacでコマンドラインからChromeを開く

$
0
0

毎回Dockとかに置いてあるアイコンをダブルクリックするのは面倒なので。
openコマンドを使うとイケる。

open /Applications/Google\ Chrome.app/

エイリアスを貼っておくと便利。

alias chrome='open /Applications/Google\ Chrome.app/'

MacのTerminalをカラフルに彩る"exa"コマンド【Bash】

$
0
0

A. はじめに

以下のように、普段terminalでlsと打ち、そのディレクトリのフォルダーやファイルを確認することが多いと思います。

ls.png

この記事では、このlsを見やすく彩ってくれるコマンドexaを紹介します。
exaを使うと、以下のようにフォルダと色がついたりして、見やすくなります。

exa.png

また、以下のようにより見やすくするためのオプションコマンドも紹介します。

exa_la.png

exa_tree.png

B. インストール方法

1. brewのインストール

brewをインストールしている方はスキップして大丈夫です。
terminalで以下のコマンドを実行すると、Githubから自動的にダウンロードしてくれます。
時間がかかるため、安定したネットワーク環境で実行することをおすすめします。

$ /bin/bash -c"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

こちらが参考リンクです。

2. exaのインストール

terminalで以下のコマンドw実行します。

$ brew install exa

3. 動作チェック

以下のコマンドを実行して、ファイルが表示されれば確認完了です。

$ exa

C. オプションの紹介

オプションとは、コマンドの後につけるものです。
主要なオプションとしては、以下のようなものがあります。

オプション機能lsexa
-l一列にして表示。権限や所有者などの詳細情報も表示
-a隠しファイルも含めて、全て表示
-Ttree形式で表示×
-L 22階層分のtreeを表示×
--gitgitの状況を表示×
$ ls
README.md   ios     pubspec.lock
android     lib     pubspec.yaml
build       my_app.iml  test$ ls-a.       .idea       build       pubspec.yaml
..      .metadata   ios     test
.dart_tool  .packages   lib
.git        README.md   my_app.iml
.gitignore  android     pubspec.lock

$ ls-l
total 40
-rw-r--r--   1 masumi.morishige  staff   536  4  9 00:48 README.md
drwxr-xr-x  12 masumi.morishige  staff   384  4  9 00:48 android
drwxr-xr-x   5 masumi.morishige  staff   160  4  9 00:57 build
drwxr-xr-x   7 masumi.morishige  staff   224  4  9 00:48 ios
drwxr-xr-x   3 masumi.morishige  staff    96  4  9 00:48 lib
-rw-r--r--   1 masumi.morishige  staff   896  4  9 00:48 my_app.iml
-rw-r--r--   1 masumi.morishige  staff  4340  4  9 00:48 pubspec.lock
-rw-r--r--   1 masumi.morishige  staff  2658  4  9 00:48 pubspec.yaml
drwxr-xr-x   3 masumi.morishige  staff    96  4  9 00:48 test$ ls-la
total 72
drwxr-xr-x  17 masumi.morishige  staff   544  4  9 16:34 .
drwxr-xr-x   3 masumi.morishige  staff    96  4  9 00:48 ..
drwxr-xr-x   4 masumi.morishige  staff   128  4  9 00:57 .dart_tool
drwxr-xr-x   9 masumi.morishige  staff   288  4  9 16:34 .git
-rw-r--r--   1 masumi.morishige  staff   615  4  9 00:48 .gitignore
drwxr-xr-x   6 masumi.morishige  staff   192  4  9 00:48 .idea
-rw-r--r--   1 masumi.morishige  staff   305  4  9 00:48 .metadata
-rw-r--r--   1 masumi.morishige  staff  4106  4  9 01:17 .packages
-rw-r--r--   1 masumi.morishige  staff   536  4  9 00:48 README.md
drwxr-xr-x  12 masumi.morishige  staff   384  4  9 00:48 android
drwxr-xr-x   5 masumi.morishige  staff   160  4  9 00:57 build
drwxr-xr-x   7 masumi.morishige  staff   224  4  9 00:48 ios
drwxr-xr-x   3 masumi.morishige  staff    96  4  9 00:48 lib
-rw-r--r--   1 masumi.morishige  staff   896  4  9 00:48 my_app.iml
-rw-r--r--   1 masumi.morishige  staff  4340  4  9 00:48 pubspec.lock
-rw-r--r--   1 masumi.morishige  staff  2658  4  9 00:48 pubspec.yaml
drwxr-xr-x   3 masumi.morishige  staff    96  4  9 00:48 test$ exa -T-L 2
.├── android
│  ├── app
│  ├── build.gradle
│  ├── gradle
│  ├── gradle.properties
│  ├── gradlew
│  ├── gradlew.bat
│  ├── local.properties
│  ├── my_app_android.iml
│  └── settings.gradle
├── build
│  ├── flutter_assets
│  ├── ios
│  └── snapshot_blob.bin.d
├── ios
│  ├── Flutter
│  ├── Runner
│  ├── Runner.xcodeproj
│  └── Runner.xcworkspace
├── lib
│  └── main.dart
├── my_app.iml
├── pubspec.lock
├── pubspec.yaml
├── README.md
└── test└── widget_test.dart

$ exa -la--git
drwxr-xr-x    - masumi.morishige  9 4  0:57 -I .dart_tool
drwxr-xr-x    - masumi.morishige  9 4 16:34 -- .git
.rw-r--r--  615 masumi.morishige  9 4  0:48 -N .gitignore
drwxr-xr-x    - masumi.morishige  9 4  0:48 -I .idea
.rw-r--r--  305 masumi.morishige  9 4  0:48 -N .metadata
.rw-r--r-- 4.1k masumi.morishige  9 4  1:17 -I .packages
drwxr-xr-x    - masumi.morishige  9 4  0:48 -N android
drwxr-xr-x    - masumi.morishige  9 4  0:57 -I build
drwxr-xr-x    - masumi.morishige  9 4  0:48 -N ios
drwxr-xr-x    - masumi.morishige  9 4  0:48 -N lib
.rw-r--r--  896 masumi.morishige  9 4  0:48 -I my_app.iml
.rw-r--r-- 4.3k masumi.morishige  9 4  0:48 -N pubspec.lock
.rw-r--r-- 2.7k masumi.morishige  9 4  0:48 -N pubspec.yaml
.rw-r--r--  536 masumi.morishige  9 4  0:48 -N README.md
drwxr-xr-x    - masumi.morishige  9 4  0:48 -Ntest

Qiitaのシンタックスハイライトでは、彩りがないですが、冒頭でも説明したようにこちらだと彩りが出ます。

ls -laの場合
ls_la.png

exa -laの場合
exa_la.png

D. Aliasの登録

最後に、余談ですが、lsを打ったときにexaを実行させる方法について紹介します。
Aliasを用いて、この目的を達成したいと思います。

エイリアスとは、偽名、別名、通称などの意味を持つ英単語。ITの分野では、ある対象や実体を、複数の異なるシンボルや識別子で同じように参照できるする仕組みを指す。別名。
引用文献

1. bashrcファイルの作成

まず以下のコマンドを実行し、bashrcファイルを編集します。

$ vim ~/.bashrc

vimエディターが開いたら、iを押し、===INSERT===(挿入モード)にします。
そしたら、以下のように入力して、エイリアスを登録します。

~/.bashrc
alias ls='exa'

最後に、escを押して、挿入モードを解除して、:wqを入力して実行を行うことで、保存して、エディターを閉じます。

2. bash_profileへのbashrcの登録

次に、以下のコマンドを実行し、bash_profileファイルを編集します。

$ vim ~/.bash_profile

vimエディターが開いたら、iを押し、===INSERT===(挿入モード)にします。
そしたら、以下のように入力して、terminalを開いたときにbashrcが実行されるようにします。

~/.bash_profile
source ~/.bashrc

最後に、escを押して、挿入モードを解除して、:wqを入力して実行を行うことで、保存して、エディターを閉じます。

3. bash_profileの再読み込み

terminalを再起動するか、source ~/.bash_profileと実行してみましょう。
すると、ls -laと入力しても、exaのような綺麗な表示なると思います。

以上です。
皆さんのterminal生活が少しでも彩れば幸いです。


移植性・可搬性の高いシェルスクリプトを書くための技術まとめ

$
0
0

はじめに

この記事は私がシェルスクリプト用のBDDテスティングフレームワーク ShellSpecの開発を通して得た移植性・可搬性の高いPOSIX準拠のシェルスクリプトを書くための技術のまとめ、および関連する私の記事へのリンク集です。関連する新しい記事を書いたらここからリンクしますので、このページをストックするなりブックマークしておくと良いと思います。

Q & A

なぜシェルスクリプトで書くのか?

シェルスクリプトには他の言語にはない特有のメリットがあるからです。それは多くの環境でスクリプトがそのまま動くということです。ターゲットOS向けにコンパイルする必要もないですし、各言語用の実行環境やライブラリをインストールする必要もありません。シェルさえ入っていれば、Linux、macOS、BSD, Unix, Windows (WSL, Cygwin, MSYS) いずれの環境でも動きます。(その証拠に ShellSpec は並列実行、ランダム実行、プロファイラなどの機能を持ちながら、Docker の busybox イメージといった最小構成でも Debian 2.2 の bash 2.03 といった古い環境でも動作します。)

なぜ POSIX 準拠なのか?

「POSIX 準拠」は目的ではなく道具です。より多くの環境でシェルスクリプトを動くようにするのが本当の目的です。POSIX で標準化された機能のみを使用していれば、未知の環境でもそのまま動くことが期待できます。

どこでも動くようにするのは大変では?

はっきり言って大変です。実は本当の意味でどこでも動くようにするには POSIX 準拠だけでは足りません。シェルには実装の違いやバグもあります。bash などで使える便利な配列も使えませんし POSIX 準拠のコマンドも最低限の機能しかありません。なによりそれらを吸収するライブラリが不足しています。まるで往年の JavaScript の世界のようです。なんの手がかりもなしに進んでしまえば、最初の段階で諦めてしまうことでしょう。そこでこの記事というわけです。

本当にどこでも動きますか?テストはしていますか?

これらの技術は原則として ShellSpec の実装技術を元にしており、一部を除き ShellSpec と同様の環境でテストしています。(時々、記事にする時にミスしてたりバグが発覚したりしてますが・・・。一応全部修正してるつもりです。) ShellSpec のテスト環境は現時点で「bash≧2.03」「bosh≧2018/10/17」「busybox≧1.10.2」「dash≧0.5.2」「ksh≧93s」「mksh≧28」「posh≧0.3.14」「yash≧2.30」「zsh≧3.1.9」、OS は Linux (Debian 2.2 以降)、Windows (WSL, Cygwin, MSYS 基本的に最新のみ)、macOS (10.10以降)、FreeBSD (10.x以降) です。残念ながら UNIX は扱える環境がないのでテストできていません。(Solaris だけはテストしていますが CI ではなく手動で時々やってるだけなので少し不安です。)

移植性・可搬性の高いシェルスクリプトを書くための技術

それぞれ個別の記事にしています。

互換性の違いを吸収する技術

この記事をただのリンク集にするのはもったいないので、他の記事では説明しないであろう、互換性の違いを吸収する方法を紹介します。

私が可搬性のあるシェルスクリプトを書く場合、以下のような順番で使う道具を決めています。

  1. シェルの機能だけで実現できるならシェルだけで実装する
  2. 機能、パフォーマンスの理由でシェルで実装するのが難しい場合は POSIX で規定されたコマンド(及びそのオプション)を使用する
  3. それでも難しい場合は環境固有のシェル機能やコマンドをを使うが、このとき互換性の違いを吸収する関数を作成する

私が書く記事は基本的にシェルだけで実装するか POSIX 準拠のコマンドだけを使用するのが大半なので 3.の互換性の違いを吸収する方法について説明する場所は少ないと思うのです。なのでここで紹介したいと思います。

例 stat

statは POSIX で規定されているコマンドではありませんが、Linux でも macOS (BSD) でも使用できます。しかしその仕様(オプション)は大きく異なります。例えばファイルサイズを取得する方法です。

# ファイルサイズの取得stat-c %s /etc/hosts # Linuxstat-f %z /etc/hosts # macOS (BSD)

これを例に互換性を吸収する方法を紹介します。まずそれぞれで関数を定義します。

filesize(){# Linuxstat-c %s "$1"}

filesize(){# macOS (BSD)stat-f %z "$1"}

実装

互換性を吸収するには現在の環境を判別し、この2つの関数のうち適切な方の関数を定義します。環境の判別は unameを使用して Linuxという文字が入っているか? Darwin (macOS) が入っているか?で分ける方法を思いつくかもしれませんが、これはおすすめしません。なぜなら Darwinであっても Linux版 (GNU 版) の statが使われる場合があるからです。(Homebrew では実際にそのようなことが出来ます。)

ベストな方法は実際にコマンドを実行してその挙動で判別することです。具体的には statの場合 stat -f /を使って判別することが出来ます。このオプションは Linux では --file-systemオプションと同じ意味で、ルートファイルシステムのステータスを返します。macOS (BSD) では -fはフォーマット文字列なのでそのままの文字列 /を返します。

# Linux$ stat-f /
  File: "/"
    ID: f148a0b23b752507 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 122661614  Free: 116726932  Available: 110478625
Inodes: Total: 31227904   Free: 30595983

# macOS (BSD)$ stat-f /
/

このように挙動の違いを利用して定義する関数を分けることができます。(長いので関数は一行にします。)

if["$(stat-f /)"= / ];then
  filesize(){stat-f %z "$1";}# macOS (BSD)else
  filesize(){stat-c %s "$1";}# Linuxfi

注意点としては、どちらでもすでに動作が定義されていて実行しても無害なオプションを使用するということです。「このオプションは Linux でしか使えない。」ということを利用して判別すると、のちに macOS で実装されて動かなくなる可能性があります。(無害なオプションを使う理由は言うまでもないですね。)調べれば大抵使えるオプションが見つかると思うのですが、なければロングオプションの --version--helpであれば比較的安心して使えると思います。さすがに --version--helpを他の目的に使うことはないでしょう。

発展

この関数を「最初に使われた時」に定義する方法です。

filesize(){if["$(stat-f /)"= / ];then
    filesize(){stat-f %z "$1";}# macOS (BSD)else
    filesize(){stat-c %s "$1";}# Linuxfi# 初回はここで定義した関数を呼び出す。次回からは直接定義した関数が使用される。
  filesize "$@"}

この方法のメリットは、コマンド判別のタイミングを実際に使用するタイミングまで遅らせることが出来るので、必要がない場合に判別のコストを減らしたり、また複雑な判別処理を(グローバル)変数を使わずに位置パラメータを使って行うことができるということです。あまり意味がない例ですが、グローバル変数を使わないというのはこういうものです。

filesize(){# 位置パラメータを変数の代わりとして使うことで# ret=$(stat -f /) のようなグローバル変数(ret)の使用を避けることが出来るset--"$(stat-f /)""$@"if["$1"= / ];then
    filesize(){stat-f %z "$1";}# macOS (BSD)else
    filesize(){stat-c %s "$1";}# Linuxfi

  shift
  filesize "$@"}

ただし、この方法はサブシェルが使われると毎回定義することになってしまうので注意してください。

size1=$(filesize "file1")# サブシェルが使われてるので定義しても破棄されるsize2=$(filesize "file2")# よってここでもまた定義される
filesize "file3"# サブシェルを使っていないので以降は定義された状態が続く

これを避けるには read -r lineように引数に指定した名前の変数に戻り値が返るように filesize関数のインターフェースを変更します。

size1=''# 参考 shellcheck の未定義変数の参照という警告を騙す方法
filesize size1 "file1"# サブシェルではないので定義されたままecho"$size1"# ファイルサイズは size1 変数に代入されている

実装です。

filesize(){if["$(stat-f /)"= / ];then
    filesize(){eval"$1=\$(stat -f %z \"\$2\")";}# macOS (BSD)else
    filesize(){eval"$1=\$(stat -c %s \"\$2\")";}# Linuxfi

  filesize "$@"}

引数で指定した変数に戻り値を返すというテクニックは応用の幅が広いので覚えておくと良いです。サブシェルを使用しないのでパフォーマンスにも優れています。ただし evalを使用するので変数名等に意図しない文字が入らないように注意して下さい。脆弱性につながる可能性があります。

さいごに

シェルスクリプトを POSIX 準拠で作成すると多くの場所で簡単に動かすことが出来ます。しかしそれはかなり大変な作業です。一番の理由は互換性を吸収するライブラリがないことではないでしょうか? ブラウザ・JavaScript の世界でも今でこそ脱 jQuery などと言われていますが最初は Prototype.js や jQuery という互換性を吸収するライブラリの登場をきっかけに大きく変わっていったと記憶しています。開発者各々がシェルやコマンドの互換性や罠やバグに振り回されている状態ではシェルスクリプトで快適な開発はできません。目指すはバッドノウハウの共有ではなくバッドノウハウが不要な世界です。ライブラリを .で読み込んで提供されている関数を使えば細かいことを考えずに移植性・可搬性が実現できる。シェルスクリプトの世界にもそういう流れが訪れれば良いなと私は考えます。

私見ですがこれまではシェルスクリプトの"ライブラリ"のテストは大変だったと思います。テスティングフレームワークはありますが、それらはシェルスクリプトを使って(外部コマンドの)テストを記述・実行するためのものであり、シェルスクリプト自身をテストするものになっていない(不可能ではないがやりづらい)からです。しかし今は ShellSpec があります。これはシェルスクリプト自身とライブラリに最適化されたテスティングフレームワークなのです。ここから多くのライブラリが生まれ、シェルスクリプトの有用性が再認識され、ゆくゆくはシェルそのものが強化され(bash等の拡張はほんと標準化されてほしいです。)各コマンドも互換性向上と機能強化されればシェルスクリプトでの開発はもっと簡単になるでしょう。他の言語で実装すればいいと思うかもしれませんが、標準でどこでも使える今のシェルがなくなる未来なんて誰も想像できないですよね?どうせ使うなら快適な方が良いに越したことはありません。

環境変数PATHの整理

$
0
0

What's Problem

Macのターミナルにて。

自分好みの設定を入れるのに .bash_profile.bashrcや色々といじっているのだけれど、ちょっと前から起動時に以下の様な Unknown option:1headコマンドのUsageが出る様になった。

Last login: Fri Mar 27 09:03:29 on ttys002
~/.bash_profile is read.
~/.bashrc is read.
Unknown option: 1
Usage: head [-options] <url>...
    -m <method>   use method for the request (default is 'HEAD')
    -f            make request even if head believes method is illegal
    -b <base>     Use the specified URL as base
    -t <timeout>  Set timeout value
    -i <time>     Set the If-Modified-Since header on the request
    -c <conttype> use this content-type for POST, PUT, CHECKIN
    -a            Use text mode for content I/O
    -p <proxyurl> use this as a proxy
    -P            don't load proxy settings from environment
    -H <header>   send this HTTP header (you can specify several)

    -u            Display method and URL before any response
    -U            Display request headers (implies -u)
    -s            Display response status code
    -S            Display response status chain
    -e            Display response headers
    -d            Do not display content
    -o <format>   Process HTML content in various ways

    -v            Show program version
    -h            Print this message

    -x            Extra debugging output

無論、 .bash_profile.bashrcheadコマンドを書いた覚えは毛頭ない。
仕方ないので、少し調べてみることにした。

「Usage: head [-options] ...」で検索して、以下の記事(英語)を見つける。
rvm - Head usage unknown option -1 / -n error. Possibly ruby related - Stack Overflow

bashは大文字と小文字を区別しないため、XAMPPのHEADと他のheadが混同してしまっている(で、$PATHの最初に記載されている方を利用する)のが問題とのこと。
(zshではこの問題は起きないらしい。大文字と小文字が区別出来てるんだろうね。)

解決策としては2件ほど示されていた(執筆当時)が、コメントを読む限り有力なのは以下。

  • $PATHからXAMPPのパスを取り除く or
  • $PATHの最後にXAMPPのパスを移す

現在の自分の $PATHの設定を確認。

$ echo $PATH
/Users/***/.nodebrew/current/bin:/etc/httpd:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin:/Applications/XAMPP/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/bin

非常にごちゃついておる。。分かりやすい様に分解すると、

/Users/***/.nodebrew/current/bin:
/etc/httpd:
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin:
/Applications/XAMPP/bin:
/usr/local/bin:
/usr/bin:
/bin:
/usr/sbin:
/sbin:
/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/bin

確かにXAMPPがおります。
記事とは若干フォルダは異なるものの /Applications/XAMPP/bin:がおりました。
(後、なんで、JDK2つも入れてるんだ自分。。)

Resolve

.bash_profile.bashrcの設定を見直す。

修正前
.bash_profile($PATH関連の設定に絞って掲載)

export PATH=/etc/httpd:$PATH:/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/bin
export JAVA_HOME=$PATH:/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home
export PATH=$HOME/.nodebrew/current/bin:$PATH

.bashrc($PATH関連の設定に絞って掲載)

export JAVA_HOME=`/System/Library/Frameworks/JavaVM.framework/Versions/A/Commands/java_home -v "1.8"`;
PATH=${JAVA_HOME}/bin:/Applications/XAMPP/bin:${PATH};

う〜ん、、、何も整理出来てなかったんだな。。
JDKが被ってる理由も同時に判明。良かった。

修正後
整理します。本当に正しい .bashrc と .bash_profile の使ひ分け - Qiitaによると、

環境変数はプロセス間で勝手に受け継がれるのでログイン時のみ設定すれば十分です。

とあるため、環境変数の記述は .bash_profile側に統一する。

.bash_profile($PATH関連の設定に絞って掲載)

export JAVA_HOME=`/usr/libexec/java_home -v 1.8`
export NODE_HOME=$HOME/.nodebrew/current
export PATH=${JAVA_HOME}/bin:${NODE_HOME}/bin:/etc/httpd:$PATH

.bashrc($PATH関連の設定に絞って掲載)

# 設定なし

随分とすっきりした。やったことは以下の通り。

  • 記述を .bash_profileに統一
  • もう使わないので、XAMPPは削除
  • JDKのバージョンを一つに固定
  • その他、全般的に見やすい様、記述見直し・修正

動作確認
一旦、ターミナル自体を落として、再起動。

Last login: Fri Apr 10 09:44:23 on ttys004
~/.bash_profile is read.
~/.bashrc is read.

警告出なくなったので、一先ず良し。
今回のことで、 $PATH変数やXAMPPの変数の取り扱いについて(後、英語についても少し)学べた。
思いのほか、収穫があって良かった。

Thanks

VPN時にGoogle Meet等の通信をVPNスルーさせる設定(IPv4 Only)

$
0
0

IT系企業の方は多くの方が在宅勤務を余儀なくされている今日このごろかと思います。
オンラインでのビデオMTGありますよね。

VPNにつないだままMeetやZoomに接続すると、VPNルータや回線に大きく負荷がかかってしまいます。
そのため、弊社ではCTOよりMeetにつなぐ際にはVPNを切るようにという指示が出ています。

でも、MTGのときにいちいちVPN切ったり、MTG終わったらVPNつなぎ直したり、超めんどくさくないですか?
そのお悩みを解消するためのスクリプトを書いてみました。

ちなみに、macOS Mojave, Catalinaでの動作を想定しています、
理論上はこれで経路分離できるはず。

Wi-Fi等のネットワークが変わった際にはスクリプトを再度流してやる必要があります。

vpnroute.sh
#!/bin/bash# Default Gatewayの取得ROUTE=$(route -n get 0.0.0.0 | grep gateway)echo${ROUTE}# 区切り文字をコロンに変更IFS=': '# 取得したDefault Gatewayを変数に代入set--${ROUTE}GATEWAY=$2# 以下指定経路をVPN通さないようにする# Google Meetsudo route add -net 74.125.250.0/24 ${GATEWAY}# Amazon Chimesudo route add -net 99.77.128.0/18 ${GATEWAY}sudo route add -net 13.248.147.139/32 ${GATEWAY}sudo route add -net 76.223.18.152/32 ${GATEWAY}sudo route add -net 3.80.16.0/23 ${GATEWAY}sudo route add -net 52.55.62.128/25 ${GATEWAY}sudo route add -net 52.55.63.0/25 ${GATEWAY}sudo route add -net 34.212.95.128/25 ${GATEWAY}sudo route add -net 34.223.21.0/25 ${GATEWAY}sudo route add -net 99.77.253.0/24 ${GATEWAY}# Zoomsudo route add -net 207.226.132.110 ${GATEWAY}sudo route add -net 162.255.37.11 ${GATEWAY}sudo route add -net 162.255.36.11 ${GATEWAY}sudo route add -net 221.122.88.195 ${GATEWAY}sudo route add -net 115.114.131.7 ${GATEWAY}sudo route add -net 213.19.144.110 ${GATEWAY}sudo route add -net 103.122.166.55 ${GATEWAY}sudo route add -net 209.9.211.110 ${GATEWAY}sudo route add -net 64.211.144.160 ${GATEWAY}sudo route add -net 69.174.57.160 ${GATEWAY}

rails -v を行った際の Permission denied

$
0
0

macos
自分用のメモです

/usr/local/bin/rbenv-communal-gem-home を確認

mkdir: /usr/local/bin/../version_cache: Permission denied
/usr/local/bin/rbenv-communal-gem-home: line 21: /usr/local/bin/../version_cache/2.6.3: No such file or directory

となっていたので、rbenv-communal-gem-home をエディタに表示して確認してみる

$ open /usr/local/bin
# usr/local/binディレクトリをオープン
rbenv-communal-gem-home.
if [ "$1" = "--complete" ]; then
  exec rbenv-versions --bare
fi

rbenv_version="${1:-$(rbenv-version-name)}"
if [ -L "$RBENV_ROOT/versions/$rbenv_version/lib/ruby/gems" ]; then
  cachedir="${BASH_SOURCE%/*}/../version_cache"
  cachefile="$cachedir/$rbenv_version"
  if [ -f "$cachefile" ]; then
    communal_version="$(cat $cachefile)"
  else
    mkdir -p "$cachedir"
    communal_version="$("$RBENV_ROOT/versions/$rbenv_version/bin/ruby" -rrbconfig -e 'puts RbConfig::CONFIG["ruby_version"]')"
    echo "$communal_version" > "$cachefile"
  fi

  echo "$RBENV_ROOT/gems/$communal_version"
else
  exit 1
fi

こうなってた。すなわち

mkdir: /usr/local/bin/../version_cache: Permission denied

は、 mkdir -p "$cachedir" でディレクトリ作りたいけど、このgemにはその権限がない。だと思う。

/usr/local/bin/../version_cache/2.6.3: No such file or directory

は、 echo "$communal_version" > "$cachefile" の際に cachefileが存在しないために出てきたエラーだと思う。(cachedirが存在しない、作成できなかったため)

cachefileのパスに含まれているcachedirを有効にする

# cachedir="${BASH_SOURCE%/*}/../version_cache" を消して
cachedir="${BASH_SOURCE%/*}/../Cellar/rbenv-communal-gems/1.0.1_1/version_cache"

sh / Makefile で特定のエラーコードを無視したい

$
0
0

TL; DR

shell スクリプトの場合

# エラーコード 1 を無視
cmd.sh ||exit$(($?-1))# 複数のエラーコードを無視したい場合 (下記は 1, 2 を無視)
cmd.sh || sh -c"exit $(($?-1))"|| sh -c"exit $(($?-1))"

Makefile の場合

TARGET:# エラーコード 1 を無視
cmd.sh||exit$$(($$?-1))# 複数のエラーコードを無視したい場合 (下記は 1, 2 を無視)
cmd.sh||sh-c"exit $$(($$? - 1))"||sh-c"exit $$(($$? - 1))"

はじめに

shell スクリプトや Makefile の処理で、「このエラーは処理を続けてほしいんだけど」みたいなこと、たまにありますよね。

sh だと

cmd.sh ||true# エラーコードが常に 0 になる

Make だと

TARGET:# 頭の "-" で、エラーを無視するようになる
-cmd.sh

とかやると、エラーを無視してくれますが、あらゆるエラーが無視されます。そうなると、想定外のエラー発生時にも処理が進んでしまうので、できれば特定のエラー時だけ無視するようにしたいところです。

そこで

# エラーコード 1 を無視
cmd.sh ||exit$(($?-1))

1のところを別の数値にすれば、好きなエラーコードを指定することができます。

||は、前のコマンドがエラーコード 1 以上で終わった場合に、後ろのコマンドを実行するようにします。
$?で直前のコマンドのエラーコードとり、$(( ))で計算ができるので、1 を引いて、それを新しいエラーコードにしています。

これで、もしエラーコードが 1なら新しいエラーコードが 0になって、正常とみなされ処理が継続される、その他のエラーコードならやはり非 0なので、エラーで停止する、という寸法です。

Makefile でも基本的に同じです。$$としないと sh のコマンドに $が渡らないので、そこだけ注意です。

TARGET:# エラーコード 1 を無視
cmd.sh||exit$$(($$?-1))

応用編: 複数のエラーコードを無視したい

複数のエラーコードを正常としたいときも、同じものをつなげていけばOK、、、なのですが、少し一工夫して sh -c "..."でくくってあげましょう。そうしないと、最初の exitで処理が終わってしまい、意図した動作になりません。

# 複数のエラーコードを無視したい場合 (下記は 1, 2 を無視)
cmd.sh || sh -c"exit $(($?-1))"|| sh -c"exit $(($?-1))"

また、2 つめ以降の $?には、それまでの計算が反映された値が入るので、そこには注意が必要です。上の例の場合、2つめの exit-1してますが、一つ前ですでに 1引かれているので、最初のエラーコードが 2の場合に、ここで 0となります。

cmd.sh || sh -c"exit $(($?-1))"|| sh -c"exit $(($?-2))"

とすると、エラーコード 1, 3を無視することになります。

あとがき

わりとよくあるニーズな気がするのですが、少しググってもページを見つけられなかったので、書いてみました。

「もっとよいやり方があるよ!」という方、ぜひ教えてもらえるとありがたいです :pray:

Viewing all 2757 articles
Browse latest View live