ShellScript

目指せシェル芸人

主にbashについて書きます

Basic

if文

if [ 条件1 ]
then
  処理1
elif [ 条件2 ]
then
  処理2
else
  処理3
fi

数値比較

数値評価演算子 意味
数値1 -eq 数値2 数値1と数値2が等しい場合に真
数値1 -ne 数値2 数値1と数値2が等しくない場合に真
数値1 -gt 数値2 数値1が数値2より大きい場合に真
数値1 -lt 数値2 数値1が数値2より小さい場合に真
数値1 -ge 数値2 数値1が数値2より大きいか等しい場合に真
数値1 -le 数値2 数値1が数値2より小さいか等しい場合に真

こういう書き方

if [ "1" -eq "0" ]

文字列比較

文字列評価演算子 意味
文字列 文字列の長さが0より大きければ真
-n 文字列 文字列の長さが0より大きければ真
! 文字列 文字列の長さが0であれば真
-z 文字列 文字列の長さが0であれば真
文字列1 = 文字列2 2つの文字列が等しければ真
文字列1 != 文字列2 2つの文字列が等しくなければ真

こういう書き方

if [ "ABC" = "abc" ]
if [ -z "${FILEPATH}" ]
if test -z "${FILEPATH}"

ファイルチェック

ファイルチェック演算子 意味
-d ファイル名 ディレクトリなら真
-f ファイル名 通常ファイルなら真
-L ファイル名 シンボリックリンクなら真
-r ファイル名 読み取り可能ファイルなら真
-w ファイル名 書き込み可能ファイルなら真
-x ファイル名 実行可能ファイルなら真
-s ファイル名 サイズが0より大きければ真
ファイル1 -nt ファイル2 ファイル1がファイル2より新しければ真
ファイル1 -ot ファイル2 ファイル1がファイル2より古ければ真

こういう書き方

if [ -f "${FILEPATH}" ]
if test -f "${FILEPATH}"
if [ "${FILE1}" -nt "${FILE2}" ]

論理結合

論理結合演算子 意味
! 条件 条件が偽であれば真
条件1 -a 条件2 条件1が真、かつ、条件2が真であれば真
条件1 -o 条件2 条件1が真、または、条件2が真であれば真

こういう書き方

# AND(論理積演算子)
if [ -f a.txt -a -f b.txt ]
if [ "$a" -eq "$b" ] && [ "$c" -eq "$d" ]

# OR(論理和演算子)
if [ -f a.txt -o -f b.txt ]
if [ "$a" -eq "$b" or "$c" -eq "$d" ]
if [ "$a" -eq "$b" ] || [ "$c" -eq "$d" ]

# NOT(否定論理演算子)
if [ ! "$a" -eq "$b" ]

変数に任意の文字が含まれているかどうか

if echo $WORD | grep "任意の文字" >/dev/null
then
  echo "文字含まれている"
else
  echo "文字含まれていない"
fi

bashのワイルドカード

実はif文内でワイルドカードの判定ができる。

STRING="abc"
if [[ $TEST == *"b"* ]]
    echo "matched"
else
    echo "unmatched"
fi

ワイルドカード部分 * は、ダブルコーテーションで囲んだりすると動かなくなるので注意。

bashの正規表現

そもそもgrepを使わないでも、変数に任意の文字が含まれているかどうか判定できる。

STRING="abc"
if [[ "$STRING" =~ ^ab[cd]$ ]]; then
    echo "matched"
else
    echo "unmatched"
fi

正規表現部分 ^ab[cd]$ は、ダブルコーテーションで囲んだりすると動かなくなるので注意。

展開

bash入門
https://rat.cis.k.hosei.ac.jp/article/linux/bash_intro.html

ブレース展開

カンマ区切りの文字列をブレースで囲んだものはブレース展開されます。

$ echo {a,b,c}
a b c

ブレースの前後に文字列があるとそれを反映した展開がなされます。

$ echo 0{a,b,c}1
0a1 0b1 0c1

ブレース展開はまとめてファイルやディレクトリを作成するときなどによく使われます。

$ find . -type d
.

$ mkdir -p a/b{0,1}/c{0,1}
$ find . -type d
.
./a
./a/b0
./a/b0/c0
./a/b0/c1
./a/b1
./a/b1/c0
./a/b1/c1

チルダ展開

チルダはユーザーのホームディレクトリに展開されます。

$ whoami
user1
$ echo ~
/home/user1

ファイル名展開

ファイル名展開はグロブ(glob)とも呼ばれます。

パターン 効果
* 0文字以上の任意の文字列にマッチ。
? 1文字の任意の文字列にマッチ。
[...] ブラケットで挟まれている文字のうち任意の1文字にマッチ。正規表現におけるブラケットの解釈とほぼ同じです。
$ ls
a  adc.txt  aec.txt  b

$ echo *
a adc.txt aec.txt b

$ echo ?
a b

$ echo a[abcd]*
adc.txt

# マッチするものが無かった場合パターンはそのままの形で残ります
$ echo xyz*
xyz*

変数展開

変数を参照する場合に文字列を加工することができる。

