基礎コマンド

echo

適当なことを表示できます。

echo hello
# hello

# 環境変数の中を見たり
echo $HOME
# /Users/hoge

less

ファイルの中身をじっくり見る。デフォルトだと監視モード(追加データがきた時に即表示)で立ち上がります。

less +F -N log.txt
# 1 foo
# 2 bar
# 3 baz
# :

監視モード時にctrl+cを押すことでファイルを読むモードになります。vimのようなキーバインドが効くので、例えばggと入力するとファイルの一番から内容を見ることができます。

  • j 1行下に
  • k 1行上に

監視モードに戻りたくなったら再度F(shift+f)を入力します。

qで終わります。

pwd

現在いるディレクトリを表示

pwd
# /Users/hoge

mkdir

ディレクトリを作る。以下はfoo/ディレクトリを作りその中にさらにbar/ディレクトリを作っています。

mkdir -p foo/bar

cd

ディレクトリ移動に使います。

  • ..は1つ上のディレクトリを
  • ~$HOMEディレクトリを
  • /から始めると一番トップ(ルートディレクトリ)

をそれぞれ指します。

pwd
# /Users/hoge

cd foo/bar
pwd
# /Users/hoge/foo/bar

cd ../..
pwd
# /Users/hoge

cd ~/foo
pwd
# /Users/hoge/foo

cd /Users
pwd
# /Users

ls

現在のディレクトリにあるディレクトリやファイルを一覧する。頭がdから始まるものがディレクトリです。

ls -al
# total 0
# drwxr-xr-x    4 nju33  staff   128 Feb  5 21:30 .
# drwxr-xr-x  109 nju33  staff  3488 Feb  5 21:30 ..
# -rw-r--r--    1 nju33  staff     0 Feb  5 21:30 index.js
# drwxr-xr-x    2 nju33  staff    64 Feb  5 21:30 node_modules

cp

ファイルやディレクトリをコピーします。

ls -al
# total 0
# drwxr-xr-x    2 nju33  staff    64 Feb  5 21:30 foo

cp -r foo bar
ls -al
# total 0
# drwxr-xr-x    2 nju33  staff    64 Feb  5 21:30 foo
# drwxr-xr-x    2 nju33  staff    64 Feb  5 21:31 bar

mv

ファイルやディレクトリを移動します。

ls -al
# total 0
# drwxr-xr-x    2 nju33  staff    64 Feb  5 21:30 foo

mv foo bar
ls -al
# total 0
# drwxr-xr-x    2 nju33  staff    64 Feb  5 21:30 bar

rm

ファイルやディレクトリを削除します。

ls -al
# drwxr-xr-x    4 nju33  staff   128 Feb  5 21:30 .
# drwxr-xr-x  109 nju33  staff  3488 Feb  5 21:30 ..
# -rw-r--r--    1 nju33  staff     0 Feb  5 21:30 index.js
# drwxr-xr-x    2 nju33  staff    64 Feb  5 21:30 node_modules

rm index.js
# ディレクトリ削除には -r が必要
rm -r node_modules

ls -al
# drwxr-xr-x    4 nju33  staff   128 Feb  5 21:30 .
# drwxr-xr-x  109 nju33  staff  3488 Feb  5 21:30 ..

grep

絞り込み。

ls -al                                                                                   
# total 0
# drwxr-xr-x    3 nju33  staff    96 Feb  5 21:41 .
# drwxr-xr-x  109 nju33  staff  3488 Feb  5 21:30 ..
# -rw-r--r--    1 nju33  staff     0 Feb  5 21:30 index.js

ls -al | grep index.js
ls -al                                                                                   
# -rw-r--r--    1 nju33  staff     0 Feb  5 21:30 index.js

cat

ファイルの中身を標準出力に出す。

cat index.js
# function hello() {
#   console.log(123);
# }
cat index.js | grep console.log
#   console.log(123);

zip ファイルを作る

-rの後に生成したい zip ファイル名が来るので注意。

zip -r name.zip ../dir-name

zip ファイルを展開する

指定した ZIP ファイルを展開します。-d <dest-dir>オプションを使うと展開先のディレクトリを<dest-dir>にできます。

unzip name.zip
unzip name.zip -d foo # foo/ に展開

pbcopy

使えるのは、 Mac だけです。標準入力できた内容をクリップボードにコピーします。

echo hello | pbcopy
# 上のあとペーストすると
hello

条件分岐

基本シンタックス

条件を[ ]で囲むか、testの後に条件を書きます。

if [ "a" = "a" ]
  then
    echo 1
  else
    echo 2
fi
# 1

改行が必要なところに;を置くことで1行で書くこともできます。

if test "a" = "a"; then echo 1; else echo 2; fi
# 1

[ ! ... ]test !のように!を付けると条件の結果が反対になります。

文字列の比較

同じ文字列

上記で使ったように=で文字列同士が等しいかチェックできます。左右の"a""a"は等しいので1が表示されます。

if test "a" = "a"; then echo 1; fi
# 1

違う文字列

違うかをチェックしたい場合は!=を使います。

if test "a" != "b"; then echo 2; fi
# 2

空文字列

空文字列('')かどうか調べるときは-n(non zero length)または-z(zero length)オプションを使います。

if test -z ''; then echo 1; fi
# 1

if test ! -n ''; then echo 1; fi
# 1

if test -n 'a'; then echo 1; fi
# 1

### 数値の比較

数値の比較では`=`は使いません。代わりに以下のようなフラグを使います。

- `-eq` 左と右が同じ数値か
- `-ne` 左と右が違う数値か
- `-lt` 左のほうが小さい
- `-le` 左は右以下
- `-gt` 右のほうが大きい
- `-ge` 右は左以上

