Skip to main content

This is documentation for Caché & Ensemble. See the InterSystems IRIS version of this content.Opens in a new tab

For information on migrating to InterSystems IRISOpens in a new tab, see Why Migrate to InterSystems IRIS?

DO

ルーチンを呼び出します。

Synopsis

DO:pc doargument,...
D:pc doargument,...

doargument には、以下を指定できます。

entryref(param,...):pc

引数

pc オプション — 後置条件式。
entryref 呼び出されるルーチン名。
param オプション — 呼び出されるルーチンに渡されるパラメータ値。

説明

DODO WHILE は異なる無関係のコマンドです。ここでは、DO コマンドについて説明します。DO WHILE コマンドでは、DO キーワードと WHILE キーワードは、複数のコード行によって分割されています。しかし、DO キーワードの後には左中括弧が続くので DO WHILE コマンドの識別は簡単です。

DO コマンドには、以下の 2 つの基本形式があります。

  • 引数なし

    Note:

    引数なしの DO は、DO WHILE の { } ブロック構造によって取って代わられた、従来のブロック構造構文 (各行の先頭にピリオド (.) を使用) を使用します。したがって、引数なしの DO は、廃止されたと見なされています。新規のコードでは使用せず、既存のアプリケーションを保持するためだけに使用する必要があります。

  • 引数付き

引数付きの DO は、特定のオブジェクト・メソッド、サブルーチン、関数、またはプロシージャを呼び出します。Caché は、呼び出されたルーチンを実行し、DO コマンドの次のコマンドを実行します。ルーチンの呼び出しでは、パラメータは渡しても渡さなくてもかまいません。

DO コマンドは呼び出されたルーチンからの返り値を受け取れません。呼び出されたルーチンが引数付きの QUIT で終了している場合、DO コマンドは正常に終了しますが、QUIT の引数値は無視されます。

DO の各呼び出しは、新しいコンテキスト・フレームをプロセスのコール・スタックに配置します。$STACK 特殊変数は、コール・スタックのコンテキスト・フレームの現在の番号を含みます。このコンテキスト・フレームは、新しい実行レベルを作成して、$STACK$ESTACK をインクリメントし、DO の実行中に発行された NEW および SET $ZTRAP の操作のスコープを提供します。正常に終了すると、DO$STACK$ESTACK をデクリメントし、NEWSET $ZTRAP の操作を元に戻します。

引数

pc

オプションの後置条件式です。後置条件式が DO コマンド・キーワードに追加されている場合、Caché は後置条件式が True (0 以外の数値に評価される) の場合に DO コマンドを実行します。後置条件式が False (0 に評価される) の場合、Caché は DO コマンドを実行しません。

後置条件式が引数に追加されている場合、Caché は後置条件式が True (0 以外の数値) の場合に引数を実行します。False (0) の場合、Caché はその引数をスキップし、次の引数、もしくは次のコマンドの評価に進みます。Caché は左から右へ式を処理するため、後置条件式が評価される前に、式に含まれている引数部分 (パラメータ値またはオブジェクト参照) を評価してエラーが発生する場合があります。後置条件式を追加して DO でオブジェクト・メソッドを呼び出す場合、オブジェクト・メソッド・パラメータの最大個数は 253 です。

詳細は、"Caché ObjectScript の使用法" の "コマンド後置条件式" を参照してください。

entryref

呼び出すルーチン (オブジェクト・メソッド、サブルーチンプロシージャ、またはユーザ指定関数) の名前。複数のルーチンを、コンマ区切りのリストで指定することができます。

entryref は以下のすべての形式で指定できます。

label+offset 現在のルーチン内の行ラベルを指定します。オプションの +offset は、パラメータを渡されていないサブルーチンを呼び出すためにだけ使用します。プロシージャの呼び出し、もしくはサブルーチンへパラメータを渡すためには使用できません。offset は負でない整数で、ラベルの何行後からサブルーチンの実行を開始するかを指定します。
label+offset^routine ディスクに保存されている指定したルーチン内の行ラベルを指定します。Caché は、ディスクからそのルーチンをロードし、指定されたラベルから実行を続けます。+offset はオプションです。
^routine ディスクにあるルーチンの名前。システムは、ディスクからそのルーチンをロードし、そのルーチン内の最初の実行可能行から実行を開始します。リテラル値である必要があります。変数は、routine の指定には使用できません。(^ 文字は区切り文字で、ルーチン名の一部ではないことに注意してください。) ルーチンが変更されている場合、DO がそのルーチンを呼び出すときに、Caché は更新バージョンのルーチンをロードします。ルーチンが現在のネームスペースにない場合、拡張ルーチン参照を使用して、ルーチンを含むネームスペースを ^|"namespace"|routine のように指定できます。
oref.Method()

