[gimp] ScriptFu: Add script-fu-interpreter akin to other interpreters



commit d5a83429b464df4c87aaffe8779ddc3c570a0f49
Author: lloyd konneker <konnekerl gmail com>
Date:   Sun Jun 19 16:03:06 2022 -0400

    ScriptFu: Add script-fu-interpreter akin to other interpreters
    
    Why:
    1) users can install .scm scripts to plug-ins dir
    2) Crashing scripts do not crash extension-script-fu
    
    Scripts (.scm files) have a shebang and are executable
    and in a same-named subdir of plugin dir.
    
    Interpreter/scripts create PDB procs of type PLUGIN unlike extension-script-fu
    which creates PDB procs of type TEMPORARY, owned by extension-script-fu.
    
    Unlike other interpreters, the interpreter is-a plugin outright,
    not by virtue of the script subclassing GimpPlugin and using GI.
    
    More details in /plug-ins/script-fu/interpreter/README

 build/windows/installer/gimp3264.iss               |  72 +++++---
 configure.ac                                       |   1 +
 plug-ins/script-fu/Makefile.am                     |   2 +-
 plug-ins/script-fu/interpreter/.gitignore          |   6 +
 plug-ins/script-fu/interpreter/Makefile.am         | 102 ++++++++++
 plug-ins/script-fu/interpreter/README              | 178 ++++++++++++++++++
 plug-ins/script-fu/interpreter/meson.build         |  42 +++++
 .../interpreter/script-fu-interpreter-plugin.c     | 140 ++++++++++++++
 .../script-fu/interpreter/script-fu-interpreter.c  | 192 +++++++++++++++++++
 .../script-fu/interpreter/script-fu-interpreter.h  |  29 +++
 plug-ins/script-fu/libscriptfu/Makefile.am         |   4 +-
 plug-ins/script-fu/libscriptfu/meson.build         |   1 +
 plug-ins/script-fu/libscriptfu/script-fu-lib.c     |  53 +++++-
 plug-ins/script-fu/libscriptfu/script-fu-lib.h     |   6 +
 .../script-fu/libscriptfu/script-fu-proc-factory.c | 205 +++++++++++++++++++++
 .../script-fu/libscriptfu/script-fu-proc-factory.h |  27 +++
 plug-ins/script-fu/libscriptfu/script-fu-script.c  |  46 ++++-
 plug-ins/script-fu/libscriptfu/script-fu-script.h  |   6 +
 plug-ins/script-fu/libscriptfu/script-fu-scripts.c | 154 ++++++++++++----
 plug-ins/script-fu/libscriptfu/script-fu-scripts.h |   8 +
 plug-ins/script-fu/libscriptfu/script-fu-types.h   |   5 +
 plug-ins/script-fu/libscriptfu/script-fu.def       |   2 +
 plug-ins/script-fu/meson.build                     |   1 +
 plug-ins/script-fu/scripts/Makefile.am             |  12 +-
 plug-ins/script-fu/scripts/meson.build             |  21 ++-
 plug-ins/script-fu/scripts/test/README             |   5 +
 plug-ins/script-fu/scripts/test/test0/test0.scm    |  31 ++++
 plug-ins/script-fu/scripts/test/test1/test1.scm    |  43 +++++
 plug-ins/script-fu/scripts/test/test1/test3.scm    |  31 ++++
 plug-ins/script-fu/scripts/test/test4/test4.scm    |  25 +++
 plug-ins/script-fu/scripts/test/test5/test5.scm    |  16 ++
 plug-ins/script-fu/scripts/test/test6/test6.scm    |  12 ++
 plug-ins/script-fu/scripts/test/test7/test7.scm    |  28 +++
 plug-ins/script-fu/scripts/test/test8/test8.scm    |  39 ++++
 plug-ins/script-fu/scripts/ts-helloworld.scm       |   2 +
 35 files changed, 1473 insertions(+), 74 deletions(-)
---
diff --git a/build/windows/installer/gimp3264.iss b/build/windows/installer/gimp3264.iss
index ebeaf320fe..7b90639c3d 100755
--- a/build/windows/installer/gimp3264.iss
+++ b/build/windows/installer/gimp3264.iss
@@ -25,7 +25,7 @@
 ;Install script for GIMP and GTK+
 ;requires Inno Setup 6
 ;
-;See directories.isi 
+;See directories.isi
 ;
 ;Changelog:
 ;
@@ -751,7 +751,7 @@ begin
        begin
 
                DebugMsg('DoConfigOverride', 'First call');
-               
+
                Result := False;
                ConfigOverride := coDontOverride;
 
@@ -819,7 +819,7 @@ begin
                          '/usr/bin/python=' + ExpandConstant('{app}\bin\{#PYTHON}') + #10 +
                          '/usr/bin/python3=' + ExpandConstant('{app}\bin\{#PYTHON}') + #10 +
                          ':Python:E::py::python:'#10;
-               
+
                if not SaveStringToUTF8File(InterpFile,InterpContent,False) then
                begin
                        DebugMsg('PrepareInterp','Problem writing the file. [' + InterpContent + ']');
@@ -840,7 +840,7 @@ begin
                          '/usr/bin/luajit=' + ExpandConstant('{app}\bin\luajit.exe') + #10 +
                          '/usr/bin/lua=' + ExpandConstant('{app}\bin\luajit.exe') + #10 +
                          ':Lua:E::lua::luajit:'#10;
-               
+
                if not SaveStringToUTF8File(InterpFile,InterpContent,False) then
                begin
                        DebugMsg('PrepareInterp','Problem writing the file. [' + InterpContent + ']');
@@ -848,13 +848,32 @@ begin
                end;
        end;
 #endif
+
+// not optional
+// !!! use comma for binfmt delimiter and full Windows path in interpreter field of binfmt
+begin
+       InterpFile := 
ExpandConstant('{app}\lib\gimp\{#DIR_VER}\interpreters\gimp-script-fu-interpreter.interp');
+                       DebugMsg('PrepareInterp','Writing interpreter file for gimp-script-fu-interpreter: ' 
+ InterpFile);
+
+       InterpContent := 'gimp-script-fu-interpreter=' + 
ExpandConstant('{app}\bin\gimp-script-fu-interpreter-3.0.exe') + #10 +
+                                               'gimp-script-fu-interpreter-3.0=' + 
ExpandConstant('{app}\bin\gimp-script-fu-interpreter-3.0.exe') + #10 +
+                                               '/usr/bin/gimp-script-fu-interpreter=' + 
ExpandConstant('{app}\bin\gimp-script-fu-interpreter-3.0.exe') + #10 +
+                                               ',ScriptFu,E,,scm,,' + 
ExpandConstant('{app}\bin\gimp-script-fu-interpreter-3.0.exe') + ','#10;
+
+       if not SaveStringToUTF8File(InterpFile,InterpContent,False) then
+       begin
+               DebugMsg('PrepareInterp','Problem writing the file. [' + InterpContent + ']');
+               SuppressibleMsgBox(CustomMessage('ErrorUpdatingScriptFu') + ' (2)',mbInformation,mb_ok,IDOK);
+       end;
+end;
+
 end;
 
 
 procedure PrepareGimpEnvironment();
 var EnvFile,Env: String;
 begin
-       
+
        StatusLabel(CustomMessage('SettingUpEnvironment'),'');
 
        //set PATH to be used by plug-ins
@@ -862,7 +881,7 @@ begin
        DebugMsg('PrepareGimpEnvironment','Setting environment in ' + EnvFile);
 
        Env := #10'PATH=${gimp_installation_dir}\bin';
-       
+
        if IsComponentSelected('gimp32on64') then
        begin
 
@@ -941,7 +960,7 @@ procedure CustomizeOnClick(Sender: TObject);
 begin
        DebugMsg('Install mode','Custom');
        InstallMode := imCustom;
-       
+
        CleanUpCustomWelcome();
 
        WizardForm.NextButton.OnClick(TNewButton(Sender).Parent);
@@ -1044,9 +1063,9 @@ begin
        else
                exit;
        DebugMsg('SelectComponentsFaceLift','2');
-                       
+
        WizardForm.ComponentsList.OnClick := @ComponentsListOnClick;
-                       
+
        lblDescription := TNewStaticText.Create(WizardForm.ComponentsList.Parent)
        with lblDescription do
        begin
@@ -1055,7 +1074,7 @@ begin
                AutoSize := True;
                Caption := CustomMessage('ComponentsDescription');
        end;
-                       
+
        pnlDescription := TPanel.Create(WizardForm.ComponentsList.Parent);
        with pnlDescription do
        begin
@@ -1069,7 +1088,7 @@ begin
        end;
 
        lblDescription.Parent := WizardForm.ComponentsList.Parent; //place lblDescription above pnlDescription
-                       
+
        lblComponentDescription := TNewStaticText.Create(pnlDescription);
        with lblComponentDescription do
        begin
@@ -1081,7 +1100,7 @@ begin
                Height := Parent.Height - ScaleY(20);
                Top := ScaleY(12);
        end;
-                       
+
 end;
 
 
@@ -1090,7 +1109,7 @@ var rtfNewReadyMemo: TRichEditViewer;
 begin
        DebugMsg('ReadyFaceLift','');
        WizardForm.ReadyMemo.Visible := False;
-               
+
        rtfNewReadyMemo := TRichEditViewer.Create(WizardForm.ReadyMemo.Parent);
        with rtfNewReadyMemo do
        begin
@@ -1214,7 +1233,7 @@ begin
 
        Components := ['Gimp','Deps','Debug','Translations','MyPaint','Python','Ghostscript','Lua','Gimp32'];
        ComponentDesc := '';
-       
+
        for i := 0 to TNewCheckListBox(pSender).Items.Count - 1 do
                if TNewCheckListBox(pSender).Selected[i] then
                begin
@@ -1277,7 +1296,7 @@ var sText: String;
 begin
        DebugMsg('UpdateReadyMemo','');
        (* Prepare the text for new Ready Memo *)
-       
+
        sText := RTFHeader;
        if pMemoDirInfo <> '' then
                sText := sText + ParseReadyMemoText(pSpace,pMemoDirInfo) + '\sb100';
@@ -1286,7 +1305,7 @@ begin
 
        If pMemoTasksInfo<>'' then
                sText := sText + '\sb100' + ParseReadyMemoText(pSpace,pMemoTasksInfo);
-               
+
        ReadyMemoRichText := Copy(sText,1,Length(sText)-6) + '}';
 
        Result := 'If you see this, something went wrong';
@@ -1299,7 +1318,7 @@ var NewBitmap1,NewBitmap2: TFileStream;
 begin
        WelcomeBitmapBottom := TBitmapImage.Create(WizardForm);
        with WelcomeBitmapBottom do
