\newenvironment 中の filecontents のファイル名の指定

\newenvironment 中の filecontents のファイル名の指定

- Beginner Beginner の投稿
返信数: 13
環境の中身を別ファイルに書き出したいと思い、以下のようなコードを書いたのですが、期待通りに動きません。
以下で \input{\filename} を入れなければコンパイルはできますが、当然なにも表示されません。
\input{\filename} を入れると、ファイルは作成されるものの、コンパイルができません。
(TeXLive 2019 の pdflatex を使用しています。)

より具体的には、

./output/q-hoge-fuga.tex が無い状態で実行すると、

LaTeX Warning: Writing file `././output/q-hoge-fuga.tex'.

(./output/q-hoge-fuga.tex
! Missing number, treated as zero.
<to be read again>
\protect
l.1 %% LaTeX2e file `./output/q-hoge-fuga.tex'

となります。
この状態でもう一度、pdflatex を実行すると、

LaTeX Warning: File `./output/q-hoge-fuga.tex' already exists on the system.
Not generating it from this source.

(./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex
(./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex
(./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex
(./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex
(./output/q-hoge-fuga.tex (./output/q-hoge-fuga.tex
! TeX capacity exceeded, sorry [text input levels=15].
<argument> "./output/q-hoge-fuga.tex"

l.1 %% LaTeX2e file `./output/q-hoge-fuga.tex'

となります。 (再帰的に読み込もうとしている?)

どのようにすれば意図通りの動作になりますでしょうか?


\documentclass{article}
\usepackage{xstring}

\begin{document}

