LoginSignup
2814
2940

More than 3 years have passed since last update.

プログラマーの君! 騙されるな! シェルスクリプトはそう書いちゃ駄目だ!! という話

Last updated at Posted at 2016-12-07

SoftwareDesign 201812


Shell Script Advent Calendar52Piro

BashTwitter



使

 

BashTwitterTwitter bot使


使






PromiseDeferred
















Node.jsRubyPython


ImageMagickcurlRESTfulWeb API1

sed

Twitter bot使


JavaScript1RubyJS 





使使

使
pipeline.png


使使使使

TwitterURL2
シェルスクリプトでの実装
url_encode() {
  nkf -W8MQ |
    sed 's/=$//' |
    tr '=' '%' |
    paste -s -d '\0' - |
    sed -e 's/%7E/~/g' \
        -e 's/%5F/_/g' \
        -e 's/%2D/-/g' \
        -e 's/%2E/./g'
}
実行例
$ echo "こんにちは" | url_encode
%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF

echonkf

sed3

JavaScript


JavaScript1
引数と戻り値で愚直にデータを引き回した例
function url_encode(input) {
  return sed(
    paste(
      tr(
        sed(
          nkf(input, '-W8MQ'), // ここから評価される
          's/=$//'
        )
        '=', '%'
      ),
      '-s', '-d', '\0'
    ),
    '-e', 's/%7E/~/g',
    '-e', 's/%5F/_/g',
    '-e', 's/%2D/-/g',
    '-e', 's/%2E/./g'
  );
}

関数の戻り値を次の関数に渡すという事を愚直にやると、最初にやって欲しい処理ほど内側に書く必要があって、こんな感じでどんどんネストが深くなってしまいます。

行う処理の順番通りに読めるように書くなら、変数を使って以下のように書くことになるでしょう。

行う処理の順番通りにコードを書いた例
function url_encode(input) {
  input = nkf(input, '-W8MQ');
  input = sed(input, 's/=$//');
  input = tr(input, '=', '%');
  input = paste(input, '-s', '-d', '\0');
  input = sed(input, 
              '-e', 's/%7E/~/g',
              '-e', 's/%5F/_/g',
              '-e', 's/%2D/-/g',
              '-e', 's/%2E/./g');
  return input;
}

各機能をメソッドチェーンで呼べるようになっていたら、以下のように書けるかもしれません。

何か便利なライブラリを使って、メソッドチェーンで処理を書くようにした例
function url_encode(input) {
  // createStringStream() の戻り値は、
  // 各関数名のメソッドを持つオブジェクトとする。
  // 渡されたデータは内部で保持されていて、
  // メソッドを呼ぶ度にそれが加工されていく。
  return createStringStream(input).
           nkf('-W8MQ').
           sed('s/=$//').
           tr('=', '%').
           paste('-s', '-d', '\0').
           sed('-e', 's/%7E/~/g',
               '-e', 's/%5F/_/g',
               '-e', 's/%2D/-/g',
               '-e', 's/%2E/./g');
}

実際にこういう事をやるためには、ここで仮定したcreateStringStream()のような機能を提供するライブラリが必要になります。事実、「ファイルの内容に対してこのように処理を適用していく」という場面を想定しているGulp.jsでは、その名もpipeという名前のモジュールを使ってこれに近い書き方をするようになっています。

シェルスクリプトらしくない書き方

むしろシェルスクリプトでは、変数にいちいち結果を代入する方が面倒です。

シェルスクリプトで、最初と同じことを引数と変数を使ってやった例
url_encode() { # 引数で文字列が渡されると仮定
  local input="$*"
  input="$(echo "$input" | nkf -W8MQ)"
  input="$(echo "$input" | sed 's/=$//')"
  input="$(echo "$input" | tr '=' '%')"
  input="$(echo "$input" | paste -s -d '\0' -)"
  input="$(echo "$input" | sed -e 's/%7E/~/g' \
                               -e 's/%5F/_/g' \
                               -e 's/%2D/-/g' \
                               -e 's/%2E/./g')"
  echo "$input"
}
実行例
$ url_encode "こんにちは"
%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF

変数への代入とコマンド置換で同じような記述が何度も出てきてうんざりですね。ここまでやるなら、最初に1回だけechoで文字列を出力して、後はパイプラインにした方がずっと簡単です。

最初のデータの受け取りだけ引数を使った例
url_encode() { # 引数で文字列が渡されると仮定
  local input="$*"
  echo "$input" |
    nkf -W8MQ |
    sed 's/=$//' |
    tr '=' '%' |
    paste -s -d '\0' - |
    sed -e 's/%7E/~/g' \
        -e 's/%5F/_/g' \
        -e 's/%2D/-/g' \
        -e 's/%2E/./g'
}