-       begin 
+       begin
                Left := 0;
                Top := 0;
                Parent := WizardForm;
@@ -1309,7 +1328,7 @@ begin
        end;
 
        DebugMsg('UpdateWizardImages','Height: ' + IntToStr(WizardForm.WizardBitmapImage.Height));
-       
+
        if WizardForm.WizardBitmapImage.Height < 386 then //use smaller image when not using Large Fonts
        begin
                try
@@ -1375,7 +1394,7 @@ begin
                        DebugMsg('DoUninstall','Install directory doesn'#39't exist: ' + InstallDir + ', 
resuming install');
                        oResult := InResult
                end else
-               begin   
+               begin
                        oResult := rogUninstallFailed;
                end;
 
@@ -1472,7 +1491,7 @@ begin
                        UninstallString := UninstallString + ' /NORESTART';
 
                        DoUninstall(UninstallString, OldPath, lblInfo2, Result);
-                                       
+
                end;
 
        end;
@@ -1509,7 +1528,7 @@ begin
 end;
 
 
-procedure CurPageChanged(pCurPageID: Integer); 
+procedure CurPageChanged(pCurPageID: Integer);
 begin
        DebugMsg('CurPageChanged','ID: '+IntToStr(pCurPageID));
        case pCurPageID of
@@ -1678,15 +1697,15 @@ begin
                SetArrayLength(Buttons,2);
                Message[0] := CustomMessage('Require32BPP');
                Buttons[0] := CustomMessage('Require32BPPContinue');
-               Buttons[1] := CustomMessage('Require32BPPExit');                
-               if (not WizardSilent) and 
+               Buttons[1] := CustomMessage('Require32BPPExit');
+               if (not WizardSilent) and
                   (MessageWithURL(Message, CustomMessage('Require32BPPTitle'), Buttons, mbError, 2, 2) = 2) 
then
                        Result := False
                else
                        Result := True;
        end
        else
-               Result := True; 
+               Result := True;
 end;
 
 
@@ -1733,7 +1752,7 @@ begin
        Result := RestartSetupAfterReboot(); //resume install after reboot - skip all setting pages, and 
install directly
 
        if Result then
-               Result := BPPTooLowWarning();           
+               Result := BPPTooLowWarning();
 
        if not Result then //no need to do anything else
                exit;
@@ -1743,7 +1762,7 @@ begin
        SetArrayLength(Buttons,2);
        Buttons[0] := CustomMessage('DevelopmentButtonContinue');
        Buttons[1] := CustomMessage('DevelopmentButtonExit');
-       if (not WizardSilent) and 
+       if (not WizardSilent) and
           (MessageWithURL(Message, CustomMessage('DevelopmentWarningTitle'), Buttons, mbError, 2, 2) = 2) 
then
        begin
                Result := False;
@@ -1810,4 +1829,3 @@ end;
 #include "uninst.isi"
 
 #expr SaveToFile(AddBackslash(SourcePath) + "Preprocessed.iss")
-
diff --git a/configure.ac b/configure.ac
index 00e1f1e612..e370c1319c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -3106,6 +3106,7 @@ plug-ins/script-fu/libscriptfu/Makefile
 plug-ins/script-fu/libscriptfu/ftx/Makefile
 plug-ins/script-fu/libscriptfu/tinyscheme/Makefile
 plug-ins/script-fu/server/Makefile
+plug-ins/script-fu/interpreter/Makefile
 plug-ins/script-fu/scripts/Makefile
 plug-ins/script-fu/scripts/images/Makefile
 plug-ins/selection-to-path/Makefile
diff --git a/plug-ins/script-fu/Makefile.am b/plug-ins/script-fu/Makefile.am
index cb6def24e5..af6d5c8632 100644
--- a/plug-ins/script-fu/Makefile.am
+++ b/plug-ins/script-fu/Makefile.am
@@ -49,7 +49,7 @@ AM_LDFLAGS = \
        $(framework_cocoa)      \
        $(xnone)
 
-SUBDIRS = libscriptfu scripts server
+SUBDIRS = libscriptfu scripts server interpreter
 
 # Each plugin must be installed in a dir of the same name as the executable
 script_fudir = $(gimpplugindir)/plug-ins/script-fu
diff --git a/plug-ins/script-fu/interpreter/.gitignore b/plug-ins/script-fu/interpreter/.gitignore
new file mode 100644
index 0000000000..361a0bb007
--- /dev/null
+++ b/plug-ins/script-fu/interpreter/.gitignore
@@ -0,0 +1,6 @@
+/Makefile.in
+/Makefile
+/.deps
+/_libs
+/.libs
+/script-fu-interpreter.exe
diff --git a/plug-ins/script-fu/interpreter/Makefile.am b/plug-ins/script-fu/interpreter/Makefile.am
new file mode 100644
index 0000000000..d957c3acf8
--- /dev/null
+++ b/plug-ins/script-fu/interpreter/Makefile.am
@@ -0,0 +1,102 @@
+## Process this file with automake to produce Makefile.in
+
+# derived from  app/Makefile.am, another installed binary
+
+if PLATFORM_OSX
+xobjective_c = "-xobjective-c"
+xobjective_cxx = "-xobjective-c++"
+xnone = "-xnone"
+framework_cocoa = -framework Cocoa
+endif
+
+if OS_WIN32
+win32_ldflags = -mwindows -Wl,--tsaware $(WIN32_LARGE_ADDRESS_AWARE)
+
+if HAVE_EXCHNDL
+exchndl = -lexchndl
+endif
+
+else
+libm = -lm
+endif
+
+if ENABLE_RELOCATABLE_RESOURCES
+munix = -Wl,-rpath '-Wl,$$ORIGIN/../lib'
+endif
+
+if HAVE_WINDRES
+include $(top_srcdir)/build/windows/gimprc-plug-ins.rule
+script_fu_interpreter_RC = script-fu-interpreter.rc.o
+endif
+
+libgimpui = $(top_builddir)/libgimp/libgimpui-$(GIMP_API_VERSION).la
+libgimpwidgets = $(top_builddir)/libgimpwidgets/libgimpwidgets-$(GIMP_API_VERSION).la
+libgimp = $(top_builddir)/libgimp/libgimp-$(GIMP_API_VERSION).la
+libgimpcolor = $(top_builddir)/libgimpcolor/libgimpcolor-$(GIMP_API_VERSION).la
+libgimpbase = $(top_builddir)/libgimpbase/libgimpbase-$(GIMP_API_VERSION).la
+libgimpconfig = $(top_builddir)/libgimpconfig/libgimpconfig-$(GIMP_API_VERSION).la
+libgimpmath = $(top_builddir)/libgimpmath/libgimpmath-$(GIMP_API_VERSION).la $(libm)
+
+# link to libscriptfu
+libgimp_scriptfu = $(top_builddir)/plug-ins/script-fu/libscriptfu/libgimp-scriptfu-$(GIMP_API_VERSION).la
+
+# include srcdir parent to find libscriptfu include files
+AM_CPPFLAGS = \
+       -I$(top_srcdir)         \
+       $(GTK_CFLAGS)           \
+       $(GEGL_CFLAGS)          \
+       -I$(includedir)         \
+       -I$(srcdir)/.. \
+       -DG_LOG_DOMAIN=\"scriptfu\"
+
+AM_CFLAGS = \
+       $(xobjective_c)
+
+AM_CXXFLAGS = \
+       $(xobjective_cxx)
+
+AM_LDFLAGS = \
+       $(munix)           \
+       $(win32_ldflags)   \
+       $(framework_cocoa) \
+       $(xnone)
+
+# interpreter is-a plugin but is-a bin_PROGRAM
+# i.e. installs to usual place of executables e.g. /usr/bin so shebangs find it.
+bin_PROGRAMS = gimp-script-fu-interpreter-@GIMP_API_VERSION@
+
+gimp_script_fu_interpreter_@GIMP_API_VERSION@_SOURCES = \
+       script-fu-interpreter.c       \
+       script-fu-interpreter.h       \
+       script-fu-interpreter-plugin.c
+
+# link with libgimp-scriptfu
+# link with the usual gimp suspects
+
+gimp_script_fu_interpreter_@GIMP_API_VERSION@_LDADD = \
+       $(libgimp_scriptfu)  \
+       $(libgimpmath)       \
+       $(libgimp)           \
+       $(libgimpbase)       \
+       $(libgimpui)         \
+       $(libgimpwidgets)    \
+       $(libgimpconfig)     \
+       $(libgimpcolor)      \
+       $(GTK_LIBS)          \
+       $(GTK_MAC_INTEGRATION_LIBS) \
+       $(RT_LIBS)           \
+       $(INTLLIBS)          \
+       $(exchndl)           \
+       $(script_fu_interpreter_RC)
+
+install-exec-hook:
+if DEFAULT_BINARY
+       cd $(DESTDIR)$(bindir) \
+       && rm -f gimp-script-fu-interpreter$(EXEEXT) \
+       && $(LN_S) gimp-script-fu-interpreter-$(GIMP_APP_VERSION)$(EXEEXT) gimp-script-fu-interpreter$(EXEEXT)
+endif
+
+uninstall-local:
+if DEFAULT_BINARY
+       rm -f $(DESTDIR)$(bindir)/gimp-script-fu-interpreter$(EXEEXT)
+endif
diff --git a/plug-ins/script-fu/interpreter/README b/plug-ins/script-fu/interpreter/README
new file mode 100644
index 0000000000..370077aa4f
--- /dev/null
+++ b/plug-ins/script-fu/interpreter/README
@@ -0,0 +1,178 @@
+# About script-fu-interpreter
+
+It is GIMP's Scheme interpreter akin to any other language interpreter,
+rather than a GIMP extension as is the plugin extension-script-fu.
+This interpreter (like all the ScriptFu plugins) embeds a TinyScheme interpreter.
+
+It is an executable program that is passed the name of a .scm file.
+The combination defines one or more PDB procedures of type PLUGIN.
+
+Differences from extension-script-fu
+====================================
+
+Since gimp-script-fu-interpreter and extension-script-fu use the same
+embedded interpreter (they both wrap TinyScheme)
+there is no difference in the language interpreted.
+Any differences are in the larger behavior of plugins.
+
+### PDB Procedure type
+
+Defines PDB procedure(s) of type PLUGIN.
+Unlike for the scriptfu extension,
+where a .scm file defines PDB procedure(s) of type TEMPORARY
+(owned by the PDB procedure of type PLUGIN named extension-script-fu)
+
+### Protocol
+
+Uses protocol to GIMP like other interpreters e.g. Python
+(query, create, and run phases.)
+The protocol to GIMP is unlike the protocol to the GIMP extension extension-script-fu.
+
+(Note that "extension" has many meanings.  It can denote: a protocol,
+or "suffix of a filename", or "a resource that extends GIMP."
+extension-script-fu is "a resource that extends GIMP" and it also uses
+the "extension" protocol,
+while gimp-script-fu-interpreter is a "a resource that extends GIMP" but uses
+the "plugin" protocol.)
+
+### Process lifetimes
+
+Executed many times, for many phases,
+unlike extension-script-fu which stays executing and gets a remote procedure call
+from GIMP to run a PDB procedure.
+
+### Process concurrency
+
+Each invocation of a plugin is in a separate process.
+One plugin process crash does not affect others.
+Unlike extension-script-fu, where a crash means the GIMP app must be restarted
+to restart extension-script-fu.
+
+### GUI concurrency
+
+Each plugin can have its own GUI visible concurrently
+with the GUI of other ScriptFu plugins.
+
+For extension-script-fu, an open dialog
+prevents other plugins in /scripts (implemented by extension-script-fu)
+from opening a dialog.
+Instead extension-script-fu opens such dialogs sequentially.
+
+This difference is not very important,
+since most users work sequentially.
+Most dialogs for plugins do not do anything substantive
+until a user closes the dialog with the OK button.
+
+### Calls between scripts
+
+In extension-script-fu, a call to another PDB procedure
+implemented by TEMPORARY procedure owned by extension-script-fu
+does not leave the process.
+
+In gimp-script-fu-interpreter, a call to another PDB procedure
+implemented in another plugin file starts another process.
+
+For other plugins, most calls to another PDB procedure starts another process.
+The exception is when one plugin file implements many PDB procedures.
+One common case is when one plugin file implements its own TEMPORARY PDB procedures that
+exist only for the duration of the plugin's lifetime.
+
+Naming
+======
+
+script-fu-interpreter is the informal name.
+
+Source is located in /plug-ins/script-fu/interpreter
+
+Filename of the executable is gimp-script-fu-interpreter-3.0.
+The name is versioned by a number corresponding to the API
+and the major version of GIMP (when script-fu-interpreter was introduced).
+We expect plugin authors to be insulated from changes to script-fu-interpreter,
+for the duration of the GIMP 3 version.
+
+
+About .scm scripts for script-fu-interpreter
+============================================
+
+The contents of a .scm file queried by script-fu-interpreter
+are the same as those handled by extension-script-fu
+except for the addition of a shebang:
+
+    #!/usr/bin/env gimp-script-fu-interpreter-3.0
+    (define (script-fu-test img  drawable)
+    ...
+    (script-fu-register "script-fu-test"
+    ...
+
+### Query of scripts
+
+As for other interpreters, a plugin script file must have certain attributes
+to be queried by GIMP.  But a queried file may define many PDB procedures.
+
+A .scm file queried by script-fu-interpreter:
+
+- must have permission to execute.
+- must be in a directory names the same as the file's base name (less suffix.)
+
+A directory containing an .scm file queried by script-fu-interpreter
+is usually a subdirectory of one of the /plug-ins directories,
+unlike for extension-script-fu, where the .scm files are in a /scripts dir.
+
+A plugin directory should contain only one queriable .scm file:
+only one file can have the same base name as the directory and
+GIMP will query that file.
+
+### Defining many PDB procedures per directory
+
+When GIMP queries, script-fu-interpreter will load *ALL* the .scm files
+in the same directory (regardless of shebang or execute permission.)
+Any of the .scm files can declare and register a PDB procedure.
+Any single  .scm file can declare and register many PDB procedures.
+
+Similarly, when GIMP runs a named PDB procedure defined in an .scm file,
+script-fu-interpreter will actually load *ALL* the .scm files
+in the same directory, but run only the define run function so named.
+
+A plugin directory that contains many .scm files having a shebang
+will also work, since only one can be named the same as the parent directory,
+and GIMP will only query it,
+but find the other .scm files with a shebang.
+
+### Requery of scripts
+
+As with other plugins, GIMP caches plugin definitions between sessions.
+GIMP queries plugin files at startup.
+GIMP will not requery any plugin files which have not changed since cached.
+GIMP will not query a plugin file that is added to the file system
+until GIMP is restarted.
+(Filters>Development>ScriptFu>Refresh Scripts  will not requery script files
+handled by gimp-script-fu-interpreter, only those handled by extension-script-fu.)
+
+### Errors during query
+
+Most errors occurring during query appear only in the console.
+If you are a plugin author, you should start GIMP in a console
+so you can see such errors.
+
+A script may be malformed because it does not define
+a "run" function having the same name as the PDB procedure name declared
+in the call to script-fu-register().
+Formerly, such a malformed script was successfully queried but
+the script would throw a "undefined variable" error at run time.
+Now, such a malformed script is not queried successfully,
+but throws a warning to the console:
+"Run function not defined, or does not match PDB procedure name:"
+
+
+
+Test scripts
+============
+
+Small test scripts for gimp-script-fu-interpreter
+are located in /plug-ins/script-fu/scripts/test
+
+The script ts-helloworld.scm,
+formerly installed in /scripts and handled by extension-script-fu,
+is now installed in /plug-ins/ts-helloworld/ts-helloworld.scm
+and is now handled by gimp-script-fu-interpreter.
+It appears as "Filters>Development>Script-Fu>Test>Hello World..."
diff --git a/plug-ins/script-fu/interpreter/meson.build b/plug-ins/script-fu/interpreter/meson.build
new file mode 100644
index 0000000000..b02523d104
--- /dev/null
+++ b/plug-ins/script-fu/interpreter/meson.build
@@ -0,0 +1,42 @@
+
+scriptfuInclude = include_directories('..')
+
+executable_name = 'gimp-script-fu-interpreter-' + gimp_api_version
+
+plugin_sources = [
+  'script-fu-interpreter.c',
+  'script-fu-interpreter-plugin.c',
+]
+
+if platform_windows
+  plugin_sources += windows.compile_resources(
+    plugin_rc,
+    args: [
+      '--define', 'ORIGINALFILENAME_STR="@0@"'.format(executable_name+'.exe'),
+      '--define', 'INTERNALNAME_STR="@0@"'    .format(executable_name),
+      '--define', 'TOP_SRCDIR="@0@"'          .format(meson.source_root()),
+    ],
+    include_directories: [
+      rootInclude, appInclude,
+    ],
+  )
+endif
+
+# !!! Installs as a usual binary say to /usr/bin, unlike extension-script-fu
+# GIMP queries scripts with shebangs, which invokes gimp-script-fu-interpreter-3.0.
+
+executable(executable_name,
+  plugin_sources,
+  dependencies: [
+    libgimpui_dep,
+    math,
+  ],
+  c_args: [
+    '-DG_LOG_DOMAIN="scriptfu"',
+  ],
+  include_directories: [
+    scriptfuInclude,
+  ],
+  link_with : libscriptfu,
+  install: true,
+)
diff --git a/plug-ins/script-fu/interpreter/script-fu-interpreter-plugin.c 
b/plug-ins/script-fu/interpreter/script-fu-interpreter-plugin.c
new file mode 100644
index 0000000000..1ea2492397
--- /dev/null
+++ b/plug-ins/script-fu/interpreter/script-fu-interpreter-plugin.c
@@ -0,0 +1,140 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* This file understands how to make a GimpPlugin of the interpreter.
+ * This is mostly boilerplate for any plugin.
+ * It understands little about ScriptFu internals,
+ * hidden by script-fu-interpreter.[ch] and libscriptfu.
+ */
+
+#include "config.h"
+#include <glib.h>
+#include <libgimp/gimp.h>
+
+#include "libscriptfu/script-fu-intl.h"
+
+#include "script-fu-interpreter.h"
+
+
+/* ScriptFuInterpreter subclasses GimpPlugIn */
+
+#define SCRIPT_FU_INTERPRETER_TYPE (script_fu_interpreter_get_type ())
+G_DECLARE_FINAL_TYPE (ScriptFuInterpreter, script_fu_interpreter, SCRIPT, FU_INTERPRETER, GimpPlugIn)
+
+struct _ScriptFuInterpreter
+{
+  GimpPlugIn      parent_instance;
+};
+
+static GList          * script_fu_interpreter_query_procedures (GimpPlugIn  *plug_in);
+static GimpProcedure  * script_fu_interpreter_create_procedure (GimpPlugIn  *plug_in,
+                                                                const gchar *name);
+
+G_DEFINE_TYPE (ScriptFuInterpreter, script_fu_interpreter, GIMP_TYPE_PLUG_IN)
+
+/* An alias to argv[1], which is the path to the .scm file.
+ * Each instance of ScriptFuInterpreter is specialized by the script passed in argv[1]
+ * This var need not belong to the class or to an instance,
+ * because there will only be one instance of ScriptFuInterpreter per plugin process.
+ */
+static gchar * path_to_this_script;
+
+/* Connect to Gimp.  See libgimp/gimp.c.
+ *
+ * Can't use GIMP_MAIN macro, it doesn't omit argv[0].
+ *
+ * First arg is app cited in the shebang.
+ * Second arg is the .scm file containing the shebang.
+ * Second to last arg is the "phase" e.g. -query or -run
+ * Last arg is the mode for crash dumps.
+ * Typical argv:
+ *   gimp-script-fu-interpreter-3.0 ~/.config/GIMP/2.99/plug-ins/fu/fu
+ *          -gimp 270 12 11 -query 1
+ */
+int main (int argc, char *argv[])
+{
+  g_debug ("Enter script-fu-interpreter main");
+
+  /* Alias path to this plugin's script file. */
+  path_to_this_script = argv[1];
+
+  /* gimp_main will create an instance of the class given by the first arg, a GType.
+   * The class is a subclass of GimpPlugIn (with overridden query and create methods.)
+   * GIMP will subsequently callback the query or create methods,
+   * or the run_func of the PDB procedure of the plugin,
+   * depending on the "phase" arg in argv,
+   * which is set by the gimp plugin manager, which is invoking this interpreter.
+   */
+  /* Omit argv[0] when passing to gimp */
+  gimp_main (SCRIPT_FU_INTERPRETER_TYPE, argc-1, &argv[1] );
+
+  g_debug ("Exit script-fu-interpreter.");
+}
+
+DEFINE_STD_SET_I18N
+
+static void
+script_fu_interpreter_class_init (ScriptFuInterpreterClass *klass)
+{
+  GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
+
+  plug_in_class->query_procedures = script_fu_interpreter_query_procedures;
+  plug_in_class->create_procedure = script_fu_interpreter_create_procedure;
+  plug_in_class->set_i18n         = STD_SET_I18N;
+}
+
+
+/* called by the GType system to initialize instance of the class. */
+static void
+script_fu_interpreter_init (ScriptFuInterpreter *script_fu)
+{
+  /* Nothing to do. */
+}
+
+
+/* Return the names of PDB procedures implemented. A callback from GIMP. */
+static GList *
+script_fu_interpreter_query_procedures (GimpPlugIn *plug_in)
+{
+  GList *result = NULL;
+
+  g_debug ("queried");
+
+  result = script_fu_interpreter_list_defined_proc_names (plug_in, path_to_this_script);
+  if (g_list_length (result) < 1)
+    g_warning ("No procedures defined in %s", path_to_this_script);
+
+  /* Caller is GIMP and it will free the list. */
+  return result;
+}
+
+
+/* Create and return a GimpPDBProcedure,
+ * for the named one of the PDB procedures that the script implements.
+ * A callback from GIMP.
+ *
+ * Also set attributes on the procedure, most importantly, menu items (optional.)
+ * Also create any menus/submenus that the script defines e.g. Filters>My
+ */
+static GimpProcedure *
+script_fu_interpreter_create_procedure (GimpPlugIn  *plug_in,
+                                        const gchar *proc_name)
+{
+  return script_fu_interpreter_create_proc_at_path (plug_in,
+                                                    proc_name,
+                                                    path_to_this_script);
+}
diff --git a/plug-ins/script-fu/interpreter/script-fu-interpreter.c 
b/plug-ins/script-fu/interpreter/script-fu-interpreter.c
new file mode 100644
index 0000000000..4113565d30
--- /dev/null
+++ b/plug-ins/script-fu/interpreter/script-fu-interpreter.c
@@ -0,0 +1,192 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include <glib.h>
+#include <libgimp/gimp.h>
+
+#include "libscriptfu/script-fu-lib.h"
+
+#include "script-fu-interpreter.h"
+
+/* Implementation of the outer ScriptFuInterpreter.
+ * This understands ScriptFu internals
+ * i.e. uses libscriptfu shared with other ScriptFu plugins e.g. extension-script-fu.
+ */
+
+/* We don't need to load (into the interpreter) any .scm files handled by extension-script-fu.
+ * Namely, the .scm in the GIMP installation /scripts or in the user local /scripts dirs.
+ *
+ * During startup, GIMP might call gimp-script-fu-interpreter
+ * to query new files such as /plug-ins/fu/fu.scm.
+ * This is before extension-script-fu starts.
+ * But all the .scm files handled by extension-script-fu are type TEMPORARY and not needed
+ * for a /plug-ins/fu.scm to be queried.
+ * The only Scheme executed during query are calls to script-fu-register.
+ * Later, when a /plug-ins/fu.scm is run, it can call temporary PDB procedures
+ * that extension-script-fu provides.
+ *
+ * When we call script_fu_init_embedded_interpreter(),
+ * the passed paths should include the path to /scripts
+ * because that is the location of scripts for initialization and compatibility
+ * (script-fu.init, plug-in-compat.init and script-fu-compat.init,
+ * which are really scheme files.)
+ *
+ * scrip-fu-interpreter always inits embedded interpreter(allow_register=TRUE)
+ * In the "run" phase, you don't need script-fu-register to be defined, but its harmless.
+ */
+
+static GFile *script_fu_get_plugin_parent_path (const gchar *path_to_this_script);
+static void   script_fu_free_path_list         (GList      **list);
+
+
+/* Return a list of PDB procedure names defined in all .scm files in
+ * the parent dir of the given path, which is a filename of the one being queried.
+ *
+ * Each .scm file may contain many calls to script-fu-register, which defines a PDB procedure.
+ * All .scm files in the parent dir are searched.
+ *
+ * This executable is named script-fu-interpreter
+ * but no PDB procedure is named "script-fu-interpreter".
+ * Instead, the interpreter registers PDB procs named from name strings
+ * give in the in script-fu-register() calls in the interpreted scripts.
+ *
+ * Caller must free the list.
+ */
+GList *
+script_fu_interpreter_list_defined_proc_names (GimpPlugIn  *plug_in,
+                                               const gchar *path_to_this_script)
+{
+  GList *name_list = NULL;  /* list of strings */
+  GList *path_list = NULL;  /* list of GFile */
+
+  /* path_list is /scripts dir etc. from which we will load compat and init scripts.
+   * second argument TRUE means define script-fu-register into the interpreter.
+   */
+  path_list = script_fu_search_path ();
+  script_fu_init_embedded_interpreter (path_list, TRUE, GIMP_RUN_NONINTERACTIVE);
+  script_fu_free_path_list (&path_list);
+
+  /* Reuse path_list, now a list of one path, the parent dir of the queried script. */
+  path_list = g_list_append (path_list,
+                             script_fu_get_plugin_parent_path (path_to_this_script));
+  name_list = script_fu_find_scripts_list_proc_names (plug_in, path_list);
+  script_fu_free_path_list (&path_list);
+
+  /* Usually name_list is not NULL i.e. not empty.
+   * But an .scm file that is not an actual GIMP plugin, or broken, may yield empty list.
+   */
+  return name_list;
+}
+
+
+/* Create a PDB proc of type PLUGIN with the given name.
+ * Unlike extension-script-fu, create proc of type PLUGIN.
+ *
+ * We are in "create procedure" phase of call from GIMP.
+ * Create a PDB procedure that the script-fu-interpreter wraps.
+ *
+ * A GimpPDBProcedure has a run function, here script_fu_script_proc()
+ * of this outer interpreter.
+ * Sometime after the create, GIMP calls the run func, passing a name aka command.
+ * In ScriptFu, the same name is used for the PDB proc and the Scheme function
+ * which is the inner run func defined in the script.
+ * script_fu_script_proc calls the TinyScheme interpreter to evaluate
+ * the inner run func in the script.
+ */
+GimpProcedure *
+script_fu_interpreter_create_proc_at_path (GimpPlugIn  *plug_in,
+                                           const gchar *proc_name,
+                                           const gchar *path_to_this_script
+                                          )
+{
+  GimpProcedure *procedure = NULL;
+  GList         *path_list = NULL;  /* list of GFile */
+
+  g_debug ("script_fu_interpreter_create_proc_at_path, name: %s", proc_name);
+
+  /* Require proc_name is a suitable name for a PDB procedure eg "script-fu-test".
+   * (Not tested for canonical name "script-fu-<something>")
+   * Require proc_name is a name that was queried earlier.
+   * Require the proc_name was defined in some .scm file
+   * in the same directory as the .scm file passed as argv[0].
+   * The name of the .scm file eg "/plug-ins/fu/fu.scm"
+   * can be entirely different from proc_name.
+   *
+   * Otherwise, we simply won't find the proc_name defined in any .scm file,
+   * and will fail gracefully, returning NULL.
+   */
+
+  path_list = script_fu_search_path ();
+  path_list = g_list_append (path_list,
+                             script_fu_get_plugin_parent_path (path_to_this_script));
+  /* path_list are the /scripts dir, for .init and compat.scm, plus the path to this.
+   * second arg TRUE means define script-fu-register so it is effective.
+   */
+  script_fu_init_embedded_interpreter (path_list, TRUE, GIMP_RUN_NONINTERACTIVE);
+
+  /* Reuse path_list, now a list of only the path to this script. */
+  script_fu_free_path_list (&path_list);
+  path_list = g_list_append (path_list,
+                             script_fu_get_plugin_parent_path (path_to_this_script));
+
+  procedure = script_fu_find_scripts_create_PDB_proc_plugin (plug_in, path_list, proc_name);
+  script_fu_free_path_list (&path_list);
+
+  /* When procedure is not NULL, assert:
+   *    some .scm was evaluated.
+   *    the script defined many PDB procedures locally, i.e. in script-tree
+   *    we created a single PDB procedure (but not put it in the GIMP PDB)
+   *
+   * Ensure procedure is-a GimpProcedure or NULL.
+   * GIMP is the caller and will put non-NULL procedure in the PDB.
+   */
+  return procedure;
+}
+
+
+/* Return GFile of the parent directory of this plugin, whose filename is given.
+ *
+ * Caller must free the GFile.
+ */
+static GFile *
+script_fu_get_plugin_parent_path (const gchar *path_to_this_script)
+{
+  GFile *path        = NULL;
+  GFile *parent_path = NULL;
+
+  /* A libgimp GimpPlugin does not know its path,
+   * but its path was passed in argv to this interpreter.
+   * The path is to a file being queried e.g. "~/.config/GIMP/2.99/plug-ins/fu/fu.scm"
+   */
+  g_debug ("path to this plugin %s", path_to_this_script);
+  path = g_file_new_for_path (path_to_this_script);
+  parent_path = g_file_get_parent (path);
+  g_object_unref (path);
+  return parent_path;
+}
+
+/* Free a list of paths at the given handle.
+ * Ensures that the pointer to the list is NULL, prevents "dangling."
+ * g_list_free_full alone does not do that.
+ */
+static void
+script_fu_free_path_list (GList **list)
+{
+  /* !!! g_steal_pointer takes a handle. */
+  g_list_free_full (g_steal_pointer (list), g_object_unref);
+}
diff --git a/plug-ins/script-fu/interpreter/script-fu-interpreter.h 
b/plug-ins/script-fu/interpreter/script-fu-interpreter.h
new file mode 100644
index 0000000000..ec48ce52fb
--- /dev/null
+++ b/plug-ins/script-fu/interpreter/script-fu-interpreter.h
@@ -0,0 +1,29 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SCRIPT_FU_INTERPRETER_H__
+#define __SCRIPT_FU_INTERPRETER_H__
+
+GList         *script_fu_interpreter_list_defined_proc_names (
+                                            GimpPlugIn  *plug_in,
+                                            const gchar *path_to_this_plugin);
+GimpProcedure *script_fu_interpreter_create_proc_at_path (
+                                            GimpPlugIn  *plug_in,
+                                            const gchar *proc_name,
+                                            const gchar *path_to_this_script);
+
+#endif /*  __SCRIPT_FU_INTERPRETER_H__  */
diff --git a/plug-ins/script-fu/libscriptfu/Makefile.am b/plug-ins/script-fu/libscriptfu/Makefile.am
index 4f8c329a4f..3800578fe6 100644
--- a/plug-ins/script-fu/libscriptfu/Makefile.am
+++ b/plug-ins/script-fu/libscriptfu/Makefile.am
@@ -119,7 +119,9 @@ libgimp_scriptfu_@GIMP_API_VERSION@_la_SOURCES = \
        scheme-wrapper.c    \
        scheme-wrapper.h    \
        script-fu-lib.c     \
-       script-fu-lib.h
+       script-fu-lib.h     \
+       script-fu-proc-factory.h \
+       script-fu-proc-factory.c
 
 EXTRA_libgimp_scriptfu_@GIMP_API_VERSION@_la_DEPENDENCIES = $(scriptfu_def)
 
diff --git a/plug-ins/script-fu/libscriptfu/meson.build b/plug-ins/script-fu/libscriptfu/meson.build
index a7f1ecf03a..ffad8b51bf 100644
--- a/plug-ins/script-fu/libscriptfu/meson.build
+++ b/plug-ins/script-fu/libscriptfu/meson.build
@@ -14,6 +14,7 @@ libscriptfu_sources = [
   'script-fu-errors.c',
   'script-fu-compat.c',
   'script-fu-lib.c',
+  'script-fu-proc-factory.c',
 ]
 
 # !! just "library(...)" which means shared versus static depends on configuration of project.
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-lib.c b/plug-ins/script-fu/libscriptfu/script-fu-lib.c
index 93b988e256..235b2d35a7 100644
--- a/plug-ins/script-fu/libscriptfu/script-fu-lib.c
+++ b/plug-ins/script-fu/libscriptfu/script-fu-lib.c
@@ -19,7 +19,7 @@
 #include "config.h"
 
 #include <libgimp/gimp.h>
-/* FIXME script-fu-types.h refers to GtkAdjustment. */
+/* FIXME We only need gimpui because script-fu-types.h refers to GtkAdjustment. */
 #include <libgimp/gimpui.h>
 
 #include "script-fu-lib.h"
@@ -28,6 +28,7 @@
 #include "scheme-wrapper.h"      /* tinyscheme_init etc, */
 #include "script-fu-scripts.h"   /* script_fu_find_scripts */
 #include "script-fu-interface.h" /* script_fu_interface_is_active */
+#include "script-fu-proc-factory.h"
 
 
 /*
@@ -67,13 +68,43 @@ script_fu_find_and_register_scripts ( GimpPlugIn     *plugin,
   script_fu_find_scripts (plugin, paths);
 }
 
+/*
+ * Init the embedded interpreter.
+ *
+ * allow_register:
+ * TRUE: allow loaded scripts to register PDB procedures.
+ * The scheme functions script-fu-register and script-fu-menu-register are
+ * defined to do something.
+ * FALSE:  The scheme functions script-fu-register and script-fu-menu-register are
+ * defined but do nothing.
+ *
+ * Note that the embedded interpreter always defines scheme functions
+ * for all PDB procedures already existing when the interpreter starts
+ * (currently bound at startup, but its possible to lazy bind.)
+ * allow_register doesn't affect that.
+ */
 void
 script_fu_init_embedded_interpreter ( GList          *paths,
                                       gboolean        allow_register,
                                       GimpRunMode     run_mode)
 {
+  g_debug ("script_fu_init_embedded_interpreter");
   tinyscheme_init (paths, allow_register);
   ts_set_run_mode (run_mode);
+  /*
+   * Ensure the embedded interpreter is running
+   * and has loaded its internal Scheme scripts
+   * and has defined existing PDB procs as Scheme foreign functions
+   * (is ready to interpret PDB-like function calls in scheme scripts.)
+   *
+   * scripts/...init and scripts/...compat.scm are loaded
+   * iff paths includes the "/scripts" dir.
+   *
+   * The .scm file(s) for plugins are loaded
+   * iff paths includes their parent directory (e.g. /scripts)
+   * Loaded does not imply yet registered in the PDB
+   * (yet, they soon might be for some phases of the plugin.)
+   */
 }
 
 void
@@ -155,7 +186,6 @@ script_fu_search_path (void)
   GList *path = NULL;
 
   path_str = gimp_gimprc_query ("script-fu-path");
-
   if (path_str)
     {
       GError *error = NULL;
@@ -170,6 +200,23 @@ script_fu_search_path (void)
           g_clear_error (&error);
         }
     }
-
   return path;
 }
+
+
+GimpProcedure *
+script_fu_find_scripts_create_PDB_proc_plugin (GimpPlugIn  *plug_in,
+                                               GList       *paths,
+                                               const gchar *name)
+{
+  /* Delegate to factory. */
+  return script_fu_proc_factory_make_PLUGIN (plug_in, paths, name);
+}
+
+GList *
+script_fu_find_scripts_list_proc_names (GimpPlugIn *plug_in,
+                                        GList      *paths)
+{
+  /* Delegate to factory. */
+  return script_fu_proc_factory_list_names (plug_in, paths);
+}
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-lib.h b/plug-ins/script-fu/libscriptfu/script-fu-lib.h
index 09ac45a1b3..4b22445f12 100644
--- a/plug-ins/script-fu/libscriptfu/script-fu-lib.h
+++ b/plug-ins/script-fu/libscriptfu/script-fu-lib.h
@@ -42,4 +42,10 @@ void         script_fu_run_read_eval_print_loop  (void);
 void         script_fu_register_quit_callback         (void (*func) (void));
 void         script_fu_register_post_command_callback (void (*func) (void));
 
+GimpProcedure *script_fu_find_scripts_create_PDB_proc_plugin (GimpPlugIn  *plug_in,
+                                                              GList       *paths,
+                                                              const gchar *name);
+GList         *script_fu_find_scripts_list_proc_names        (GimpPlugIn  *plug_in,
+                                                              GList       *paths);
+
 #endif /* __SCRIPT_FU_LIB_H__ */
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-proc-factory.c 
b/plug-ins/script-fu/libscriptfu/script-fu-proc-factory.c
new file mode 100644
index 0000000000..5c74a755ae
--- /dev/null
+++ b/plug-ins/script-fu/libscriptfu/script-fu-proc-factory.c
@@ -0,0 +1,205 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include <glib.h>
+#include <libgimp/gimp.h>
+#include <libgimp/gimpui.h>
+
+#include "tinyscheme/scheme-private.h"
+#include "script-fu-types.h"
+#include "script-fu-scripts.h"
+#include "script-fu-script.h"
+
+#include "script-fu-proc-factory.h"
+
+/* Local functions */
+static void  script_fu_add_menu_to_procedure (GimpProcedure *procedure,
+                                              SFScript      *script);
+
+
+/* Methods to register PDB procs. A factory makes objects, here PDB procedures.
+ *
+ * Used by the outer script-fu-interpreter
+ *
+ * This is in libscriptfu to hide the SFScript type from outer plugins.
+ * These methods use instances of type SFScript as specs for procedures.
+ *
+ * FUTURE: migrate code.
+ * There are two flavors of factory-like code: for PDBProcType TEMPORARY and PLUGIN.
+ * extension-script-fu outer plugin only makes TEMPORARY
+ * script-fu-interpreter outer plugin only makes PLUGIN type
+ * This source file supports only script-fu-interpreter.
+ * script_fu_find_scripts() in script-fu-scripts.c is also a factory-like method,
+ * and could be extracted to a separate source file.
+ * Maybe more code sharing between the two flavors.
+ */
+
+
+/* Create and return a single PDB procedure of type PLUGIN,
+ * for the given proc name, by reading the script file in the given paths.
+ * Also add a menu for the procedure.
+ *
+ * PDB proc of type PLUGIN has permanent lifetime, unlike type TEMPORARY.
+ *
+ * The list of paths is usually just one directory, a subdir of /plug-ins.
+ * The directory may contain many .scm files.
+ * The plugin manager only queries one .scm file,
+ * having the same name as its parent dir and and having execute permission.
+ * But here we read all the .scm files in the directory.
+ * Each .scm file may register (and define run func for) many PDB procedures.
+ *
+ * Here, one name is passed, and though we load all the .scm files,
+ * we only create a PDB procedure for the passed name.
+ */
+GimpProcedure *
+script_fu_proc_factory_make_PLUGIN (GimpPlugIn  *plug_in,
+                                    GList       *paths,
+                                    const gchar *proc_name)
+{
+  SFScript      * script    = NULL;
+  GimpProcedure * procedure = NULL;
+
+  /* Reads all .scm files at paths, even though only one is pertinent.
+   * The returned script_tree is also in the state of the interpreter,
+   * we don't need the result here.
+  */
+  (void) script_fu_find_scripts_into_tree (plug_in, paths);
+
+  /* Get the pertinent script from the tree. */
+  script = script_fu_find_script (proc_name);
+
+  if (script)
+    {
+      procedure = script_fu_script_create_PDB_procedure (
+        plug_in,
+        script,
+        script_fu_script_proc,     /* run_func */
+        GIMP_PDB_PROC_TYPE_PLUGIN);
+      script_fu_add_menu_to_procedure (procedure, script);
+    }
+  else
+    {
+      g_warning ("Failed to find script: %s.", proc_name);
+    }
+  return procedure;
+}
+
+ /* Traverse the list of scripts, for each defined name of a PDB proc,
+  * add it list whose handle is given.
+  *
+  * Order is not important.  Could just as well prepend.
+  *
+  * This is a GTraverseFunction
+  */
+static gboolean
+script_fu_append_script_names (gpointer      *foo G_GNUC_UNUSED,
+                               GList         *scripts,
+                               GList        **name_list)
+{
+  for (GList * list = scripts; list; list = g_list_next (list))
+    {
+      SFScript *script   = list->data;
+
+      if ( !script_fu_is_defined (script->name))
+        {
+          g_warning ("Run function not defined, or does not match PDB procedure name: %s",
+                     script->name);
+          continue;
+        }
+
+      /* Must assign result from g_list_append back to name_list */
+      *name_list = g_list_append ( (GList *) *name_list, g_strdup (script->name));
+    }
+  return FALSE; /* We traversed all. */
+ }
+
+/* Load script texts (.scm files) in the given paths.
+ * Iterate over all loaded scripts to get the PDB proc names they define.
+ * Return a list of the names.
+ */
+GList *
+script_fu_proc_factory_list_names (GimpPlugIn *plug_in,
+                                   GList      *paths)
+{
+  GList * result_list = NULL;
+  GTree * script_tree = NULL;
+
+  /* Load (eval) all .scm files in all dirs in paths. */
+  script_tree = script_fu_find_scripts_into_tree (plug_in, paths);
+
+  /* Iterate over the tree, adding each script name to result list */
+  g_tree_foreach (script_tree,
+                  (GTraverseFunc) script_fu_append_script_names,
+                  &result_list);
+
+  return result_list;
+}
+
+/* From scriptfu's internal data, add any menu to given procedure in the PDB.
+ * Requires that a script was just eval'ed so that scriptfu's list of menus
+ * declared in a script is valid.
+ * Requires the proc exists in PDB.
+ *
+ * Not ensure the PDB proc has a menu, when no menu was defined in the script.
+ *
+ * Derived from script_fu_install_menu, but that is specific to TEMPORARY procs.
+ * Also, unlike script_fu_install_menu, we don't nuke the menu list as we proceed.
+ *
+ * For each "create" of a procedure, the gimp-script-fu-interpreter is started anew,
+ * and a new script_menu_list is derived from the .scm file.
+ * We don't traverse the menu list more than once per session, which soon exits.
+ */
+static void
+script_fu_add_menu_to_procedure (GimpProcedure *procedure,
+                                  SFScript      *script)
+{
+  GList    *menu_list;
+  gboolean  did_add_menu = FALSE;
+
+  menu_list = script_fu_get_menu_list ();
+  /* menu_list might be NULL: for loop will have no iterations. */
+
+  /* Each .scm file can declare many menu paths.
+   * Traverse the list to find the menu path defined for the procedure.
+   * Each SFMenu points to the procedure (SFScript) it belongs to.
+   */
+  for (GList * traverser = menu_list; traverser; traverser = g_list_next (traverser))
+    {
+      SFMenu *menu = traverser->data;
+      if (menu->script == script)
+        {
+          g_debug ("Add menu: %s", menu->menu_path);
+          gimp_procedure_add_menu_path (procedure, menu->menu_path);
+          did_add_menu = TRUE;
+          break;
+        }
+    }
+
+  /* Some procedures don't have menu path.
+   * It is normal, but not common, to define procs of type PLUGIN that don't appear in the menus.
+   * No part of GIMP defaults a menu path for procedures.
+   * A menu label without a menu path is probably a mistake by the script author.
+   */
+  if ( ! did_add_menu )
+    {
+      /* Unusual for a .scm file to have no menu paths, but not an error. */
+      g_debug ("No menu paths! Does the procedure name in script-fu-menu-register match?");
+      /* FUTURE if the script defines a menu *label*, declare an error. */
+    }
+  /* script_menu_list is a reference we do not need to free. */
+ }
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-proc-factory.h 
b/plug-ins/script-fu/libscriptfu/script-fu-proc-factory.h
new file mode 100644
index 0000000000..5daa7cc2b9
--- /dev/null
+++ b/plug-ins/script-fu/libscriptfu/script-fu-proc-factory.h
@@ -0,0 +1,27 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SCRIPT_FU_PDB_PROC_FACTORY_H__
+#define __SCRIPT_FU_PDB_PROC_FACTORY_H__
+
+GimpProcedure *script_fu_proc_factory_make_PLUGIN (GimpPlugIn  *plug_in,
+                                                   GList       *paths,
+                                                   const gchar *name);
+GList         *script_fu_proc_factory_list_names  (GimpPlugIn  *plug_in,
+                                                   GList       *paths);
+
+#endif /*  __SCRIPT_FU_PDB_PROC_FACTORY__  */
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-script.c 
b/plug-ins/script-fu/libscriptfu/script-fu-script.c
index d9fa92eb6a..c1da3838e5 100644
--- a/plug-ins/script-fu/libscriptfu/script-fu-script.c
+++ b/plug-ins/script-fu/libscriptfu/script-fu-script.c
@@ -43,6 +43,8 @@ static gboolean   script_fu_script_param_init (SFScript             *script,
                                                gint                  n);
 
 
+
+
 /*
  *  Function definitions
  */
@@ -165,26 +167,59 @@ script_fu_script_free (SFScript *script)
   g_slice_free (SFScript, script);
 }
 
