glueパッケージでスクリプトを生成する

Rのglueパッケージを使って、他の言語の一部分だけ変更したスクリプトを生成する。
R
glue
Shell
Published

August 18, 2024

Modified

August 27, 2024

Rのglueパッケージを使うと文字列のプレースホルダをRの式を評価した結果に置き換えることができる。 これを利用して、他の言語のスクリプトで変更したい部分をプレースホルダにして、 glue::glue()で一部分だけ変更したスクリプトを簡単に生成することができる。

Interpreted String Literals

An implementation of interpreted string literals, inspired by Pythons Literal String Interpolation <https://www.python.org/dev/peps/pep-0498/> and Docstrings <https://www.python.org/dev/peps/pep-0257/> and Julias Triple-Quoted String Literals <https://docs.julialang.org/en/v1.3/manual/strings/#Triple-Quoted-String-Literals-1>.

https://glue.tidyverse.org/

Bash scriptの生成

Rでスクリプト(ここではBash)を生成したい時というのは、 多くの場合複数のファイルや処理時のパラメータが存在するときである。

Bashスクリプトだけで、多数のファイルに対して同じ処理をするようなスクリプトを記述することも可能だが、 Rに比べるとBashスクリプトの機能は弱くファイル名や文字列処理が難しいという欠点がある。 また、文字列操作やファイル名操作、繰り返し処理をBashスクリプトで実装するための文法を追加で覚える労力が必要となる。

RでBashスクリプトを生成するようにすれば、 ファイルや文字列操作に使い慣れたRの機能やパッケージを利用することができ、 また覚えるBashスクリプトの文法も最小限ですむという利点がある。

複数のcsvファイルについてBashで何らかの処理を行いたい場合を考えてみよう。 まずは、csvファイルを用意する。 RのirisデータをSpecies列の値("setosa", "versicolor", "virginica")で分割し、 それぞれcsvファイルに書き出す。

library(magrittr)

iris %>%
  split(.$Species) %>%
  purrr::iwalk(~ readr::write_csv(.x, glue::glue("{.y}.csv")))

fs::dir_tree(regexp = ".csv$")
.
├── setosa.csv
├── versicolor.csv
└── virginica.csv

csvファイルに書き出すことが出来たので、これらのcsvファイルそれぞれについてBashでデータ処理を行うことを考えよう。 今回は、awkコマンドを使ってそれぞれのcsvファイルについて、 一列目 Sepal.Lengthと2列目 Sepal.Widthの平均値を計算して出力するような処理を行う。 glue::glue()を使ってBashスクリプトを生成してみよう。 ちなみに、シェルスクリプトではバックスラッシュ\をよく使う場合があるが、 その様な時はRのraw stringを使うと良い。 raw stringは"なにかの文字列"のように囲む代わりに、r"(なにかの文字列)"の様に囲むことで作成できる。 (raw stringについて)

# 1. scriptの最初の処理を`header`として保存する。
header <-
r"(#!/bin/bash
set -euC

echo "Process started!"
echo ""
)"
# 2. scriptの最後の処理を`footer`として保存する。
footer <-
r"(echo "All finished!"
)"
# 3. scriptのテンプレートを作成する。プレースホルダ `{label}`の部分を置換する。
#    `awk`のスクリプト部分で`{}`を使う必要があるので、その部分はエスケープするために二重に囲う
cmd_template <-
r"(echo "Processing: '{label}'"
cat {label}.csv | \
  awk -F, 'NR > 1 {{
  sum1 += $1
  sum2 += $2
  }} END {{
   print "Avg. Sepal.Length:", sum1/(NR-1)
   print "Avg. Sepal.Width :", sum2/(NR-1)
  }}'
echo ""
)"

# 4. 先ほど作成したcsvファイルのファイル名(拡張子以外)を`labels`として、
#    文字列ベクトルに入れる。
labels <-
  fs::dir_ls(regexp = ".csv$") %>%
  fs::path_file() %>%
  stringr::str_remove(".csv")

# 5. `label`変数を`labels`の各要素に順番に変更しながら、
#    `glue::glue()`で`cmd_template`を置換して、
#    `cmd_temp`ベクトルの要素として追加していく。
cmd_temp <- character()
for(label in labels) {
  cmd_temp <- c(cmd_temp, glue::glue(cmd_template, .trim = FALSE))
}

# 6. `header`, `cmd_temp`, `footer`を併せて、
#    スクリプトファイル`temp.sh`に書き込む。
c(header, cmd_temp, footer) %>% readr::write_lines("temp.sh")

上記のRのコードから出力されたBashスクリプトは以下の様になる。

temp.sh
#!/bin/bash
set -euC

echo "Process started!"
echo ""

echo "Processing: 'setosa'"
cat setosa.csv | \
  awk -F, 'NR > 1 {
  sum1 += $1
  sum2 += $2
  } END {
   print "Avg. Sepal.Length:", sum1/(NR-1)
   print "Avg. Sepal.Width :", sum2/(NR-1)
  }'
echo ""

echo "Processing: 'versicolor'"
cat versicolor.csv | \
  awk -F, 'NR > 1 {
  sum1 += $1
  sum2 += $2
  } END {
   print "Avg. Sepal.Length:", sum1/(NR-1)
   print "Avg. Sepal.Width :", sum2/(NR-1)
  }'
echo ""

echo "Processing: 'virginica'"
cat virginica.csv | \
  awk -F, 'NR > 1 {
  sum1 += $1
  sum2 += $2
  } END {
   print "Avg. Sepal.Length:", sum1/(NR-1)
   print "Avg. Sepal.Width :", sum2/(NR-1)
  }'
echo ""

echo "All finished!"

このようにglue::glue()を用いると、 複数のファイルに対して同じ処理を行う(Bash)スクリプトを手軽に作成することができる。

実際にtemp.shを実行してみた結果が以下になる。

bash temp.sh
Process started!

Processing: 'setosa'
Avg. Sepal.Length: 5.006
Avg. Sepal.Width : 3.428

Processing: 'versicolor'
Avg. Sepal.Length: 5.936
Avg. Sepal.Width : 2.77

Processing: 'virginica'
Avg. Sepal.Length: 6.588
Avg. Sepal.Width : 2.974

All finished!

Sessioninfo

sessionInfo()
R version 4.3.2 (2023-10-31)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Asia/Tokyo
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] magrittr_2.0.3

loaded via a namespace (and not attached):
 [1] crayon_1.5.2      vctrs_0.6.5       cli_3.6.3         knitr_1.48       
 [5] rlang_1.1.4       xfun_0.46         stringi_1.8.4     purrr_1.0.2      
 [9] jsonlite_1.8.8    glue_1.7.0        bit_4.0.5         htmltools_0.5.7  
[13] hms_1.1.3         fansi_1.0.6       rmarkdown_2.25    evaluate_0.24.0  
[17] tibble_3.2.1      tzdb_0.4.0        fastmap_1.1.1     yaml_2.3.9       
[21] lifecycle_1.0.4   stringr_1.5.1     compiler_4.3.2    fs_1.6.3         
[25] htmlwidgets_1.6.4 pkgconfig_2.0.3   digest_0.6.34     R6_2.5.1         
[29] tidyselect_1.2.1  readr_2.1.5       utf8_1.2.4        parallel_4.3.2   
[33] vroom_1.6.5       pillar_1.9.0      tools_4.3.2       bit64_4.0.5