45JS

 echocat


sedgreptr



便echocatechocat6

cat
ヒアドキュメントの内容を標準出力に出力する関数の例
common_params() {
  cat << FIN
oauth_consumer_key $CONSUMER_KEY
oauth_nonce $(date +%s%N)
oauth_signature_method HMAC-SHA1
oauth_timestamp $(date +%s)
oauth_token $ACCESS_TOKEN
oauth_version 1.0
FIN
}

繰り返し処理は配列ではなくイテレータで






Bashbash-oo-framework

Bash使 Linux67TwitterTwitter bot使

whileread -rwhileread -r
read.png

RubyJavaScript


whileread -r

Rubyeach
Rubyでのイテレータを使った繰り返し処理
uris = [
  "http://www.example.com/",
  "http://www.example.net/",
  "http://www.example.jp/",
]
for uri in uris do
  p uri
end

ES2015などのモダンな仕様のJavaScriptでも似たような書き方ができます。

JavaScriptでのイテレータを使った繰り返し処理
uris = [
  "http://www.example.com/",
  "http://www.example.net/",
  "http://www.example.jp/"
];
for (let uri of uris) {
  console.log(uri);
}

ところで、JavaScriptのループといえば以下のような書き方もあります。上記の記法が導入される前からJavaScriptを使ってる人なら、こっちの方が馴染みがあるのではないでしょうか。

JavaScriptでのイテレータを使わない繰り返し処理
for (let i = 0; i < uris.length; i++) {
  console.log(uris[i]);
}

23使

23使


URL使
複数行の入力を受け取って、各行をURLエンコードして出力するシェル関数の実装
url_encode() {
  while read -r line
  do
    echo "$line" |
      nkf -W8MQ |
      sed 's/=$//' |
      tr '=' '%' |
      paste -s -d '\0' - |
      sed -e 's/%7E/~/g' \
          -e 's/%5F/_/g' \
          -e 's/%2D/-/g' \
          -e 's/%2E/./g'
  done
}

関数の標準入力にパイプライン経由で渡されてきたデータをwhile read -r lineで1行ずつ8取り出して、echo "$line"でエンコード処理を行い標準出力に出力しています。これによって、このシェル関数は入力行すべてをきちんとエンコードできるようになっています。

複数行の入力に対する実行例
$ cat data.txt
おはよう
こんにちは
こんばんは
おやすみ
$ cat data.txt | url_encode
%E3%81%8A%E3%81%AF%E3%82%88%E3%81%86
%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF
%E3%81%93%E3%82%93%E3%81%B0%E3%82%93%E3%81%AF
%E3%81%8A%E3%82%84%E3%81%99%E3%81%BF

11while read -r




readreadwhileTwitterUser streams API
Twitterの新しい通知を待ち受けて処理する例
watch_twitter_events() {
  curl --get ... https://userstream.twitter.com/1.1/user.json |
    while read -r event
    do
      # eventには1つ1つのツイートやイベントを表すJSON文字列が格納されている。
    done
}
watch_twitter_events & # 末尾に「&」を付けて関数を実行すると、子プロセスで非同期に実行される。

使11Slackslackcat11while read -r
Slackのチャンネルの新しい発言を待ち受けて処理する例
watch_slack_channel() {
  slackcat --channel team:general --stream --plain |
    while read -r post
    do
      # postには1つ1つの発言が格納されている。
    done
}
watch_slack_channel &

シェルスクリプトやシェル関数では、readをイテレータ的に使うことによって、データが静的な物だろうが動的に生成(返却)されるものだろうが全く等価に扱えるということをお分かり頂けるでしょう。

for ... inループは標準入力から受け取るデータには使わない

ところで、シェルスクリプトでループというとこんな書き方もあります。

シェルスクリプトでのforループ
for host in fileserver mailserver authserver
do
  scp /path/to/file uploader@$host:~/
done

for ...in

使cat
シェルスクリプトでのforループで標準入力を処理する例
for line in $(cat)
do
  # 各行に対する処理
done




(一) API使

(二)cat

(三) for9