オブジェクト・メソッドを指定します。システムはオブジェクトにアクセスして、指定したメソッドを実行します。(存在する場合は) param で指定した引数とメソッドの引数リストを渡します。オブジェクトの呼び出しは、ドット構文を使用します。oref (オブジェクト参照) と Method() は、ドットで区切られます。空白スペースは許可されません。param 引数がない場合でも、開き括弧と閉じ括弧は必要です。

サポートされている構文形式は、DO oref.Method()DO (oref).Method()DO ..Method()DO ##class(cname).Method() です。

"Caché での JSON の使用" の “ダイナミック・オブジェクト式での DO コマンドの使用” および “動的配列式での DO コマンドの使用” も参照してください。

CACHESYS % ルーチンの呼び出し時における offset の指定はできません。指定しようとすると、Caché は <NOLINE> エラーを発行します。

存在しないラベルを指定した場合、Caché は <NOLINE> エラーを返します。存在しないルーチンを指定した場合、Caché は <NOROUTINE> エラーを返します。存在しないメソッドを指定した場合、Caché は <METHOD DOES NOT EXIST> エラーを返します。拡張参照 (例えば、DO ^|"%SYS"|MyProg) を使用して、存在しないネームスペースを指定した場合、Caché は <NAMESPACE> エラーを返します。拡張参照を使用して、特権を持たないネームスペースを指定した場合、Caché は <PROTECT> エラーを返し、続けてデータベース・パスを表示します (例 : <PROTECT> *^|^^c:\intersystems\cache\mgr\|MyRoutine)。これらのエラーの詳細は、"$ZERROR" 特殊変数を参照してください。

複数行の構文中を指す offset を指定すると、システムでは次の構文の最初で実行を開始します。

param

サブルーチン、プロシージャ、ユーザ指定関数、またはオブジェクト・メソッドに渡されるパラメータ値です。単一の param 値、あるいはコンマで区切られたparam 値のリストを指定できます。param リストは、括弧で囲まれます。param が指定されていない場合、プロシージャ、ユーザ指定関数、またオプションでサブルーチンを呼び出す際に括弧が必要です。パラメータは、値または参照によって渡されます。同じ呼び出しで、値渡しによるパラメータと参照渡しによるパラメータを混合して指定できます。値渡しの場合、パラメータを値定数、式、または添え字なしのローカル変数名として指定できます。("値渡し" を参照してください。)参照渡しの場合、パラメータは、ローカル変数の名前、または .name. という形式の添え字なしの配列を参照する必要があります (詳細は "参照渡し" を参照してください)。

1 つの DO エントリポイントの param 値の最大合計数は 382、1 つの DO メソッドまたは DO 間接演算の param 値の最大合計数は 380 です。この合計には、最大で 254 の実パラメータと 128 の後置条件パラメータを含めることができます。

... 構文を使用して可変個数のパラメータを指定できます。

引数なしの DO コマンド

引数なしの DO コマンドは、同じプログラム内の DO コマンドの直後に続くコード・ブロックを実行します。このコード・ブロックの各行は、先頭のピリオド (.) によって示されます。その後、Caché は、このコード・ブロックの次のコマンドを実行します。引数なしの DO ブロックは入れ子にできます。後置条件式を引数なしの DO コマンド・キーワードに追加し、DO コマンドの直後に続くコード・ブロックを実行するかスキップするかを指定できます。

IFFORDO WHILEWHILE コマンドのブロック構造は、同じ処理を実行するためには効果的な方法です。引数なしの DO コマンドは継続してサポートされますが、新規コーディングへの使用はお勧めしません。DO は新しい実行レベル DO WHILE を作成し、その他のブロック構造コマンドは実行レベルを変更しません。詳細は、"引数なしの DO (従来のバージョン)" を参照してください。

引数付きの DO コマンド