パターン 効果
${変数名#word} 変数から値を取り出し、その先頭部分がwordと一致した場合その部分を取り除きます。(先頭から前方最短一致した位置まで)
${変数名##word} 変数から値を取り出し、その先頭部分がwordと一致した場合その部分を取り除きます。(先頭から前方最長一致した位置まで)
${変数名%word} 変数から値を取り出し、その後方部分がwordと一致した場合その部分を取り除きます。(末尾から後方最短一致した位置まで)
${変数名%%word} 変数から値を取り出し、その後方部分がwordと一致した場合その部分を取り除きます。(末尾から後方最長一致した位置まで)
${変数名/pattern/word} 変数から値を取り出し、最初にpatternとマッチする部分だけをwordで置換します。
${変数名/#pattern/word} 変数から値を取り出し、最初にpatternとマッチする部分だけをwordで置換します。(上と同じ)
${変数名/%pattern/word} 変数から値を取り出し、最後にpatternとマッチする部分だけをwordで置換します。
${変数名//pattern/word} 変数から値を取り出し、patternとマッチする全ての部分をwordで置換します。
${変数名:=word} 変数が NULL の場合 word に置換され、かつ変数に代入される。
${変数名:-word} 変数が NULL の場合 word を出力する。
${変数名:+word} 変数が NULL 以外の場合 word に置換され、かつ変数に代入される。
${変数名:?word} 変数が NULL の場合、標準エラー出力にwordを表示し、現在実行中のスクリプトを中止する。
${#変数名} 変数に設定されている文字数を返す。
${変数名:offset} 変数に設定されているoffset文字+1から出力する。offsetを負の値にすると語尾から数える。
${変数名:offset:length} 変数に設定されているoffset文字+1からlength分出力する。
${変数名,} 先頭だけ小文字に変換
${変数名,,} 全部小文字に変換
${変数名^} 先頭だけ大文字に変換
${変数名^^} 全部大文字に変換

算術式展開

expr や、bcコマンドを使用しなくても、$((計算式))で計算ができる。

$ echo $(( 1 + 2 + 3 ))
6

Qiita - Bash $((算術式)) のすべて
https://qiita.com/akinomyoga/items/2dd3f341cf15dd9c330b

コマンド置換

コマンド置換には以下の 2 つの形式があります。

$(command)
`command`

今までは一般的にバッククォート command が使われてきたが、$(command) も使えます。
$(command)は、入れ子ができるので、$(command)の方を使ったほうが良い。

$ TEST=`date`;echo $TEST
2016年 8月 28日 日曜日 18:04:33 JST
$ TEST=$(date);echo $TEST
2016年 8月 28日 日曜日 18:04:41 JST

次のコマンドはカレントディレクトリの中で「abc」を含むすべてのファイルの先頭10行を表示します。

$ head -10 $(grep -l abc *)

ヒアドキュメント

こんな感じ。ftpぐらいなら自動化できる。

$ command << token
text
token

#!/bin/bash

# Script to retrieve a file via FTP

FTP_SERVER=ftp.nl.debian.org
FTP_PATH=/debian/dists/lenny/main/installer-i386/current/images/cdrom
REMOTE_FILE=debian-cd_info.tar.gz

ftp -n << _EOF_
open $FTP_SERVER
user anonymous me@linuxbox
cd $FTP_PATH
hash
get $REMOTE_FILE
bye
_EOF_

ヒアストリング

変数を擬似的にファイルのように扱える。

$ cat < hoge
-bash: hoge: そのようなファイルやディレクトリはありません
 cat <<< hoge
hoge
$ HOGE=hogefuga
$ cat < $HOGE
-bash: hogefuga: そのようなファイルやディレクトリはありません
$ cat <<< $HOGE
hogefuga

特殊なシェル変数

変数 内容
$0 実行したスクリプトの文字列
$1, $2, ..., $9 第1引数, 第2引数, ..., 第9引数
$# 与えられた引数の数
$* 与えられたすべての引数. 引数全体が"で囲まれている
$@ 与えられたすべての引数. 引数一つ一つが"で囲まれている
$- シェルに与えられたフラグ
$? 最後に行ったコマンドの戻り値
$$ 現在のシェルのプロセス番号
$! 最後にバックグラウンドで行ったコマンドのプロセス番号
$_ 実行シェルの文字列
IFS 区切り文字のリスト

シェル変数と環境変数

特徴 確認 設定 削除
シェル変数 シェル変数は子プロセスに引き継がれない set 変数名=値 unset # シェル変数、環境変数両方消去される
環境変数 シェル変数は子プロセスに引き継がれる env export 変数名 or export 変数名=値 unset # シェル変数、環境変数両方消去される

ちなみに、環境変数を定義するには export コマンド以外にも declare コマンドに -x オプションを付けて実行する方法もある。

プロセス置換

以下の文法に従えば、コマンドの実行結果を一時ファイルに吐いておく必要がなくなる。

<(command)

$ sdiff <(date;sleep 1;date) <(date;date)
2016年  8月 28日 日曜日 18:00:15 JST                            2016年  8月 28日 日曜日 18:00:15 JST
2016年  8月 28日 日曜日 18:00:16 JST                          | 2016年  8月 28日 日曜日 18:00:15 JST

連想配列

連想配列が使える。

ver4系から使える機能です。Macのbashはver3系のため、brew等でアップデートしましょう。

$ declare -A hashTable
$ hashTable["hoge"]="fuga"
$ echo ${hashTable["hoge"]}
fuga

bash開発機能

文法チェック

$ bash -n test.sh

実行内容をトレース

$ bash -x test.sh

ステップ実行

擬似シグナルDEBUGを使う。擬似シグナルDEBUGはシェルが文を実行するたびに発行される。

#!/bin/bash

trap 'read -p "next(LINE:$LINENO)>> $BASH_COMMAND"' DEBUG

未定義の変数をエラーとして扱う

#!/bin/bash
set -u

# ${TEST}が未定義ならエラーになる
echo ${TEST}

終了ステータスが0以外ならその時点で終了

#!/bin/bash
set -e

#ls -yでエラーになり、後続の処理は実行されない
ls -y
ls -l

Sample

ファイルを一行ずつ読み込んで処理する

#!/bin/sh
# ファイルを1行ずつ読み込んで表示

TESTFILE=./hoge.txt
while read LINE
do
    echo $LINE
done < $TESTFILE

処理単位も行にしたい時は、冒頭に以下を書く

IFS='
'

区切り文字(デミリタ)で区切られた文字列の任意のフィールドを取得する

cut方式とawk方式がある

echo "abc:def:ghi:jkl"|cut -d: -f3
echo "abc:def:ghi:jkl"|awk -F':' '{print $3}'

ダブルクォーテーションで囲まれている場合

$ grep 設定項目名 設定ファイル | sed 's/[^"]*"\([^"]*\)"[^"]*/\1/g'

シングルクォーテーションで囲まれている場合

$ grep 設定項目名 設定ファイル | sed "s/[^']*'\([^']*\)'[^']*/\1/g"

イコールで定義している場合

$ grep 検索する設定項目 設定ファイルPATH | sed 's/ //g' | awk -F'=' '{ print $2}'

標準入力とファイルの内容を結合

$ cat test
efg
$ echo -n abcd |cat - test
abcdefg

大抵のコマンドには - (標準入力の内容をファイル相当として扱う)を実装している

サーバと連続でやりとりするシェルスクリプト

以下は、サーバからもらった計算式を計算して、送り返している例

#!/bin/sh

exec 5<>/dev/tcp/[host]/[port]

for I in {1..101}
do
  cat 0<&5>test.txt &
  sleep 1
  pkill cat
  WORD=`cat test.txt|tail -1|sed 's/=//g'`
  ANSWER=`ecgo ${WORD}|bc`
  echo ${ANSWER} > &5
  echo Debug [${I}] ${WORD} '=' ${ANSWER}
done

exit 0

文字列⇔16進数表現⇔2進数表現

文字列→16進数表現

echo "ABC"|xxd

文字列→2進数表現

echo "ABC"|xxd -b

16進数表現→文字列

echo 4142434445464748 | xxd -ps -r

16進数表現→2進数表現

echo 4142434445 | xxd -ps -r|xxd -b

2進数表現→16進数表現

echo "obase=16; ibase=2; 010000010100001001000011010001000100010100001010" | bc

2進数表現→文字列

echo "obase=16; ibase=2; 010000010100001001000011010001000100010100001010" | bc|xxd -ps -r

bcコマンドなくても、このぐらいはできる。

2進数表現→10進数表現

echo $((2#101010))
echo "obase=10; ibase=2; 101010" | bc

8進数表現→10進数表現

echo $((04567))
echo "obase=10; ibase=8; 4567" | bc

16進数表現→10進数表現

echo $((0xABCD))
echo "obase=10; ibase=16; ABCD" | bc

指定した相対PATHからの、絶対パス・ディレクトリ名・ファイル名の取得

$ cd /tmp/trash
$ readlink  -f test.txt 
/tmp/trash/test.txt
$ dirname `readlink  -f test.txt`
/tmp/trash
$ basename `readlink  -f test.txt`
test.txt

IPアドレスの取得

いろんな方法があるが、NICのインタフェース名が不定だし、複数NICが刺さっていたりもするので、これという方法が無い。

$ ifconfig eth1 | grep -w 'inet' | cut -d: -f2 | awk '{ print $1}'
$ ip addr list venet0 | grep "inet " | cut -d' ' -f6 | cut -d/ -f1
$ ip a s | grep "inet " | cut -d' ' -f6 | cut -d/ -f1
$ hostname -i
$ hostname -I

実はこういう技がある
素晴らしい

$ curl ifconfig.io

IPアドレスのソート

$ cat ip_list.txt | sort -n -t'.' -k1,1 -k2,2 -k3,3 -k4,4

特定のファイルのバックアップファイルを作成する

$ find PATH -type f -name 'ファイル名' | xargs -n 1 -I{} cp {} {}.bk

特定のファイルをバックアップディレクトリにコピーする

$ find PATH -type f -name 'ファイル名' | xargs -n 1 -I{} cp {} バックアップ先PATH

特定のファイルに対してのみgrepを行う

$ find PATH -type f -name 'ファイル名' | xargs -n 1 -I{} grep '検索キーワード' {}

指定したファイルの合計サイズを取得する

$ du -bhc [ファイルPATH等条件] | tail -n 1

または

$ ls -la [ファイルPATH等条件] | awk '{ total += $5 }; END { print total }'

対話型コマンドを自動実行する

対話中に判断や計算が入るとexpectでやるしかない。

#!/bin/expect -f

spawn ./121-calculation

set i 0
while {$i <= 100000} {

puts "\n#### $i ######"

expect "*="
set quest $expect_out(buffer)

set answer [exec echo $quest | tail -1 | tr -d '=' | tr -d '\r' | bc]
send "$answer\n"

incr i 1
}

命令解説

  • spawn コマンド実行
  • puts 画面表示(実行したコマンドには影響しない)
  • expect コマンド出力する文字にマッチングするまでここで止まる
  • set 変数 定数 変数設定
  • set 変数 $expect_out(buffer) コマンド実行結果を変数に(ただし、直前のではなく、全部入るっぽい)
  • set 変数 [外部コマンド] 外部コマンドの実行結果を変数に(最新行を選択するtail -1 と 変な改行コード入ってエラーになるので tr -d '\r' を入れるのがコツ)
  • send "変数名\n" コマンドに実行結果を送る(データ終端を\nにしているコマンドが多いので、明示的に\nを書く)
  • incr 変数名 定数 加算

どういう形式で圧縮されているか判別して展開する

環境合わせて、適当に修正が必要

#!/bin/sh

LIST=`ls|grep -v flatting.sh`

for WORD in ${LIST}
do
  FILE_TYPE=`file ${WORD}`

  if echo ${FILE_TYPE}|grep "Zip archive data" >/dev/null
  then
    unzip ${WORD}
    rm ${WORD}
  elif echo ${FILE_TYPE}|grep "shell archive text" >/dev/null
  then
    sh ${WORD}
    rm ${WORD}
  elif echo ${FILE_TYPE}|grep "bzip2 compressed data" >/dev/null
  then
    bunzip2 -c ${WORD} > ${WORD}_out
    rm ${WORD}
  elif echo ${FILE_TYPE}|grep "POSIX tar archive" >/dev/null
  then
    tar xvf ${WORD}
    rm ${WORD}
  elif echo ${FILE_TYPE}|grep "gzip compressed data" >/dev/null
  then
    gunzip -c ${WORD} > ${WORD}_out
    rm ${WORD}
  elif echo ${FILE_TYPE}|grep "xz compressed data" >/dev/null
  then
    xz -d -c ${WORD} > ${WORD}_out
    rm ${WORD}
  elif echo ${FILE_TYPE}|grep "ASCII cpio archive" >/dev/null
  then
    cpio -idv < ${WORD}
    rm ${WORD}
  elif echo ${FILE_TYPE}|grep "ASCII text" >/dev/null
  then
    if cat ${WORD} |grep "This is dummy file" >/dev/null
    then
      rm ${WORD}
    else
      base64 -d ${WORD} > ${WORD}_out  
      if [ ${?} = 0 ]
      then
        rm ${WORD}
      fi
    fi
  else
    :
  fi

done

exit 0

一定時間ごとに任意のコマンドを実行する

もちろんwatchコマンドを使ってもいいが、以下の方法でも可能。

$ yes '[任意のコマンド]; sleep [任意のインターバル秒数];' | sh

文字列への色つけ

txtund=$(tput sgr 0 1)    # Underline
txtbld=$(tput bold)       # Bold
txtred=$(tput setaf 1)    # Red
txtgrn=$(tput setaf 2)    # Green
txtylw=$(tput setaf 3)    # Yellow
txtblu=$(tput setaf 4)    # Blue
txtpur=$(tput setaf 5)    # Purple
txtcyn=$(tput setaf 6)    # Cyan
txtwht=$(tput setaf 7)    # White
txtrst=$(tput sgr0)       # Text reset

echo "${txtbld}This is bold text output from shell script${txtrst}"

プログレスバーの表示

show_progress() {
    echo -ne '#####                     (33%)\r'
    sleep 1
    echo -ne '#############             (66%)\r'
    sleep 1
    echo -ne '#######################   (100%)\r'
    echo -ne '\n'    
}

スピナーの表示

show_spin () {
  rotations=3
  delay=0.1
  for i in `seq 0 $rotations`; do
    for char in '|' '/' '-' '\'; do
      echo -n $char
      sleep $delay
      printf "\b"
    done
  done
}

あらゆるファイルの解凍

function extract {
 if [ -z "$1" ]; then
    # display usage if no parameters given
    echo "Usage: extract <path/file_name>.<zip|rar|bz2|gz|tar|tbz2|tgz|Z|7z|xz|ex|tar.bz2|tar.gz|tar.xz>"
 else
    if [ -f $1 ] ; then
        # NAME=${1%.*}
        # mkdir $NAME && cd $NAME
        case $1 in
          *.tar.bz2)   tar xvjf ../$1    ;;
          *.tar.gz)    tar xvzf ../$1    ;;
          *.tar.xz)    tar xvJf ../$1    ;;
          *.lzma)      unlzma ../$1      ;;
          *.bz2)       bunzip2 ../$1     ;;
          *.rar)       unrar x -ad ../$1 ;;
          *.gz)        gunzip ../$1      ;;
          *.tar)       tar xvf ../$1     ;;
          *.tbz2)      tar xvjf ../$1    ;;
          *.tgz)       tar xvzf ../$1    ;;
          *.zip)       unzip ../$1       ;;
          *.Z)         uncompress ../$1  ;;
          *.7z)        7z x ../$1        ;;
          *.xz)        unxz ../$1        ;;
          *.exe)       cabextract ../$1  ;;
          *)           echo "extract: '$1' - unknown archive method" ;;
        esac
    else
        echo "$1 - file does not exist"
    fi
