Edited at

シェル芸人のためのfish入門


概要

fish shellでシェル芸(ないしワンライナー業)を行う上で、一般的にシェル芸人が使用するであろうBashとの違いについて情報収集・記述する。

自分が移行するために調べながら書いており、今現在fishマスターでは全然ないので間違いがあるかもしれない。



シェル芸とは

curl -s 'https://b.ueda.tech/?page=01434'| grep -A1 h2 | head -n 2 | perl -pe 's/<.*?>//g'                                               (378ms)

シェル芸の定義バージョン1.1
マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理をCLI端末へのコマンド入力一撃で終わらすこと。あるいはそ
のときのコマンド入力のこと。


コマンド例の元データとしてcode4fukui/localgovjp地方自治体一覧を使います。

$ curl https://raw.githubusercontent.com/code4fukui/localgovjp/gh-pages/localgovjp-utf8.csv



実はそんなに違わない

fishの設計思想より。


可能な限り上記の目標を崩すことなく、fishはPOSIXの構文に従うべきです。


ということでだいたい普通にかける。


パイプでコマンドを並べるぶんには全く変わらない。

(例)localgovjp-utf8.csvから東京都の上5つだけ取得してみる。

$ grep 東京都 localgovjp-utf8.csv | head -n 5                                                                                                                                                 (2s 639ms)

"13","東京都","13101","千代田区","ちよだく","35.69388889","139.7536111","http://www.city.chiyoda.lg.jp/","都心の魅力にあふれ、文化と伝統が息づくまち千代田"
"13","東京都","13102","中央区","ちゅうおうく","35.67083333","139.7722222","http://www.city.chuo.lg.jp/","生涯躍動へ 都心再生 ― 個性がいきる ひととまち"
"13","東京都","13103","港区","みなとく","35.65805556","139.7516667","http://www.city.minato.tokyo.jp/","やすらぎある世界都心・MINATO -誰もが誇りに思えるまち・港区-"
"13","東京都","13104","新宿区","しんじゅくく","35.69388889","139.7036111","http://www.city.shinjuku.lg.jp/","「新宿力」で創造する、やすらぎとにぎわいのまち"
"13","東京都","13105","文京区","ぶんきょうく","35.70805556","139.7522222","http://www.city.bunkyo.lg.jp/","歴史と文化と緑に育まれた、みんなが主役のまち「文(ふみ)の京(みやこ)」"


後述するがクオートやエスケープもさほど違いはない。

入力をスクリプトとして実行もしてくれる。


(例)東京都の自治体と同じ名前を含む東京都以外の自治体を出す例

$ grep 東京都 localgovjp-utf8.csv | tr -d \" | cut -d, -f 4 | xargs -I@ grep @ localgovjp-utf8.csv | grep -v 東京都 | head

"1","北海道","1101","札幌市 中央区","さっぽろし ちゅうおうく","43.0553865","141.3409671","http://www.city.sapporo.jp/chuo/","市電のふるさと中央区"
"11","埼玉県","11105","さいたま市 中央区","さいたまし ちゅうおうく","35.88388889","139.6261111","http://www.city.saitama.jp/chuo/index.html","新しい都市文化
の創造と交流(ふれあい)が育てる安心な暮らし"
"12","千葉県","12101","千葉市 中央区","ちばし ちゅうおうく","35.60888889","140.1247222","http://www.city.chiba.jp/chuo/index.html","うるおいと活気に満ちた
文化の香り高いまち 中央区"
"14","神奈川県","14152","相模原市 中央区","さがみはらし ちゅうおうく","35.57138889","139.3733333","http://www.city.sagamihara.kanagawa.jp/chuoku/index.html",""
"15","新潟県","15103","新潟市 中央区","にいがたし ちゅうおうく","37.91611111","139.0363889","http://www.city.niigata.lg.jp/chuo/index.html","都心が賑わい、人
々が集い交流する水辺のまち"
"27","大阪府","27128","大阪市 中央区","おおさかし ちゅうおうく","34.68111111","135.5097222","http://www.city.osaka.lg.jp/chuo/","ようこそ!歴史と文化のまち中
央区へ"
"28","兵庫県","28110","神戸市 中央区","こうべし ちゅうおうく","34.695","135.1977778","http://www.city.kobe.lg.jp/ward/kuyakusho/chuou/","くらす魅力、つどう魅
力、多彩な個性が響きあう都市(まち) 中央区"
"40","福岡県","40133","福岡市 中央区","ふくおかし ちゅうおうく","33.58916667","130.3930556","http://www.city.fukuoka.lg.jp/chuo/index.html","アジアに輝く成熟
のまち"
"43","熊本県","43101","熊本市 中央区","くまもとし ちゅうおうく","32.80328437","130.708129","http://www.city.kumamoto.jp/chuo/default.aspx",""
"23","愛知県","23111","名古屋市 港区","なごやし みなとく","35.10777778","136.8855556","http://www.city.nagoya.jp/minato/","「信頼」と「安心」を高め、暮らしや
すいまちづくり"