```shell
if test 1 -eq 1; then echo 1; fi
# 1

if test 1 -ne 2; then echo 1; fi
# 1

if test 1 -lt 2; then echo 1; fi
# 1

if test 1 -le 2; then echo 1; fi
# 1

if test 2 -gt 1; then echo 1; fi
# 1

if test 2 -ge 1; then echo 1; fi
# 1

シェルスクリプトファイルの作り方

1. ファイル名

.shという拡張子を付けます。

2. 頭にshebang(シバン)コメントを付ける

#!で始まるコメントのことです。#!/bin/shと1行目に書きます。

#!/bin/sh

echo 1

これをecho.shと名付けて保存したとします。

nodejsなら#!/usr/bin/env nodeだったり色々あります。

3. 実行権限を与える

chmod +x echo.shを実行します。

4. 使う

以下のように使います。

./echo.sh
# 1

オプション付きのシェルスクリプトを書く

Mac のgetoptsではロングオプションが使えないみたいなので、そういうの使いたい場合は他の言語つかいましょう。

getopts

-a-bなどハイフンで始まる cli オプションを解析してくれるコマンドです

while getopts <条件> <変数名>; do ...; doneという書き方で書けて、すべてのオプションを繰り返し処理できます。また、このブロックの中では$OPTARGという名前でオプションの値を使うことができます。

条件

オプションでどの文字を使うか設定します。aと書くと-a単体で使うオプションだと設定でき、a:とコロンを置くと-a fooのようにオプション値が必要なオプションだと設定できます。

#!/bin/sh

show_list() {
  # 何か
  return 0
}

show_help() {
  echo 'command'
  echo '  何かコマンド'
  echo ''
  echo 'options'
  echo '  -l <arg>  一覧を表示'
  echo '  -h        ヘルプを表示'
  return 0
}

while getopts hl: option
do
  case "$option" in
    "l" ) show_list $OPTARG ;;
    "h" ) show_help ;;
  esac
done

Linux で fish shell を使う

以下をやります。

  1. ユーザーを作る
  2. Linuxbrew を入れる
  3. fish shell を入れる

ちなみにマシンはAmazonLinux2です。

ユーザーを作る

自分(ここではnju33)ユーザーを作ります。パスワードも設定しておきます。

sudo useradd -G wheel nju33
sudo passwd nju33

そして、sudoが使えるようにvisudoで以下の行のコメントを外しておきます。(#を消す)

# %wheel        ALL=(ALL)       NOPASSWD: ALL

Linuxbrew を入れる

公式サイトにかかれている以下のインストールコマンドを実行します。

sh -c "$(curl -fsSL https://raw.githubusercontent.com/Linuxbrew/install/master/install.sh)"
# ==> This script will install:
# /home/linuxbrew/.linuxbrew/bin/brew
# /home/linuxbrew/.linuxbrew/share/doc/homebrew
# ...

おそらく/home/linuxbrewができたと思います。今のところ/home/linuxbrew/.linuxbrew/bin/brewbrewコマンドを使います。

fish shell を入れる

以下でfishをインストールします。

/home/linuxbrew/.linuxbrew/bin/brew instaill fish
# ==> Installing dependencies for hello: patchelf, zlib, binutils, linux-headers, glibc, m4, gmp, mpfr, libmpc, isl@0.18 and gcc
# ==> Installing hello dependency: patchelf

そしてfishを使いたいユーザーの~/.config/fish/fish.configを以下の内容にします。まず~/.config/fish/を作ります。

mkdir -p ~/.config/fish

そしてviなどで以下のようにファイルを作ります。これでbrewとそれを使ってインストールしたものへのパスが通り使えるようになります。

set -x HOMEBREW_PREFIX /home/linuxbrew/.linuxbrew
set -x HOMEBREW_CELLAR /home/linuxbrew/.linuxbrew/Cellar
set -x HOMEBREW_REPOSITORY /home/linuxbrew/.linuxbrew/Homebrew
set -x PATH /home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin $PATH
set -x MANPATH /home/linuxbrew/.linuxbrew/share/man $MANPATH
set -x INFOPATH /home/linuxbrew/.linuxbrew/share/info $INFOPATH

デフォルトシェルを fish shell にする

そのユーザーでログインした時にfishで始められるようにします。それにはまず、/etc/shellsfishbinまでのパスを追加してあげます。

sudo viなどで/etc/shellsを開いて、以下の一行を追加します。

/home/linuxbrew/.linuxbrew/bin/fish

chsh -lで上の一行が増えていれば大丈夫です。

chsh -s [chsh -l 結果のどれか]でそのユーザーのデフォルトシェルを切り替えられます。ではfishに変えます。

chsh -s /home/linuxbrew/.linuxbrew/bin/fish
# Changing shell for nju33.
# Password: 
# Shell changed.

ここまで終わったらログインし直します。

おまけ oh-my-fish を入れる

ログインし直すと、brewfishなどが使えるようになっていると思います。(もし、使えない場合は.config/fish/config.fishを確認してください)

fishが使えるのでoh-my-fish推奨の以下でただ入れることができます。

curl -L https://get.oh-my.fish | fish

僕が好きなedenテーマも入れます。

omf install eden
# Updating https://github.com/oh-my-fish/packages-main master... Done!
# Installing package eden
# ✔ eden successfully installed.

デフォルトシェルを変更する

shellbin自体は何らかの方法で取得済みという状態だとします。ここの例ではfishをデフォルトシェルに変更してみます。

/etc/shellsへ追記

そのbinへのパスを/etc/shellsへ追記します。
ここではそのパスは/home/linuxbrew/.linuxbrew/bin/fishになるのでこれを追記します。

sudo vi /etc/shells
# /home/linuxbrew/.linuxbrew/bin/fish と追記

追記内容の確認

以下で確認できます。

chsh -l
# /bin/sh
# /bin/bash
# /sbin/nologin
# /bin/dash
# /home/linuxbrew/.linuxbrew/bin/fish

シェルの変更

chsh -l [切り替えたいシェルの bin] を実行します。binchsh -lからコピペするといいと思います。
fishへ切り替えます。

chsh -l /home/linuxbrew/.linuxbrew/bin/fish

再ログイン

一度そのユーザーから離れて再ログインするとfishで入れると思います。

ユーザーの作成と削除

デフォルト値の確認

-Dを付けて実行すると、対象のオプション(ホームディレクトリ設定)などを指定しなかった場合、何が設定されるのかを確認できます。

useradd -D
# GROUP=100
# HOME=/home
# INACTIVE=-1
# EXPIRE=
# SHELL=/bin/sh
# SKEL=/etc/skel
# CREATE_MAIL_SPOOL=no

例えば、ホームディレクトリを変えたい場合は-m-dオプションを、デフォルトシェルを変えたい場合は-sオプションを作る時に指定してあげます。

ユーザーを作る

sudo useradd <user-name>

# -m で ホームディレクトリ $HOME/<user-name> を作り
# -s で デフォルトシェル /usr/bin/fish を指定
# sudo useradd <user-name> -m -s /usr/bin/fish

以下のコマンドで表示されれば追加できていると確認できます。

ユーザー確認

cat /etc/passwd | grep <user-name>

ここで「ユーザー名間違えてた」って時は、

sudo usermod -l <current-name> <new-name>

で変更できます。もし、ホームディレクトリも作ってしまっているなら、

sudo usermod -l ... -d /home/<new-name> -m

上記のように一緒に-d-mを付けることで変更できます。

sudoer にする

/etc/sudoers/<user-name>を作ります。

<user-name> ALL=(ALL) ALL

上だとsudoを付けた時に毎回パスワードを入力する必要がある設定です。パスワードを省きたい場合は以下も追記します。

<user-name> ALL=(ALL) NOPASSWD:ALL

ちなみにこのファイルは、/etc/sudoersにこう書かれていることでsudoers.d内に作ったファイルが読み込まれるようです。

#includedir /etc/sudoers.d

ユーザーのパスワードを設定する

sudo passwd <user-name>
# Changing password for user test.
# New password: ***
# Retype new password: ***

シェルスクリプトなどで標準入力などで渡したい場合は次のように設定します。

echo <password> | passwd <user> --stdin

ubuntu ではpasswdの代わりにchpasswdを使います。

echo <user>:<password> | chpasswd

ユーザーを削除する

--remove -rを付けるとそのユーザーのホームディレクトリも削除します。

sudo userdel <user-name>
# sudo userdel --remove <user-name>

ssh できるようにする

ローカル側

まずはsshするときに使う鍵を作ります。これにはssh-keygenコマンドを使います。

# ~/.ssh/test/id_rsa に置きたいので作っておきます。
mkdir ~/.ssh/test

ssh-keygen -f ~/.ssh/test/id_rsa -N '' -t rsa -b 4096 -m PEM -C ''

ssh-keygenには5つオプションを渡していますが、これはそれぞれ

  • -fは鍵のベース名
  • -Nは鍵のパスワード
  • -tは鍵のタイプ
  • -bは鍵のサイズ
  • -mは鍵の形式(よく分かってません)
  • -Cは公開鍵の後ろに付くuser@host部分

を指定します。2019年2月ぐらいからこの-m PEMというオプションを付けないと-----BEGIN OPENSSH PRIVATE KEY-----と出力されてしまいうまくsshできないようなので付けるように覚えないと駄目みたいです。
ちなみにこのオプションを付けるとそれ以前と同じように-----END RSA PRIVATE KEY-----で出力されます。

これで~/.sshディレクトリにmy-key(秘密鍵)とmy-key.pub(公開鍵)ができたはずです。

そして、その公開鍵をコピーします。

cat ~/.ssh/test/id_rsa.pub | tr -d '\n' | pbcopy 

tr -d '\n'で最後の改行を消す必要があります。(ハマった)

リモート側

使いたいユーザーの~/.ssh/authorized_keysにコピーしたものを貼り付けます。

# .ssh ディレクトリを作る
mkdir ~/.ssh
# パーミッションを 600(自分だけが読み書き実行できる) に
chmod 700 ~/.ssh

# authorized_keys を作る
touch ~/.ssh/authorized_key
# パーミッションを 600(自分だけが読み書きできる) に
chmod 600 ~/.ssh/authorized_key

接続できるか確認

以下のようなコマンドで接続できれば完了です。

ssh -i ~/.ssh/test/id_rsa <user>@<host>
# [<user>@ip-<host> ~]$

接続できない時のチェック項目

リモート

  1. "$HOME/.ssh"のパーミッションが700
  2. "$HOME/authorized_keysのパーミッションが600
  3. authorized_keysは空白行で終わってるか(<id_rsa.pubの中身>\nみたいな形)

ローカル

  1. 秘密鍵のパーミッションが600または400

ローカルの鍵情報をリモートでも引き継ぐ

GitHubへ普段からローカルでsshを使った方法でpushしたりcloneしている場合、リモート先からだと秘密鍵がないのでそういったことができなくなります。

そういうときはssh-addコマンドを使います。以下で登録できます。

ssh-add <リモートで必要な秘密鍵へのパス>

ssh-add -l
# 登録できていればここにそれが出る

上記だと再起動した後リセットされてしまいますが-Kで登録することでこれを回避できます。

ssh-add -K <リモートで必要な秘密鍵へのパス>

接続する

ssh時に-Aオプションを付けてあげるだけです。

ssh -A ...

リモート先でもssh-add -lして出てくれば完璧です。

.ssh/config の設定

.ssh/configの設定でForwardAgent yesと設定することで-Aを指定してるのと同じ意味で接続できます。常に-A`したいようなマシンに接続したい場合はこの設定をすると良いです。

