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

POSIX準拠シェルスクリプトだけで1秒未満sleepを実現する

$
0
0

はじめに

いつもと違って今回のはネタです。でも本当に POSIX 準拠シェルスクリプトだけで 1 秒未満のスリープを実現しています。bash 依存もしていません。使うのはシェルと POSIX 準拠では最小 1 秒単位でしか指定できないはずの sleepコマンドだけです。ネタというのは精度が良くないのと最近の sleepコマンドの実装は大抵小数点以下の指定ができるんだからもう使ってしまえばいいじゃん?という実用上の理由からです。また今回はネタなのでまともにテストしていません。(どのシェルでも動くと思いますが。)

実現方法

さて最小 1 秒しか指定できない sleepでどうやれば 1 秒未満のスリープが実現できるでしょうか?実現方法を簡単に言うならば「パソコンの性能どれくらい?一秒間に何回足し算できる?」ということです。ピンときましたでしょうか?

さて計測してしてみましょう。

trap'stop=1' HUP
(sleep 1;kill-HUP$$)&

stop=''cnt=0
until["$stop"];do
  cnt=$((cnt+1))done

echo"$cnt"

私の PC では 706,027回でした。(ちなみに同等のスペックの WSL1上では 565,384回でした。)つまりおよそ 70 万回ループで足し算すれば、1 秒間、時間が経過するということです。もうわかりましたね? 0.1 秒スリープするには約 7 万回、0.01 秒スリープするには約 7000 回足し算を行えばいいのです。(いにしえの NOPを使ったウェイトテクニック。年がバレる。)もちろんこの回数はマシンの性能によって変わるのでスクリプト実行時に 1 秒の時間を使って何回足し算が出来るか計測する必要があります。またマシンの負荷などによって計測結果にずれが生じますので正確性は期待できません。計測を何回か繰り返せば精度は上がると思いますがその分時間がかかります。(計測結果を保存しておけば次回以降は計測する必要はなくなりますね。)

実装

ではこれを使用して、0.01 秒単位で更新するカウンターを作ってみます。

# 計測trap'sleep=0' HUP
(sleep 1;kill-HUP$$)&
loop=0 sleep=2147483647 # 1秒でカウントできない十分大きな値while["$loop"-lt"$sleep"];do
  loop=$((loop +1))done
echo"$loop"# カウンターsleep=$((loop /100))echo"$sleep"while :;do
  printf"\r%d.%02d"$((n /100))$((n %100))loop=0 # sleep 0.01 相当のループ処理while["$loop"-lt"$sleep"];do
    loop=$((loop +1))done

  n=$((n +1))done

「何回実行できるか?」の計測処理とスリープ処理のコードを合わせていることに注意して下さい。カウント回数を計測していますが、実際にはループの比較処理も実行時間に含まれるため、そこで計測結果とスリープ時間で違いがでないようにするためです。

改善

「なんだビジーウェイトかよ」と思った方。その通りです。(ネタですし・・・。)ただし条件次第でビジーウェイトさせずに sleepさせることが出来ます。その条件とは一定間隔で処理を行うために sleepしている場合です。(例えばアニメーションなどで 0.1 秒単位でコマを進めるといった処理です。JavaScript でいえば setInterval関数です。)

シェルスクリプトは最小 1 秒のスリープが出来ます。言い換えると 1 秒間隔で処理を行えます。つまりバックグラウンドでサブシェル(サブプロセス)を実行して呼び出し元に 1 秒間隔でシグナルを送ることが出来るということです。では、このサブシェルが 10 個いれば・・・?

はい、そういうことです。サブシェルを 0.1 秒ごとにタイミングをずらして 10 個バックグラウンド実行すれば、10 個のサブシェルが 1 秒毎に 0.1 秒タイミングをずらしてシグナルを送ってくれる、つまり 0.1 秒ごとのシグナルが実現できるということです。呼び出し元で waitを実行していれば(trapを使う実装もありでしょう。)シグナルが送られてきたタイミングで waitが解除されるので、ビジーウェイトすることなしに 0.1秒スリープが実現できます。もちろんサブシェルの数を 100 個増やせば 0.01スリープも出来ると思いますが、プロセスツリーが酷いことになりますね(笑)

では実装です。

# 計測trap'sleep=0' HUP
loop=0 sleep=2147483647 # 1秒でカウントできない十分大きな値(sleep 1;kill-HUP$$)&
while["$loop"-lt"$sleep"];do
  loop=$((loop +1))done
trap':' HUP
echo"$loop"# カウンターsleep=$((loop /10))echo"$sleep"i=0
while["$i"-lt 10 ]&&i=$((i+1));do
  loop=0 # sleep 0.1 相当のループ処理while["$loop"-lt"$sleep"];do
    loop=$((loop +1))done

  while kill-HUP$$ 2>/dev/null;do
    sleep 1
  done&
done

while :;do
  printf"\r%d.%d"$((n /10))$((n %10))wait
  n=$((n +1))done

tophtopなどで見てみると CPU 使用率が減っていることがわかると思います。

おまけ

ShellSpec のプロファイラ機能

この記事はネタですが、実はこの記事と同じ発想で ShellSpec のプロファイラ機能は実現されています。(正確に言うと ShellSpec で使用した技術を応用したのがこの記事です。)ShellSpecは私が開発しているシェルスクリプト用のテスティングフレームワークですが、個々のテストの実行時間を計測するプロファイラ機能を持っています。出力は以下のような感じです。

$shellspec --profileRunning: /bin/sh [sh]
...................................................................................................
(略)
...................................

Finished in 2.73 seconds (user 3.37 seconds, sys 0.43 seconds)
830 examples, 0 failures, 9 skips (muted 9 skips)

Top 10 slowest examples of the 830 examples
  1 0.0144 spec/libexec/reporter_spec.sh:248-266
  2 0.0106 spec/install_spec.sh:36-39
  3 0.0087 spec/install_spec.sh:157-161
  4 0.0084 spec/install_spec.sh:173-177
  5 0.0082 spec/install_spec.sh:19-23
  6 0.0081 spec/libexec/list_spec.sh:19-30
  7 0.0077 spec/core/dsl/text_spec.sh:4-19
  8 0.0073 spec/libexec/shellspec_spec.sh:12-24
  9 0.0072 spec/core/matchers_spec.sh:21-24
 10 0.0071 spec/core/dsl/data_spec.sh:16-22

POSIX 準拠の範囲ではシェルスクリプトで 1 秒未満の時間を取得できるのは timeコマンドと timesコマンドの 2 つしかありません。このうち timesコマンドはシェルで使用された CPU 時間を測定するものですが、ユーザーCPU時間、システムCPU時間時間しか取れず、ウェイト(何もしない)時間を含めた実時間が取得できないので使用していません。ShellSpec では timeコマンドを使って実行時間を計測しています。timeコマンドは外部コマンドを実行し、その実時間、ユーザー時間、システム時間を取得することが出来ます。上記の出力の「Finished・・・」の行は timeコマンドの出力を整形して表示しています。出力例からもわかるように time (-p) の出力は多くの場合、小数点以下 2 桁です。しかしプロファイラの出力(「Top 10・・・」以下)は、小数点以下 4 桁まで出力できています。(この機能は POSIX 準拠シェルの全てで同じように使用できます。)

よくあるプロファイラ機能の実現方法は、処理(テスト)の実行前の時間を計測し、処理終了後の時間との差を求めるのが一般的だと思います。これと同じことをシェルスクリプトでやるならば(ミリ秒単位の時間は POSIX 準拠の範囲で取れないというのは置いておいて) dateコマンドを呼び出して現在の時刻を取得することになるでしょう。しかしそうすると dateコマンドの呼び出しで時間がかかってしまいます。遅くなりますし計測結果にも影響します。(ちなみに timeコマンドは外部コマンドの実行時間を計測するためのものなので、外部プロセスではない個々のテストの計測には使用できません。)そのため ShellSpec ではその方法は採用していません。ここで出てくるのが「何回足し算できる?」です。

プロファイラ機能を有効にしてテストの実行を開始すると、テスト実行プロセス(個々のテストではなく全てのテストを一つのプロセスで実行しています。)とは別のプロセス(プロファイラー)がバックグラウンドプロセスとして起動します。そのプロファイラーは「何回足し算できる?」と同じように無限ループで数値をカウントしています。そしてテスト実行プロセスは個々のテストを実行する時、テストの実行前と実行後にシグナル(実装の都合でシグナルで実現するのが難しかったのでファイルの有無で代用しています。)をプロファイラーに送りプロファイラーはシグナルを受け取った時点のカウントの値を記録しています。そして最後にはテスト完了時点のカウントの値も知ることが出来ます。

ここまで来たら後は簡単です。テスト全体にかかった時間は timeコマンドで小数点以下第二位まで取得できるので、テスト全体にかかったカウント値で割って、個々のテストにかかったカウント値をかければ、個々のテストの実行時間が更に小さい単位まで求めることが出来ます。