entryref 引数付きの DO コマンドは、1 つまたは複数の定義済みのコード・ブロックの実行を呼び出します。実行する各コード・ブロックは、entryref で指定されます。DO コマンドは、コンマで区切られたリストとして、実行する複数のコード・ブロックを指定できます。DO コマンドの実行とコンマで区切られたリストにある各 entryref の実行は、オプションの後置条件式で管理できます。

DO は、(パラメータ付き、あるいはパラメータなしの) サブルーチン、プロシージャ、ユーザ指定関数の実行を呼び出します。ブロック・コードの実行が終了した場合、DO コマンドの直後のコマンドで再開します。DO コマンドで呼び出されたコード・ブロックは、DO コマンドに値を返すことはできません。返された値はいずれも無視されます。したがって、DO はユーザ指定関数を実行しますが、関数の返り値の取得はできません。

DO は、ほとんどの Caché 指定のシステム関数を呼び出すことができません。呼び出しを試みると、<SYNTAX> エラーが返されます。いくつかのシステム関数は、DO によって呼び出すことができます。この関数には、$CASE$CLASSMETHOD$METHOD$ZF(-100)、および非推奨の $ZUTIL があります。DO は関数の返り値を受け取ることはできません。すべての DO コマンド引数と同様に、これらの関数は後置条件パラメータを取ることができます。

DO コマンドの引数として $CASE 関数を指定できます。プログラム例については、"$CASE" 関数を参照してください。

パラメータ渡しなしの DO コマンド

パラメータ渡しなしの DO コマンドは、サブルーチンと一緒の場合にのみ使用されます。DO entryref にパラメータを渡さない (つまり、param オプションを指定しない) で使用すると、呼び出し元のルーチンとそれによって呼び出されたサブルーチンで、同じ変数環境が共有されるという事実を利用できます。サブルーチンにより変数の更新が行われた場合、これらはすべて自動的に DO コマンドの後のコードで使用可能となります。

パラメータを渡さずに DO を使用する場合は、呼び出し元のルーチンと呼び出されるサブルーチンが同じ変数を参照していることを確認する必要があります。

Note:

プロシージャによる変数の処理は、まったく異なります。詳細は、"Caché ObjectScript の使用法" の "プロシージャ" を参照してください。

以下の例では、Start (呼び出し元のルーチン) と Exponent (呼び出されるサブルーチン) が 3 つの変数 numpowr、および result へのアクセスを共有しています。Start では、numpowr がユーザ指定の値に設定されます。これらの値は、Exponent が DO コマンドによって呼び出されたときに、自動的に Exponent でも利用できます。Exponent は、numpowr を参照し、計算した値を result に代入します。Exponent が RETURN コマンドを実行する場合、制御は DO の後の WRITE コマンドに即座に戻ります。WRITE コマンドは、result を参照して計算された値を出力します。

Start  ; Raise an integer to a specified power.
  READ !,"Integer= ",num QUIT:num="" 
  READ !,"Power= ",powr QUIT:powr=""
  DO Exponent()
  WRITE !,"Result= ",result,!
  RETURN
Exponent()
  SET result=num
  FOR i=1:1:powr-1 { SET result=result*num }
  RETURN

以下の例で、DO は pat によって参照されるオブジェクトで、Admit() メソッドを呼び出します。このメソッドはパラメータを受け取らず、また値も返しません。

   DO pat.Admit()

以下の例では、DO が、現在のルーチン内の Init サブルーチン、Read1 サブルーチン、Test ルーチン内の Convert サブルーチンを連続して呼び出します。

   DO Init,Read1,Convert^Test

以下の例では、DO は拡張参照を使用して、別のネームスペース (SAMPLES ネームスペース) の fibonacci ルーチンを呼び出しています。

  NEW $NAMESPACE
  SET $NAMESPACE="USER"
  DO ^|"SAMPLES"|fibonacci

DO と GOTO

DO コマンドを使用して、(パラメータ付き、あるいはなしの) サブルーチン、プロシージャ、ユーザ指定関数を呼び出します。呼び出しの終了時、Caché は DO コマンドに続く次のコマンドを実行します。

GOTO コマンドは、パラメータ渡しなしのサブルーチンを呼び出すためにのみ使用します。呼び出しの終了時、Caché は QUIT コマンドを呼び出し、実行を終了します。