パスワードが必要な sudo だけど聞かれないようにする

そういう設定がされてないとsudoコマンドを実行した時にパスワードを入力しないといけません。

sudo echo 1
# Password:

普通にターミナルでそれだけ実行する程度ならそれでも特に問題ないのですが、シェルスクリプトなどで処理の流れを止めたくない時にそれはちょっと困ります。

sudo -S

そんなときは-Sまたは--stdinオプションを使います。これでパスワードが標準入力で渡すことができます。

-S, --stdin read password from standard input

例えばこんな感じです。

echo 'passwd' | sudo -S echo 1 2>/dev/null
# 1

2>/dev/nullは、sudoを実行したときのPassword:という標準エラー出力のテキストが表示されないように/dev/nullに捨てています。

fish shell でバージョン Bump する function

リポジトリはnju33/fish-age

インストール

以下でage(読み「あげ」)をインストールします。

mkdir -p ~/.config/fish/{functions,completions}
curl -o ~/.config/fish/functions/age.fish https://raw.githubusercontent.com/nju33/fish-age/master/functions/age.fish
curl -o ~/.config/fish/completions/age.fish https://raw.githubusercontent.com/nju33/fish-age/master/completions/age.fish

ここまで終わったらfishを再読込します。

使い方

バージョンを直接書く

