LoginSignup
49
52

More than 1 year has passed since last update.

[Bash] クォートの種類と変数への格納

Last updated at Posted at 2017-02-18

この世には、引用符(クォート、クォーテーション)には種類があり、Bashの世界ではその意味が異なります。
その問題にぶち当たったので、少し記事にしてみようと思います。

ことの発端


pecoを使ってbash_historyから履歴を漁るコマンドを改良していた時、どうしてもエスケープが必要だったのでJavaのお仕事で培ったシングルクォートとダブルクォートを両方使うという荒業をしたところ巧く動作しなかったのが発端です。

私は土日を挟むと金曜日以前のことが思い出せない病気を患っているので、処理毎に関数や変数にまとめて名前を付けるクセがあります。
また、コメントは冗長と思っていて、メンテする気が失せるのでドキュメントコメントしか書きません。
sedコマンドはうまくエスケープできたのですが、(うまく実行できたとは言っていない)
awkコマンドをダブルクォートでエスケープすると、vimのハイライトがおかしくなってしまいました。

filtered_history.sh
filtered_history(){
  local reverse_order
  if which tac &> /dev/null; then
    reverse_order='tac'
  else
    reverse_order='tail -r'
  fi
  local trim_line_number='sed -re "s/^\s+[0-9]+\s+//"'
  local trim_duplication="awk '!dictionaty[$0]++'" # <- こいつが問題
  local CMD=$(\history \
              | $reverse_order \
              | $trim_line_number \
              | $trim_duplication \
              | peco --query "${READLINE_LINE}")
  READLINE_LINE="${CMD}"  # Input to terminal's readline
  READLINE_POINT=${#CMD}  # Set cursor
}

このコマンドで実験


sample.sh
#!/bin/bash -eu

string='Hello World!!'
echo "${string}" # ダブルクォート
echo '${string}' # シングルクォート
echo `${string}` # バッククォート
実行結果
$ ./sample.sh
$ Hello World!!
$ ${string}
$ ./sample.sh: 行 6: Hello: コマンドが見つかりません

なにやら違いがありそうですね……

ダブルクォート


sample.sh
echo "${string}" # ダブルクォート
$ Hello World!!

**変数が展開**されて、正しくHello World!!が出力されました。
ああ、それでawkには、関数の$0(関数名)が渡っていたんですねぇ……

シングルクォート


実行結果
echo '${string}' # シングルクォート
$ ${string}

あれ!?echoで渡した引数がそのまま出力されてしまっています。
どうやら、シングルクォートで囲った内容は**ただの文字列**として認識されるようです。

バッククォート


「バッククォートなんて打ったことないぞい」という方は、JISキーボード(?)ならShift+@で入力できます。

実行結果
echo `${string}` # バッククォート
$ ./sample.sh: 行 6: Hello: コマンドが見つかりません

なにやら怒られてしまいました。
「Hello」をコマンド、「World!!」を引数と認識して解決しようとしたが、Helloなるコマンドが見つからなかったようです。
バッククォートで囲った内容は**コマンド**として認識するようです。

補足:Google Shell Style Guide

Command Substitution
link ▽ Use $(command) instead of backticks.
Nested backticks require escaping the inner ones with . The $(command) format doesn't change when nested and is easier to read.
Example:

# This is preferred:
var="$(command "$(command1)")"
# This is not:
var="`command \`command1\``"

(テキトー訳)
バッククォートの代わりに$(command)を使うように。
バッククォートを入れ子にすると、内側のモノをエスケープ処理する必要があります。$(command)の書式では、ネストしたときに変更されませんし、読み易いですね。

Googleのスタイルガイドによると、「ネスト大変だし、読みにくいし、素直に$(command)書式を使おうね」とあります。
私は視力が悪くて、バッククォートのネストどころか、シングルクォートとの違いが見えないので、$(command)書式を使います。

エスケープ


調べると、シングルクォートのエスケープは'\'を使うらしいです。
関数を書き換えます!

(前略)
  local trim_duplication='awk '\''!dictionaty[$0]++'\''' # <- 綺麗にハイライトされた。やったぜ!!(星矢風)
(後略)

関数を実行!


$ filtered_history
sed: -e expression #1, char 1: 不明なコマンド: `"'
awk: 1: unexpected character '''
No buffer to work with was available

OH MY GOD!!って感じですね。
エスケープしたsedもawkもうまくいってやがりません。

変数内のコマンドを使うには?


evalコマンドを使えば解決しました。
こうなります。

filtered_history.sh
filtered_history(){
  local reverse_order
  if which tac &> /dev/null; then
    reverse_order='tac'
  else
    reverse_order='tail -r'
  fi
  local trim_line_number='sed -re "s/^\s+[0-9]+\s+//"'
  local trim_duplication="awk '!dictionaty[$0]++'" # <- こいつが問題
  local CMD=$(\history \
              | eval $reverse_order \
              | eval $trim_line_number \
              | eval $trim_duplication \
              | peco --query "${READLINE_LINE}")
  READLINE_LINE="${CMD}"  # Input to terminal's readline
  READLINE_POINT=${#CMD}  # Set cursor
}
49
52
1

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
49
52