fi
}

このコマンドも有用と思われ

俺的備忘録 〜なんかいろいろ〜 - ファイルの圧縮方式に合わせて自動的に解凍してくれる『dtrx』コマンド
https://orebibou.com/2016/07/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E5%9C%A7%E7%B8%AE%E6%96%B9%E5%BC%8F%E3%81%AB%E5%90%88%E3%82%8F%E3%81%9B%E3%81%A6%E8%87%AA%E5%8B%95%E7%9A%84%E3%81%AB%E8%A7%A3%E5%87%8D%E3%81%97%E3%81%A6/

POSIX仕様の中でif文中でパターンマッチングする

if [ -n "$X" -a -z "${X%%pattern}" ] ; then echo "true"; else echo "false"; fi

KnowHow

シェルスクリプトを極める
http://www.slideshare.net/bsdhack/ss-43064758

バッファリングさせない

stdoutがttyじゃないとbufferingするコマンドは多い
stdbufで解決できる

$ command | stdbuf -oL command | command

行単位でバッファを吐き出す

$ command | stdbuf -oO command | command

バッファリングさせない

以下が神解説です

俺的備忘録 〜なんかいろいろ〜 - sedやawk、grepをバッファさせずに動作させる
https://orebibou.com/2017/06/sed%e3%82%84awk%e3%80%81grep%e3%82%92%e3%83%90%e3%83%83%e3%83%95%e3%82%a1%e3%81%95%e3%81%9b%e3%81%9a%e3%81%ab%e5%8b%95%e4%bd%9c%e3%81%95%e3%81%9b%e3%82%8b/

ファイルディスクリプタ

  • 標準入力は 0
  • 標準出力は1
  • 標準工ラー出力は2

シエルではリダイレクト記号の前に数字

 &> ファイル  # 標準出力・標準エラー出力を共にリダイレクト
 3> ファイル  # fd3をファイルにリダイレクト
 4< ファイル  # fd4をファイルからリダイレクト
 5<&-         # fd5をクローズ 
 6>&7         # fd7をfd6に複製(fd6をfd7にリダイレクト)

実装

command > file

結果、fdが標準出力として使用される

command > file 2>&1

結果、fdが標準出力、および、標準エラー出力として使用される

commandA |commandB

結果、commandA の標準出力が commandB の標準入力になる

$ ./echo "test" 3>&1 1>&2 2>&3

標準エラー出力と標準出力を入れ替える

プロセスの出力同士をdiffで比較

$ diff <(commandA)<(commandB)

$ commandA|(commandB|diff /dev/fd/3 -) 3<&0

3<&0 で標準入力(fd0) を fd3 に複製
commandA の出力が fd3 に出力される
/dev/fd/3 からの入力が commandA の出力

パイプの途中のプロセスの終了コード

$ exec 3>&1 
ret1=`{ { commandA 1; echo $? 1>&3; } | commandB 2; } 3>&1` 
ret2=$?

コマンド 1 の $? を 1>&3 で fd3 に出力
fd3 の内容を 3>&1 で ret1 に格納

ループ処理でのファイルディスクリプタの活用

exec 3<&0 0<<EOF
 `command` 
EOF 
while read line 
do
  : 
done 
exec 0<&3 3<&-

exec 3<&0 で fd0 を fd3 に複製
exec 0<<EOF でヒアドキュメントを fd0 として使用
exec 0<&3 で複製した fd0 を復帰
exec 3<&- で fd3 をクローズ
パイプを使わないのでプロセスが生成されない

evalを用いた配列的な変数アクセス

# $1: 配列名 
# $2: インデックス 
# $3-: 値 

setarray() 
{
  _index_="__`echo $1 $2 | sed -e 's/ //g'`"
  shift 2
  eval $_index_'='"$@" 
}