バージョンを書いた後に--patch,--minor,--majorのどれかを指定します。ちなみに、

  • --patchは最後の数値をインクリメント
  • --minorは真ん中の数値をインクリメント
  • --majorは最初の数値をインクリメント

するという意味になります。

age v1.2.3 --patch
# v1.2.4

age v1.2.3 --minor
# v1.3.0

age v1.2.3 --major
# v2.0.0

gitのタグからバージョンを自動取得

バージョンを記した Git タグがある場合、コマンドのバージョンを省略するとタグから最新のバージョンを取得し、指定したオプションに合わせて Bump したバージョンを表示します。

# 以下のようなタグの状態のとき
git tag
# v1.1.0
# v1.2.0
# v1.2.1
# v1.2.2
# v1.2.3

age --patch
# v1.2.4

age --minor
# v1.3.0

age --major
# v2.0.0

sed で n行目以降だけ表示

sed -n 2pとかすると1行目だけ取得できますが、2行目以降だけ取得したい場合はどうしたらいいだろうということでした。

結論としてはこのように実行するでした。

sed -n '2,$p'

$pというのは最後の行を指すようです。つまり2行目から最後の行というような意味になるんでしょうか。

パイプ処理の進歩を表

これはpvコマンドに依存します。Mac に人は Homebrew を使って以下でインストールできます。

brew install pv

pv コマンド

pvコマンドはcatのようにファイルの内容を表示することができます。そしてそれを別コマンドでパイプ(標準入力)で渡すことで、どれだけ処理が進んだかをプログレスで表示してくれます。

MySql のリストアの進歩を表示

以下のようにするとa.dumpのリストアの進歩がどれくらいか見ることができます。

pv a.dump | mysql --defaults-extra-file=db.cnf db_name
# 400MiB 0:02:18 [2.89MiB/s] [=========================================>] 100%

tree コマンド

treeコマンドは誰かにディレクトリ・ファイル構造を見せて説明したりする時に便利なコマンドです。

コマンドの使い方を見る前に以下で現在のワーキングディレクトリをベースに適当な構造を作ります。

mkdir -p {a,b}/c
touch {a,b}/c/d.{txt,log,js,css,html}

では構造を表示してみます。ワーキングディレクトリをベースとする場合、単にtreeと実行します。

tree
# .
# ├── a
# │   └── c
# │       ├── d.css
# │       ├── d.html
# │       ├── d.js
# │       ├── d.log
# │       └── d.txt
# └── b
#     └── c
#         ├── d.css
#         ├── d.html
#         ├── d.js
#         ├── d.log
#         └── d.txt

もしa/cをベースで表示したい場合はtree a/cとコマンドの後に起きます。

tree a/c
# a/c
# ├── d.css
# ├── d.html
# ├── d.js
# ├── d.log
# └── d.txt

treeには他にフィルターのようなオプションがいくつかあります。

フィルターオプション

-P

-P <pattern>オプションを使うと、パターンにマッチしたファイル名以外は非表示にできます。

tree -P '*.js|*.css' a/c  
# a/c
# ├── d.css
# └── d.js

-I

-I <pattern>-Pの逆で、マッチしたものが非表示になります。

tree -I '*.js|*.css' a/c
# a/c
# ├── d.html
# ├── d.log
# └── d.txt

-L

-L <number>を使うと何階層目まで表示するかを確認します。

tree -L 1
# .
# ├── a
# └── b

-L 1で1階層目だけなのでabが表示されます。

プロセス置換: <(...) & >(...)

プロセス置換という技を使うと、本来ファイルを指定して実行してその中身を使うようなコマンドで、直接文字列を渡すといったことができるようになります。<(...)が入力、>(...)が出力向きになります。

🤔色々と憶測ですが、僕的にはこれは戻り値がファイルのように扱えるワンライナーな関数みたいなイメージでかなと思ってます。

