Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

この世には、引用符(クォート、クォーテーション)には種類があり、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
}
Riliumph
しがないC++/Pythonが大好きなエンジニア。 Java(Servlet/JSP), HTML5, C/C++(with Boost), Python3, C#とか色々仕事してきました。 今は組み込みC言語やってます。 もっと羽振りのいい会社に行きたいよね(´・_・`)ハァー
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away