getarray() 
{
  eval echo '${'__`echo $* | sed -e 's/ //g'`'}' 
}

ファイルの上書き

元ファイルを書き替える

$ コマンド < ファイル > ファイル # 絶対ダメ!
$ (rm ファイル ; コマンド > ファイル ) < ファイル # ファイルの中身が消えない方法
        ②                     ③          ①

① でファイルが読み込みモードでオープンされる。
② でオープンされたファイルが削除される。
③ で新しい(別な)ファイルが書き込みモードでオープンされる。

①のファイルと③のファイルは同じファイル名ですが、 inode が異なっているため、別のファイルとして扱われる。

1<>を使う方法もある

cat srcfile 1<> srcfile

シェルスクリプトでの排他処理

lockfile="/var/tmp/`basename ${0}`" 
test -f ${lockfile} || touch ${lockfile}

test(1) と touch(1) の間に他のプロセスの test(1) が実行されると 排他処理が失敗する

lockfile="/var/tmp/`basename ${0}`" 
if ln -s $$ ${lockfile} 2> /dev/null 
then
  : # メイン処理を実行 
else
  echo "${0}: exist another instance" 1>&2 
  exit 255 
fi 
trap 'rm -f ${lockfile}; exit' 0 1 2 3 11 15

シンボリックリンクの作成は atomic
PID をリンクする事でロックしたプロセスが特定できる

英単語の先頭文字を大文字に変換する

$ echo "foo" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2, length($0) - 1) }'

awk 組み込みの substr() 、 toupper() を使用する

$ eval eval `echo "foo" | sed 's/(.)(.*)//bin/echo -n 1 | tr "[a-z]" "[A-Z]"; echo 2/g'`

sed 単体では実現できないのでコマンド列を出力して eval

$ echo foo bar buz|sed 's/\b\([a-z]\)/\U\1/g'

実はsed単体でできました

IF文のような分岐処理をワンライナーで行わせる

$ コマンド && TRUE || FALSE

「&&」以降はコマンドがTRUEの場合に、「||」以降はFALSEの場合に実行されるコマンドとなる。

$ cat test.log 
abc [31/Oct/2015:20:36:51 +0900] ghi [20]
jkl [31/Oct/2015:20:36:55 +0900] pqr [0]
stu [31/Oct/2015:20:37:00 +0900] yz  [40]
000 [31/Oct/2015:20:37:05 +0900] ghi [5]
$ grep abc test.log >/dev/null && echo 1 || echo 2
1
$ grep azc test.log >/dev/null && echo 1 || echo 2
2

exの活用

ファイルの行操作に ex を活用する

  • 直接行の追加や削除が可能(一時ファイル不要) – 実は vi なので正規表現など強力な編集操作が可能 – echo やヒアドキュメントで編集コマンドを指定可能

exで行の追加

$ /bin/ex -s ファイル << EOF
行番号 a
コンテンツ
コンテンツ
    :
.
w!
EOF

行番号で指定した行の下にコンテンツを挿入する
. で挿入モードを終了し w! で内容をファイルに出力する

exで行削除処理

$ /bin/ex -s ファイル << EOF
行番号 d
w!
EOF

行番号で指定した行を削除する
w! で内容をファイルに出力する

exで行番号を指定した行置換処理

$ /bin/ex -s ファイル << EOF
行番号 s/パターン/置換文字列/
w!
EOF

行番号で指定した行のパターンを置換文字列に置換する
w! で内容をファイルに出力する

exでパターンを指定した行置換処理

$ /bin/ex -s ファイル << EOF
/パター /s/ 置換文字 /
w!
EOF

最初に発見したパターンを置換文字列に置換する
s の後ろの連続した // は直前の正規表現(パターン)を示す
w! で内容をファイルに出力する

exでファイルから指定された行を削除する

$ cat basefile
first line
second line
third line
    :
    :


$ cat target
2
3
5
:
:


元のファイル (basefile) には複数の行が含まれている
削除する行は別なファイル (target) に格納されている
target ファイルには行番号が 1 行ずつ格納されている

パターンを指定した行置換処理

$ sort target | awk '{ printf "%ddn", $1-(NR-1); } END { print "w!" }' | ex -s basefile

削除済みの行を考慮する必要がある

→ 2 行目を削除すると今までの 3 行目が 2 行目になる
ex で行を削除する

awk で削除した行を考慮した行番号に対する行削除を出力

シェルスクリプトで標準入力を受け取る

3パターンある。

cat - で 標準入力を受け取り、利用できる。

#!/bin/sh
if [ -p /dev/stdin ] ; then
    a=$(cat -)
    echo "input string is ${a}"
else
    echo "nothing stdin"
fi

readを使うと、標準入力を読むことができる

#!/bin/bash
while read i ; do
    #数字を読み込んで1足して出力する。
    echo $((i+1))
done

他の方法に比べて高コストになりがち

/dev/stdinを使う

#!/bin/bash
awk '{print $1+1}' < /dev/stdin

一時ファイルを作らずにコマンドの出力結果を利用する(Command Substitution)

<(コマンド)記法、ksh、bash、zsh限定。

コマンドに対する入出力を、ファイルのように指定することが出来る。

  • コマンドの標準出力を入力ファイルとして <(コマンド)
  • コマンドの標準入力を出力ファイルとして >(コマンド)

例えば、

$ diff <(ls) <(ls -a)

これと同義

$ ls > a
$ ls -a > b
$ diff a b

パイプラインに任意の値を入れ込む

文字列入れ

$ seq 2 | (echo 'Header'; cat; echo 'Footer')
Header
1
2
Footer
$ seq 5 | (echo 'obase=2'; cat) | bc
1
10
11
100
101

ファイル入れ

"-"が標準入力のこと

$ seq 11 12 | cat data -
1
2
11
12

パイプで渡したコマンドの終了ステータスを得る

$ cat test.txt |cat|cat|cat|cat|cat
$ echo ${PIPESTATUS[@]}
0 0 0 0 0 0

bashでパイプで受け取った値を自動エスケープして出力する

俺的備忘録 〜なんかいろいろ〜 - bashでパイプで受け取った値を自動エスケープして出力する
https://orebibou.com/2017/07/bash%e3%81%a7%e3%83%91%e3%82%a4%e3%83%97%e3%81%a7%e5%8f%97%e3%81%91%e5%8f%96%e3%81%a3%e3%81%9f%e5%80%a4%e3%82%92%e8%87%aa%e5%8b%95%e3%82%a8%e3%82%b9%e3%82%b1%e3%83%bc%e3%83%97%e3%81%97%e3%81%a6/

echo、printfでパイプから受けた値を出力する

俺的備忘録 〜なんかいろいろ〜 - echo、printfでパイプから受けた値を出力する
https://orebibou.com/2017/07/echo%e3%80%81printf%e3%81%a7%e3%83%91%e3%82%a4%e3%83%97%e3%81%8b%e3%82%89%e5%8f%97%e3%81%91%e3%81%9f%e5%80%a4%e3%82%92%e5%87%ba%e5%8a%9b%e3%81%99%e3%82%8b/

サロゲートペアやUnicode結合文字を考慮して文字単位に分解する

NG

$ echo そのチェㇷ゚は𩸽🇯🇵 | grep -o .|tr '\n' '/'
そ/の/チ/ェ/ㇷ/゚/は/𩸽/🇯/🇵

OK

$ ruby -e 'puts "そのチェㇷ゚は𩸽🇯🇵".scan(/\X/).join("/")'
そ/の/チ/ェ/ㇷ゚/は/𩸽/🇯🇵

awk KnowHow

AWK 一行野郎百裂拳 - 日本 GNU AWK ユーザー会 - No-ip.org
http://gauc.no-ip.org/awk-users-jp/material/100_one_liners_20131223.pdf

特定の N 行を表示する

$ awk 'NR==N'

空行を削除する

$ awk 'NF'
$ awk '$0'
$ awk '/./'

文字数をカウントする(wc -c)

$ awk '{n+=length($0)} END{print n}'

単語数をカウントする(wc -w)

$ awk '{n+=NF} END{print n}'

行数をカウントする(wc -l)

$ awk 'END{print NR}'

行末の空白やタブを削除する

$ awk '{sub(/[ \t]+$/, "")}1'