まずLinux 上でstat -c%N /dev/fd/*でどんな/dev/fs/*があるか見ることができます。例えばその中には'/dev/fd/0' -> '/dev/pts/9'というのがあるんですが、この時ターミナル1で、

cat /dev/fd/0

とし、ターミナル2でecho 123 > /dev/pts/9を実行するとターミナル1に123と流せるというような仕組みがあります。echo >(echo)'などと実行する時も/dev/fd/63という結果がでてくるので、どこかからの標準出力が/dev/fd/63に送られてきたりしてるんだろうなと。

これがファイルの代わりとして扱えるのは/dev/fd/63が実際にコマンドが生きてる間存在するファイルか何かなんだと思います。cat /dev/nullが使えるように、(内容が渡ってくる点だけ異なる)ファイルであるならファイルを期待する箇所におけるのはおかしくないかなと。

(⚠️この機能はshでは使えない為、bashを使う必要があります。)

例えばファイルを期待するコマンドの有名なものにcatがあります。これはcat <file-path>でそのファイルパスの内容を表示するコマンドですが、プロセス置換を使うと、

bash -c 'cat <(echo -n 123)'
# 123

<(...)の中の実行結果を表示することができます。この使い方ではechoと変わらないですが、例えばbase64コマンドなどだと少し便利さを感じられます。(これもbase64 <file-path>を期待)

bash -c 'base64 <(echo -n username:password)'
# dXNlcm5hbWU6cGFzc3dvcmQ=

またdiffなどもその類です。

bash -c 'diff <(echo aaa) <(echo abb)'
# 1c1
# < aaa
# ---
# > abb

>(...)にすると前のコマンドの結果をその中のコマンドに続けて渡すことができます。

bash -c 'echo 123 > >(cat)'
# 123

使われてるポートを調べる・削除する

使われてるポートを調べる

localhost8000から8100までのポートを調べるには以下のように実行します。

# nc -zv <host> <start>-<end>
nc -zv localhost 8000-8100

<start>-<end>には範囲を指定し3000から4000までの範囲を調べたいなら、3000-4000のようにします。

実行すると、範囲内のポートがどうだったかログが出力され、そのポートが閉じてる場合は、

nc: connectx to localhost port 8080 (tcp) failed: Connection refused

のように出力され開いてた場合は、

found 0 associations
found 1 connections:
     1: flags=82<CONNECTED,PREFERRED>
        outif lo0
        src ::1 port 53010
        dst ::1 port 8080
        rank info not available
        TCP aux info available

Connection to localhost port 8080 [tcp/http-alt] succeeded!

のように出力されます。

またすべてがfailedの場合、このコマンドの終了ステータスは1になり、1つでもsucceededの場合は0になります。

ポートのプロセスを終了する

上記では8080番がsucceededとなりました。そのポートの内容を終了させたい場合、まずポートを使ってるプロセスの PID を以下のように調べます。

# lsof -i:<port>
lsof -i:8080
# COMMAND     PID  USER   FD   TYPE            DEVICE SIZE/OFF NODE NAME
# com.docke 96307 nju33   43u  IPv6 0x5e43f653a65638b      0t0  TCP *:http-alt (LISTEN)

この結果のPIDの列の番号が終了させるのに必要な番号です。後はその番号をkillコマンド実行時に渡します。

kill -9 96307

Bash などの場合は、これは一行で行えます。

kill -9 $(lsof -t -i:8080)

-tにより プロセスID だけを取得できるので上のコマンドは以下と同じになります。

kill -9 96307

tar コマンドによる圧縮と展開

tarは、アーカイブファイルを作るためのコマンドです。

アーカイブを作る為の cf オプション

アーカイブファイルは-cfオプションの指定で作れます。それぞれ以下の意味があります。

  1. -c 新たにアーカイブファイルを作ります
  2. -f ファイル名は...です。

-fは値を取るオプションで、これに渡したファイル名でアーカイブファイルが作られます。ファイル名には.tarという拡張子を付けることが一般的です。

以下は作成例です。

tar -cf document.tar document

最後にアーカイブに含めたいディレクトリやファイルを続けます。これは複数指定でき、その場合はdocument html foo.ymlのように渡します。

アーカイブの中身を確認する為の tf オプション

上記セクションで作成したdocument.tarにちゃんと特定のファイルが含まれてるか確認したい場合は、-tfを使います。さらに-vを追加するとlsの結果ような形で一覧できます。

  1. -t アーカイブファイルを一覧する
  2. -v 詳しく表示

以下は確認例です。

tar -tf document.tar
# document/
# document/1.md

アーカイブに含めるファイルを絞る

「しまった、アレ含ませたくなかった!」なんて場合は--excludeオプションで含ませたくないディレクトリやファイルのパターンを渡します。

tar -f document.tar --exclude document/1.md

tar -tf document.tar
# document/

複数のパターンがある場合は、その数だけ--excludeを指定する必要があります。

tar -cf archive.tar --exclude .git/ --exclude .DS_Store .

tar tf archive.tar
# .git/ と .DS_Store を除いた現在のディレクトリ以下の
# すべてのディレクトリとファイル

アーカイブにファイルを追加する rf オプション

「しまった、アレ指定し忘れた!」なんて場合は-rfで後から追加できます。

  1. -r アーカイブに指定ディレクトリやファイルを追加

以下は追加例です。

mkdir html
touch html/1.html

tar -rf document.tar html

tar -tf document.tar
# document/
# document/1.md
# html/
# html/1.html

ちなみに作り直しても大丈夫です。

圧縮アーカイブファイルを czf, cjf オプション

targzipbzip2による圧縮を行えます。これらを使ったアーカイブファイルの拡張子は一般的にそれぞれtar.gztar.bz2になります。

オプションに出てきたzjはそれぞれ以下のような意味です。

  1. -z gzipで圧縮する
  2. -j bzip2で圧縮する

以下例です。

# gzip で
tar czf document.tar.gz document

# bzip2 で
tar cjf document.tar.bz2 document

違いはbzip2の方がアーカイブファイルサイズを小さくできますが、圧縮時間も倍以上掛かるようです。

アーカイブを展開する為の xf オプション

上記セクションで作成したdocument.tarに展開したい場合は、-xfを使います。

  1. -x アーカイブファイルからファイルを取り出す
tar -xf document.tar

後にアーカイブ内のあるファイルへのファイルパスを指定することで、それだけを取り出す事もできます。

tar -xf document.tar document/1.md

find document
# document
# document/1.md

取り出すのでは無く中身だけ見る

-xOfとして渡すことで、取り出すさずその中身を見れます。

tar -xOf document.tar document/1.md
# 1.md の中身

オプションについて

tarコマンドでは最初の短いオプション(cfなど)の塊に限って-を省略できます。つまり-cf-tfの代わりにcftfと使うことができます。

tar cf document.tar document
tar tf document.tar
tar xf document.tar document

zip で圧縮 unzip で解凍

.zip拡張子は Windows などで親しまれている圧縮形式です。また.zipで提出を期待されるような Web サービスも多いです。

圧縮

.zip拡張子のファイルを作成するにはzipコマンドを使います。

zip archive.zip 1.txt

ディレクトリを.zip化したい場合は-rオプションを指定します。以下はdir1/ディレクトリ以下をすべて含めます。

zip archive.zip dir1

含めたいディレクトリやファイルは複数指定できます。

zip -r archive.zip dir1 dir2 file1.txt file2.txt

含めたくないファイルがある場合は、-x,--excludeオプションを一緒に使います。

# dir1/.DS_Store dir2/.DS_Store は無視
zip -r archive.zip dir1 dir2 --exclude */.DS_Store 