+
+/*
+ * From the script, create a temporary PDB procedure,
+ * and install it as owned by the scriptfu extension PDB proc.
+ */
 void
 script_fu_script_install_proc (GimpPlugIn  *plug_in,
                                SFScript    *script,
                                GimpRunFunc  run_func)
 {
   GimpProcedure *procedure;
-  const gchar   *menu_label            = NULL;
-  gint           arg_count[SF_DISPLAY] = { 0, };
-  gint           i;
 
   g_return_if_fail (GIMP_IS_PLUG_IN (plug_in));
   g_return_if_fail (script != NULL);
   g_return_if_fail (run_func != NULL);
 
+  procedure = script_fu_script_create_PDB_procedure (plug_in,
+                                                     script,
+                                                     run_func,
+                                                     GIMP_PDB_PROC_TYPE_TEMPORARY);
+
+  gimp_plug_in_add_temp_procedure (plug_in, procedure);
+  g_object_unref (procedure);
+}
+
+
+/*
+ * Create and return a GimpProcedure.
+ * Caller typically either:
+ *    install it owned by self as TEMPORARY type procedure
+ *    OR return it as the result of a create_procedure callback from GIMP (PLUGIN type procedure.)
+ *
+ * Caller must unref the procedure.
+ */
+GimpProcedure *
+script_fu_script_create_PDB_procedure (GimpPlugIn     *plug_in,
+                                       SFScript       *script,
+                                       GimpRunFunc     run_func,
+                                       GimpPDBProcType plug_in_type)
+{
+  GimpProcedure *procedure;
+  const gchar   *menu_label            = NULL;
+  gint           arg_count[SF_DISPLAY] = { 0, };
+  gint           i;
+
+  g_debug ("script_fu_script_create_PDB_procedure: %s of type %i", script->name, plug_in_type);
+
   /* Allow scripts with no menus */
   if (strncmp (script->menu_label, "<None>", 6) != 0)
     menu_label = script->menu_label;
 
   procedure = gimp_procedure_new (plug_in, script->name,
-                                  GIMP_PDB_PROC_TYPE_TEMPORARY,
+                                  plug_in_type,
                                   run_func, script, NULL);
 
   gimp_procedure_set_image_types (procedure, script->image_types);
@@ -509,8 +544,7 @@ script_fu_script_install_proc (GimpPlugIn  *plug_in,
       gimp_procedure_add_argument (procedure, pspec);
     }
 
-  gimp_plug_in_add_temp_procedure (plug_in, procedure);
-  g_object_unref (procedure);
+  return procedure;
 }
 
 void
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-script.h 
b/plug-ins/script-fu/libscriptfu/script-fu-script.h
index c9a16f607a..a9901cd200 100644
--- a/plug-ins/script-fu/libscriptfu/script-fu-script.h
+++ b/plug-ins/script-fu/libscriptfu/script-fu-script.h
@@ -46,5 +46,11 @@ gchar    * script_fu_script_get_command             (SFScript             *scrip
 gchar    * script_fu_script_get_command_from_params (SFScript             *script,
                                                      const GimpValueArray *args);
 
+GimpProcedure * script_fu_script_create_PDB_procedure (GimpPlugIn         *plug_in,
+                                                       SFScript           *script,
+                                                       GimpRunFunc         run_func,
+                                                       GimpPDBProcType     plug_in_type);
+
+
 
 #endif /*  __SCRIPT_FU_SCRIPT__  */
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-scripts.c 
b/plug-ins/script-fu/libscriptfu/script-fu-scripts.c
index eac9d0f49d..914ea1a6c6 100644
--- a/plug-ins/script-fu/libscriptfu/script-fu-scripts.c
+++ b/plug-ins/script-fu/libscriptfu/script-fu-scripts.c
@@ -43,13 +43,6 @@
 #include "script-fu-intl.h"
 
 
-typedef struct
-{
-  SFScript *script;
-  gchar    *menu_path;
-} SFMenu;
-
-
 /*
  *  Local Functions
  */
@@ -65,11 +58,6 @@ static void             script_fu_install_menu   (SFMenu               *menu);
 static gboolean         script_fu_remove_script  (gpointer              foo,
                                                   GList                *scripts,
                                                   gpointer              data);
-static GimpValueArray * script_fu_script_proc    (GimpProcedure        *procedure,
-                                                  const GimpValueArray *args,
-                                                  gpointer              data);
-
-static SFScript       * script_fu_find_script    (const gchar          *name);
 
 static gchar          * script_fu_menu_map       (const gchar          *menu_path);
 static gint             script_fu_menu_compare   (gconstpointer         a,
@@ -88,13 +76,25 @@ static GList *script_menu_list = NULL;
  *  Function definitions
  */
 
-void
-script_fu_find_scripts (GimpPlugIn *plug_in,
-                        GList      *path)
+/* Traverse list of paths, finding .scm files.
+ * Load and eval any found script texts.
+ * Script texts will call Scheme functions script-fu-register()
+ * and script-fu-menu-register(),
+ * which insert a SFScript record into script_tree,
+ * and insert a SFMenu record into script_menu_list.
+ * These are side effects on the state of the outer (SF) interpreter.
+ *
+ * Return the tree of scripts, as well as keeping a local pointer to the tree.
+ * The other result (script_menu_list) is not returned, see script_fu_get_menu_list().
+ *
+ * Caller should free script_tree and script_menu_list,
+ * This should only be called once.
+ */
+GTree *
+script_fu_find_scripts_into_tree ( GimpPlugIn *plug_in,
+                                   GList      *paths)
 {
-  GList *list;
-
-  /*  Make sure to clear any existing scripts  */
+  /*  Clear any existing scripts  */
   if (script_tree != NULL)
     {
       g_tree_foreach (script_tree,
@@ -103,16 +103,46 @@ script_fu_find_scripts (GimpPlugIn *plug_in,
       g_tree_destroy (script_tree);
     }
 
-  if (! path)
-    return;
-
   script_tree = g_tree_new ((GCompareFunc) g_utf8_collate);
 
-  for (list = path; list; list = g_list_next (list))
+  if (paths)
     {
-      script_fu_load_directory (list->data);
+      GList *list;
+
+      for (list = paths; list; list = g_list_next (list))
+        {
+          script_fu_load_directory (list->data);
+        }
     }
 
+  /*
+   * Assert result is not NULL, but may be an empty tree.
+   * When paths is NULL, or no scripts found at paths.
+   */
+
+  g_debug ("script_fu_find_scripts_into_tree found %i scripts", g_tree_nnodes (script_tree));
+  return script_tree;
+}
+
+/*
+ * Return list of SFMenu for recently loaded scripts.
+ * List is non-empty only after a call to script_fu_find_scripts_into_tree().
+ */
+GList *
+script_fu_get_menu_list (void)
+{
+  return script_menu_list;
+}
+
+/* Find scripts, create and install TEMPORARY PDB procedures,
+ * owned by self PDB procedure (e.g. extension-script-fu.)
+ */
+void
+script_fu_find_scripts (GimpPlugIn *plug_in,
+                        GList      *path)
+{
+  script_fu_find_scripts_into_tree (plug_in, path);
+
   /*  Now that all scripts are read in and sorted, tell gimp about them  */
   g_tree_foreach (script_tree,
                   (GTraverseFunc) script_fu_install_script,
@@ -571,12 +601,13 @@ script_fu_run_command (const gchar  *command,
   GString  *output;
   gboolean  success = FALSE;
 
+  g_debug ("script_fu_run_command: %s", command);
   output = g_string_new (NULL);
   ts_register_output_func (ts_gstring_output_func, output);
 
   if (ts_interpret_string (command))
     {
-      g_set_error (error, 0, 0, "%s", output->str);
+      g_set_error (error, GIMP_PLUG_IN_ERROR, 0, "%s", output->str);
     }
   else
     {
@@ -593,6 +624,8 @@ script_fu_load_directory (GFile *directory)
 {
   GFileEnumerator *enumerator;
 
+  g_debug ("Load dir: %s", g_file_get_parse_name (directory));
+
   enumerator = g_file_enumerate_children (directory,
                                           G_FILE_ATTRIBUTE_STANDARD_NAME ","
                                           G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
@@ -665,8 +698,10 @@ script_fu_load_script (GFile *file)
     }
 }
 
-/*
- *  The following function is a GTraverseFunction.
+/* This is-a GTraverseFunction.
+ *
+ * Traverse.  For each, install TEMPORARY PDB proc.
+ * Returning FALSE means entire list was traversed.
  */
 static gboolean
 script_fu_install_script (gpointer  foo G_GNUC_UNUSED,
@@ -680,8 +715,12 @@ script_fu_install_script (gpointer  foo G_GNUC_UNUSED,
     {
       SFScript *script = list->data;
 
-      script_fu_script_install_proc (plug_in, script,
-                                     script_fu_script_proc);
+      const gchar* name = script->name;
+      if (script_fu_is_defined (name))
+        script_fu_script_install_proc (plug_in, script,
+                                       script_fu_script_proc);
+      else
+        g_warning ("Run function not defined, or does not match PDB procedure name: %s", name);
     }
 
   return FALSE;
@@ -690,13 +729,14 @@ script_fu_install_script (gpointer  foo G_GNUC_UNUSED,
 static void
 script_fu_install_menu (SFMenu *menu)
 {
-  GimpPlugIn    *plug_in = gimp_get_plug_in ();
-  GimpProcedure *procedure;
+  GimpPlugIn    *plug_in   = gimp_get_plug_in ();
+  GimpProcedure *procedure = NULL;
 
   procedure = gimp_plug_in_get_temp_procedure (plug_in,
                                                menu->script->name);
 
-  gimp_procedure_add_menu_path (procedure, menu->menu_path);
+  if (procedure)
+    gimp_procedure_add_menu_path (procedure, menu->menu_path);
 
   g_free (menu->menu_path);
   g_slice_free (SFMenu, menu);
@@ -726,7 +766,15 @@ script_fu_remove_script (gpointer  foo G_GNUC_UNUSED,
   return FALSE;
 }
 
-static GimpValueArray *
+/* This is the outer "run func" for this plugin.
+ * When called, the name of the inner run func (code in Scheme language)
+ * is the first element of the value array.
+ * Form a command (text in Scheme language) that is a call to the the inner run func,
+ * evaluate it, and return the result, marshalled into a GimpValueArray.
+ *
+ * In the name 'script_fu_script_proc',  'proc' is a verb meaning 'process the script'
+ */
+GimpValueArray *
 script_fu_script_proc (GimpProcedure        *procedure,
                        const GimpValueArray *args,
                        gpointer              data)
@@ -841,7 +889,7 @@ script_fu_lookup_script (gpointer      *foo G_GNUC_UNUSED,
   return FALSE;
 }
 
-static SFScript *
+SFScript *
 script_fu_find_script (const gchar *name)
 {
   gconstpointer script = name;
@@ -916,3 +964,43 @@ script_fu_menu_compare (gconstpointer a,
 
   return retval;
 }
+
+/* Is name a defined symbol in the interpreter state?
+ * (Defined in any script already loaded.)
+ * Where "symbol" has the usual lisp meaning: a unique name associated with
+ * a variable or function.
+ *
+ * The most common use is
+ * test the name of a PDB proc, which in ScriptFu must match
+ * a defined function that is the inner run function.
+ * I.E. check for typos by author of script.
+ * Used during query, to preflight so that we don't install a PDB proc
+ * that won't run later (during the run phase)
+ * giving "undefined symbol" for extension-script-fu.
+ * Note that if instead we create a PDB proc having no defined run func,
+ * script-fu-interpreter would load and define a same-named scheme function
+ * that calls the PDB, and can enter an infinite loop.
+ */
+gboolean
+script_fu_is_defined (const gchar * name)
+{
+  gchar   *scheme_text;
+  GError  *error = NULL;
+  gboolean result;
+
+  /* text to be interpreted is a call to an internal scheme function. */
+  scheme_text = g_strdup_printf (" (symbol? %s ) ",  name);
+
+  /* Use script_fu_run_command, it correctly handles the string yielded.
+   * But we don't need the string yielded.
+   * If defined, string yielded is "#t", else is "Undefined symbol" or "#f"
+   */
+  result = script_fu_run_command (scheme_text, &error);
+  if (!result)
+    {
+      g_debug ("script_fu_is_defined returns false");
+      /* error contains string yielded by interpretation. */
+      g_error_free (error);
+    }
+  return result;
+}
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-scripts.h 
b/plug-ins/script-fu/libscriptfu/script-fu-scripts.h
index b0079a39d3..b81e779ab1 100644
--- a/plug-ins/script-fu/libscriptfu/script-fu-scripts.h
+++ b/plug-ins/script-fu/libscriptfu/script-fu-scripts.h
@@ -26,5 +26,13 @@ pointer   script_fu_add_script    (scheme     *sc,
 pointer   script_fu_add_menu      (scheme     *sc,
                                    pointer     a);
 
+GTree          * script_fu_find_scripts_into_tree (GimpPlugIn  *plug_in,
+                                                   GList       *path);
+SFScript       * script_fu_find_script            (const gchar *name);
+GList          * script_fu_get_menu_list          (void);
+GimpValueArray * script_fu_script_proc            (GimpProcedure        *procedure,
+                                                   const GimpValueArray *args,
+                                                   gpointer              data);
+gboolean         script_fu_is_defined             (const gchar          *name);
 
 #endif /*  __SCRIPT_FU_SCRIPTS__  */
diff --git a/plug-ins/script-fu/libscriptfu/script-fu-types.h 
b/plug-ins/script-fu/libscriptfu/script-fu-types.h
index f0231b549e..981e450e0e 100644
--- a/plug-ins/script-fu/libscriptfu/script-fu-types.h
+++ b/plug-ins/script-fu/libscriptfu/script-fu-types.h
@@ -103,5 +103,10 @@ typedef struct
   SFArg        *args;
 } SFScript;
 
+typedef struct
+{
+  SFScript *script;   // script which defined this menu path and label
+  gchar    *menu_path;
+} SFMenu;
 
 #endif /*  __SCRIPT_FU_TYPES__  */
diff --git a/plug-ins/script-fu/libscriptfu/script-fu.def b/plug-ins/script-fu/libscriptfu/script-fu.def
index 4d1f385df0..1c625bf088 100644
--- a/plug-ins/script-fu/libscriptfu/script-fu.def
+++ b/plug-ins/script-fu/libscriptfu/script-fu.def
@@ -13,3 +13,5 @@ EXPORTS
   script_fu_register_quit_callback
   script_fu_register_post_command_callback
   script_fu_search_path
+  script_fu_find_scripts_create_PDB_proc_plugin
+  script_fu_find_scripts_list_proc_names
diff --git a/plug-ins/script-fu/meson.build b/plug-ins/script-fu/meson.build
index c65cefcda7..c19d040f03 100644
--- a/plug-ins/script-fu/meson.build
+++ b/plug-ins/script-fu/meson.build
@@ -10,6 +10,7 @@
 subdir('libscriptfu')
 subdir('scripts')
 subdir('server')
+subdir('interpreter')
 
 
 executable_name = 'script-fu'
diff --git a/plug-ins/script-fu/scripts/Makefile.am b/plug-ins/script-fu/scripts/Makefile.am
index 75d89327a6..c9564d8f78 100644
--- a/plug-ins/script-fu/scripts/Makefile.am
+++ b/plug-ins/script-fu/scripts/Makefile.am
@@ -59,9 +59,17 @@ scripts = \
 
 test_scripts = \
        contactsheet.scm                \
-       test-sphere.scm                 \
+       test-sphere.scm
+
+# scripts interpreted by gimp-script-fu-interpreter
+# Each installs to a subdir of /plug-ins
+# Each should have a shebang and execute permission
+independent_scripts = \
        ts-helloworld.scm
 
+ts_helloworlddir = $(gimpplugindir)/plug-ins/ts-helloworld
+ts_helloworld_SCRIPTS = ts-helloworld.scm
+
 
 scriptdata_DATA = $(scripts)
 
@@ -69,4 +77,4 @@ if GIMP_UNSTABLE
 scriptdata_DATA += $(test_scripts)
 endif
 
-EXTRA_DIST = $(scripts) $(test_scripts)
+EXTRA_DIST = $(scripts) $(test_scripts) $(independent_scripts)
diff --git a/plug-ins/script-fu/scripts/meson.build b/plug-ins/script-fu/scripts/meson.build
index 840f954240..5dc33b52b1 100644
--- a/plug-ins/script-fu/scripts/meson.build
+++ b/plug-ins/script-fu/scripts/meson.build
@@ -1,5 +1,6 @@
 subdir('images')
 
+# scripts interpreted by extension-script-fu, installed to /scripts
 scripts = [
   'add-bevel.scm',
   'addborder.scm',
@@ -58,7 +59,6 @@ if not stable
   scripts += [
     'contactsheet.scm',
     'test-sphere.scm',
-    'ts-helloworld.scm',
   ]
 endif
 
@@ -66,3 +66,22 @@ install_data(
   scripts,
   install_dir: gimpdatadir / 'scripts',
 )
+
+# scripts interpreted by gimp-script-fu-interpreter
+# Each installed in subdirectory of /plug-in
+# Each have a shebang and executable permission.
+# Like other interpreted plugins.
+# Lacking a shebang, a .interp file is needed to associate .scm suffix
+
+scripts_independent = [
+  { 'name': 'ts-helloworld' },
+]
+
+foreach plugin : scripts_independent
+  name = plugin.get('name')
+  srcs = plugin.get('srcs', name + '.scm')
+
+  install_data(srcs,
+               install_dir: gimpplugindir / 'plug-ins' / name,
+               install_mode: 'rwxr-xr-x')
+endforeach
diff --git a/plug-ins/script-fu/scripts/test/README b/plug-ins/script-fu/scripts/test/README
new file mode 100644
index 0000000000..4e2f613210
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/README
@@ -0,0 +1,5 @@
+Scripts to test script-fu-interpreter.
+
+Not usually installed.
+
+To use, copy the test<x> dirs into /plugins and ensure the .scm files have execute permission.
diff --git a/plug-ins/script-fu/scripts/test/test0/test0.scm b/plug-ins/script-fu/scripts/test/test0/test0.scm
new file mode 100644
index 0000000000..2680258873
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/test0/test0.scm
@@ -0,0 +1,31 @@
+#!/usr/bin/env gimp-script-fu-interpreter-3.0
+
+; Basic test of a .scm file interpreted by script-fu-interpreter
+;
+; Setup: copy this file w/ executable permission, and its parent dir to /plug-ins
+; Example: to ~/.gimp-2.99/plug-ins/test0/test0.scm
+; (That is custom to one user.)
+
+; Expect "Test>Test SF interpreter 0" in the menus
+; Expect when chosen, message on GIMP message bar.
+
+; Also, remove the execute permission.
+; Then expect not appear in GIMP menus (not queried.)
+
+; Also, make the name different from its parent dir.
+; Then expect not appear in GIMP menus (not queried.)
+
+(define (script-fu-test0)
+  (gimp-message "Hello script-fu-test0")
+)
+
+(script-fu-register "script-fu-test0"
+  "Test SF interpreter 0"
+  _"Just gives a message from Gimp"
+  "lkk"
+  "lkk"
+  "2022"
+  ""  ; all image types
+)
+
+(script-fu-menu-register "script-fu-test0" "<Image>/Test")
diff --git a/plug-ins/script-fu/scripts/test/test1/test1.scm b/plug-ins/script-fu/scripts/test/test1/test1.scm
new file mode 100644
index 0000000000..04f27af67a
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/test1/test1.scm
@@ -0,0 +1,43 @@
+#!/usr/bin/env gimp-script-fu-interpreter-3.0
+
+; Basic test that a second .scm file is also queried.
+; Expect "Test>Test SF interpreter 1" in the menus
+; Expect when chosen, message on GIMP message bar.
+
+; Also tests that one .scm file can define two PDB procedures
+; File is queried once, yielding two names.
+; Two separate procedures created.
+
+
+(define (script-fu-test1)
+  (gimp-message "Hello script-fu-test1")
+)
+
+(script-fu-register "script-fu-test1"
+  "Test SF interpreter 01"
+  _"Just gives a message from Gimp"
+  "lkk"
+  "lkk"
+  "2022"
+  ""  ; all image types
+)
+
+(script-fu-menu-register "script-fu-test1" "<Image>/Test")
+
+
+
+
+(define (script-fu-test2)
+  (gimp-message "Hello script-fu-test2")
+)
+
+(script-fu-register "script-fu-test2"
+  "Test SF interpreter 02"
+  _"Just gives a message from Gimp"
+  "lkk"
+  "lkk"
+  "2022"
+  ""  ; all image types
+)
+
+(script-fu-menu-register "script-fu-test2" "<Image>/Test")
diff --git a/plug-ins/script-fu/scripts/test/test1/test3.scm b/plug-ins/script-fu/scripts/test/test1/test3.scm
new file mode 100644
index 0000000000..49a89a5447
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/test1/test3.scm
@@ -0,0 +1,31 @@
+; !!! No shebang here
+
+; Test a second .scm file in the same directory as a queried .scm.
+; The second .scm file need not be executable.
+; The second .scm file need not have a shebang.
+; The gimp-script-fu-interpreter will nevertheless load the second .scm
+; while it is querying the first, executable .scm in the dir.
+; The plugin manager queries the first executable,
+; and gimp-script-fu-interpreter loads (and returns defined names in)
+; the second during the query of the first.
+
+; Expect "Test>Test SF interpreter 3" in the menus
+; Expect when chosen, message on GIMP message bar.
+
+; plug-ins/test1/test1.scm is executable
+; plug-ins/test1/test3.scm is NOT executable
+
+(define (script-fu-test3)
+  (gimp-message "Hello script-fu-test3")
+)
+
+(script-fu-register "script-fu-test3"
+  "Test SF interpreter 3"
+  _"Just gives a message from Gimp"
+  "lkk"
+  "lkk"
+  "2022"
+  ""  ; all image types
+)
+
+(script-fu-menu-register "script-fu-test3" "<Image>/Test")
diff --git a/plug-ins/script-fu/scripts/test/test4/test4.scm b/plug-ins/script-fu/scripts/test/test4/test4.scm
new file mode 100644
index 0000000000..de515831dd
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/test4/test4.scm
@@ -0,0 +1,25 @@
+#!/usr/bin/env gimp-script-fu-interpreter-3.0
+
+; Test a .scm file that does not call script-fu-menu-register
+; The menu will NOT default.
+; Expect "Test SF interpreter 4" to NOT EXIST in any menu
+; Expect the PDB proc "script-fu-test4" does appear in the PDB Brower
+
+; Two test cases:
+;    Alongside an executable script:  plug-ins/test4/test4.scm NOT executable
+;    Executable, in its own directory: plug-ins/test1/test4.scm is executable
+
+(define (script-fu-test4)
+  (gimp-message "Hello script-fu-test4")
+)
+
+(script-fu-register "script-fu-test4"
+  "Test SF interpreter 4"
+  _"Just gives a message from Gimp"
+  "lkk"
+  "lkk"
+  "2022"
+  ""  ; all image types
+)
+
+; !!! No call to script-fu-menu-register
diff --git a/plug-ins/script-fu/scripts/test/test5/test5.scm b/plug-ins/script-fu/scripts/test/test5/test5.scm
new file mode 100644
index 0000000000..79f3937886
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/test5/test5.scm
@@ -0,0 +1,16 @@
+#!/usr/bin/env gimp-script-fu-interpreter
+
+; Test a .scm file with an invalid shebang
+; Note "-3.0" missing above.
+
+; The test depends on platform and env and .interp
+; Must not be a file system link from gimp-script-fu-interpreter to gimp-script-fu-interpreter-3.0
+; Must not be a .interp file having  "gimp-script-fu-interpreter=gimp-script-fu-interpreter-3.0"
+
+; Expect in the console: "/usr/bin/env: 'script-fu-interpreter': No such file or directory"
+
+(define (script-fu-test5)
+  (gimp-message "Hello script-fu-test5")
+)
+
+; !!! No call to script-fu-menu-register
diff --git a/plug-ins/script-fu/scripts/test/test6/test6.scm b/plug-ins/script-fu/scripts/test/test6/test6.scm
new file mode 100644
index 0000000000..f5035f2a97
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/test6/test6.scm
@@ -0,0 +1,12 @@
+#!/usr/bin/env gimp-script-fu-interpreter-3.0
+
+; Test a .scm file that does not register any procedure
+
+; Expect in the console:
+; "(test6.scm:164): scriptfu-WARNING **: 10:06:07.966: No procedures defined in 
/work/.home/.config/GIMP/2.99/plug-ins/test6/test6.scm"
+
+(define (script-fu-test6)
+  (gimp-message "Hello script-fu-test6")
+)
+
+; !!! No call to script-fu-register
diff --git a/plug-ins/script-fu/scripts/test/test7/test7.scm b/plug-ins/script-fu/scripts/test/test7/test7.scm
new file mode 100644
index 0000000000..913772eb43
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/test7/test7.scm
@@ -0,0 +1,28 @@
+#!/usr/bin/env gimp-script-fu-interpreter-3.0
+
+; Test non-canonical name for PDB procedure
+; gimp-script-fu-interpreter does not enforce canonical name.
+; Other parts of GIMP (PDB) does not enforce canonical name
+; for PDB procedures defined by .scm scripts.
+
+; Canonical means starts with "script-fu-"
+; Here the name doesn't, its just "test7"
+
+; Expect "Test>Test SF interpreter 7" in the menus
+; Expect when chosen, message on GIMP message bar.
+
+
+(define (test7)
+  (gimp-message "Hello test7")
+)
+
+(script-fu-register "test7"
+  "Test SF interpreter 7"
+  _"Just gives a message from Gimp"
+  "lkk"
+  "lkk"
+  "2022"
+  ""  ; all image types
+)
+
+(script-fu-menu-register "test7" "<Image>/Test")
diff --git a/plug-ins/script-fu/scripts/test/test8/test8.scm b/plug-ins/script-fu/scripts/test/test8/test8.scm
new file mode 100644
index 0000000000..f7cd41998b
--- /dev/null
+++ b/plug-ins/script-fu/scripts/test/test8/test8.scm
@@ -0,0 +1,39 @@
+#!/usr/bin/env gimp-script-fu-interpreter-3.0
+
+; Test mismatch between name of defined run function and name for PDB procedure
+; Not a high priority: a rare syntax error in a plugin text.
+; If authors follow a template, they won't make this mistake.
+
+; The names must match exactly.
+; Here, "mismatch" the name of the defined run function
+; does not match "script-fu-test8" the name of the PDB proc.
+
+; Expect a warning in the text console as the plugin text is queried:
+; script-fu: WARNING: Run function not defined, or does not match PDB procedure name: script-fu-test8.
+; Expect the PDB procedure to not exist
+
+; If we don't detect this syntax error:
+; A PDB procedure is created.
+; When invoked from Test>Test SF interpreter 8"
+; the interpreter enters an infinite loop.
+; There is no harm to the GIMP app, but the interpreter process can only be killed.
+; During the run phase, the "(define foo)"
+; should re-define an existing definition in the interpreter state.
+; Instead, since the name is mismatched,
+; the foo function remains defined to be a call to the PDB procedure named foo.
+; So script-fu-script-proc instead calls the PDB again, an infinite loop.
+
+(define (mismatch)
+  (gimp-message "mismatch")
+)
+
+(script-fu-register "script-fu-test8"
+  "Test SF interpreter 8"
+  _"Just gives a message from Gimp"
+  "lkk"
+  "lkk"
+  "2022"
+  ""  ; all image types
+)
+
+(script-fu-menu-register "script-fu-test8" "<Image>/Test")
diff --git a/plug-ins/script-fu/scripts/ts-helloworld.scm b/plug-ins/script-fu/scripts/ts-helloworld.scm
index 075c1ef1ed..9611d520e3 100644
--- a/plug-ins/script-fu/scripts/ts-helloworld.scm
+++ b/plug-ins/script-fu/scripts/ts-helloworld.scm
@@ -1,3 +1,5 @@
+#!/usr/bin/env gimp-script-fu-interpreter-3.0
+
 ; "Hello, World" Test v1.00 February 29, 2004
 ; by Kevin Cozens <kcozens interlog com>
 ;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]