標準入力を子プロセスに渡してくれるのも同様。

(例)静岡を東から順に並べ替えて番号を降ってみる。

(実際はfishに渡さなくてもよいが例のためそうしている)

$ grep 静岡 localgovjp-utf8.csv | tr -d \" | sort -r -t, -n -k7,7 | cut -d, -f 4 | fish -c nl | head

1 伊東市
2 熱海市
3 東伊豆町
4 小山町
5 河津町
6 函南町
7 伊豆市
8 下田市
9 御殿場市
10 伊豆の国市



クオートとエスケープ

bashと同じように'と"がクオート文字で、ダブルクォートでは変数展開するがシングルクォートではしないも同じだけど、シングルクォート中のシングルクォートがちゃんとエスケープできる


(例)県名と自治体名のディレクトリを掘るスクリプトを出力してみる。

tr -d \" <localgovjp-utf8.csv | awk -F, '{print "mkdir -p \'"$2"/"$4"\'"}' | head | sed '1d'

mkdir -p '北海道/札幌市'
mkdir -p '北海道/札幌市 中央区'
mkdir -p '北海道/札幌市 北区'
mkdir -p '北海道/札幌市 東区'
mkdir -p '北海道/札幌市 白石区'
mkdir -p '北海道/札幌市 豊平区'
mkdir -p '北海道/札幌市 南区'
mkdir -p '北海道/札幌市 西区'
mkdir -p '北海道/札幌市 厚別区'



コマンド置換(プロセス置換)

fishのシンタックスには``,\$()相当の()しかない。

シェル芸的には<()を比較的よく使うと思うが、fishだとpsubを使う。


(例)前の問題の別解を考えてみて、結果が同じかどうか差分をとってみる。