\def\outdir{./output/}
\def\mkfilename#1{
\StrSubstitute{#1}{:}{-}[\filename]
\edef\filename{\outdir\filename.tex}
% 結果を変数に格納する
% (できれば文字列を返し、\newenvironment の中で、\def\filename{\mkfilename{#1}} とかしたい)
}

% 第一引数は xx:yy:zz のような形式のラベル。
% xx 等には特殊な文字や空白などは入らないと仮定。
% この文字列中の : を - に変えたファイル名でファイル作成したい。
% (変更前の文字列は、tcolorbox でラベルを付けるために使用)
\newenvironment{question}[2]{%
\mkfilename{#1}
\csname filecontents\endcsname{\filename}%
%
% \begin{filecontents}{\filename}%
% だと上手くいかない
%
% 後でここに \begin{tcolorbox} を追加したい
% (#2 は tcolorbox 用)
}{% 後でここに \end{tcolorbox} を追加したい
%
% \end{filecontents} % だと上手くいかない
\csname endfilecontents\endcsname%
\input{\filename}% (*1) これが上手くいかない <-----
}

% ./output/q-hoge-fuga.tex としてファイルを作成したい
\begin{question}{q:hoge:fuga}{Title}
$E = mc^{2}$
\end{question}

\end{document}


追加の質問 (こちらは無視していただいても結構です):

上記コードで \begin{filecontents}{\filename}, \end{filecontents} だと以下のようなエラーが出るのですが、
なぜなのでしょう?
(上のコードは検索で出てきたものを参考に書きました)。

LaTeX Warning: Writing file `././output/q-hoge-fuga.tex'.

)
Runaway argument?
! File ended while scanning use of ^^M.
<inserted text>
\par
<*> q.tex

Beginner Beginner への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 本田 知亮 の投稿
検証するのに邪魔だったので
\outdirは空っぽにしています.

また,そのままだと
question環境の中身が変わっても
上書きされずに更新されないので
上書き許可にしています.

修正点は
\bgroupと\egroupを追加したこと

何度も読み込むことになった原因は
たぶんカテゴリーコードの変更で
おかしなことになっている,
書き出されるファイルに毎回
\inputが入り込んだり
ファイルの上書きとかが起こったり
妙な状況になってるんでしょう.

ですので
filecontentsが変更する
カテゴリーコードがもとに戻るように
グループ化を追加してみました.

ちなみに,\begin{filecontents}で
失敗する理由もカテゴリーコードでしょうね.
filecontentsのようなカテゴリーコードを
変更するものは,
他のマクロの中ではうまくいかないものです.
一度トークン化されてしまうと
カテゴリーコードは変更できないので
そのあたりがfilecontetsの
^^Mの定義でかみ合わないのでしょう.
#LuaTeXは確か一度トークン化されたものの
#カテゴリーコードの変更できますよね



\documentclass{article}
\usepackage{xstring}

\begin{document}

\def\outdir{}
\def\mkfilename#1{%
\StrSubstitute{#1}{:}{-}[\filename]
\edef\filename{\outdir\filename.tex}}

\newenvironment{question}[2]{%
\mkfilename{#1}%
\bgroup
\csname filecontents\endcsname[overwrite]{\filename}%
}{\csname endfilecontents\endcsname%
\egroup%
\input{\filename}%
}

\begin{question}{q:hoge:fuga}{Title}
$E = mc^{2}$
\end{question}

\end{document}


本田 知亮 への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- Beginner Beginner の投稿
早速のご返答ありがとうございます。
ご提示いただいた変更で動きました。

ただ、以下のように、filecontents の後に別の環境を入れると、

LaTeX Warning: Writing or overwriting file `./q-hoge-fuga.tex'.

)
Runaway argument?
! File ended while scanning use of ^^M.
<inserted text>
\par
<*> q2.tex

などとなり、コンパイルできません。
q-hoge-fuga.tex も正しく生成されません (冒頭のコメントのみで、環境の内容が含まれていない)。
これを解決する方法はありますでしょうか?
(こちらもカテゴリーコードの問題でしょうか?)


以下では問題の切り分けのために quote 環境を使いましたが、最終的には tcolorbox (正確には \NewTColorBox で定義した環境) を使いたいと思っています。
なお、以下で quote を tcolorbox にすると、

LaTeX Warning: Writing or overwriting file `./q-hoge-fuga.tex'.

! Missing number, treated as zero.
<to be read again>

l.24 $E = mc^{2}$

?

とエラー内容が変わります。
よろしくお願いいたします。


\documentclass{article}
\usepackage{xstring}

\begin{document}

\def\outdir{}
\def\mkfilename#1{%
\StrSubstitute{#1}{:}{-}[\filename]
\edef\filename{\outdir\filename.tex}}

\newenvironment{question}[2]{%
\mkfilename{#1}%
\bgroup
\csname filecontents\endcsname[overwrite]{\filename}%
\begin{quote}%
}{\end{quote}%
\csname endfilecontents\endcsname%
\egroup%
\input{\filename}%
}

\begin{question}{q:hoge:fuga}{Title}
$E = mc^{2}$
\end{question}

\end{document}

Beginner Beginner への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 本田 知亮 の投稿
filecontents環境はLaTeXの環境の振りしてますが,
実際は普通の意味でのLaTeXの環境ではありません.

\newenvironment{question}[2]{%
\mkfilename{#1}%
\bgroup
\csname filecontents\endcsname[overwrite]{\filename}%
\begin{quote}%
}{\end{quote}%
\csname endfilecontents\endcsname%
\egroup%
\input{\filename}%
}


なんとなく,\filecontents~\endfilecontentsの間に
\begin{quote}~\end{quote}が入り込むように見えますけど,そうはなりません.最初の定義の段階で

\filecontents\begin{quote}

というのが保存されて,
それが実際に展開されるときには
\filecontentsがverbatimのように記号類を文字のようにしますけど,そのときには\begin{quote}はすでに確定してて,文字扱いにはなりません.

つまり,
\filecontentsが展開されたあとでquote環境が展開されます.quote環境はlist環境であって内部で\itemが展開されます.この\itemの内部では\reserved@a/\reserved@bが定義されていますが,\filecontentsも内部で\reserved@bを使います.


この段階で,\filecontentsが期待している\reserved@bが\itemの\reserved@bになってしまいます.そうなると,\filecontentsが本来期待する\reserved@bの働き(ファイルへの書き出し関係)が\itemの処理に置き換わって,改行が\itemになるようなもので,わけがわからなくなります.


じゃあ,\filecontentsの\reseved@aとか\reserved@bを別名にすればいいじゃないかとなります.たぶん.これで\itemとの衝突はきっと回避できます.
けど今度は^^M(改行)の扱いで訳の分からないことが起きる気がします(^^Mをいつ普通の改行に戻せばいいのか?どうすれば戻せるのかがきっと問題になる).


もしくは環境の終了処理で何かが起きるかもししれません.filecontentsは環境名を利用しているので,今回のように\filecontentsをquestion環境にいれると,question環境と認識して,\end{question}を\filecontentsが利用するようになります.ここでも何かの齟齬が起きるかもしれません.

実際はtcolorboxを期待しているとのことですが,あそこまで複雑になると小手先の回避ではどうにもならないと思います.


たぶん思い切ってfilecontentsそのものを書き直して,ばっさりいくしかないんでしょうが,^^Mの扱いをどうすればいいのやら。。。

と思ってたら,北見さんの追記を発見.
なんとかなるんですね.


Beginner Beginner への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 北見 けん の投稿
(追記:本田さんと同時でしたね)

細かいところを省いて一般化すると、

\newenvironment{hogehoge}{
    \filecontents{filename}
    (A)
}{
    (B)
    \endfinecontents
}

のように定義して

\begin{hogehoge}
(C)
\end{hogehoge}

と使ったときに、
filename というファイルに

(A)
(C)
(B)

と書きだしたいということですね。

\filecontents の動作については別のコメントに書きました。
それを踏まえると、次のようなことになると思います。

これを実現するには、(A)の内容は、
あらかじめ \filecontents でのカテゴリーコード変更が施された状態のままで
直接(A)のところに書く必要があります。これはまあ hogehoge環境の定義のところで
少し工夫すれば可能で、手元の実験でもできました。ファイルにも書き出せます。

しかし(B)のほうは、\filecontents そのものを書き換える必要があるようです。
というのも、(B)は \end{hogehoge} が展開されたときに現れますが、
\filecontents の終了処理を鑑みれば、
その時点ですでにファイル書き込み処理が終わっているわけなので、
その前に(B)を書く処理を潜り込ませる必要があるからです。
そのためにはLaTeXの内部マクロである \filec@ntents を改変して
終了処理に割り込むことになります。
同時に他の filecontents環境を使うようなことはあまり考えられませんが、
改変を忘れたころに事故が起こるのは心配なので、
\filec@ntents を改変するよりは、今回用の独自名称の書き出し環境マクロを
創るほうが安心でしょう。
それなら、上の(A)を書きだすのも割と自由自在にできますし。
北見 けん への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 北見 けん の投稿
本田さんの回答を見て思いつきましたが、

\newenvironment{hogehoge}{
    \filecontents{filename}
    (A)
}{
    (B)
    \endfinecontents
}

として(A)や(B)をファイルに書き出すのはあきらめて、
読み込んだときに生きればいいのであれば、

\newenvironment{hogehoge}{
    \begingroup
    \filecontents{filename}
}{
    \endfinecontents
    \endgroup
    (A)
    \input{filename}
    (B)
}

とすれば簡単そうですね。
これなら、filecontentsの改変しなくて済みそう。
試していませんが。
北見 けん への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- Beginner Beginner の投稿
本田さま, 北見さま,

正直なところ内容が高度すぎて付いていけてないのですが、いろいろご助言ありがとうございます。

ところで、

\newenvironment{hogehoge}{
\begingroup
\filecontents{filename}
}{
\endfinecontents
\endgroup
(A)
\input{filename}
(B)
}

だと、hogehoge で受けとった引数を (A) で使えない、という問題があるかと思います。
具体的には以下のようなものを想定していました。
begin の定義部分で変数に入れるとかすれば解決できそうですが。

\newenvironment{question}[2]{%
\mkfilename{#1}%
\bgroup%
\filecontents{\filename}%
\begin{mytcolorbox}[label=#1,title=#2]
}{
\end{mytcolorbox}%
\endfinecontents%
\endgroup%
\input{\filename}
}

また、書き出したファイルを別のファイルで\inputしたいのですが、その際に

(A)
\input{filename}
(B)

と書くのはできれば避けたかったというのもあります。

いずれにせよ、(A), (B) もファイルに書き出すべきか等も含めて、もう少し検討してみます。
また疑問点が出てきたら質問させていただければと思います。

いろいろありがとうございました。

Beginner Beginner への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 北見 けん の投稿
いろいろ振り回してすみません。
他のコメントでは一般化してがんばっていたのですが、
ファイルに追記するのが1、2行程度なのであれば、
次のものでもよいですね。
今回の目的だとこれが簡単でよいでしょう。

\documentclass{article}

\makeatletter
\newenvironment{question}[2]{%
\def\currfilename{#1}%
\begingroup
\filecontents{\currfilename}%
\immediate\write\reserved@c{\string\begin\string{mytcolorbox\string}[label=#1,title=#2]}
}{%
\immediate\write\reserved@c{\string\end\string{mytcolorbox\string}}%
\endfilecontents
\endgroup
\input{\currfilename}
}
\makeatother

\newenvironment{mytcolorbox}[1][]{}{}

\begin{document}

\begin{question}{q-hoge-fuga}{Title}
$E = mc^{2}$
\end{question}

\end{document}
北見 けん への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 北見 けん の投稿
これはあまり現実的ではないほうですが、
マクロ定義の技術的な興味があるので追記します。

\newenvironment{hogehoge}{
    \filecontents{filename}
    (A)
}{
    (B)
    \endfinecontents
}



> これを実現するには、(A)の内容は、
> あらかじめ \filecontents でのカテゴリーコード変更が施された状態のままで
> 直接(A)のところに書く必要があります。これはまあ hogehoge環境の定義のところで
> 少し工夫すれば可能で、手元の実験でもできました。ファイルにも書き出せます。

と書いた実験です。こんな感じ。

\begingroup
\long\def\fixcatcode#1{#1}
\def\readhook#1endhook{%
\endgroup
\newenvironment{hogehoge}[1]{%
\filecontents{##1}
#1
}{%
\endfilecontents
}
}
\fixcatcode{%
\expandafter\let\expandafter\do\csname @makeother\endcsname\dospecials
\catcode`\^^M\active
\readhook
}
\begin{quote}% <- 上記(A)のところに入れたいものをここに書きます。
endhook

北見 けん への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 北見 けん の投稿
見直してみたらちょっと違っていたようなので、訂正します。

二つ上の書き込みで

> しかし(B)のほうは、\filecontents そのものを書き換える必要があるようです。
> というのも、(B)は \end{hogehoge} が展開されたときに現れますが、
> \filecontents の終了処理を鑑みれば、
> その時点ですでにファイル書き込み処理が終わっているわけなので、
> その前に(B)を書く処理を潜り込ませる必要があるからです。
> そのためにはLaTeXの内部マクロである \filec@ntents を改変して
> 終了処理に割り込むことになります。

と書いたところです。もう一つの解説のほうにも訂正を追記しましたが、
「ファイル書き込み処理が終わ」るのは \filecontents の中でした。
なので、

\newenvironment{hogehoge}{
    \filecontents{filename}
    (A)
}{
    (B)
    \endfinecontents
}

の(B)のところではまだ書き出し先のファイルが使えますね。
ただし、一行ごとにファイルに書き出す機能を持っていた改行文字マクロは
この時点ですでに終了処理用のものに変更されていますから、
書き出す内容だけを置くのではなくて
次のように書き出し作業そのものを置く必要がありますね。

\newenvironment{hogehoge}{
    \filecontents{filename}
    (A)
}{%
    \immediate\write\reserved@c{(B)}%
    \endfinecontents
}

これなら(A)のほうにも適用出来て

\newenvironment{hogehoge}{%
    \filecontents{filename}% 行末文字のコメントアウトが必要かも
    \immediate\write\reserved@c{(A)}
}{% 行末文字のコメントアウトが必要かも
    \immediate\write\reserved@c{(B)}% 行末文字のコメントアウトが必要かも
    \endfinecontents
}

とすればよいのかも。

(A)や(B) の部分に特殊文字を含む場合には、この上の書き込みの手法が使えそうです。

## 実は手元の latex.ltx は古いので、新しい filecontents では試せていません。
## 「TeXを使ってみよう」( https://oku.edu.mie-u.ac.jp/~okumura/texonweb/ )
## にあるのも古いもののようなので、新しい latex.ltx はダウンロードして中を眺めただけです。
北見 けん への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 北見 けん の投稿
少し間違があったのも直して、完成版として動くものを作ってみました。
書き出す環境の中身を \fbox{...} をで囲った状態でファイルに書き出す例です。
コメントを読んでいただければ、必要な形に作り変えることも難しくないと思います。

% 手元の古いLaTeXでも、filecontentsをプリアンブル以外でも使えるように修正。
\makeatletter
\def\remove#1{%
\def\reserved@a##1\do#1##2\reserved@a{\gdef\@preamblecmds{##1##2}}%
\expandafter\reserved@a\@preamblecmds\reserved@a
}
\remove\filecontents
\remove\endfilecontents
\remove\filec@ntents
\makeatother
% 古いLaTeX用の修正はここまで。

\documentclass{article}

\begingroup
\def\fixcatcode#1{#1}
\makeatletter
\long\def\gethooks#1and#2.{%
\endgroup
\newenvironment{hogehoge}[1]{%
\def\currfilename{##1}%
\begingroup
\filecontents{##1}% <- 古いLaTeXなので[overwrite]オプションは試していません。
\immediate\write\reserved@c{#1}%
}{%
\immediate\write\reserved@c{#2}%
\endfilecontents
\endgroup
\input{\currfilename}
}
}
\fixcatcode{%
\let\do\@makeother\dospecials
\catcode`\^^M=12% <- ここ間違っていました。
\catcode`\^^L=12% <- ここ間違っていました。
\catcode`\^^I=12% <- ここ間違っていました。
\gethooks
}\fbox{% コメントも書き出せます
% 複数行も書き出せます。
and}
おまけ
.
% ↑の\gethooks}(A)and(B). の(A)と(B)の部分が、改行やコメントも含めて、
% hogehoge環境の中身を挟むように書き出されます。

\begin{document}

\tracingall
\begin{hogehoge}{exfile1}
$E=mc^2$
\end{hogehoge}

\begin{hogehoge}{exfile2}
$e^{\pi i}=-1$
\end{hogehoge}

\end{document}
Beginner Beginner への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 北見 けん の投稿
これでエラーになる事情について、書いてみます。(自分の備忘のつもり)

filecontents環境の中では、改行文字がマクロ化されて次の一行を読み取る動作を担います。
改行文字マクロは、その改行文字に続く次の一行をすべて読み取り、
「その行の処理+[改行文字]」
となるように展開されます。
この(その行の処理)のところでは、たいていはただその行をファイルに書き出すだけで、
書きだした後に、見えている[改行文字]が発動して次の行の処理を続けます。
ただし、処理するはずの(その行)が環境の終わりを含んで「半端\end{目下の環境名}残り」のように
なっていた場合には、半端を書きだしてファイルを開放した後で残りを捨てて、
動作をその行で止めるために改行文字マクロの意味を次のように定義し直します。
終了処理用の新しい改行文字マクロは、単に「\end{目下の環境名}」に展開されるマクロとなります。
このマクロが、(その行の処理)の後に見えている[改行文字]のところで発動して、
読み取り動作は終了します。

そのあと、通常通りに\end{目下の環境名}が実行されるわけですが、
\end の機能によって、これは概ね \end目下の環境名 + \endgroup処理 という形になります。
filecontents の開始時点で変更されたカテゴリーコードなどは、
この \end 由来の \engroup処理 によって復帰することが期待されています。

今回のケースでは、この \end{question} が
\endquestion + \endgroup処理 となり、さらに
\endfilecontents + input{file} + \endgroup処理
となるわけで、\endgroup処理 の前に外部ファイルの読み込みが起こり、
その外部ファイルの中の行末には改行文字があります。
ところがそこではまだ filecontents の終了処理のために「\end{question}」と展開されるように
定義された改行マクロが生きていますから、
そこで再び \end{question} が実行されてループに陥るということでしょう。

ということなので、本田さんの修正の通り、
\endfilecontents のあとですぐに filecontents の影響を封じ込めるのが良いということですね。

ところで、\filecontents と \endfilecontents を使うのに
\csname filecontents\endcsname とやらなくてもよいのじゃないかと思います。
北見 けん への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 本田 知亮 の投稿
ありがとうございます.

そうなんですよねー.
filecontentsって質が悪くて,
verbatimでobeylines部分にファイル書き込み
って感じで,
どうにもこうにもうまくいかないです.

verbatimなら単に\end{verbatim}は
パターンマッチで消費されるだけですが,
filecontentsは\endfilecontentsにも
しっかり意味があったりしますし.

>\csname filecontents\endcsname とやらなくてもよいのじゃないかと思います

はっ!その通りでした.
北見 けん への返信

Re: \newenvironment 中の filecontents のファイル名の指定

- 北見 けん の投稿
見直してみたらちょっと違っていました。

> ただし、処理するはずの(その行)が環境の終わりを含んで「半端\end{目下の環境名}残り」のように
> なっていた場合には、半端を書きだしてファイルを開放した後で残りを捨てて、
> 動作をその行で止めるために改行文字マクロの意味を次のように定義し直します。

と書いたのですが、「ファイルを開放」するタイミングが違っていましたね。
ファイルを開放するのは \endfilecontents の中でした。
訂正します。