使使使3forwhile read使
関数に渡された標準入力を一旦蓄えて、複数回使う例
url_encode() {
  input="$(cat)"
  echo "  -------------------" 1>&2
  echo "  Input:" 1>&2
  echo "$input" | sed 's/^/  /' 1>&2
  output="$(echo "$input" |
    while read -r line
    do
      echo "$line" |
        nkf -W8MQ |
        sed 's/=$//' |
        tr '=' '%' |
        paste -s -d '\0' - |
        sed -e 's/%7E/~/g' \
            -e 's/%5F/_/g' \
            -e 's/%2D/-/g' \
            -e 's/%2E/./g'
    done)"
  echo "  -------------------" 1>&2
  echo "  Output:" 1>&2
  echo "$output" | sed 's/^/  /' 1>&2
  echo "  -------------------" 1>&2
  echo "$output"
}
実行例
$ cat data.txt
おはよう
こんにちは
$ cat data.txt | url_encode
  -------------------
  Input:
  おはよう
  こんにちは
  -------------------
  Output:
  %E3%81%8A%E3%81%AF%E3%82%88%E3%81%86
  %E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF
  -------------------
%E3%81%8A%E3%81%AF%E3%82%88%E3%81%86
%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF

catinputecho使


使使

1
改行区切りのテキストを受け取って、すべてをURLエンコードして返すJavaScriptの関数の実装
function url_encode(input) {
  var lines = input.split('\n');
  var encodedLines = lines.map(function(line) {
    line = nkf(line, '-W8MQ');
    line = sed(line, 's/=$//');
    line = tr(line, '=', '%');
    line = paste(line, '-s', '-d', '\0');
    line = sed(line, 
               '-e', 's/%7E/~/g',
               '-e', 's/%5F/_/g',
               '-e', 's/%2D/-/g',
               '-e', 's/%2E/./g');
    return line;
  });
  return encodedLines.join('\n'); // ←ここで初めて値が返される
}

この発想のままシェルスクリプトを書くと、こんな風になります。

JavaScriptと同じ発想のシェルスクリプトの場合
url_encode() {
  result=''
  while read -r line
  do
    # 結果の文字列を収集
    result="${result}\n$(echo "$line" |
      nkf -W8MQ |
      sed 's/=$//' |
      tr '=' '%' |
      paste -s -d '\0' - |
      sed -e 's/%7E/~/g' \
          -e 's/%5F/_/g' \
          -e 's/%2D/-/g' \
          -e 's/%2E/./g')"
  done
  echo -e "$result" # ←ここで初めて結果が標準出力に出力される
                    #   (echoで「\n」を改行として出力するには
                    #     「-e」オプションを付ける必要がある)
}

1
シェルスクリプトらしい書き方の場合
url_encode() {
  while read -r line # ←標準入力からデータが渡ってくる度にループが1回実行される
  do
    echo "$line" |
      nkf -W8MQ |
      sed 's/=$//' |
      tr '=' '%' |
      paste -s -d '\0' - |
      sed -e 's/%7E/~/g' \
          -e 's/%5F/_/g' \
          -e 's/%2D/-/g' \
          -e 's/%2E/./g' # ←ループが回る度に毎回ここで結果が標準出力に出力される
  done
}

2

10while read
非同期処理との組み合わせ
list_tweet_bodies() {
  curl --get ... https://userstream.twitter.com/1.1/user.json |
    while read -r tweet # ←Web APIからデータが1件届く度にループが1回実行される
    do
      echo "$tweet" |
        jq -r .text |
        url_encode # ←ループが回る度に毎回ここで結果が標準出力に出力される
    done
}

list_tweet_bodies |
  url_encode |
  sed ... # Web APIからデータが1件届く度にパイプラインを通じてデータが流れてくる

使

1


pipeline.png 1cat

使


便gemnpm

1111slackcatgem11使

使使使--helpman使

使


sourceTwitter bot1sourceRubyrequireincludeMix-in

source

source

Twitter botTwitter使


使

112bash-oo-frameworkGUI




ifcase13

TwitterTwitterTweetDeckTwitter1






使


TwitterAPItwurlSlackAPIslackcat






14



調JavaScriptJSJS使

使使

2019120







  

:   



xargs

202156


xargs使130JSRubyGo

NSISLogicLib使ifNSIS




  256

sed


使




while read line  使 Linux1

2011LinuxSSHwhileread

1

Twitter使bot15WebAdvent Calendar

Shell Script Advent Calendar8


使join()split()


  1. WebNode.jsFirefoxThunderbird 



    1 



     



     



     



    cat使cat使 << cat 



    使使xargs 



    read-r-r\n11readIFS= read -r 



    使 



    echoechoecho -e使  



    slackcat使SlackAPIHTTPAPIWebSocketslackcatAPI 



    Bash使while使 



    1 6 



     



     
2814
2940
18

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

2814
2940