パラメータ渡しの DO

パラメータ渡しが使用される場合、DO entryref は、呼び出したサブルーチン、プロシージャ、ユーザ指定関数、またはオブジェクト・メソッドに 1 つ、または複数の値を明示的に渡します。渡す値は、コンマで区切られたリストとして param オプションで指定されたものです。パラメータ渡しを行う場合は、呼び出されるサブルーチンがパラメータ・リスト付きで定義されているかを確認する必要があります。サブルーチンの定義の形式は、以下のとおりです。

>label( param)

label では、サブルーチン、プロシージャ、ユーザ指定関数、またはオブジェクト・メソッドのラベル名を指定します。param では、コンマで区切られた添え字なしのローカル変数名 1 つ、または複数からなるリストを指定します。以下はその例です。

Main
  SET x=1,y=2,z=3
  WRITE !,"In Main ",x,y,z
  DO Sub1(x,y,z)
  WRITE !,"Back in Main ",x,y,z
  QUIT
Sub1(a,b,c)
  WRITE !,"In Sub1 ",a,b,c
  QUIT

DO コマンドで渡されるパラメータ・リストは、実パラメータ・リスト (実リスト) と呼ばれます。コード化されたルーチン・ラベルの一部として定義されるパラメータ変数のリストは、仮パラメータ・リスト (仮リスト) と呼ばれます。DO コマンドがルーチンを呼び出すとき、実リストのパラメータは、その位置に従って、仮リスト内の対応する変数にマップされます。上述の例では、1 番目の実パラメータ (x) の値は、サブルーチンの仮パラメータ・リストにある 1 番目の変数 (a) に代入され、2 番目の実パラメータ (y) の値は 2 番目の変数 (b) に代入されます。その後、サブルーチンは、仮パラメータ・リストの変数を参照することによって、渡された値にアクセスできます。

実リスト内の変数が、仮リスト内のパラメータよりも多い場合、Caché は <PARAMETER> エラーを発行します。

仮リスト内の変数が、実リスト内のパラメータよりも多い場合は、余った変数は未定義のままになります。以下の例は、仮パラメータ c が未定義のままになっています。

Main
  SET x=1,y=2,z=3
  WRITE !,"In Main ",x,y,z
  DO Sub1(x,y)
  WRITE !,"Back in Main ",x,y,z
  QUIT
Sub1(a,b,c)
  WRITE !,"In Sub1 "
  IF $DATA(a) {WRITE !,"a=",a}
     ELSE {WRITE !,"a is undefined"}
  IF $DATA(b) {WRITE !,"b=",b}
     ELSE {WRITE !,"b is undefined"}
  IF $DATA(c) {WRITE !,"c=",c}
     ELSE {WRITE !,"c is undefined"}
  QUIT

実パラメータ値が指定されていない場合、仮パラメータに対し既定値を指定できます。

DO コマンドの実パラメータ・リストから、対応するパラメータを削除して、変数を未定義のままにできます。ただし、実パラメータを削除した個所に、プレース・ホルダとしてコンマを入れる必要があります。以下の例は、仮パラメータ b が未定義のままになっています。

Main
  SET x=1,y=2,z=3
  WRITE !,"In Main ",x,y,z
  DO Sub1(x,,z)
  WRITE !,"Back in Main ",x,y,z
  QUIT
Sub1(a,b,c)
  WRITE !,"In Sub1 "
  IF $DATA(a) {WRITE !,"a=",a}
     ELSE {WRITE !,"a is undefined"}
  IF $DATA(b) {WRITE !,"b=",b}
     ELSE {WRITE !,"b is undefined"}
  IF $DATA(c) {WRITE !,"c=",c}
     ELSE {WRITE !,"c is undefined"}
  QUIT

... 構文を使用して、可変個数のパラメータを指定できます。

Main
  SET x=3,x(1)=10,x(2)=20,x(3)=30
  DO Sub1(x...)
  QUIT
Sub1(a,b,c)
  WRITE a," ",b," ",c
  QUIT 

DO コマンドは、パラメータの渡し (DO Sub1(x,y,z) など)、または参照渡し (DO Sub1(.x,.y,.z) など) のいずれかを行います。同じ DO コマンド内で、値渡しと参照渡しを混合して指定することもできます。詳細は、"Caché ObjectScript の使用法" の "パラメータ渡し" を参照してください。