一覧と解凍

unzipを使います。

-lオプションで指定.zipの中身を一覧できます。

unzip -l archive.zip
#   Length      Date    Time    Name
# ---------  ---------- -----   ----
#         0  04-19-2020 09:39   document/
#         0  04-19-2020 07:16   document/1.md

解凍したい場合は単に引数に対象の.zipへのパスを渡すだけです。

unzip archive.zip

trap フック

trapコマンドは「終了時」「エラー時」といったタイミングに処理したい事を設定できるコマンドです。例えば、シェルスクリプトの中で一時的ファイルを作成していて、最後にそれを忘れずに削除したいような場合に使うことがあります。

以下のコードは、「何十・何百行の処理」が終わった後にclean_files関数が呼ばれ一時的なファイルが削除される、という例です。

clean_files() {
  rm tempfile
}

trap 'clean_files' EXIT

# ...何十・何百行の処理

このEXITというのがフック対象の名前です。これには以下のものがあります。

  • EXIT シェルスクリプト終了時
  • ERR エラーキャッチ時
  • SIGHUP ターミナルの切断
  • SIGINT ctrl + c で中断された時
  • SIGTERM kill される時

などがあります。3 つ目からはkill -lで確認できるものが指定できます。

kill -l
#  1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
#  5) SIGTRAP      6) SIGABRT      7) SIGEMT       8) SIGFPE
#  9) SIGKILL     10) SIGBUS      11) SIGSEGV     12) SIGSYS
# 13) SIGPIPE     14) SIGALRM     15) SIGTERM     16) SIGURG
# 17) SIGSTOP     18) SIGTSTP     19) SIGCONT     20) SIGCHLD
# 21) SIGTTIN     22) SIGTTOU     23) SIGIO       24) SIGXCPU
# 25) SIGXFSZ     26) SIGVTALRM   27) SIGPROF     28) SIGWINCH
# 29) SIGINFO     30) SIGUSR1     31) SIGUSR2

呼ばれる順番はSIG*系の後、ERREXITの順番で呼ばれます。

トラップのリセット

「ここに到達したらトラップを解除したい」という場合、trap - <trap_name>を実行します。

trap - EXIT

ERR トラップ

ERRは実行コマンドの終了ステータスが0以外、または関数の終了ステータスが0以外の時にっ呼ばれます。

以下のようなスクリプトではerrorと 2 回表示されます。②回

trap 'echo error' ERR

return_err()   # 存在しないファイルを rm しようとする
{
 m ... || r return 1
}

# 存在しないファイルを rm しようとする
rm ...
return_err
```ややこしいですが、`ERR`トラップは`EXIT`トラップのコマンドや関数が`0`以外の終了ステータスで終わった時にも呼ばれます。以下の場合も`error`と表示されます。

```bash
f() {
  return 1
}

trap 'f' EXIT
trap 'echo error' ERR

同じ処理をするトラップ

フックしたい処理が同じならトラップを複数羅列することで、複数のタイミングで同じ処理ができます。

trap 'echo ...' SIGHUP SIGINT SIGTERM  

gpg コマンド

対称暗号(共通鍵暗号)方式でファイルの暗号化と復号

暗号化には-c,--symmetricオプションを指定します。そして暗号化したいファイルをパラメーターとして渡します。
暗号化のアルゴリズムはデフォルトでAES128になります。これを変えたい場合は--cipher-algoオプションを使います。

例えば周りに中身を隠したいsecrets.jsonファイルを暗号化するには以下のようなコマンドを実行します。

# cat password
# foo

cat password \
| gpg --passfrase-fd 0 --batch --yes --symmetric --cipher-algo AES256 secrets.json

いくつか追加のオプションが出てきました。

--passfrase-fd 0は、標準入力からパスフレーズを読み込む為のオプションです。passwordファイルにパスフレーズを記載して、そこからcatで標準入力へ渡します。

--passfrase-fd--batchと共に使う必要がありますが、[y/N]と聞かれるようなユーザーの入力待ちの状態にしてしまうとエラーになってしまうので、さらに--yesを追加して全てyesで通します。
[y/N]で聞かれるのは例えば、「ファイルを上書きしますか」などがあります。