この方法のメリットは呼び出しに時間がかかる外部コマンドを使ってないので(シングルコア CPU の場合を除き)テスト実行時間にはほとんど影響しないということです。(CPU をフルに使用する仕組みなので CPU のターボブースト機能によって周波数があがり、逆に速くなる場合すらあります。最初はこの理由がわからず謎でした。そしてこれを利用してプロファイラを出力件数 0 件で実行する --boostというネタオプションを作りました。笑)

精度が気になるかと思いますが、細かく検証はしていないのですが大幅に外れてないようです。少なくともプロファイラの目的である「どのテストの時間がかかってるか?」の目安にはなると思います。(個人的にテストの細かい実行時間を知った所でどうするんだ?って思ってるのでこの程度で十分だと思っています。)


BashとZshの "**" (globstar) の挙動の違い

MSYS2_ARG_CONV_EXCL="*" によるパス変換の抑制

$
0
0

概要

NSIS で作ったセットアップは /Sオプションの指定により、サイレントセットアップになるのですが…

MinGW の bash から起動するとうまくいきません。

ところが、こうすると /Sを渡せるようになりました:

MSYS2_ARG_CONV_EXCL="*" ./Setup_pdftifgather.exe /S

うまくいかない理由

./Setup_pdftifgather.exe /S

2020-04-11_23h35_09.png

S:/という、S ドライブのルートディレクトリを指し示すパスに変換されていました。

MSYS_NO_PATHCONV

MSYS_NO_PATHCONV=1が良いらしいとか、良薬的効果を期待して試しましたが、効果は出ず…

そもそもググってもドキュメントもソースコードも出てこないので、現状追求のしようがありません…

テレワーク時代の MacOS X Terminal の Proxy切り替え

$
0
0

先月から本格的な自宅勤務体制が始まりました。

ネットワーク環境が変わると、TerminalのProxy設定やらsshの設定やらを変える必要があり、従来までは、Mac OS X で ネットワークプロファイルをアクセスポイント名で自動切り替えといった方法をを使っていまいた。多くの場合、場所=アクセスポイントで良かったのです。このやり方ではWiFiのアクセスポイント名を使ってネットワークプロファイルを切り替えています。なお、 ssh関連も .ssh/config_no_proxy.ssh/config_proxyでproxyの有無で切り替えています。

ところが、本格的に自宅勤務するようになり、自宅のアクセスポイントでも社内にVPNするようなケースがでてきたため、自宅アクセスポイントに接続していても、ネットワーク環境が社内、というケースがでてきました。

そこで .bashrcをこんな感じに変更しました。加えた処理としては、社内ネットワークにいないと疎通しないサーバーにpingをして、その結果も使ってproxy/ssh設定を切り替えます。アクセスポイントを見る処理はなくてもいいのですが、社内でもロケーションによって切り替えしたところがあったので残しています。

#!/bin/bash## proxy trigger settings ##proxy_name=http://internal.company.com:10080
switch_trigger=INTERNALWIFI # アクセスポイントの名前INTERNAL_SITE=internal.company.com #疎通確認用# no-proxy env.ln-fs ~/.ssh/config_no_proxy ~/.ssh/config

# proxy env.if["`networksetup -getairportnetwork  en0  | awk'{print $4}'`"="$switch_trigger"];then
    export http_proxy=$proxy_nameexport https_proxy=$proxy_nameexport ftp_proxy=$proxy_nameexport all_proxy=$proxy_nameln-fs ~/.ssh/config_proxy ~/.ssh/config
else#vpn
  ping ${INTERNAL_SITE}-c 1 >> /dev/null 2>&1
  if[$?-eq 0 ];then
      export http_proxy=$proxy_nameexport https_proxy=$proxy_nameexport ftp_proxy=$proxy_nameexport all_proxy=$proxy_nameln-fs ~/.ssh/config_proxy ~/.ssh/config
  fi
fi

VPNの接続・切断後、ネットワーク設定が変わって、proxy/sshの設定がずれていたら、新規でTerminalを開いて.bashrcを動かせばサクッとgithubに繋がるようになりました。めでたしめでたし。

Work from Home の、ご参考になれば幸いです。

Bashでポートスキャン

$
0
0

概要

ペネトレーションテストなどで侵害したLinuxホストから内部のネットワークに対してポートスキャンを実施することがあります。Nmapが無いことはほとんどですが、そのような時はBashの簡単なスクリプトでtimeoutコマンドをかましながら実行していましたが、timeoutコマンドがないことがありましたのでURL(※1)を参考にスクリプトを書いてみました。

スクリプト

portscan.sh
#!/bin/bashhost=$1for port in{1..65535};do(bash -c"echo > /dev/tcp/$host/$port")&&echo"$port port is open"& sleep 0.1 ;kill-9$!> /dev/null 2>&1
done
echo"Done"

使い方

$ bash ./portscan.sh 10.11.xx.xx > portscan.txt

実行するとオープンしているポートがあればportscan.txtファイルに
21 port is open
22 port is open
80 port is open

のように記載されます。

参考URL

※1 bashタイムアウトを実装する

シェルスクリプトで文字列を置換するreplace_all関数を作りました(実はコーディングスタイルの解説)

$
0
0

はじめに

タイトルのとおりですがシェルスクリプトで文字列を置換する replace_all関数を作りました。一応テストはしているのですがまだ実戦投入はしていません。もしかしたら仕様変更するかもしれないしバグもあるかもしれませんありました、後日修正しますが、関数だけでも十分利用価値がある(例えば HTML エスケープなどもこの関数を使えばシェル依存することなく簡単に実装することができるはずです。)のと、これを題材に私のコーディングスタイルの解説をするのにちょうど良さそうだと思ったので記事として公開します。

ちなみに実は同等の関数は以前に作成しているのですが、今とコーディングスタイルが異なってるのと当時はいきあたりばったりで実装していたので再実装しています。近いうちに今使っている実装と置き換える予定ですがまだ十分テストが出来ていません。(Linux以外のOSでクリティカルな問題が発生しないことを祈っています・・・)

前提

コードの詳細な解説の前に私の(独特な)コードはなぜそうしているのかの方針を説明しておきます。まず実装したのは純粋な"シェル関数"です。外部コマンドの呼び出しはしていません。ビルトインコマンドのみを使用しています。なぜ外部コマンドを使用していないかと言うと遅いからです。例えば同じ echoコマンドでもビルトインコマンドと外部コマンドの実行時間はこれぐらいの差があります。(CPU 3.4 Ghz、SSD 搭載の Linux 物理マシン上で計測。およそ 131 倍。1 秒で終わる処理が 2 分以上かかる計算です。)

# ビルトインの echo コマンドを実行$ time sh -c'i=0; while [ "$i" -lt 100000 ]; do echo a > /dev/null; i=$((i + 1)); done'
real    0m0.778s
user    0m0.424s
sys     0m0.349s

# 外部コマンドの echo コマンドを実行$ time sh -c'i=0; while [ "$i" -lt 100000 ]; do /bin/echo a > /dev/null; i=$((i + 1)); done'
real    1m42.183s
user    1m16.502s
sys     0m28.248s

もちろん外部コマンドの使用を一切を禁止しているわけではなく大量の文字列をフィルタの形で置換する場合は sedコマンドや trコマンドを使ったほうが速いです。ですが(例えばループの中などで)サイズの小さい文字列を何度も変換したい場合など、関数として使いたい場合は外部コマンド呼び出しのコストが大きくなります。つまり外部コマンドの代替ではなく用途の違いによって使い分けるためのものです。

また外部コマンドは末尾の複数の改行が消えてしまうという問題があります。例えば ret=$(printf 'hello\n\n\n\n\n')のようにいくら改行を追加したところで retに入る文字は helloのみです。これを外部コマンドを使って文字するなら、このようにするしかありません。不可能ではありませんが面倒ですよね?

ret=$(printf'hello\n\n\n\n\n';echo _)# 改行が保持されるように _ を追加ret=${ret%_}# 最後に追加した余計な _ を削除

そのため コマンド置換(ret=$(func 1 2 3))の形を使用せず func ret 1 2 3のように関数の第一引数に戻り値を返す変数名を指定して、その変数に代入しています。(標準の read関数 の read lineと同じ仕様です。)