以下の例は、値渡しと参照渡しの違いを示しています。

Main /* Passing by Value */
  SET x=1,y=2,z=3
  WRITE !,"In Main ",x,y,z
  DO Sub1(x,y,z)
  WRITE !,"Back in Main ",x,y,z
  QUIT
Sub1(a,b,c)
  SET a=a+1,b=b+1,c=c+1
  WRITE !,"In Sub1 ",a,b,c
  QUIT
Main /* Passing by Reference */
  SET x=1,y=2,z=3
  WRITE !,"In Main ",x,y,z
  DO Sub1(.x,.y,.z)
  WRITE !,"Back in Main ",x,y,z
  QUIT
Sub1(&a,&b,&c)   /* The & prefix is an optional by-reference marker */
  SET a=a+1,b=b+1,c=c+1
  WRITE !,"In Sub1 ",a,b,c
  QUIT

間接指定の DO

間接指定を使用して、DO にターゲットのサブルーチンの場所を提供することができます。例えば、さまざまなメニュー関数を別のルーチン内の異なる場所に組み込む方法で、一般的なメニュー・プログラムを実装するとします。メイン・プログラム・コードで、名前による間接指定を使用して、DO コマンドにメニューの各選択項目に対応するサブルーチンの場所を提供することができます。

Caché オブジェクト・ドット構文を使用して、間接指定をすることはできません。ドット構文が実行時ではなく、コンパイル時に構文解析されるためです。

名前による間接指定の場合、間接指定演算子 (@) の右側の式の値が名前 (行ラベル またはルーチン) でなければなりません。以下のコード部分では、名前による間接指定によって、DO コマンドに Menu ルーチン内のターゲット・サブルーチンの場所が提供されています。

  READ !,"Enter the number for your choice: ",num QUIT:num=""
  DO @("Item"_num)^Menu

DO コマンドは、Menu 内のサブルーチンを呼び出します。このサブルーチンのラベルは、Item とユーザ指定の num 値が連結されたもの (Item1、Item2 など) です。

間接指定の引数形式を使用して、完全な DO 引数の代わりに、式の値を使用することもできます。例えば、以下の DO コマンドを見てみましょう。

   DO @(eref_":fstr>0")

このコマンドは、fstr の値が 0 より大きい場合に、eref の値で指定されたサブルーチンを呼び出します。

詳細は、"Caché ObjectScript の使用法" の "間接演算" を参照してください

引数後置条件付きの DO

DO コマンドのターゲット・サブルーチンを選択するには、引数後置条件式を使用できます。後置条件式が False (0) で評価される場合、Caché は関連付けられているサブルーチン引数を無視します。後置条件式が True (1) で評価される場合、Caché は関連付けられているサブルーチン呼び出しを実行してから、DO コマンドに戻ります。DO コマンドの引数とその引数の両方で、後置条件を使用できます。

例えば、以下のコマンドを考えてみます。

   DO:F>0 A:F=1,B:F=2,C

DO コマンドには後置条件式があり、F が 0 以下の場合、DO は実行されません。DO コマンドの引数にも、後置条件式があります。DO は後置条件のこれらの引数を使用して、実行するサブルーチン (A、B、C) を選択します。True 条件を満たすすべてのサブルーチンが、指定されている順番に実行されます。つまり、この例では、後置条件式のない C は常に実行されます。F が 1 の場合は A と C が実行され、F が 2 の場合は B とC が実行され、F が 3 (または他の任意の数) の場合は C が実行されます。True の既定として C を指定する場合は、次のようにします。

   DO:F>0 A:F=1,B:F=2,C:((F'=1)&&(F'=2))

この例では、実行されるサブルーチンは 1 つのみになります。

以下の例で、DO コマンドは後置条件式を取り、その引数もそれぞれ後置条件式を取ります。この場合、最初の引数は後置条件式が 0 であるため、実行されません。2 つ目の引数は後置条件式が 1 であるため実行されます。

Main
  SET x=1,y=2,z=3
  WRITE !,"In Main ",x,y,z
  DO:1 Sub1(x,y,z):0,Sub2(x,y,z):1
  WRITE !,"Back in Main ",x,y,z
  QUIT
Sub1(a,b,c)
  WRITE !,"In Sub1 ",a,b,c
  QUIT