$ diff (tr -d \" <localgovjp-utf8.csv | awk -F, '{print "mkdir -p \'"$2"/"$4"\'"}' | sed '1d' |psub) (tr -d \" <localgovjp-utf8.csv | awk -F, '{print $2,$4}' | sed '1d' | tr \  / | xargs -I@ echo mkdir -p \'@\' | psub) | head

2,11c2,11
< mkdir -p '北海道/札幌市 中央区'
< mkdir -p '北海道/札幌市 北区'
< mkdir -p '北海道/札幌市 東区'
< mkdir -p '北海道/札幌市 白石区'
< mkdir -p '北海道/札幌市 豊平区'
< mkdir -p '北海道/札幌市 南区'
< mkdir -p '北海道/札幌市 西区'
< mkdir -p '北海道/札幌市 厚別区'
< mkdir -p '北海道/札幌市 手稲区'


同じ結果にはなりませんでした。


微妙な違いだが$()と違って()は行頭に書けない。

$ (ls;ls)

fish: Illegal command name '(ls;ls)'



条件式

(追記)fish3.0から&&||及び!のシンタックスが導入されたのでbashと同様に書ける。and/or/notコマンドもまだ生きているので旧fish風にも書ける。

bashでいう&&,||にはand, or, notを使う(使っていた)。直前のコマンドの終了ステータスを見て実行可否を決定するコマンドという位置づけのようだ。

$ true; and echo success

success
$ false; or echo failed
failed
$ not false; and echo not success
not success

notはif文と組み合わせたほうがわかりやすそう。


ちゃんと三項演算子的な動きをする。

Bash

$ true && echo success || echo failed 

success
$ false && echo success || echo failed
failed

Fish

$ true; and echo success; or echo failed

success
$ false; and echo success; or echo failed
failed



制御構文

大きな違いはif,forなど構文ではなくコマンド扱いというところ。

あと制御構文内に変数スコープを導入するというのがある。

構文的にはdoがなくて必ずendで終わると思っておけば良い。


if/whileがコマンドの終了ステータスで判定するという基本的な考え方は変わらない。

if

$ if true;echo success;end

success


ありがちなwhile read lineも変わらず、readもちゃんと複数の変数に展開してくれる。

$ seq 10 20 | nl | while read x y;echo $x $y;end

1 10
2 11
3 12
4 13
5 14
6 15
7 16
8 17
9 18
10 19
11 20


forはfor in listの形式しかない。

とはいえfor ((i=0;i<10;i++))的なのはシェル芸ではあまりやらないが。

$ for x in foo bar; echo $x;end

foo
bar
$ ls
apple avocado banana cinnamon melon
$ for f in *; realpath $f | awk -F / '{print $5,$6}';end
fishtest apple
fishtest avocado
fishtest banana
fishtest cinnamon
fishtest melon


なおtest[はあるがbashでいう[[はない。

switchはフェイルスルーでないなど仕様が違うが、シェル芸ではほぼ使わないので省略。



変数

代入はx=1でなくset x 1

set x a b cのように複数の値を列挙すると配列になる。bashだとx=(a b c)

bashみたいに${array[@]}と書かなくても変数名だけで配列をその場に展開してくれるので楽。

配列内の個数を数えるにはcountコマンドを使う。bashだと${#array[@]}


bashの\$x[@]/\$x[*]の違いはfishでも同じように表現できるようだ。

まずbash。

$ x=(a "b c" d)

$ for y in "${x[@]}";do echo $y;done
a
b c
d
for y in "${x[*]}";do echo $y;done
a b c d


fishだとクォートするかどうかで動作が変わる。

$ set x a "b c" d

$ for y in $x;echo $y;end
a
b c
d
$ for y in "$x";echo $y;end
a b c d


シェル芸ではあまり関係ないけど特殊変数系が違う。終了ステータスは\$?ではなく$statusを使うこと。



ブレース展開(など)

ブレース展開に数値の範囲展開がない。bashだと{1..3}1 2 3になるが、代替は(seq 3)あたりか。そのかわりecho (seq 10 20)[5 10](14と19を返す)みたいにコマンド置換の結果を配列としてアクセスできたりする。


ただしbashと同じようにx{1,2,3}x1 x2 x3になるし{foo,bar,bazz}{1,2,3}はfoo1 foo2 foo3 bar1 bar2 bar3 bazz1 bazz2 bazz3になる。

fishのドキュメント的にはデカルト積とのこと。


seqだと{a..z}みたいなことはできないが、jotを使うとできるようだ。

$ echo (jot -c 26 a z 1)

a b c d e f g h i j k l m n o p q r s t u v w x y z


ブレース展開とかパラメータ展開の、未定義変数のデフォルト値とか初期化と同時に参照するみたいな便利機能はない。



数値計算

シェル芸でもあまり使わないけど、fishの場合はmath(bcのラッパーらしい→3.0から組み込みになった)とかexprを使うことになる。


bashだとletとか((expr))とかを使う。

存在しない変数は0扱いになるようだ。

$ a=1

$ let ++a
$ echo $a
2
$ echo hoge is "$hoge";((hoge++));echo hoge is "$hoge"
hoge is
hoge is 1


fishの場合は外部コマンドなのでよしなに。

$ set a 1

$ set a (math $a + 1)
$ echo $a
2



glob

基本的には変わらないもののzshのように**が使える。が、zshとも仕様が違うらしい。

ドキュメントを読んでくれ。


*の場合

$ for f in *; [ -d $f ]; and echo $f is dir; or echo $f is file; end

a is dir
apple is file
avocado is file
b is dir
banana is file
cinnamon is file
melon is file


**の場合

$ for f in **; [ -d $f ]; and echo $f is dir; or echo $f is file; end

a is dir
a/aaaa is file
apple is file
avocado is file
b is dir
b/bbbb is file
banana is file
cinnamon is file
melon is file


extglobがない。discussionを読むと「なんでそれが組み込みで必要なんだ」とか言っていて面白い。



リダイレクト

ファイルディスクリプタのアクセスは普通に&1のように&数字。標準エラーは^とも書ける。

bashで標準出力+標準エラーを別のプロセスに渡すときcommand |& commandになるが、fishだとcommand ^| commandになる。


(例)

shell-session

$ for f in *; [ -d $f ]; and echo $f is dir 1>&2 ; or echo $f is file; end >/dev/null ^| cat

a is dir

b is dir



複合コマンド

元記事を読むとわかるけど、bashだと{ foo;...;}ないし(foo;...)で複数のコマンドの結果をパイプで次のコマンドにまとめて渡せる。前者は同じプロセス、後者は子プロセスで動く。


(例)都道府県に新しい県を混ぜてみる。

$  { tr -d \" <localgovjp-utf8.csv | awk -F, '{print $2}' | uniq; echo ネオサイタマ県; } | tail

高知県
福岡県
佐賀県
長崎県
熊本県
大分県
宮崎県
鹿児島県
沖縄県
ネオサイタマ県


fishではbegin/endを使う。

$ begin;tr -d \" <localgovjp-utf8.csv | awk -F, '{print $2}' | uniq; echo ネオサイタマ県;end | tail

高知県
福岡県
佐賀県
長崎県
熊本県
大分県
宮崎県
鹿児島県
沖縄県
ネオサイタマ県




  • 組み込みのコロン:コマンドがない。地味に不便な気が。代案はtrueですかね。

  • 組み込みのドット.コマンドがない。source使えとのこと。

  • !$がない

TODO: 気づいたら追記する



参考資料

公式サイトとhelpを見るのが当然良いと思うが、るびきちさんがfishの公式ドキュメントを和訳してるので助かる。

hyperpolyglotシェル全体の機能を比較した記事。これの文量が多すぎるので抽出したものがほしいというのもこれを書いた動機だったりする。



練習

シェル芸勉強会の模範回答をfishで書き直してみようということで、第9回を抜粋してやってみる。

第1問、ファイルをディレクトリ掘って移動する問題。

変数を直接文字列操作するシンタックスがないのではやくもつらい。


bashだと

$ ls | while read f ; do mkdir -p "${f:0:1}" ; mv $f "${f:0:1}" ; done


が、fishだとdo,endを直し、変数の1文字目はstringで取得する。

$ ls | while read f ; mkdir -p (string sub -l1 $f) ; mv $f (string sub -l1 $f) ; end



2問目、bash

$ ls | while read f ; do mv "$f" "$(echo $f | sed 's/ /_/g')" ; done


fishだとdo/doneを直し、文字置換はsedでもtrでもいいけど、fishで完結するならstring replaceがよいのでは。スペースを含む文字列でもクオートがいらない。

$ ls | while read f ; mv $f (string replace \  _ $f) ;end


3問目、bash

$  d=20140101 ;

while [ $d -lt 20150101 ] ;
do echo $d; d=$(gdate -d "$d 1 day" +%Y%m%d) ; done |
while read d ; do gdate -d $d > $d ; done


fish。do/doneを置換。代入はset。testコマンドはそのままでOK。コマンド置換は$()()

$ set d 20140101 ;

while [ $d -lt 20150101 ] ; echo $d; set d (gdate -d "$d 1 day" +%Y%m%d) ; end | while read d ; gdate -d $d > $d ; end


一応psubの問題もやっておきたいので、20回のQ3。元ファイルを作るところから。bashだと

$ for x in 1 4 2 9 5 8;do echo $x;done > Q3

$ paste <(awk '$1%2' Q3 | sort) <(awk '$1%2==0' Q3 | sort -r) | tr "\t" ' '
1 8
5 4
9 2


fish。forのdo/doneendに。<()(...|psub)に。trのエスケープが不要

なのは、エスケープの展開がパースの後なのだろうか。

$ for x in 1 4 2 9 5 8; echo $x;end > Q3

$ paste (awk '$1%2' Q3 | sort|psub) (awk '$1%2==0' Q3 | sort -r|psub ) | tr \t ' '
1 8
5 4
9 2



感想文


  • そんなに違わないような、結構違うような微妙な感じ。

  • クオートなどfishのほうが優れている点もあるので問題によってはbashより便利かも。

  • デフォルトインストールで普段遣いのシェルとして普通に便利なのは確か。

  • 仕事だとコマンド実行履歴をドキュメントに残す時があり、その場合はちょっとよくないかもしれない。