bash などでは ret=${target//"from"/"to"}という書き方で文字列の置換(パターンの置換ではなく文字列です。)ができます。 対応しているシェルでは速度が速いので使っていますが、実装した replace_all関数は dash などの POSIX 準拠シェル全てに対応しています。

POSIX 準拠シェルといっても実際には完全に準拠していない(バグ?)シェルもあります。POSIX 準拠してないという理由で切り捨てるのもありだとは思いますが、シェルスクリプトの移植性・可搬性を上げることを目的の一つとしているので、そういうバグにも可能な限り対応する方針です。

(一時的なものを除き)変数は使用していません。それは POSIX 準拠の範囲ではグローバル変数しかないからです。変数レスを実現するために位置パラメーターをローカル変数の代わりとして多用しています。(注意 変数の使用を完全に禁止しているわけではありません。変数を使わないようにしているのは関数、ライブラリの話です。呼び出し元では変数を使って構いませんし私も使っています。またパフォーマンスや実装が困難なために断念することもあります。その場合は変数がかぶらないようにプリフィックスをつけています。)

全般的にショートコーディングを目指しています。(短い名前にするとか無理やり ;でつなぐかそういう小手先の技のことではありません。)横 80 文字以内でなるべく行数が少なくなるような書き方をしています。

実装

解説の前に実装コードを提示します。(使いたい人はご自由にどうぞ)

最終的に replace_all関数を定義しています。(補助的に replace_all_fast, replace_all_posix, replace_all_pattern, meta_escape関数も定義しておりシェルに応じて適切な関数が自動的に使われます。)

使い方は replace_all [返り値を返す変数名] [置換対象の文字列] [置換する文字列] [置換後の文字列]です。(例 replace_all ret "Hello World" "Hello" "Good"

# $1: ret, $2: value, $3: from, $4: to
replace_all_fast(){eval"$1=\${2//\"\$3\"/\"\$4\"}"}# $1: ret, $2: value, $3: from, $4: to
replace_all_posix(){set--"$1""$2""$3""$4"""until[ _"$2"= _"${2#*"$3"}"]&&eval"$1=\$5\$2";do
    set--"$1""${2#*"$3"}""$3""$4""$5${2%%"$3"*}$4"done}# $1: ret, $2: value, $3: from, $4: to
replace_all_pattern(){set--"$1""$2""$3""$4"""until eval"[ _\"\$2\" = _\"\${2#*$3}\" ] && $1=\$5\$2";do
    eval"set -- \"\$1\"\"\${2#*$3}\"\"\$3\"\"\$4\"\"\$5\${2%%$3*}\$4\""done}

meta_escape(){# shellcheck disable=SC1003if["${1#*\?}"];then# posh <= 0.5.4set--'\\\\:\\\\\\\\''\\\[:[[]''\\\?:[?]''\\\*:[*]''\\\$:[$]'elif["${2%%\\*}"];then# bosh = all (>= 20181007), busybox <= 1.22.0set--'\\\\:\\\\\\\\''\[:[[]''\?:[?]''\*:[*]''\$:[$]'else# POSIX compliantset--'\\:\\\\''\[:[[]''\?:[?]''\*:[*]''\$:[$]'fi

  set"$@"'\(:\\(''\):\\)''\|:\\|''\":\\\"''\`:\\\`'\'\{:\\{''\}:\\}'"\\':\\\\'"'\&:\\&''\=:\\=''\>:\\>'"end"echo'meta_escape() { set -- "$1" "$2" ""'until["$1"="end"]&&shift&&printf'%s\n'"$@";do
    set--"${1%:*}""${1#*:}""$@"set--"$@"'until [ _"$2" = _"${2#*'"$1"'}" ] && set -- "$1" "$3$2" ""; do'set--"$@"'  set -- "$1" "${2#*'"$1"'}" "$3${2%%'"$1"'*}'"$2"'"'set--"$@"'done'shift 3
  done
  echo'eval "$1=\"\$3\$2\""; }'}eval"$(meta_escape "a?""\\")"

replace_all(){(eval'v="*#*/" p="#*/"; [ "${v//"$p"/-}" = "*-" ]') 2>/dev/null &&return 0
  ["${1#"$2"}"="a*b"]&&return 1 ||return 2
}eval'replace_all "a*b" "a[*]" &&:'&&:
case$?in
  0)# Fast version (Not POSIX compliant)# ash(busybox)>=1.30.1, bash>=3.1.17, dash>=none, ksh>=93?, mksh>=54# yash>=2.30?, zsh>=3.1.9?, pdksh=none, posh=none, bosh=none
    replace_all(){ replace_all_fast "$@";};;;
  1)# POSIX version (POSIX compliant)# ash(busybox)>=1.1.3, bash>=2.05b, dash>=0.5.2, ksh>=93q, mksh>=40# yash>=2.30?, zsh>=3.1.9?, pdksh=none, posh=none, bosh=none
    replace_all(){ replace_all_posix "$@";};;;
  2)# Pattern version
    replace_all(){
      meta_escape "$1""$3"eval"replace_all_pattern \"\$1\"\"\$2\"\"\${$1}\"\"\$4\""}esac

解説

基本的に上の方から解説していきます。

replace_all_fast

パラメーター展開を使った置換です。POSIX 準拠ではありませんが、対応しているシェルではこれが一番速い実装です。evalと組み合わせているため一見分かりづらいかもしれませんが、以下のコード相当のことをしています。