Sub2(d,e,f)
  WRITE !,"In Sub2 ",d,e,f
  QUIT

DO によって呼び出されるオブジェクト (oref) メソッドのほとんどは、引数後置条件式を取ることができます。ただし、$SYSTEM オブジェクト・メソッドは、引数後置条件式を取ることができません。これを実行しようとすると、<SYNTAX> エラーが生成されます。

Caché では、式は必ず左から右の順番で評価されるため、引数後置条件式が評価される前に、式を含む引数が評価されます (エラーが生成される場合もあります)。

引数の後置条件を使用するときは、副作用がないことを確認してください。例えば、以下のコマンドを考えてみます。

   DO @^Control(i):z=1

この場合 ^Control(i) には、後置条件 z=1 が True の場合に呼び出すサブルーチンの名前が含まれています。z=1 であるかどうかにかかわらず、Caché は ^Control(i) の値を評価し、それに従って、現在のグローバル・ネイキッド・インジケータを設定します。z=1 が False の場合、Caché は DO コマンドを実行しません。ただし、グローバル・ネイキッド・インジケータは、DO を実行したかのようにリセットされます。ネイキッド・インジケータに関する詳細は、"Caché グローバルの使用法" の "ネイキッド・グローバル参照" を参照してください。

後置条件式の評価方法に関する詳細な情報は、"Caché ObjectScript の使用法" の "コマンド後置条件式" を参照してください。

DO を使用した場合の $TEST の振る舞い

引数なしの DO では、常に $TEST 特殊変数の値を保持します。

DO を使用してプロシージャを呼び出すと、Caché では $TEST の値を保持します。これは、プロシージャを終了するときの呼び出し時に、値をその状態にリストアすることで保持されます。ただし、DO を使用してサブルーチンを呼び出す場合 (パラメータを渡す場合も渡さない場合も) は、その呼び出しの間、Caché では $TEST の値は保存しません

$TEST 値を DO 呼び出しの間も保存するためには、その呼び出しの前に、この値を明示的に変数に代入します。その後、呼び出しが続くコードで変数を参照できます。

以下のコードでは、($TEST を設定する) 従来の IF コマンドを使用した DO 呼び出しの結果、$TEST が予期しなかった振る舞いをします。通常の (コード・ブロック) IF コマンドは $TEST を設定しないため、この振る舞いは生じません。

Start ; This routine uses the legacy IF command syntax
  SET x=1
  IF x=1 DO Sub1(x) ; sets $TEST to TRUE (1)
  ELSE  DO Sub2(x)
  QUIT
Sub1(y)  ; a subroutine that evaluates a FALSE IF
  WRITE !,"hello from subroutine 1"
  IF y=2 WRITE " - IF in Sub1 was TRUE" ; Set $TEST to FALSE (0)
  ELSE  WRITE " - IF in Sub1 was FALSE"
  QUIT
Sub2(z)  ; another subroutine
  WRITE !,"hello from subroutine 2"
  QUIT

一目見ると、Start は、Sub1 だけを呼び出して終了するように見えます。

しかし、実際にこのコードを実行すると、以下のようになります。

USER>DO ^Start
hello from subroutine 1 - IF in Sub1 was FALSE
hello from subroutine 2

このように予期しなかった振る舞いをするのは、$TEST 値が Sub1 でリセットされるためです。この結果、Start で ELSE コマンドが実行されます。実行される処理を順に見ていきましょう。

  1. Caché は、Start の IF コマンド式を True と評価します。$TEST に True を設定し、Sub1 を呼び出します。

  2. Caché は、Sub1 の IF コマンド式を False と評価します。$TEST に False を設定し、次の ELSE コマンドを実行します。

  3. QUIT に出会うと、Caché は Start に戻ります。

  4. Caché は、Start 内の ELSE を実行し、DO の Sub2 呼び出しを実行します。ELSE が実行されるのは、$TEST が、Start の IF コマンドで設定された True の値の代わりに、Sub1 で False に設定されたからです。

    予期する動作にするために、Start または Sub1 の ELSE を追加の IF と置き換えることができます。例えば、Start を以下のように書き換えることができます。

    Start
      SET x=1
      IF x=1 DO Sub1(x)
      IF x'=1 DO Sub2(x)
      QUIT

関連項目

FeedbackOpens in a new tab