Unix の改行コードに変換する

$ awk 'sub(/\r$/,"")'

Windows の改行コードに変換する

$ awk 'sub(/$/,"\r")'

逆順出力をする(tac)

$ awk '{a[i++]=$0} END{for(j=i-1; j>=0;) print a[j--]}'

重複するレコードを削除する(uniq)

$ awk '!a[$0]++'

ソートしないで重複行を削除する

awk '!a[$0]++' FILE

行番号を付ける(nl)

$ awk '$0 = NR OFS $0'

標準出力にそのまま出力する(cat -)

$ awk '1'

正規表現にマッチした行を表示する(grep)

$ awk '/hogehoge/'

正規表現にマッチしない行を表示する(grep -v)

$ awk '! /hogehoge/'

コメント行を削除する

$ awk '! /^#/'

C言語のように複数行にまたがったコメント行を削除する

/* と */ に囲まれた行の場合

$ cat file | awk '/\/\*/, /\*\//{next}{print}'

指定行から指定行までを表示する

$ awk 'NR==10,NR==20'

偶数行を表示する

$ awk 'NR%2==0'

奇数行を表示する

$ awk 'NR%2'

特定のフィールド数を持つ行のみを抜き出す

$ cat file
りんご 100円 192個
ばなな 170円 210個
爽健美茶 150円
グラタン ソース マカロニ チーズ じゃがいも
$ cat file | awk 'NF>=2 && NF<=3'
りんご 100円 192個
ばなな 170円 210個
爽健美茶 150円

awkで[](カギカッコ)内の値に応じて行を抽出する

$ awk -F '[][]' '$4 >= 〇〇' ログファイルPATH

$ cat log.txt
abc [31/Oct/2015:20:36:51 +0900] ghi [20]
jkl [31/Oct/2015:20:36:55 +0900] pqr [0]
stu [31/Oct/2015:20:37:00 +0900] yz  [40]
000 [31/Oct/2015:20:37:05 +0900] ghi [5]
$ awk -F '[][]' '$4 >= 20' log.txt
abc [31/Oct/2015:20:36:51 +0900] ghi [20]
stu [31/Oct/2015:20:37:00 +0900] yz  [40]

異なるデミリタを指定して、要素を取り出す

$ cat log.txt
abc [31/Oct/2015:20:36:51 +0900] ghi [20]
jkl [31/Oct/2015:20:36:55 +0900] pqr [0]
stu [31/Oct/2015:20:37:00 +0900] yz  [40]
000 [31/Oct/2015:20:37:05 +0900] ghi [5]
$ awk -F '[][]' '{print $1}' log.txt
abc 
jkl 
stu 
000 
$ awk -F '[][]' '{print $2}' log.txt
31/Oct/2015:20:36:51 +0900
31/Oct/2015:20:36:55 +0900
31/Oct/2015:20:37:00 +0900
31/Oct/2015:20:37:05 +0900
$ awk -F '[][]' '{print $3}' log.txt
 ghi 
 pqr 
 yz  
 ghi 
$ awk -F '[][]' '{print $4}' log.txt
20
0
40
5

awkの編集結果をファイルにリダイレクトで出力して保存する

awkでリダイレクトを行う場合、たとえば「tail -F」などと組み合わせて利用する場合、単純に「>」で指定してもリダイレクトが行われない場合がある。

  • 例:tail -F で「/work/test」というファイルを常時監視し、その内容に日付を付け足して「/work/test_log」ファイルに出力しようとしている。
$ tail -F /work/test | awk '{ print strftime("%Y/%m/%d %H:%M:%S") " " $0 }' > /work/test_log

tail -Fとawkを組み合わせてファイルに出力する場合、以下のようにリダイレクトの前に「{ system (" ")}」と記述する必要がある。

$ tail -F /work/test | awk '{ print strftime("%Y/%m/%d %H:%M:%S") " " $0 } { system (" ") }' > /work/test_log

Excelのフィルタのように、ファイルから〇〇以上、〇〇以下で行を抽出する

$ awk '列 >= 条件 && 列 <= 条件' 対象ファイル
$ awk '列 == 条件' 対象ファイル
$ awk '列 ~ /条件/ 対象ファイル'
$ cat test.txt 
1 abc 10
2 def 15
3 ghi 0
$ awk '$3 >=5 && $3 <=10' test.txt
1 abc 10
$ awk '$2=="def"' test.txt
2 def 15
$ awk '$2 ~ /abc|ghi/' test.txt
1 abc 10
3 ghi 0

◯◯時からXX時までの間のログを抽出

$ awk -F - '"Apr  5 14:30:00" < $1 && $1 <= "Apr  5 15:00:00"' /var/log/messages

grepのように行を抽出

$ awk '/文字列/' 対象ファイル

任意の行を抽出

$ awk 'NR==行' 対象ファイル
$ awk 'NR==最初の行,NR==終わりの行' 対象ファイル

文字列で範囲を指定して出力

$ awk '/任意の開始文字列/,/任意の終了文字列/' 対象ファイル

例えば、ファイル2カラム目の「00:00:02」~「00:00:04」までを抽出する場合

$ awk '$2 >= "00:00:02" && "00:00:04" >= $2' /tmp/sample.log

複数条件(1行目、文字列XXを含む行を抽出とか)で抽出する場合は、awk内でifを使用できる。

$ awk '{if(条件1||条件2) print}' 対象ファイル # or条件
$ awk '{if(条件1&&条件2) print}' 対象ファイル # and条件

sed KnowHow

基本的な使い方

$ sed 's/置換前文字列/置換後文字列/g' ファイルPATH
$ コマンド | sed 's/置換前文字列/置換後文字列/g'

◯行目~◯行目にある特定の文字列を置換する場合

$ sed '4,8s/line/gyou/g' test.txt

特定の文字列を含む行のみ置換する場合

$ sed '/検索文字列/s/置換前文字列/置換後文字列/g'

その行に表示される◯番目の文字列を置換する

$ sed 's/置換前文字列/置換後文字列/何文字目か'

マッチした文字列を再利用する

1個だけと複数指定可能な2つの方法がある

1個だけの方法

置換後文字列の所に&を書くと、その部分がマッチした文字列に置き換わる。

$ echo "abc def hij klm" |sed -e 's/a[a-z]c/XXX & XXX/g'
XXX abc XXX def hij klm

複数指定可能な方法

\( と \) で囲んだものがマッチした場合、順番に \1 \2 \3 と指定できる。

$ echo "abc def hij klm" |sed -e 's/^\([a-z][a-z][a-z]\) \(d[a-z][a-z]\) \([a-z][a-z][a-z]\) \([a-z][a-z][a-z]\)/XXX \4 \3 \2 \1 XXX/g'
XXX klm hij def abc XXX

指定した行番号の箇所に行を挿入する

$ sed '◯i 挿入する行'
$ sed '4i testline' test.txt

指定した行の後ろに行を挿入する

$ sed '◯a 挿入する行'
$ sed '4a testline' test.txt

指定した複数の各行の前、後ろに行を挿入する

$ sed '◯,◯i 挿入する行' #前に挿入する場合
$ sed '◯,◯a 挿入する行' #後に挿入する場合

指定したキーワードを持つ行の前・後に行を挿入する

$ sed '/キーワード/i 挿入する行' #前に挿入する場合
$ sed '/キーワード/a 挿入する行' #後に挿入する場

◯行目~◯行目を削除する

$ sed '◯,◯d'
$ sed '1,4d' test.txt

決まったキーワードを持つ行を除外する

$ sed '/キーワード/d'
$ sed '/line2/d' test.txt

◯行目の内容を上書きする

$ sed '◯c 置き換え後の行'
$ sed '4c testline' test.txt

特定のキーワードを持つ行を上書きする

$ sed '/キーワード/c 置き換え後の行'
$ sed '/line3/c testline' test.txt

ファイルの内容を上書きする

$ sed -i '置換条件' ファイルPATH
$ sed 's/line 1/line #/g' test.txt

複数の置換条件を適用する

$ sed -e '置換条件' -e '置換条件' ...
$ sed -e 's/line/gyou/g' -e '5c aaaaaaa' test.txt

ファイルに書いた置換条件を読み込む

$ sed -f スクリプトファイルPATH
$ cat /root/sed_script
s/line/gyou/g
5c aaaaaaa
$ sed -f ./sed_script test.txt