ログに残っても大丈夫なら--passphrase $passphraseと直接パスフレーズを指定しても大丈夫です。

これがうまくいくとsecrets.json.pgpというファイルが作られます。

次にsecrets.json.pgpを復号してみます。(長いので 1 オプション毎改行します)

gpg \
  --passphrase="$LARGE_SECRET_PASSPHRASE" \
  --batch \
  --yes \
  --output secrets.json \
  --decrypt \
  secrets.json.gpg

--decryptは「復号」コマンドになります。復号する対象はパラメーターで渡します。--outputは復号した内容を写すファイルを指定できます。これを省略すると内容は標準出力されます。よって以下でも同じ事になります。

gpg \
  --passphrase="$LARGE_SECRET_PASSPHRASE" \
  --batch \
  --yes \
  --quiet \
  --decrypt \
  secrets.json.gpg > secrets.json

--output--quietになりました。デフォルトでは以下のようなログが出る為、リダイレクトで写す為に、それらを出ないようにします。

# gpg: AES256 encrypted data
# gpg: encrypted with 1 passphrase

キーペアを生成

--full-generate-keyオプションを使います。このオプションを指定するとインタラクティブに作成できます。例えば、GitHub アカウントに登録する GPG キーなら、鍵の種類を RSA and RSA、サイズを4096にする必要があります。

正常に作成できたら以下を実行します。

gpg --list-secret-keys --keyid-format LONG
# ...
# sec   rsa4096/CAE49ABA5B7E19AB 2020-04-19 [SC]
# ...

出力結果にsecから始まる行があるので探し、この例であればCAE49ABA5B7E19ABに当たる部分をコピーします。そして「コピーした内容」を次のコマンドに渡します。うまくいけば公開鍵が表示されます。

gpg --armor --export <コピーした内容>
# -----BEGIN PGP PUBLIC KEY BLOCK-----
# ...
# -----END PGP PUBLIC KEY BLOCK-----

キーのエクスポートとインポート

以下で鍵を取り出し、

gpg --export-secret-key -a > secretkey.asc

以下のように取り込みます。

gpg --batch --import secretkey.asc

sshできる別のsshマシンにエクスポートする場合これは一行で書けます。

gpg --export-secret-key -a | ssh $machine gpg --batch --import -

ubuntu on WSL に ngrok を通して ssh する

ubuntu のバージョンは18.04を使います。また、ubuntu のインストールが終わって最初にターミナルを立ち上げた直後を想定してます。

cat /etc/lsb-release
# DISTRIB_ID=Ubuntu
# DISTRIB_RELEASE=18.04
# DISTRIB_CODENAME=bionic
# DISTRIB_DESCRIPTION="Ubuntu 18.04.4 LTS"

また ngrok pro アカウントなので、このアカウント上で以下を行ってます。異なるプランの場合は参考程度にしてください。
また、同じプランの場合でも、悩みながらやった為、無駄な過程が含まれてる可能性があります。

流れは以下の通りです。

  1. ngrok ダウンロード
  2. sshd 周り
  3. ssh 周り
  4. tcpトンネリング

1. ngrok ダウンロード

まず以下で ngrok バイナリが ZIP 化されたファイルをダウンロードしてきます。

curl -O https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
# ngrok-stable-linux-amd64.zip が作られる

ubuntu には最初この ZIP を解凍する為のコマンドが無いので、そのためのコマンドunzipをインストールします。

sudo apt update
sudo apt install unzip -y

先程のファイルを解凍します。

unzip ngrok-stable-linux-amd64.zip
ls 
# ngrok があるハズ

解凍すると同じディレクトリにngrokバイナリができてます。ngrokを使うために認証が必要なので以下で認証も行っておきます。

./ngrok authtoken <YOUR_AUTH_TOKEN>

2. sshd 周り

どうやらsshdを起動していないと、ssh時にssh_exchange_identification: Connection closed by remote hostというエラーに遭遇します。

これを回避する為にまず、sudo vim /etc/ssh/sshd_configで設定ファイルを開き以下の2行のコメントアウトと編集をします。(2行は続けてではなく離れてます)

ubuntu on WSL で作業します。

# TCPKeepAlive yes
# ClientAliveInterval 0

を以下に編集。

TCPKeepAlive yes
ClientAliveInterval 600

また以下で/etc/ssh/ssh_host_rsa_keyというファイルも作ります。

ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -C '' -b 4096 -t rsa -m PEM
# `/etc/ssh/ssh_host_rsa_key` と `/etc/ssh/ssh_host_rsa_key.pub` ができる

できたらsshdを起動します。

/etc/init.d/sshd start

3. ssh 周り

鍵でsshで接続できるようにする為authorized_keyなどを作ります。まだ ubuntu on WSL で作業します。

# `~/.ssh`ディレクトリ作成
mkdir ~/.ssh && chmod 700 ~/.ssh
# `~/.ssh/id_rsa`と`~/.ssh/id_rsa.pub`作成
ssh-keygen -f ~/.ssh/id_rsa -N '' -C '' -b 4096 -t rsa -m PEM
# 公開鍵で`~/.ssh/authorized_keys`を作る
cat ~/.ssh/id_rsa.pub > ~/.ssh/authorized_keys \
&& chmod 600 ~/.ssh/authorized_keys

ここまで済んだら、以下のコマンドで表示される、-----BEGIN RSA PRIVATE KEY-----から-----END RSA PRIVATE KEY-----\nまでをコピーしてローカルマシンで利用しやすいようにslackなどに貼り付けて送ります。

