シェルスクリプトの中でjoin()とsplit()相当の事をやる宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。2016このエントリは以下のエントリのフォローアップです。いう話(また、Qiitaにもクロスポストしています。-と覚えておくのが筆者的にはオススメです。JOINとかの方の文脈の「join」に対応する物です。この記事の話とは関係ないので、忘れて下さい。)以下、どういう場合にそれが言えるのかという事と、その理由の解説です。readのループで扱うのがベストプラクティスだ、と書きました。この前提の元では、データは基本的に行単位で加工する事になります。(自分は「初学者向けの覚えやすさ」と「メンテナンスのための読みやすさ」を重視してこのように考えていますが、処理効率の面ではベストとは言い切れません。readよりもxargsコマンドを使った方が高速だったりします。)実際、そのようなデータに対しては様々なコマンドが有効に利用できます。grepコマンドで条件に当てはまる項目を抽出する、または除外する。sedコマンドで行頭・行末の指定を含む正規表現での操作を各項目に行う。cutコマンドで各項目を切り分けて一部を取り出す。正規表現やawk等を駆使すればこういった操作も1行のままで行えるかもしれませんが、熟練が必要ですし、そのようなシェルスクリプトは達人でない一般のシェルスクリプト利用者には理解しにくいでしょう。1行のデータを複数行に分割する方法ただ、そうすると必然的に、処理対象にしたい一続きのデータを、行単位に分割したいというニーズが出てきます。これは、一般的なプログラミング言語で1つの文字列を特定の区切り文字で分割して配列にする、いわゆるsplit()の操作に相当します。'\n'というコマンド列があります。これは入力中に表れた区切り文字を\n、つまり改行文字に置き換えるという物です。逆操作のはずが逆操作にならない落とし穴ここからがこの記事のメインです。最初から改行区切りで与えられたデータや、split()相当のtrで改行区切りに展開されたデータについて、加工後に最終的に1行に結合したい、というニーズもあります。これは、一般的なプログラミング言語で1つの配列を特定の区切り文字で結合して1つの文字列にする、いわゆるjoin()の操作に相当します。......そう考えたあなた、惜しいです。確かにそれでうまくいくように思えますが、実はそれでは微妙に期待通りの結果にはならない場合があります。具体的には、この方法では入力データの最後にある改行文字までが置換されてしまいます。','a=1,b=2,c=3,c=3の後に余計な,が付いているのがわかります。前の例の逆操作なのになぜ元に戻らないのでしょうか。それは、前の例が実際には何をしていたのかをよく見ると分かります。'a=1,b=2,c=3'というコマンド列はechoに指定した内容を出力しますが、実はこの時echoコマンドの仕様として最後に必ず改行文字が付与されます。改行文字が改行と分かりやすいように⏎で示すと、この時trに渡されるのはa=1,b=2,c=3ではなく、最後に改行文字が加わったa=1,b=2,c=3⏎になっています。その上で,を改行文字に置換するので、最終的な結果はa=1⏎b=2⏎c=3⏎になります。これがコンソールに出力されて、a=1⏎b=2⏎c=3⏎と3行表示されていたのでした。今の例はここでもう1度改行文字を,に置換する操作なので、echoが付け足した最後の1つの改行文字までもが置換されるために、a=1⏎b=2⏎c=3⏎からa=1,b=2,c=3,になったというわけです。3要素の配列をjoin(',')で結合した場合には、こうはなりません。これは、trがあくまで文字置換のためのコマンドで、「行を結合する」ための物ではないから起こる現象です。最後の改行が失われる別の例として、区切り文字無しでの結合(join('')に相当)の場合も見てみましょう。█この時aaabbbcccの直後で改行されずにその次のプロンプト($)が表示されることに注目してください(実は、省略していましたが1つ前の例の場合もそうなります)。知らない人もいると思います(筆者も当初はそうでした)が、プロンプトは常に新しい行に表示されるとは限りません。この時のテキストファイルの内容は実際にはinput.txt(改行を「⏎」で示した物):aaa⏎bbb⏎ccc⏎となっています。最初のcatの実行後にプロンプトが次の行に表示されていたのは、catやBashがそうしているのではなく、単にcatが出力したデータの最後が改行だったから、そう表示されただけなのです。最後の改行がなくなるくらいはいいんじゃないの?と思うかもしれませんが、複数行の入力を扱う場面では気をつけないといけません。前の記事に出てきた「入力の各行をURLエンコードするシェル関数」では、入力を行ごとに区切った上で各行を加工していましたが、その過程で「1行が複数行になり、それをまた1行に戻す」という操作がありました。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このように各行の変換結果が1つに繋がってしまいます。nkfによって1行が複数行に分割された物を元の1行に結合するとき、最後の改行まで削除してしまったために、各行を区切る改行文字が無くなってしまったからです。これを防ぐには、各行の加工結果を出力した後で以下のように明示的に改行文字を付け足さないといけません。done}これなら、ちゃんと結果が各行に区切られて出力されます。上記のように「trで削除される改行文字を改めて出力して補う」方法もありますが、別解として、論理的な意味できちんと「複数行を1行にする」という方法があります。それを行うのがpasteコマンドです。pasteコマンドは複数の改行区切りのファイルを入力として受け付けて、それらを横方向に結合するコマンドです。具体的にはこんな風に使います。','での指定に従い、各列をカンマ区切りで示したデータ(CSV)が得られました。pasteコマンドのもう1つの機能は、行と列を入れ換えるというものです。-sオプションを指定すると、行として与えられた物が列になります。data.txt1,2,3,4おはよう,こんにちは,こんばんは,おやすみカンのいい方は気付いたかもしれませんが、入力が1ファイルだけだったり標準入力から与えられた内容だったりする場合、2つ目の操作は改行区切りのデータを任意の区切り文字で1行に結合する操作になります。4こちらの方法であれば、最後に余計な区切り文字が付く事も最後の改行が失われる事もありません(正確には、ここでの最後の改行はechoコマンドがやるのと同様、pasteコマンドが自動的に付与した物ですが)。これを使って先程の「与えた文字列をURLエンコードする関数」を書き直したのが、以下のコードです。実際、tweet.shではこの書き方を採用しています。-と書いていることに注意して下さい。''(区切り文字を空文字と指定し、入力ファイルの指定を省略)という書き方でも問題無く動くのですが、筆者が確認した限りではこの書き方はmacOSではエラーになってしまいました。これは、同じpasteという名前のコマンドでも、各ディストリビューションによって採用している実装が異なるためです。-(区切り文字を\0=null文字=文字が何もないことを指す文字と指定し、入力ファイルを-=標準入力と明示)という指定の仕方であれば、この3プラットフォームすべてで同じ結果を得られました。別の区切り文字も使えます。-を使う方法の方が好みです。「改行文字を削除(置換)して、結果的に結果が1行になる」のと「行と列を入れ換えて、指定の区切り文字で結合する」では、後者の方が論理的に意味が通るので誤解の余地がない。trを使う場合、trを実行したタイミングより後のどこかで改行文字を付け直さないといけないが、この両者が別々になる事が好きではない。ワンセットで必要な事をやっている箇所同士が不必要に離れていると、コードの可読性が落ちる。trを使っている行だけを何らかの理由でコメントアウトや削除すると、各行に余計な改行文字が追加されるようになってしまう。消すなら両方をワンセットで扱わないといけないのだが、消すべきコマンド同士が分散していると見落とす。自分が気付いてないだけで、pasteを使う方法にもデメリットはありそうな気がします。'\0'とnull文字を区切り文字に指定したり-を入力ファイルに指定したりしないとmacOSのpasteでは期待通りの結果にならないという事は、指摘を受けるまで気付いていませんでした。皆さんのご意見はどうでしょうか。......と、オープンクエスチョンにしたままで投げ出してこの記事は終わりとしたいと思います。atの末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。「noda」なら、「2016-12-15_use-paste-as-join.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。