小文字→大文字の変換をする

$ sed 's/\(.*\)/\U\1/'
$ sed 's/\(.*\)/\U\1/' test.txt

大文字→小文字の変換をする

$ sed 's/\(.*\)/\L\1/'
$ sed 's/\(.*\)/\L\1/' test.txt

ダブルコーテーション、シングルコーテーションに囲まれた文字列を抽出する

$ sed 's/^.*"\(.*\)".*$/\1/' # ダブルクォーテーションの場合
$ sed "s/^.*'\(.*\)'.*$/\1/" # シングルクォーテーションの場合

ちな、grepやawkでもできる

$ grep -Po '(?<=")[^",]+(?=")' # ダブルクォーテーション
$ grep -Po "(?<=')[^',]+(?=')" # シングルクォーテーション
$ awk -F'['\''"]' '{print $2}'

ダブルクォーテーションで囲まれた文字列に対して処理を行う

$ sed 's/"\([^"]*\)"/"置換後の値"/' 対象のファイルPATH
$ sed 's/"\([^"]*\)"/"replace"/' test.txt

シングルクォーテーションで囲まれた文字列に対して処理を行う

$ sed 's/"\([^"]*\)"/"置換後の値"/' 対象のファイルPATH
$  sed "s/'\([^']*\)'/'replace'/" test.txt

メールアドレスを『○○○@●●●●●』というようにマスキング置換する

$ sed "s/[^@ ]*@[^@]*\.[^@ ]*/○○○@●●●●●/g" ファイルPATH

sedで行頭の置換を指定する

$ sed 's/^置換前文字列/置換後文字列/g' ファイルPATH
$ sed 's/^test/aaaa/g' test.txt

sedで行頭以外の置換を指定する

$ sed 's/\([^^]\)置換前文字列/置換後文字列/g' ファイルPATH
$ sed 's/\([^^]\)test/aaaa/g' test.txt

指定したディレクトリ内のファイルを再帰的に置換する

$ sed 's/置換前/置換後/g' $(find 対象のフォルダPATH -type f)

特定の文字列~文字列間を置換する

$ sed '/文字列(開始)/,/文字列(終了)/s/○○○/●●●/g' 対象のファイルPATH
$ sed '/line2/,/line3/s/test/aaaa/g' test.txt  # line2とline3という文字の間にあるtestをaaaaに置換
$ sed '/line2/,/line3/caaaa' test.txt          # line2がある行とline3がある行、及びその間の行をaaaaに入れ替え

日本語(マルチバイト文字)のみを置換する

$ LANG=C sed 's/[\x80-\xFF]//g' ファイルPATH
$ sed 's/[\x80-\xFF]/●/g' test.txt

Other Command KnowHow

grep

or検索

$ grep -e 検索したい文字列1 -e 検索したい文字列2 検索したいテキストファイル

大文字・小文字を区別しない

$ grep -i 検索したい文字列 検索したいテキストファイル

検索にヒットする前後の行を出力

検索にヒットした前の行を出力する場合

$ grep 検索したい文字列 -B 出力したい行数 検索したいテキストファイル

検索にヒットした後の行を出力する場合

$ grep 検索したい文字列 -A 出力したい行数 検索したいテキストファイル

前後の行を同時に出力したい場合は、Cオプションを付与する。

$ grep 検索したい文字列 -C 出力したい行数 検索したいテキストファイル

検索にヒットする行数を取得する

$ grep -c 検索したい文字列 検索したいテキストファイル

ファイルの行番号を出力する

$ grep -n 検索したい文字列 検索したいテキストファイル

単語で検索する

例えばadで検索するとaddressやmadといった、別の意味を持つ単語も引っかかってしまうので

$ grep -w 検索したい文字列 検索したいファイル

複数のキーワードをパターン化したファイルを読込み、合致した行を出力する

$ grep -f 検索パターンを記述したファイル 検索したいファイル

検索キーワードを持つファイルのリストを取得する