cat ~/.ssh/id_rsa
# -----BEGIN RSA PRIVATE KEY-----
# ...
# -----END RSA PRIVATE KEY-----
#

ローカルマシンに戻り、先程コピペしたものから秘密鍵ファイル(ここでは~/.ssh/wsl)を作り、以下でパーミッションも整えます。

chmod 600 ~/.ssh/wsl
  1. tcpトンネリング

ubuntu on WSL に戻り以下で tcp トンネリングを行います。

./ngrok tcp 22 --region jp

すると以下のような感じでログがでます。

Session Status                online
Account                       純 (Plan: Pro)
Version                       2.3.35
Region                        Japan (jp)
Web Interface                 http://127.0.0.1:4040
Forwarding                    tcp://0.tcp.jp.ngrok.io:15368 -> localhost:22

ここのForwardingというのがsshするのに必要な情報です。ここで上記の場合ホストが0.tcp.jp.ngrok.io、ポートが15368で待ち構えられてるというのが分かります。

では、これまでの情報からsshしてみます。(ちなみにユーザー名はnju33

# -o 'StrictHostKeyChecking no' はホスト検証回避の為
ssh -o 'StrictHostKeyChecking no' nju33@0.tcp.jp.ngrok.io -p 15368 -i ~/.ssh/wsl
# Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.4.0-18362-Microsoft x86_64)
# ...
# nju33@DESKTOP-MLLARGC:~$

Welcome ~がでたら完了です。

実行ログをファイルに吐く

コマンドを実行すると、標準出力または標準エラー出力によってログがターミナル上に表示されることがあります。

このログを取っておきたい時や誰かに共有したい時に、そのログをコピペするのも1つの手ですが、ファイルにそのまま書き出した方が扱いやすかったり変なコピペミスなどを回避できたりします。

標準出力

標準出力をファイルに吐き出すには>または>>(もしくは1>または1>>)を使います。>>>の違いはファイル全体の上書きか、追記か、の違いがあります。
この>はリダイレクトと呼ばれます。

以下はstdout.logにログを出力しているシェルスクリプトの例です。

#!/usr/bin/env bash

# stdout.log をリセット
: > stdout.log

echo foo >> stdout.log
echo bar >> stdout.log

このシェルスクリプトを実行するとstdout.logはこうなっているハズです。

cat stdout.log
# foo
# bar

: > stdout.log部分は、これ無しで何度もこのシェルスクリプトを実行すると、foo\nbar...が追記され続けてしまうので、前回のログを消すために行ってます。

標準エラー出力

エラーの場合は2>または2>>を使ってファイルに出力できます。

以下はstderr.logにログを出力しているシェルスクリプトの例です。
この中で標準エラー出力を手っ取り早く試す為にecho ... >&2を使ってます。この>&2または1>&2は、標準出力を標準エラー出力としてログを出すという意味です。

#!/usr/bin/env bash

# stderr.log をリセット
: > stderr.log

echo foo >&2 2>> stderr.log
echo bar >&2 2>> stderr.log

このシェルスクリプトを実行するとstdout.logも先程の標準出力と同じ結果になります。

cat stderr.log
# foo
# bar

両方

1つのコマンドで両方に対処することもできます。

#!/usr/bin/env bash

log() {
  if [ $1 -eq 1 ]; then
    echo "$2"
  else
    echo "$2" >&2
  fi
}

: >stdout.log >stderr.log

log 1 foo >>stdout.log 2>>stdout.log
log 2 bar >>stderr.log 2>>stderr.log

echoでは分かりづらい為、log関数を実装してます。これは最初の引数が1なら第2引数のテキストを標準出力、そうでないなら標準エラー出力してます。

>>stdout.log 2>>stdout.log部分では、標準出力はstdout.log標準エラー出力はstderr.logにというように 1 つのコマンドに対して同時に設定してます。
このシェルスクリプトを実行するとそれらログファイルが作成され、それぞれ別れて追記されてるのが分かります。

cat stdout.log
# foo
cat stderr.log
# bar

リダイレクトを消す

シェルスクリプトの中では恐らくコマンドが 1 つや 2 つではなく、>>stdout.log 2>>stdout.logという部分を毎回記述するのは面倒だけでなく、タイプミスなど凡ミスも起こしやすいので、回避する為にexecコマンドを使います。

コマンド実行の前にexec > stdout.log 2> stderr.log(>>ではないので注意)とすることで、それ以降のすべての実行ログを、execで行ったリダイレクトに渡してくれます。

例えば先程の両方セクションのシェルスクリプトは以下のようにリファクタリングできます。

#!/usr/bin/env bash

log() {
  if [ $1 -eq 1 ]; then
    echo "$2"
  else
    echo "$2" >&2
  fi
}

exec > stdout.log 2> stderr.log

log 1 foo
log 2 bar
cat stdout.log
# foo
cat stderr.log
# bar

ログファイルに出す領域を切り分けたい

execは後で「実行時にターミナルにログ出したい」などのように思っても切り替えができません。

ですが、1 つのシェルスクリプトの中で、「この領域はファイルに出したいけど、この部分のはいらない」のように切り替えたい場合は、その領域を{...}で囲み、リダイレクトするという手もあります。

以下は上記「リダイレクトを消す」セクションのシェルスクリプトを少し書き直したものです。

#!/usr/bin/env bash

log() {
  if [ $1 -eq 1 ]; then
    echo "$2"
  else
    echo "$2" >&2
  fi
}

# ファイルにログ出し
{
  log 1 foo
  log 2 bar
} >stdout.log 2>stderr.log

# ターミナルにログ出し
log 1 foo
log 2 bar

このシェルスクリプトを実行すると{...}に囲まれた部分はファイルに書き出され、そうじゃない部分は普通にターミナルにログが出力されます。