ret=${2//"$3"/"$4"}# retは実際は $1 に入っている変数名

シェルの機能そのままなのであまり説明することはありませんが細かい説明をするなら

ret=${2//$3/"$4"}# $3 にダブルクォートをつけないとパターンとして扱われます。ret="${2//"$3"/"$4"}"# 変数代入時のダブルクォートは無くて構いません。

replace_all_posix

POSIX 準拠しているシェルで使われます。replace_all_fast関数で動くシェルであればこの関数でも動きます。

set--"$1""$2""$3""$4"""

$5を一時変数の代わりとして使用するために初期化しています。

until[ _"$2"= _"${2#*"$3"}"]&&eval"$1=\$5\$2";do

変数につけている _!という文字列を !オペレーターして扱ってしまうバグのワークアラウンドです。バグがあるシェルでは[ "!" = "foo" ][ ! "=" foo ]とみなしてしまい unknown operandのようなエラーメッセージが表示されます。このバグは(私が確認した限り)dash と busybox で発生します。dash は Debian 4.0 時代の 古い dash 0.5.3 あたりまでしか発生しませんが busybox は Debian 9 の 1.22.0 でも発生するので注意して下さい。(少なくとも 1.30.1 以降では修正されているようです。)

untilの条件に続く && eval~の部分はループを抜ける時に実行されます。(untilが終わるのは条件が真になった時です。)また &&に続く処理は変数代入など実行しても必ず真になるものしか使ってはいけません。偽になるとループ内を実行してしまいます。行を短く出来るので個人的には使いますが人によっては分かりづらいと思うので doneの後に書いたほうがいいかもしれません。

set--"$1""${2#*"$3"}""$3""$4""$5${2%%"$3"*}$4"

POSIX 版のキモです。$2(対象の文字列)を($3で切断して)減らしつつ $5に($4区切りで)移動しています。例えば ab,cd,ef,ghという文字列の ,@に置き換える場合、ループを繰り返すたびに $2:cd,ef,gh $5:ab@$2:ef,gh $5:ab@cd@$2:gh $5:ab@cd@ef@のように変化します。最後の一回(ループを抜ける時)は eval "$1=\$5\$2"によって $1の変数名に残りが結合されて代入されます。

ついでに関連する内容として posh のバグ を紹介します。(グローバル変数を避けたプログラミングにとっては辛いバグです。)

set--"abc" a &&char=$2echo${1#"$char"}# => bc 期待したとおりに動きますecho${1#"$2"}# => 空文字 poshは両方が位置パラメーターだと正しく動作しません

replace_all_pattern

POSIX 準拠しているシェルは replace_all_posix関数で動作しますが、前述の posh などバグのあるシェルもあります。新しいバージョンのシェルではほぼ問題はないのですが、posh や bosh は最新バージョンでもバグがあります。(マイナーなシェルですけどね。) replace_all_pattern関数はバグがあるシェルだけではなく全てのシェルで動作します。(つまり最終手段は replace_all_pattern、POSIX 準拠していれば replace_all_posix、拡張機能があれば replace_all_fastを使うということです。)

POSIX 準拠していないシェルでは文字列による処理ではなくパターンを使って replace_all_posix関数と同様のことを行っています。パターンを使いますが本当にやりたいことは固定の文字列による置換なのでメタ文字はエスケープしています。エスケープする処理は別で行っているので、この関数の違いはそれぐらいです。

eval"set -- \"\$1\"\"\${2#*$3}\"\"\$3\"\"\$4\"\"\$5\${2%%$3*}\$4\""

パターンを使った置換版のキモです。一見 evalなくてもできるんじゃ?と思うかもしれませんが、それだとうまくコードを共通化出来ませんでした。(おそらくシェルのバグです。)エスケープ処理の内容を見直して evalを無くしたほうが速いのでしょうが、このコードが使われる場合は少ないのでそれよりもコードの量を減らすことを優先しました。

meta_escape

もっとも苦しめられたコードです・・・。コードを見ると 3 パターンはしかないように見えますが、各シェルで挙動がバラバラでようやくここまで減らすことに成功しました。

この meta_escape関数は直接定義していません。最終的に定義したい meta_escape関数の「コードを出力する関数」になっており evalを使って定義しています。理由はその方が作りやすかった(コードを短く出来た)のとパフォーマンス向上のためです。メタ文字をエスケープする処理は関数内にインライン化されておりエスケープしたい文字種毎に関数を呼び出したりはしていません。最終的に定義される関数は一つで、この関数の中で複数の種類の文字をエスケープしています。


# shellcheck disable=SC1003

ShellCheckの以下の警告を非表示するためのものです。(そもそも警告内容がおかしい。)ダブルクォートでくくっても対処できますがバックスラッシュの数が更に倍になります。(嫌です。)

set -- '\\\\:\\\\\\\\' '\\\[:[[]' '\\\?:[?]' '\\\*:[*]' '\\\$:[$]'
                    ^-- SC1003: Want to escape a single quote? echo 'This is how it'\''s done'.

set--'\\:\\\\''\[:[[]''\?:[?]''\*:[*]''\$:[$]'

位置パラメーターにエスケープ文字をセットしています。:で挟んで左の文字を右の文字にエスケープするという意味にしています。

set"$@"'\(:\\(''\):\\)''\|:\\|''\":\\\"''\`:\\\`'\'\{:\\{''\}:\\}'"\\':\\\\'"'\&:\\&''\=:\\=''\>:\\>'"end"

どのシェルでも共通でエスケープする文字です。endは番兵君(ループで endが来たら終了という意味)です。


echo'meta_escape() { set -- "$1" "$2" ""'until["$1"="end"]&&shift&&printf'%s\n'"$@";do
  set--"${1%:*}""${1#*:}""$@"set--"$@"'until [ _"$2" = _"${2#*'"$1"'}" ]; do'set--"$@"'  set -- "$1" "${2#*'"$1"'}" "$3${2%%'"$1"'*}'"$2"'"'set--"$@"'done'shift 3
done
echo'eval "$1=\"\$3\$2\""; }'

コード生成部分です。setが多く分かりづらいですが以下のコードと同等です。(※ この場合は番兵君の endは不要)

echo'meta_escape() { set -- "$1" "$2" ""'until[$# -lt 0 ];do
  echo'until [ _"$2" = _"${2#*'"${1%:*}"'}" ]; do'echo'  set -- "$1" "${2#*'"${1%:*}"'}" "$3${2%%'"${1%:*}"'*}'"${1#*:}"'"'echo'done'shift
done
echo'eval "$1=\"\$3\$2\""; }'

まず、ちょっとしたリファクタリングをします。${1%:*}(エスケープ前文字)と ${1#*:}(エスケープ後文字)の変数が見づらいので、位置パラメーターに入れます。ループの最後で消しやすいように前の方に追加します。前に 2 つ追加したのでループの最後で shiftするのは 3 つです。(以下はリファクタリング後)

echo'meta_escape() { set -- "$1" "$2" ""'until[$# -lt 0 ];do
  set--"${1%:*}""${1#*:}""$@"echo'until [ _"$2" = _"${2#*'"$1"'}" ]; do'echo'  set -- "$1" "${2#*'"$1"'}" "$3${2%%'"$1"'*}'"$2"'"'echo'done'shift 3
done
echo'eval "$1=\"\$3\$2\""; }'

実はこのコードにはちょっとした問題があります。それは echoコマンドの非互換性です。echoコマンドは引数にバックスラッシュが入っている場合、それをメタ文字として扱うか、ただの文字として扱うか、実装がバラバラなのです。(それに関しては「シェルスクリプトのechoの移植性の問題に本気で対応する」で解説しています。)今回は運が悪いことに文字列の中にバックスラッシュがたくさん入っています。先のリンク先の記事でバックスラッシュに反応しない putsn関数を実装したのでそれを使用してもよいのですが、今回定義する関数は基本的な関数だと考えているので他の関数に依存したくはありませんでした。代わりに printfコマンドを使用したいところなのですが、シェルによってはビルトインで実装されておらず外部コマンドを使用するので printfコマンドの呼び出し回数を減らしたい所です。そうした結果、位置パラメーターに出力する文字を蓄えていき、それらを一回の printfコマンド呼び出しで行ったのが最終的なコードです。位置パラメーターに出力文字列を蓄えていくため、位置パラメーターの数が 0 個になることがないのでループの終了判定に番兵君を使いました。


eval"$(meta_escape "a?""\\")"

meta_escape関数で出力した本当の meta_escape関数のコードを evalすることで meta_escape関数を再定義しています。引数の "a?" "\\"はシェル判定に使用する $1$2を設定するためのものです。関数内で set -- "a?" "\\"した方がより明確だと思いますが、これぐらいは許容範囲かなと。

replace_all

最終的に定義したいのはこの関数ですが、この関数は二段階に分けて定義しています。一段階目はシェルを判定するための関数として利用しておりその判定結果を用いて二段階目で目的の関数を定義しています。(詳細は移植性・可搬性の高いシェルスクリプトを書くための技術まとめ

(eval'v="*#*/" p="#*/"; [ "${v//"$p"/-}" = "*-" ]') 2>/dev/null &&return 0

replace_all_fast関数が使用できるかを判定するための処理です。変数を使用していますが、対応してないシェルでは文法エラーで落ちるのでどちらにしろサブシェルを使わなくてはならないので、サブシェル使うなら中で定義した変数は消えてしまうので気兼ねなく使っています。判定する処理自体はもしかしたらもっと良い方法があるかもしれませんが、結構短く仕上げられたと思っています。

["${1#"$2"}"="a*b"]&&return 1 ||return 2

同様にパラメータ展開が POSIX 準拠しているかを判定するための処理です。この書き方は注意して下さい。foo && bar || bazだと ShellCheck で以下のような警告が出力されます。

foo && bar || baz
    ^-- SC2015: Note that A && B || C is not if-then-else. C may run when A is true.

ようするにこの書き方は、if-then-elseではないということです。例えば fooが 真であっても barで偽になれば bazが実行されてしまいます。しかし以下の場合は警告されません。

foo &&return 1 ||return 2
foo &&value=bar ||value=baz
foo &&echo bar ||echo baz
foo &&printf bar ||printf baz
foo && putsn bar || putsn baz # これは警告される (putsnは独自で定義したシェル関数)

どうやら ShellCheck は 書いてある内容をある程度解釈しているようです。例えば真偽の両方が必ず真を返すものであれば、if-then-elseのような使い方をしても問題ないため、警告されないようです。コードが短くなるので警告がされないものに関しては普通に使うことにしています。

eval'replace_all "a*b" "a[*]" &&:'&&:
# replace_all "a*b" "a[*]" &&: と同等

&&:&& trueと同じ意味です。set -e状態でもそこで停止すること無く $?を保持したまま次の行に進むためのものです。なお $?を正常終了に変えて次の行に進む(= エラーを無視する)ためには ||:と書きます。

evalを使用しているのは ksh のバグのワークアラウンドです。必ず発生するわけではないのですが(実際 WSL 上では発生しませんでした)「1. トップレベルに関数を定義している。」「2. トップレベルでその関数を呼び出している。」「3. その関数を再定義している。」のすべての条件を満たすと意味不明な不具合が発生することがあります。今回はスクリプトが停止するようになってしまったので evalを使用して2.の条件を満たさないようにしています。(おそらく1. 2. の条件を満たした時、最適化で再定義前の関数に何かしらの情報が結びついてしまってるのだと思います。)そして &&:が2つあるのは mksh のワークアラウンドです。evalの中に &&:がないと replace_all関数の終了ステータスが 0 ではないので(set -eの効果で)終了してしまいました。

case$?in

先程保持した $?を使って replace_all関数を定義するために処理を分岐させています。01の場合はそれぞれ使える関数を呼び出しているだけです。こちらは説明は不要でしょう。以下は 2 の場合のコードです。

replace_all(){
  meta_escape "$1""$3"eval"replace_all_pattern \"\$1\"\"\$2\"\"\${$1}\"\"\$4\""}

処理内容としては文字列をエスケープしてから replace_all_pattern関数に引き渡しているだけです。meta_escape関数でエスケープした文字列は $1で指定した名前の変数に代入されます。その後エスケープされた文字列を引数にして replace_all_pattern関数を呼び出しています。その後置換された文字列が再度 $1に代入されます。つまり途中で $1の名前の変数を meta_escape関数の結果を返すための一時変数として使用しています。

さいごに

以上 replace_all関数を作りましたという話と、それを題材に私のコーディングスタイルの解説でした。シェルスクリプトとしてはかなり独特なコーディングスタイルだと思いますが、これは移植性・可搬性、使いやすさ、パフォーマンス、正確性を両立するために工夫した結果出来上がったものです。大抵の人はこんな事するなら別言語で(略)と言うと思いますが、シェルは多くの環境で標準で入っているのでシェルスクリプトで作ればどこでも簡単に動かすことが出来ます。動作環境を一番選ばない言語なのです。今回作った replace_all関数は実装は大変でしたが、使うだけならば引数で指定した名前の変数に戻り値が返るというだけで、さほど違和感なく使えるのではないでしょうか?このスタイルの関数が増えてくればシェルスクリプトでのプログラミングももっと楽になると思います。そうすれば「こんな事」ももうする必要はないですよね?シェルスクリプトが使いづらいのは不足しているライブラリを補えば解決できる問題だと思っています。私もいつかは作った関数たちをライブラリとして公開できればと思います。

GitとGitHub(とTortoiseGit)を連携させる方法

$
0
0

異業種転職から1年が経過し、仕事で初めてソースを触った際にGit(とAWSのCodeCommit)を使用しました。
仕事ではスケジュールの関係で先輩が手取り足取り準備を進めて下さったのですが、
「自分でも出来るようにしておこう」ということで試行錯誤しました。
今回GitHubで試してみたのは、個人学習に使えそうだと考えたためです。

  私の環境

 ・Windows 10 Pro(64bit)
 ・Ver 1909 Build 18363.720
 ・ThinkPad T480s

0. GitHubアカウントの作成

 プロジェクト/リポジトリの有無は今回は問いません。
 ・作成済であれば、「1. Gitのインストール」に進んでください。
 ・未作成であれば、こちらから作成が可能です。
  手順は以下の通りです。

 取得したいユーザー名・メールアドレス・設定したいパスワードを入力
  ↓
 「Sign up for GitHub」をクリックし、無料版か有料版かの選択画面に遷移
  ↓
 「Unlimited public repositories for free.」を選択し、「Continue」をクリック
  ↓
 アンケート画面は回答が任意ですので、ご自由にどうぞ。
  ↓
 ユーザー登録が完了するので、メールによるアカウント認証を行えば完了です。

1. Gitのインストール

 exeファイルのダウンロード

 インストール用exeファイルをダウンロードする
 自身のOSによってページ内の踏むリンクが変わります。ご注意ください。
  ・Mac OS
  ・Windows
 また、32bitか64bitかビット数も要確認です。

 インストールに関しては、より詳細な記事を書いて下さっているこの方を参考にさせて頂きました。
 Windows10にGitをインストールして初期設定する

 Gitの初期設定

 Git Bashを起動させて、下記のコマンドを打ち込みます。
 連携に関連するので、Git用のユーザー名とメールアドレスは最初に登録します。
 なお、他の項目に関しては後から設定変更が可能です。

GitBash
$ git config --global user.name "登録させるユーザー名(英数字)"
$ git config --global user.email "登録させるメールアドレス"

2. TortoiseGitのインストール

 ※Mac OSユーザーの方は「3. 公開鍵の作成」に進んでください。
  Windowsユーザー かつ GUIでGitを操作したい方向けのツールです。

 exeファイルのダウンロード

 インストール用exeファイルをダウンロードする

 インストールに関しては、より詳細な記事を書いて下さっているこの方を参考にさせて頂きました。
 記事内の「TortoiseGitのセットアップ」を読んで頂ければ大丈夫です。
 TortoiseGitのセットアップ

3. SSH認証キーの作成・GitHubでの認証

SSH認証キーの作成など

 GitとGitHubを連携するために必要(というより最重要)なステップです。
 GitからGitHubにアクセスする機会の方が多いため、SSH認証を利用します。
 (GitHubからGitへのアクセスはクローンとプルで大体どうにかなるはず)
 Git Bashを再度使用します。

GitBash
$ ssh-keygen -t rsa -b 4096 -C "登録したメールアドレス"

 コマンド入力後は、以下の2つを聞いてきます。
  ① 鍵を保存するディレクトリ
  ② ファイル名
 .sshフォルダーを作成するディレクトリは、デフォルトで「ユーザー・ディレクトリー(C:/Users/ユーザー名)です。
 特にこだわりが無ければ、分かりやすいのでデフォルトのままで良いでしょう。
 ファイル名は、デフォルトで「id_rsa」になります。
 後の指定を考慮すれば、デフォルトのままの方が楽です。
 Github用であることを明示したい場合は「_github」と付ければ良いと思います。
 最後にパスフレーズを聞かれます。任意ですが、登録を推奨します。

 完成したキーは、(githubを付けていれば)以下のようになっていると思います。
  /c/Users/user/.ssh/id
rsa_github --- 秘密鍵
  /c/Users/user/.ssh/id_rsa_github.pub --- 公開鍵

 最後にSSHのconfigファイルを編集します。
 GitHubへSSH接続する際に、どの秘密鍵を指定するかを管理するのが目的です。
 .sshフォルダーの配下にconfigファイルを作成します。

GitBash
$ Host GitHub
$ HostName github.com
$ User git
$ IdentityFile ~/.ssh/id_rsa_github

 HostはSSH接続の識別名なので任意でOKですが、分かりやすく「github」にしました。
 HostNameは「github.com」、Userは「git」で固定しておきます。
 どうも、Githubヘルプ曰く「リモートURLへの接続を含むすべての接続は、"git"ユーザーとして作成されなければなりません。GitHubのユーザー名で接続しようとすると失敗します。」とのことです。
 IdentityFileで、秘密鍵(id_rsa_github)のパスとファイル名を指定します。

GitHubでの認証

 ここまで準備出来たら、いよいよGitとGitHubの連携を行います。
 保存した公開鍵(id_rsa_github.pub)をエディタで開き、中の文字列をコピーします。
 その際、改行や余分な文字列が入らないようにご注意ください。
 GitHubの右上にあるアイコンを押すとアカウントのメニューが登場しますので、その中からSettingsをクリックします。
 遷移先の画面でSSH and GPG keysをクリックするとSSHキーの登録画面に移ります。
 右上のNew SSH Keyを選択してください。
 Titleは任意ですので、必要に応じてPC名などを入れておきましょう。
 Keyにコピーしておいた公開鍵を貼り付けたら、Add SHH Keyを押して登録します。
 登録したら、下記のコマンドを打ち込んで設定に問題がないか確認します。

GitBash
$ ssh github

 パスフレーズを聞かれたら、SSH認証キー作成時に登録したものを入力します。
 以下のような表示になれば接続成功です。お疲れ様でした!

GitBash
$ ssh github
PTY allocation request failed on channel 0
Hi User-name! You've successfully authenticated, but GitHub does not provide shell access.
Connection to github.com closed.

4. 実際にやってみた所感

 実際にGitを操作する分にはコマンド等を調べて案外何とかなるのですが、
 そもそもの環境構築を行う機会はなかなか無いと思いました。
 そのため、自分で検索しながら実際に手を動かすといい勉強になりました。
 様々な記事を参考にさせて頂きましたが、いつか独力で書けるレベルに到達したいものです。

 ちなみに、Git(ローカル)のソース管理はVSCodeを使うと簡単です。
 先述の案件では、VSCodeでローカルコミットしてTortoiseGitでAWS CodeCommitにプッシュして…としていました。今回は、AWS CodeCommitがGitHubに変わった形です。
 TortoiseGitを組合せると、GUIでアレコレ出来るので便利でした。

最後に、以下の記事も参考にさせて頂きました、ありがとうございました。
 GitとGithubを連携してソースのバージョン管理を行う方法
 今日からはじめるGitHub 〜 初心者がGitをインストールして、プルリクできるようになるまでを解説

Bash と cURL で YouTube DATA API からプレイリストを取得するサンプル

$
0
0

YouTube API(YouTube Data API)を cURL で叩いて、YouTube プレイリストの一覧を取得したい。

youtube api curl プレイリスト 取得」と Qiita 記事に絞ってググってもピンポイントでヒットしなかったので、自分の備忘録として。

TL; DR

  • 発行した API キーには「YouTube DATA API」の制限をかけておくこと。
Bash+cURLによるGETリクエストのサンプル
#!/bin/bash# API エンドポイント(リクエスト先の URL)url_api_endpoint='https://www.googleapis.com/youtube/v3/playlistItems'# 必須クエリ・パラメーターkey_api_youtube='<Google API Key>'id_playlist_youtube='<ここに Youtube プレイリストの ID>';part='snippet'# オプションのクエリ・パラメーター(0〜50)num_result_max=50

# クエリ作成query="key=${key_api_youtube}&part=${part}&playlistId=${id_playlist_youtube}&maxResults=${num_result_max}"# リクエスト作成url_request="${url_api_endpoint}?${query}"# リクエストの実行と取得結果表示
curl -s$url_request | jq .

TS; DR

スクリーンショット 2020-04-14 17.49.15.png

  • ポイント
    • API キーの作成/発行は Google Developer Console の「認証情報」から行います。プロジェクトを作成してから、認証情報を作成します。
    • 作成/発行した API キーには「YouTube DATA API」の制限をかけておきます。制限がかかっていないと、リクエスト時に「403 Forbidden」エラーが返ってきます。

Google API のプロトコル

  • YouTube API の全体的な仕様
  • アクセス・トークン
    • クエリの keys=パラメーターで指定
  • アクセス・トークンの種類

    • API キー:
      • 自前で発行したアクセス・トークンのこと。
      • 利用する API(ここでは YouTube Data API)で API キーを制限する必要あり
      • トークンは全てのユーザーで共通のものになる。
      • :thumbsup:メリット :簡単
      • :thumbsdown:デメリット:API キー発行者のクォータ(利用制限の容量)を使う。
    • OAuth 2.0 トークン:
      • OAuth クライアント・キーを使って得たアクセス・トークンのこと。
      • トークンは個々のユーザーごとで異なる。
      • :thumbsup:メリット :クォータ(利用制限の容量)は各ユーザーごと
      • :thumbsdown:デメリット:OAuth 2.0 認証を実装してアクセス・トークンを取得する必要がある。
  • YouTube プレイリスト項目(アイテム) API の仕様


# fishの導入

$
0
0

fishとは

  • インタラクティブシェル
  • 補完機能が充実

インストール

まずはインストール

$ brew install fish

$ fish -v

デフォルトシェルの変更

$ sudo vi /etc/shells # 末尾に /usr/local/bin/fish を追加$ chsh -s /usr/local/bin/fish # デフォルトシェルを fish に変更

terminal再起動
※ターミナルを再起動して変更されているか確認します

fisherman

fishのプラグインマネージャ

docs

https://github.com/jorgebucaran/fisher

$ curl -Lo ~/.config/fish/functions/fisher.fish --create-dirs https://git.io/fisher
$ fisher -v

テーマの変更

fish__usr_local_var_workspace_practice_ionic-conference-app.png

(現状使用しているテーマ)
上記のように色々なテーマがこちらに存在するので、好きなテーマを使用できます。
※別途powerlineフォントをインストールしなければいけない場合があるのでそれは後述

powerlineフォント

多分、デフォのままだと文字化けを起こすので下記でpowerlineフォントをインストールしitermなどに設定する

$ git clone git@github.com:powerline/fonts.git

$ cd path/to/install
$ ./install.sh

プラグイン

テーマ適用

$ fisher add oh-my-fish/theme-bobthefish
  • ターミナル再起動

z - 履歴からディレクトリへ移動

z <文字列>のように入力すると、履歴から補完し補完内容から選択してディレクトリの移動が可能になります。

$ fisher add z

fish-bd - 親ディレクトリへ移動

cd ../../のようにディレクトリを遡ることがなくせます。
bdと入力すると親ディレクトリの一覧が候補として表示され、候補から選択しディレクトリの移動が可能になります。

$ fisher add 0rax/fish-bd

fish 設定

ブラウザからfishの設定が可能です。

$ fish_config

fish_shell_configuration.png

シェルスクリプトのreadを末尾改行なしやWindows改行コードに対応させる方法

$
0
0

はじめに

シェルスクリプトの readを使ってファイルを読み込む場合、対象のファイルの最後の行は末尾に改行がなければ読み込めないというのはある程度シェルスクリプトを書いてる人なら一度はハマったことがあるかと思います。一般的には改行で終わらせましょうという話ですが、対応せざるを得ない場合もあるかと思います。その場合の対応のさせかたです。なお(いつもどおり)POSIX 準拠かつ外部コマンド呼び出しなしでシェルスクリプトの機能だけで実装します。

実装

末尾改行なしに対応させる

しばしば勘違いされていると思いますが readは最後の行は末尾に改行がなければ読み込めないというのは正確ではなく、読み込んではいます。ただ read関数が正常終了になっていないだけです。なので判定方法を工夫すれば読み込めます。

# これだと最後の行は改行がなければならないwhile IFS=read-r line;do
  echo"$line"done# これならOK。最後の行は実は読み込まれてるので $line に何かしら入っているwhile IFS=read-r line ||["$line"];do
  echo"$line"done

Windows改行コードに対応させる

Linux / Unix / macOS の改行コードは LFですが、Windowsの改行コードは CRLFです。余計な CRがあるのでこれを削除するだけです。

CR=$(printf'\r')while IFS=read-r line &&line=${line%"$CR"}||["$line"];do
  echo"$line${#line}"done

関数化

ここまでの話ならただの雑学。ここからが本題です。上記のコードを何度も書くのは面倒ですよね?そこで関数化したいと考えます。read関数が少し特徴的なのもあってぱっと思い浮かばないのではないでしょうか?まあ引っ張る話でもないのでさくっと実装です。

readline(){IFS=read-r"$1"||eval"[ \"\${$1}\" ]"}line=''# shellcheck に未定義の変数を使用していると怒られないようにするためwhile readline line;do
  echo"$line"done

次はWindowsの改行コードに対応です。先程の応用です。

CR=$(printf'\r')

readline(){# shellcheck が SC2015 の警告を出すので { } でくくる{IFS=read-r"$1"&&eval"$1=\${$1%\"\$CR\"}";}||eval"[ \"\${$1}\" ]"}line=''# shellcheck に未定義の変数を使用していると怒られないようにするためwhile readline line;do
  echo"$line"done

pdksh, loksh ワークアラウンド

基本的には上記のコードで良いのですが、pdksh と lokshでバグがあります。(loksh は Alpine Linux にパッケージがあります。loksh には loksh is a Linux port of OpenBSD's kshと書いてあり、OpenBSD の kshpublic domain Korn shellと書いてあるので同じバグなのだと思います。)

pdksh, loksh は set -eした状態だと途中でスクリプトが終了してしまいます。以下はそれを再現する簡単なコードです。本来ならば FALSE と END が表示されなければいけないはずですが evalの中で [ ]を使用すると途中で中断してしまうのです。(TRUE も表示されません。)

set-eeval"[ ]"&&echo TRUE ||echo FALSE
echo END

ワークアラウンドは簡単で evalの中の [ ]の後ろに &&: (&& true) をつけるだけです。

set-eeval"[ ] &&:"&&echo TRUE ||echo FALSE
echo END

ということで関数化したコードにも追加します。

# 末尾改行なし対応
readline(){IFS=read-r"$1"||eval"[ \"\${$1}\" ] &&:"}# 末尾改行なし + Windows改行コード対応
readline(){{IFS=read-r"$1"&&eval"$1=\${$1%\"\$CR\"}";}||eval"[ \"\${$1}\" ] &&:"}

以上で完成です。

ログインシェルを変更する

$
0
0

ログインシェルを変更する際は、chshコマンドを使います。

シェルとは

人間の入力をコンピュータに伝えるプログラムのこと。
ターミナル(黒い画面)を用いて操作します:relaxed:
同じ日本語でも地域によって方言があるように、同じシェルでも様々な種類が存在します。
代表的なものだと、Mac OSで使われているbashやzsh、Cシェル系のtcshなどがあります。

利用できるシェルを確認する

$ chsh --list-shells

bashからzshに変更

$ chsh -s /bin/zsh

zshからbashに変更

$ chsh -s /bin/bash

ラズパイでファイルマネージャーを開くとすぐに消える場合の対処法メモ

$
0
0

事象

ラズパイでファイルマネージャーを開こうとすると、一瞬開いてすぐに消える。再起動しても現象は変わらない。

↓ファイルマネージャーのアイコン
FileManager.png

直前までやっていたこと

ラズパイのアップデートを行っていた。

$ sudo apt-get update
$ sudo apt-get upgrade

原因

アップデートに失敗、保留があり全て完了していなかった。

$ sudo apt-get upgrade

を実施すると、ターミナルに下記が表示されるようになった。

パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
アップグレードパッケージを検出しています ... 完了
以下のパッケージは保留されます:
xxxxx xxxxx xxxxx xxxxx xxxxx
アップグレード: 0 個、新規インストール: 0 個、削除: 0 個、保留: 5 個。

対策

保留になっているパッケージを個別にインストールする。

$ sudo apt-get install xxxxx #xxxxxはパッケージ名

保留が0個になれば完了。

パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
アップグレードパッケージを検出しています ... 完了
以下のパッケージは保留されます:
xxxxx xxxxx xxxxx xxxxx xxxxx
アップグレード: 0 個、新規インストール: 0 個、削除: 0 個、保留: 0 個。

効果確認

無事にファイルマネージャーを開くことができた。

他に

調べると色々と対処法が出てくるが、これらを試しても今回は効果が見られなかった。

$ sudo apt-get dist-upgrade
$ sudo apt-mark unhold pcmanfm #ファイルマネージャー保留解除

わがままな要望を叶えたシェルスクリプトのusage関数の書き方

$
0
0

はじめに

コマンドの使い方を表示する usage関数、書いていますか?
書いていますよね?
でも不満がありますよね?

それは・・・

 

    $\huge{インデント}$

 
普通に書くと

usage(){cat<<USAGE
Usage: command [-h | --help]
USAGE
}

みたいな感じになってインデントがすこぶる気持ち悪いです。私には他にも細かい要望がいくつもあって、でも良い書き方が思いつかなかったのでずっとモヤモヤしていたのですがようやく思いついたのでインデントに始まるいろいろな要望を叶えた usage関数の書き方を紹介したいと思います。

要望

私の細かい要望を羅列していきます。説明は省略しますが多分、既存の usageの書き方に不満があった人ならわかるんじゃないかと。

  • インデントをきちんと揃えたい
  • 本物のタブは使いたくない(インデントはスペース2文字派)
  • タブを使ったとしてもヒアドキュメントの終わりはインデントできず揃わない
  • ヒアドキュメント後の }が気持ち悪い
  • コメントの形で埋め込むやり方は頭の #が嫌
  • ファイルの一番上に書きたい
  • とは言うもののバージョン番号を入れた変数よりかは下にしたい時もある
  • set -euとどちらを上にするか悩む
  • ヘルプの中で変数やコマンド置換を使いたい
  • ヘルプの中でも未定義の変数を参照したら落ちてほしい(つまり set -uを機能させたい)
  • ヘルプが表示されない場合のパフォーマンス低下はできるだけ避けたい
  • なるべく短くシンプルに書きたい
  • 複数のヘルプを持たせたい(例えばサブコマンド専用のヘルプ)
  • POSIX 準拠の範囲でやりたい

実装

hello.sh
#!/bin/shVERSION=0.0.1

set-eu&& :<<'USAGE'
Usage: $(basename"$0") [-h | --help] [NAME]

Options:
  -h | --help   Display this help

Version: $VERSIONUSAGE

usage(){sed-n"/<<'$1'/,/^$1\$/ { s/.*<<'$1'/cat<<$1/; p }"}case${1:-}in(-h|--help)eval"$(usage "USAGE"< "$0")"exit 0
esac

echo"Hello ${1:-World}"

みたまんまですが、一応解説すると、

  • && :<<'USAGE'の左に set -eu等の簡単なコマンドを追加実行でき、一行で書くことが出来ます。
  • VERSION変数等はヘルプの上に書いても下に書いても && :<<'USAGE'の左に書いても構いません。
  • :コマンドにヘルプ内容を渡してますが、何もしないはずなのでパフォーマンス低下はわずかです。
  • 'USAGE'とシングルクォートでくくっているのでヒアドキュメントの中身はただのテキスト扱いです。
    • つまり basenameコマンドは(この時点では)実行されずパフォーマンス低下はありません。

usage関数

  • 現在のスクリプトファイルを読み取りヒアドキュメントの開始から終わりの行までを出力しています。
  • 途中の set -eu && :<<'USAGE'cat<<USAGEに変換しています。
  • 最終的に eval "$(usage "USAGE" < "$0")"で出力されたコードを実行します。
  • evalで実行される際は 'USAGE'USAGEに変更してるので変数やコマンド置換が行われます。
  • USAGEの文字を変更すれば別のヘルプを出力することが出来ます。別のファイルにあっても構いません。

おまけ

シェルスクリプト実装版(sed未使用)の usage関数です。

usage(){while IFS=read-r line &&[!"${line#*:}"="<<'$1'"];do :;done
  while IFS=read-r line &&[!"$line"="$1"];do set"$@""$line";done
  shift&&[$# -eq 0 ]||printf'%s\n'"cat<<$line""$@""$line"}

" a server is already running" エラー対応の自動化(シェルスクリプト使用)

$
0
0

エラーの原因

Dockerを使用して立ち上げたRailsコンテナのプロセスを「Ctrl + c」で強制終了し、「rails s」で再びサーバーを立ち上げようとすると" a server is already running" というエラーが発生することがあります。

原因は、pids/server.pid というファイルが残っていることです。手動ではありますが、コマンドラインで以下を実行すれば解決します。

rm tmp/pids/server.pid

毎回のように丁寧にDockerコマンドを入力すれば問題は発生しませんが、「Ctrl + c」で楽にサーバーを終了させたいというのが人間だと思います。しかし、エラーに怯え、エラーが発生した際に毎回手動対応することは大変に面倒なので、削除を自動化するシェルスクリプトを書きます。

まずは、Railsアプリのあるディレクトリに「delete-server-pid.sh 」というファイルを作成します。以下のコマンドを実行しても良いです。

touch delete-server-pid.sh 

作成したファイルにシェルスクリプトを書きます。

#!/bin/sh
set -e

if [ -f tmp/pids/server.pid ]; then
 rm tmp/pids/server.pid
fi

exec "$@"

次にコマンドラインで以下を実行し、パーミッションを設定します。

chmod +x delete-server-pid.sh

あとは、Railsのために用意されたDockerfileに作成したシェルスクリプトを実行するように設定します。

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]
ENTRYPOINT [​ "./delete-server-pid.sh " ​] 
CMD ​​ ["bin/rails", "s", "-b", "0.0.0.0"] ​ 

これで、server.pidがあれば削除されるように自動化されました。

参考書籍/参考資料

コード自体はこちらに書かれています。
https://stackoverflow.com/questions/35022428/rails-server-is-still-running-in-a-new-opened-docker-container

書籍ですが、シェルスクリプトの詳しい解説はこちらにあります。
https://www.amazon.co.jp/Docker-Rails-Developers-Applications-Everywhere/dp/1680502735

bash と jq で sqlite3 をいじるためのメモ

$
0
0

シェル芸で sqlite3 を叩く知見がわりと溜まったのでメモついでにまとめておきます。

$ sqlite3 --version
3.22.0 2018-01-22 18:45:57 0c55d179733b46d8d0ba4d88e01a25e10677046ee3da1d5b1581e86726f2alt1

あと jq 使います。便利なので。

$ jq -V
jq-1.6

まず適当な tsv を作る

bash
$ (printf'列1\t列2\t列3\n';seq-f'ほげ%g' 99 | paste - - -;) | tee sample.tsv
列1     列2     列3
ほげ1   ほげ2   ほげ3
ほげ4   ほげ5   ほげ6
ほげ7   ほげ8   ほげ9
ほげ10  ほげ11  ほげ12
ほげ13  ほげ14  ほげ15
ほげ16  ほげ17  ほげ18
ほげ19  ほげ20  ほげ21
ほげ22  ほげ23  ほげ24
ほげ25  ほげ26  ほげ27
ほげ28  ほげ29  ほげ30
ほげ31  ほげ32  ほげ33
ほげ34  ほげ35  ほげ36
ほげ37  ほげ38  ほげ39
ほげ40  ほげ41  ほげ42
ほげ43  ほげ44  ほげ45
ほげ46  ほげ47  ほげ48
ほげ49  ほげ50  ほげ51
ほげ52  ほげ53  ほげ54
ほげ55  ほげ56  ほげ57
ほげ58  ほげ59  ほげ60
ほげ61  ほげ62  ほげ63
ほげ64  ほげ65  ほげ66
ほげ67  ほげ68  ほげ69
ほげ70  ほげ71  ほげ72
ほげ73  ほげ74  ほげ75
ほげ76  ほげ77  ほげ78
ほげ79  ほげ80  ほげ81
ほげ82  ほげ83  ほげ84
ほげ85  ほげ86  ほげ87
ほげ88  ほげ89  ほげ90
ほげ91  ほげ92  ほげ93
ほげ94  ほげ95  ほげ96
ほげ97  ほげ98  ほげ99

投入する

bash
$ export TABLE_NAME=sample
s.jq
["CREATE TABLE \(env.TABLE_NAME) (",(split("\t")|map("'\(.)' TEXT")|join(",\n")),");"]|join("\n")
bash
cat sample.tsv | {read a;echo"$a" | jq -Rr-f s.jq >init.sql;cat;} | sqlite3 -separator$'\t'-init init.sql -cmd".import /dev/stdin ${TABLE_NAME}" a.db

確認

init.sql
CREATETABLEsample('列1'TEXT,'列2'TEXT,'列3'TEXT);
bash
$ sqlite3 -header a.db "SELECT * FROM sample";列1|列2|列3
ほげ1|ほげ2|ほげ3
ほげ4|ほげ5|ほげ6
ほげ7|ほげ8|ほげ9
ほげ10|ほげ11|ほげ12
ほげ13|ほげ14|ほげ15
ほげ16|ほげ17|ほげ18
ほげ19|ほげ20|ほげ21
ほげ22|ほげ23|ほげ24
ほげ25|ほげ26|ほげ27
ほげ28|ほげ29|ほげ30
ほげ31|ほげ32|ほげ33
ほげ34|ほげ35|ほげ36
ほげ37|ほげ38|ほげ39
ほげ40|ほげ41|ほげ42
ほげ43|ほげ44|ほげ45
ほげ46|ほげ47|ほげ48
ほげ49|ほげ50|ほげ51
ほげ52|ほげ53|ほげ54
ほげ55|ほげ56|ほげ57
ほげ58|ほげ59|ほげ60
ほげ61|ほげ62|ほげ63
ほげ64|ほげ65|ほげ66
ほげ67|ほげ68|ほげ69
ほげ70|ほげ71|ほげ72
ほげ73|ほげ74|ほげ75
ほげ76|ほげ77|ほげ78
ほげ79|ほげ80|ほげ81
ほげ82|ほげ83|ほげ84
ほげ85|ほげ86|ほげ87
ほげ88|ほげ89|ほげ90
ほげ91|ほげ92|ほげ93
ほげ94|ほげ95|ほげ96
ほげ97|ほげ98|ほげ99

json object で出力する

a.jq
map("'\(.)', \"\(.)\"")|join(",")
$ sqlite3 a.db "SELECT name FROM PRAGMA_table_info('${TABLE_NAME}')"\
    | jq -R | jq -sr-f a.jq \
    | xargs -I{}-0  sqlite3 a.db "SELECT json_object({}) FROM ${TABLE_NAME}"{"列1":"ほげ1","列2":"ほげ2","列3":"ほげ3"}{"列1":"ほげ4","列2":"ほげ5","列3":"ほげ6"}{"列1":"ほげ7","列2":"ほげ8","列3":"ほげ9"}{"列1":"ほげ10","列2":"ほげ11","列3":"ほげ12"}{"列1":"ほげ13","列2":"ほげ14","列3":"ほげ15"}{"列1":"ほげ16","列2":"ほげ17","列3":"ほげ18"}{"列1":"ほげ19","列2":"ほげ20","列3":"ほげ21"}{"列1":"ほげ22","列2":"ほげ23","列3":"ほげ24"}{"列1":"ほげ25","列2":"ほげ26","列3":"ほげ27"}{"列1":"ほげ28","列2":"ほげ29","列3":"ほげ30"}{"列1":"ほげ31","列2":"ほげ32","列3":"ほげ33"}{"列1":"ほげ34","列2":"ほげ35","列3":"ほげ36"}{"列1":"ほげ37","列2":"ほげ38","列3":"ほげ39"}{"列1":"ほげ40","列2":"ほげ41","列3":"ほげ42"}{"列1":"ほげ43","列2":"ほげ44","列3":"ほげ45"}{"列1":"ほげ46","列2":"ほげ47","列3":"ほげ48"}{"列1":"ほげ49","列2":"ほげ50","列3":"ほげ51"}{"列1":"ほげ52","列2":"ほげ53","列3":"ほげ54"}{"列1":"ほげ55","列2":"ほげ56","列3":"ほげ57"}{"列1":"ほげ58","列2":"ほげ59","列3":"ほげ60"}{"列1":"ほげ61","列2":"ほげ62","列3":"ほげ63"}{"列1":"ほげ64","列2":"ほげ65","列3":"ほげ66"}{"列1":"ほげ67","列2":"ほげ68","列3":"ほげ69"}{"列1":"ほげ70","列2":"ほげ71","列3":"ほげ72"}{"列1":"ほげ73","列2":"ほげ74","列3":"ほげ75"}{"列1":"ほげ76","列2":"ほげ77","列3":"ほげ78"}{"列1":"ほげ79","列2":"ほげ80","列3":"ほげ81"}{"列1":"ほげ82","列2":"ほげ83","列3":"ほげ84"}{"列1":"ほげ85","列2":"ほげ86","列3":"ほげ87"}{"列1":"ほげ88","列2":"ほげ89","列3":"ほげ90"}{"列1":"ほげ91","列2":"ほげ92","列3":"ほげ93"}{"列1":"ほげ94","列2":"ほげ95","列3":"ほげ96"}{"列1":"ほげ97","列2":"ほげ98","列3":"ほげ99"}

各所の説明書こうかと思ったけど、力尽きた。
まぁいいか、シェル芸だし。気が向いたらそのうち書くかも。


DockerFileから、引数つきのエイリアスを通す

$
0
0

やりたいこと

Dockerコンテナで引数のあるエイリアスを叩けるようにしたい。

やりかた

.bashrcにfunctionをEchoで記述するコードをDockerFileに書く。
今回の事例では、jupyter labの立ち上げ時に任意のポートを開くコードを使用しました。

RUN echo "function jlab() { \n command jupyter lab --ip=0.0.0.0 --port=\$1 --allow-root \n}" >> ~/.bashrc

'\$1' の部分が引数となる。'\'はエスケープコマンドで、\nは改行。
あとはイメージを作って、コンテナ作成&起動で入れば、
jlab 任意ポートでjupyter labサーバーが立ち上がる。

ひっかかったとこ

echo -e...とならないところ。bash&Docker初心者の俺は、bashだと必要なオプションがいらないという事実に非常に困惑した。
(コンテナ外だと、普通にエラーする)

学んだこと

Dockerとシェルを両方理解して、始めて住み心地はよくなる。
あともっと賢そうな方法あったら、教えてほしいれす...。

参考ページ

https://hacknote.jp/archives/8043/
https://eng-entrance.com/linux-command-echo#i-7
http://capm-network.com/?tag=%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E9%96%A2%E6%95%B0

WSLで末尾の".exe"を補完してコマンドを実行する

$
0
0

WSLはWindowsのバイナリを実行できますが、hoge.exehogeとして実行できません。
数が多くない場合は alias hoge=hoge.exeとしておけばよいですが、それも面倒な場合もあります。

ところで、Bashではcommand_not_found_handleというシェル関数が存在する場合、コマンドが存在しない時にこのシェル関数を呼びます。
https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html#lbBY

これを利用して、hoge "a" "b c"とコマンドを打ちコマンドが存在しない場合に、 hoge.exe "a" "b c"としてコマンドを実行することができます。

.bashrc
function command_not_found_handle(){unset command_not_found_handle
        "$1.exe""${@:2}"}

unsetしておかないとコマンドが存在しない時に再帰ループに陥るので、unsetするといい感じになります。
command_not_found_handleを呼ぶときはサブシェルで実行されるため、unsetをしても元のシェルには影響はありません。

command_not_found_handleを使う方法の欠点は、2回コマンド実行を行うケースがあるため、それがネックになる可能性があります。
そのパフォーマンスも惜しい場合は、hashであらかじめ有効なコマンドを設定しておく方法が使えるかもしれません(未調査)
WSLをあきらめるという手もあると思います

おまけ

"${@:2}"は部分文字列展開を配列@に対して行っており、"$2" "$3" ... "$n"と同じ意味になります。

https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html#lbBB

${parameter:offset:length}
parameter が @ ならば、結果は offset から始まる length 個の位置パラメータになります。

参考

WSLのIssueが上がっていますが、WSL側では対応せず、ユーザにあった方法で回避してもらう方針のようです。
https://github.com/microsoft/WSL/issues/2003
(個人的にもWSLにWindowsの都合はあまり持ち込まないほうが良いと思います。Linuxの互換レイヤーとして頑張ってもらいたいですし)

Bashズンドコ(until, 正規表現)

$
0
0

たまたま3年前に流行ったズンドコキヨシの記事 in clispを見たのでBashで短く書いた(なんで)

ズンドコキ・ヨ・シ!(120B)
until[[${a[*]}=~(ズン\ ){4}ドコ ]];do a[++j]+=`shuf-en1ズン ドコ`;echo${a[j]};done;echo キ・ヨ・シ!

多分これが一番短いと思います

Bash でタイムスタンプを扱う

$
0
0

Bash でタイムスタンプや、その比較を行いたかったが、やり方を知らないことに気づいたので、焦らずしっかり調べて理解する。

現在の時間の取得

dateコマンドは現在時間を取得する。

$ date
Fri Apr 17 14:54:18 PDT 2020

上記のは現在のタイムゾーンの値であるので、大小比較が可能な、1970-01-01 00:00:00 UTCからの経過秒数に変換します。%sがそれを表す記号ですので、`date "+フォーマット" の形式で形式をしていして表示できます。

$ date"+%s"
1587160470

Date のフォーマット

$ date"+%m/%d/%Y %H:%M"
04/17/2020 15:01

現在時のフォーマットはこれでよいですが、特定の文字列のフォーマットから違うフォーマットに変換する場合は、--dateもしくは -dの引数で文字列を渡すとよいです。

$ date-d"04/17/2020 15:10"    
Fri Apr 17 15:10:00 PDT 2020
$ date-d"04/17/2020 15:10""+%m/%d/%Y %H:%M"
04/17/2020 15:10

%s からのフォーマット

ただし、文字列ではなく、1970-01-01 00:00:00 UTCからの経過秒数 から変換をかけたい時は@を添付します。

date--date @1269553200 "+%m/%d/%Y %H:%M"
03/25/2010 14:40

大小の比較

タイムスタンプの大小比較は、1970-01-01 00:00:00 UTCからの経過秒数で比較するのがよさそうです。単純に比較するとよいです。結果も予想通り。ちなみに、Bashで数値の計算をしたいときは、$((計算))になります。

test.sh

#!/bin/bashlast=$(date"+%s")sleep 1

current=$(date"+%s")if[$current-gt$last];then 
        s=$((current - last))echo"current is bigger than Last: difference: ${s} sec."else
   s=$((last - current))echo"Last is bigger than Current: difference: ${s} sec."fi

結果は想定の通りです。

$./test.sh 
current is bigger than Last: difference: 1 sec.

Bashでechoに色付けする

$
0
0
set-eu# Color echo# usage: echo_color -b <backcolor> -t <textcolor> -d <decoration> [Text]## Text Color# 30 => Black# 31 => Red# 32 => Green# 33 => Yellow# 34 => Blue# 35 => Magenta# 36 => Cyan# 37 => White## Background color# 40 => Black# 41 => Red# 42 => Green# 43 => Yellow# 44 => Blue# 45 => Magenta# 46 => Cyan# 47 => White## Text decoration# 0 => All attributs off (ノーマル)# 1 => Bold on (太字)# 4 => Underscore (下線)# 5 => Blink on (点滅)# 7 => Reverse video on (色反転)# 8 => Concealed on

echo_color(){local backcolor
    local textcolor
    local decotypes
    local echo_opts
    local OPTIND_bak="${OPTIND}"unset OPTIND

    echo_opts="-e"while getopts'b:t:d:n' arg;do
        case"${arg}"in
            b)backcolor="${OPTARG}";;
            t)textcolor="${OPTARG}";;
            d)decotypes="${OPTARG}";;
            n)echo_opts="-n -e";;esacdone

    shift$((OPTIND -1))echo${echo_opts}"\e[$([[-v backcolor ]]&&echo-n"${backcolor}";[[-v textcolor ]]&&echo-n";${textcolor}";[[-v decotypes ]]&&echo-n";${decotypes}")m${@}\e[m"OPTIND=${OPTIND_bak}}
Viewing all 2757 articles
Browse latest View live