$ grep -l 検索パターンを記述したファイル 検索したいフォルダ/*

find

ここ読んでよく勉強すること!!1

俺的備忘録 〜なんかいろいろ〜 - findコマンドで覚えておきたい使い方12個
http://orebibou.com/2015/03/find%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%A7%E8%A6%9A%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84%E4%BD%BF%E3%81%84%E6%96%B912%E5%80%8B/

date

今日を基準として、任意の時間経過された日時を求める

$ date
2015年 10月 31日 土曜日 21:29:13 JST
$ date -d '-1day'
2015年 10月 30日 金曜日 21:29:05 JST
$ date -d '1day'
2015年 11月  1日 日曜日 21:29:08 JST
$ date -d '1hour'
2015年 10月 31日 土曜日 22:31:33 JST
$ date -d '1month'
2015年 12月  1日 火曜日 21:31:38 JST
$ date -d '1year'
2016年 10月 31日 月曜日 21:31:42 JST

来月の月初、3ヶ月先の月末日付を求める

$ date +%Y/%m/%d
2015/10/31
$ date +%Y/%m/01
2015/10/01
$ date +%Y/%m/01 -d '+1 month'
2015/12/01
$ date +%Y/%m/%d -d "-1day `date +%Y%m01 -d '+4 month'`"
2016/02/29

閏年の判断

閏年じゃない場合はエラーになる。

$ date -d 2012-02-29 +%Y
2012
$ date -d 2013-02-29 +%Y
date: `2013-02-29' は無効な日付です

-f-とすれば標準入力から複数の日付を受け付けてくれる。
こうすれば閏年だけ抜き出せる。

$ seq 1900 2400 | sed 's/$/-02-29/' | date -f- +%Y 2> /dev/null

sort

バイト数(KB、MB、GB etc…)を並び替えする

$ du -h /home | sort -hr

uniq

重複した行・重複していない行だけを表示させる

重複している行のみを出力させる場合は「-d」を、重複のない(ユニークな)行をのみを出力させる場合は「-u」オプションを付与する。

$ cat test.txt | uniq -d
$ cat test.txt | uniq -u

watch

実行コマンドがエラーになった場合、watchコマンドを終了する

$ watch -e 実行コマンド

なお、「-e」の場合はコマンドの実行は停止するが何かキーを押下するまで画面はそのままとなる。

実行コマンドに変化があった場合、watchコマンドを終了する

$ watch -g 実行コマンド

xargs

基本的な使い方

以下のようにパイプでつなぐことで前のコマンド(コマンド1)で取得した値(標準出力)を利用してxargsで指定した別のコマンド(コマンド2)に引数として渡して実行させる事ができるコマンド

$ コマンド1 | xargs コマンド2

実行されるコマンド内容を表示させる

$ コマンド1 | xargs -t コマンド2

コマンドライン一行に引数をいくつ渡すか指定する

$ コマンド1 | xargs -n 引数の数 コマンド2

引数の値を明示的に利用する

$ コマンド1 | xargs -I{} コマンド2 {}

例えば「/work」フォルダ配下にあるファイルに対し、元のファイル名の後ろに「.bk」という文字列を付け足してコピーする場合

$ find /work -type f | xargs -t -I{} cp {} {}.bk

コマンドの実行をするかどうか確認する

$ コマンド1 | xargs -p コマンド2

複数プロセスを同時に実行させる

$ コマンド1 | xargs -P 最大プロセス数 コマンド2

引数の区切り文字を指定する

$ コマンド1 | xargs -d区切り文字 コマンド2

eval

変数の2重展開

#!/bin/sh
# 変数 VARに値が入力されている
VAR="test"

# 変数 EVAL_VARに、変数名である「VAR」という文字列を入力
EVAL_VAR="VAR"

# 変数 EVAL_VARを呼び出す
# echo $EVAL_VAR
eval echo '$'$EVAL_VAL

変数が配列の場合の2重展開

#!/bin/sh
# 変数「VAL」を配列として値を代入していく
VAL[0]="line 1"
VAL[1]="line 2"
VAL[2]="line 3"

# 変数 EVAL_VALに、変数名である「VAL」という文字列を入力
EVAL_VAL="VAL"

# 変数 EVAL_VALを呼び出す
eval echo '$'$EVAL_VAL

# 変数 EVAL_VALを配列として全行呼び出す
eval echo '${'$EVAL_VAL'[@]}'

# 変数 EVAL_VALを配列として[1]を呼び出す
eval echo '${'$EVAL_VAL'[1]}'

ssh

ローカルのbashの設定使ってssh接続

$ ssh -t user@host 'bash --rcfile <( echo ' $(cat ~/.bashrc ~/.bash_function_etc... | base64 ) ' | base64 -d)'

環境によってはbase64に-w0オプションが必要かも?

Dockerにローカルのbashrcやvimrcを使って接続

$ docker run --rm -it コンテナ bash -c '/bin/bash --rcfile <(echo -e '$(cat ~/.bashrc ~/.bash_function_etc... |base64)'|base64 -d)'

今のセッションの環境変数をそのまま使う場合は

$ docker run --rm -it コンテナ bash -c '/bin/bash --rcfile <(echo -e '$(cat <(set) <(alias)|base64 -w0)'|base64 -d)'

ローカルにあるスクリプトを配布せずにリモート先で実行させる

$ ssh リモート先のユーザ名@リモート先のホスト名(IPアドレス) 'sh ' < 実行させたいスクリプトのパス
$ 

直接リモート先のマシン上でコマンドを用いたsedを行わせる

$ sed ユーザ名@リモートホスト sed s/aaaaa/`hostname`/g 対象ファイル   # ローカル側でhostnameが実行される
$ sed ユーザ名@リモートホスト 'sed s/aaaaa/`hostname`/g' 対象ファイル # リモート側でhostnameが実行される

リモート側のファイルにローカルファイルの内容を追記する方法

$ ssh ユーザ名@リモートホスト "cat >> /追記するリモートファイルのパス" < /追記させるローカルファイルのパス
$ ssh test@192.168.0.240 "cat >> /tmp/test_remote.text" < test_local.txt

ローカル側のコマンドの実行結果(ローカル側のファイルに対し、sedを実行した後の内容を追記するなど)を追記する場合は、以下のようにする。

$ ローカル側で標準出力を行うコマンド | ssh ユーザ名@リモートホスト "cat >> /追記するリモートファイルのパス"
$ sed 's/test/AAAA/g' text_local.txt | ssh test@192.168.0.240 "

ssh経由でディレクトリにあるファイル一覧をdiffする

公開鍵認証じゃないとダメかもしれない

$ diff <(ssh ユーザ名@ホスト名 'find /確認するPATH -type f | sort') <(find /確認するPATH -type f | sort)
$ diff <(ssh test@192.168.0.240 find /work -type f | sort) <(find /work -type f | sort)

rsyncコマンドで代用できる

$ rsync --checksum --dry-run -rvce "ssh -p ポート番号" ユーザ名@ホスト名:/対象ディレクトリ /対象ディレクトリ

ssh経由でファイル内容をdiffする

$ diff <(ssh ユーザ名@ホスト名 'cd /確認するPATH; grep -Rn "" ./ | sort') <(cd /確認するPATH; grep -Rn "" ./ | sort)
$ diff <(ssh test@192.168.0.240 'cd /work;grep -Rn "" ./ | sort') <(cd /work;grep -Rn "" ./ | sort)

ローカルでファイル・フォルダを圧縮し、リモートでアーカイブファイルとして保持させる

$ tar zcvf - /アーカイブ化したいディレクトリのパス | ssh ユーザ名@リモートホスト "cat > /リモート側で作成するアーカイブ・ファイルのパス
$ tar zcvf - /wort_test | ssh test@192.168.0.240 "cat > /wort_test.tar.gz"

ローカルのアーカイブファイルをリモートで解凍する

$ ssh ユーザ名@リモートホスト "tar zxvf -C リモート側で展開させたいフォルダ -" < /リモート側で解凍させたいアーカイブファイルのパス
$ ssh test@192.168.0.240 "tar zxvf - -C /test1234" < test.tar.gz

リモートでファイル・フォルダを圧縮し、ローカルでアーカイブファイルとして保持させる

$ ssh ユーザ名@リモートホスト "tar -cf - /アーカイブ化したいディレクトリのパス" | gzip > /ローカル側で作成するアーカイブ・ファイルのパス
$ ssh test@192.168.0.240 "tar -cf - /work/test" | gzip > /root/test.tar.gz

リモートのアーカイブファイルをローカルで解凍する

$ ssh ユーザ名@リモートホスト "cat /ローカル側で解凍させたいアーカイブファイル" | tar zxvf - -C ローカル側で展開させたいフォルダ
$ ssh test@192.168.0.240 "cat /work.tar.gz" | tar zxvf - -C /test/

踏み台サーバ経由でログインを行う

$ ssh ユーザ名@接続先のホスト名(IPアドレス) -o 'ProxyCommand ssh 踏み台サーバのユーザ@踏み台サーバのホスト名 nc %h %p'
$ ssh test1@192.168.0.2-o 'ProxyCommand ssh test2@192.168.0.3 nc %h %p'

ssh接続時に直接コマンドを実行する

$ ssh ユーザ名@接続先のホスト名(IPアドレス) '接続先で実行させるコマンド'

sshでsuによるユーザ切り替え時に自動でパスワード入力をさせる

$ ssh login@host "su - user -c 'command' << EOF
password
EOF"

dd

バイナリファイルでのcutコマンド相当

$ cat file | dd bs=1 skip=100

バイナリファイルでのheadコマンド相当

固定長レコード限定

$ dd bs=[レコード長] count=[抜き出したい行数] skip=[読み飛ばしたい行数] if=[入力ファイル] of=[出力ファイル]

シェル芸

UPS友の会
https://www.usptomo.com/

上田ブログ
https://blog.ueda.asia/

シェル芸人達
https://daichkr.hatelabo.jp/collection/960679194075891200

日々是迷歩
http://papiro.hatenablog.jp/

slideshare シェル芸
http://www.slideshare.net/search/slideshow?searchfrom=header&q=%E3%82%B7%E3%82%A7%E3%83%AB%E8%8A%B8&ud=&ft=&lang=&sort=

Open usp Tukubai

基本的に、以下からダウンロードして展開すれば使える
https://uec.usp-lab.com/TUKUBAI/CGI/TUKUBAI.CGI?POMPA=DOWNLOAD

コマンドの説明は以下
https://uec.usp-lab.com/TUKUBAI_MAN/CGI/TUKUBAI_MAN.CGI?POMPA=LIST

Github - ShellShoccar-jpn/Parsrs
https://github.com/ShellShoccar-jpn/Parsrs

egzact

シェルの弱点を補おう!"まさに"なCLIツール、egzact
http://qiita.com/greymd/items/3515869d9ed2a1a61a49 https://github.com/greymd/egzact

エクシェル芸

俺的備忘録 〜なんかいろいろ〜 - Excelファイル(~.xls/~.xlsx)をLinuxコンソール上でCSV方式に変換する方法
https://orebibou.com/2016/12/excel%e3%83%95%e3%82%a1%e3%82%a4%e3%83%ab%ef%bd%9e-xls%ef%bd%9e-xlsx%e3%82%92linux%e3%82%b3%e3%83%b3%e3%82%bd%e3%83%bc%e3%83%ab%e4%b8%8a%e3%81%a7csv%e6%96%b9%e5%bc%8f%e3%81%ab%e5%a4%89%e6%8f%9b/

ワンライナー

アクセスログから、接続元IPをカウントし、合計で100回以上アクセスしているIPのみアクセスが多い順に表示する

cat /var/log/httpd/access_log* |   awk '/\"GET .*\" 200 / || /\"CONNECT .*\" 200 /   {cnt[$1]+=1};END{for (key in cnt)  {if(cnt[key] > 100)  {print cnt[key],key}}}' |  sort -nr

アクセスログから、リファラなしの閲覧先ドメインをカウントし、合計で100回以上アクセスがあるURLのみアクセスが多い順に表示する

cat /var/log/httpd/access_log*  |  awk -F\" '/GET .* 200 / || /CONNECT .* 200 /  {cnt[$2]+=1};END{for (key in cnt)  {if(cnt[key] > 100)  {print cnt[key],key}}}' |  sed -e 's% HTTP/...%%g' | sort -nr

シェル芸化

シェル芸の定義はこうだ。

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

では、やってみましょう。
全ての手続きを1行に。

1行化の基本

$ echo "a"
$ echo "b"
$ echo "c"

;で区切ることで1行化できる。

$ echo "a";echo "b";echo "c"

&& の利用

$ hoge ; date
-bash: hoge: コマンドが見つかりません
2016年  6月 19日 日曜日 11:01:32 JST

&&で区切ると、hogeが正常終了した場合のみ、後続のコマンドを実行する。
正常終了とは、コマンドの復帰値($?)が0の場合を指す。

$ hoge && date
-bash: hoge: コマンドが見つかりません

これは以下と等価である。

#!/bin/bash

hoge

RET=$?
if [ $RET -eq "0" ]
then
  date
else
  exit $RET
fi

|| の利用

$ hoge ; date
-bash: hoge: コマンドが見つかりません
2016年  6月 19日 日曜日 11:01:32 JST

||で区切ると、hogeが異常終了した場合のみ、後続のコマンドを実行する。
異常終了とは、コマンドの復帰値($?)が0以外の場合を指す。

$ hoge || date
-bash: hoge: コマンドが見つかりません
2016年  6月 19日 日曜日 11:17:16 JST

これは以下と等価である。

#!/bin/bash

hoge

RET=$?
if [ $RET -nq "0" ]
then
  date
fi

if文の1行化

これが

RET="0"
if [ $RET -eq "0" ]
then
  echo "true"
else
  echo "false"
fi

こうじゃ!

RET="0";if [ $RET -eq "0" ]; then echo "true"; else echo "false"; fi

RETの値を変えれば、分岐されていることを確認できる。

&& , || の利用

以下のように、&& や || を利用することもできます。

$ RET="0";[ $RET -eq "1" ] || echo "It work"
It work
$ RET="0";[ $RET -eq "0" ] && echo "It work"
It work

for文の1行化

これが

for I in {1..10}
do
  echo $I
done

こうじゃ!

for I in {1..10} ; do echo $I ; done

これでもいけるらしい

for I in {1..5};{ echo $I;}
for((;;)){ echo 1;sleep 1; } # 無限ループ

while文の1行化

これが

I=1
while [ $I -le 10 ]
do
  echo $I
  I=$((I+1))
done

こうじゃ!

I=1; while [ $I -le 10 ]; do echo $I;I=$((I+1)); done

case文の1行化

これが

case $I in
  a ) echo "a1"
      echo "a2";;
  b ) echo "b1"
      echo "b2";;
  * ) echo "default";;
esac

こうじゃ!

case $I in a ) echo "a1"; echo "a2";; b ) echo "b1"; echo "b2";; * ) echo "default";; esac

関数の1行化

これが

funcSample(){
echo "It work"
}
funcSample

こうじゃ!

funcSample(){ echo "It work"; };funcSample

シェル芸ブブ化

シェルスクリプトをシェル芸化(ワンライナー)するスクリプト

シェル芸ブブ化

Test/Lint

POSTD - Bashアプリケーションをテストする
http://postd.cc/bash%e3%82%a2%e3%83%97%e3%83%aa%e3%82%b1%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3%e3%82%92%e3%83%86%e3%82%b9%e3%83%88%e3%81%99%e3%82%8b/

ShellCheck - ShellCheck, a static analysis tool for shell scripts
http://www.shellcheck.net/ https://github.com/koalaman/shellcheck

Batsを使ったシェルスクリプトのテスト
https://rcmdnk.com/blog/2019/10/11/computer-github-bash/

pre-commitでShellCheckを使う
https://rcmdnk.com/blog/2023/01/09/computer-shell-python/

AWK commands equivalent to SQL query – Data manipulation
https://subhadip.ca/unixlinux/awk-commands-equivalent-to-sql-query-data-manipulation/4/

SQLのINNER JOINやLEFT OUTER JOINをawkで実現

シェルスクリプトにxUnitを使ってみる
https://qiita.com/filunK/items/aa067383aaa317594d17

Memo

sed

Qiita - sedのパターンスペース・ホールドスペースの動作を図で学ぶ
http://qiita.com/gin_135/items/773fec1343a69c9f90d6

俺的備忘録 〜なんかいろいろ〜 - sedのデバッグやHTML化ができる『sedsed』
https://orebibou.com/2016/11/sed%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E3%82%84html%E5%8C%96%E3%81%8C%E3%81%A7%E3%81%8D%E3%82%8B%E3%80%8Esedsed%E3%80%8F/

Misc

Defensive BASH Programming
http://www.kfirlavi.com/blog/2012/11/14/defensive-bash-programming

Perl1行野郎
http://web.archive.org/web/20020203071153/www13.cds.ne.jp/~ohsaru/perl/oneline.html

WICKED COOL SHELL SCRIPTS
http://www.intuitive.com/wicked/wicked-cool-shell-script-library.shtml

Bourne Shell自習テキスト
http://lagendra.s.kanazawa-u.ac.jp/ogurisu/manuals/sh-text/sh/

Qiita - richmikan@github
http://qiita.com/richmikan@github

glot.io - 様々な言語のコードスニペットが共有できるサイト
https://glot.io/

CMD-CHALLENGE - なんかCTFのようなシェル芸勉強サイト!!
https://cmdchallenge.com/

Thanks Driven Life - shellcheck を Docker で実行する
http://gongo.hatenablog.com/entry/2017/03/28/223757

Command Challenge - ★CTFみたい!!★
https://cmdchallenge.com/

explainshell.com - 難しめのシェル芸の解析
https://explainshell.com/

Yakst - 私が他人のシェルスクリプトから学んだこと
https://yakst.com/ja/posts/31

Qiita - bashの組込みコマンド自作によるスクリプトの高速化
https://qiita.com/satoru_takeuchi/items/7d424fa5ef1e33ace4df

Qiita - ネットワーク越しでパイプしたり、あらゆるデバイス間でデータ転送したい!
https://qiita.com/nwtgck/items/78309fc529da7776cba0

ble - Bashを動的にシンタックスハイライトする
https://github.com/akinomyoga/ble.sh

if 文と test コマンド | UNIX & Linux コマンド・シェルスクリプト リファレンス
https://shellscript.sunone.me/if_and_test.html

bashスクリプティング研修の資料を公開します
https://www.m3tech.blog/entry/2018/08/21/bash-scripting

set -eのもとで特定のコマンドの終了ステータスを変数に入れるシェルスクリプトのスニペット
https://gfx.hatenablog.com/entry/2021/12/15/153937

bash スクリプトの実行中上書き動作について
https://zenn.dev/mattn/articles/5af86b61004bdc

pure sh bible (📖 A collection of pure POSIX sh alternatives to external processes). - コマンドを使わずpure bashであれやこれやする一覧
https://github.com/dylanaraps/pure-bash-bible#get-the-directory-name-of-a-file-path

shell.how - インタラクティブにコマンド内容やオプションを解説してくれる
https://www.shell.how/

なぜシェルスクリプトで高度なデータ管理にSQLiteを使うべきなのか? ~ UNIX/POSIXコマンドの欠点をSQLで解決する
https://qiita.com/ko1nksm/items/33ab7ced0f9f8acdff28

onceupon/Bash-Oneliner
https://github.com/onceupon/Bash-Oneliner

シェルスクリプトの表示を豊かにするgumの使い方
https://dev.classmethod.jp/articles/eetann-gum-suburi/

GoogleのShell Style Guideの邦訳
https://qiita.com/yabeenico/items/72b904d4bb0b6d732a86

シェルスクリプトでgetoptsで解析する引数をポジショナルな引数と混合して使えるようにする
https://rcmdnk.com/blog/2023/11/01/computer-bash/

Bashの文字列で特殊文字を使う方法
https://azisava.sakura.ne.jp/programming/0010.html

謝辞

以下のサイトを中心に参考にさせて頂いております。
ありがとうございます。

日々是迷歩
http://papiro.hatenablog.jp/

俺的備忘録 〜なんかいろいろ〜
http://orebibou.com/

Qiita - 実用 awk ワンライナー
http://qiita.com/b4b4r07/items/45d34a434f05aa896d69

SlideShare - 検索:シェル芸
http://www.slideshare.net/search/slideshow?searchfrom=header&q=%E3%82%B7%E3%82%A7%E3%83%AB%E8%8A%B8

クリップボードから画像を追加 (サイズの上限: 100 MB)