summaryrefslogtreecommitdiff
path: root/Master/bin/win32/runscript.tlu
blob: 40e9577fc06afcc27566dc4e9f43f32419d26afb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534

-- $Id$

local svnrevision = string.match("$Revision$", "%d+") or "0"
local svndate     = string.match("$Date$", "[-%d]+") or "2009-12-04"
local bannerstr   = "runscript wrapper utility (rev. " ..
                    svnrevision .. ", " .. svndate .. ")\n" .. 
                    "usage:   runscript script-name [arguments]\n" ..
                    "try -help [-v] for more information"

local helpstr = [[

  Script wrappers in TeX Live on Windows
  
  Rationale
  
    Wrappers enable use of scripts on Windows as regular programs.  
    They are also required for some binary programs to set up the 
    right environment for them. 
    
    Batch scripts can be used for wrapping but they are not as universal 
    as binaries (there are some odd cases where they don't work) and 
    it is hard to make them robust and secure.  Compiled binary wrappers 
    don't suffer from these problems but they are harder to write, debug
    and maintain in comparison to scripts.  For these reasons a hybrid 
    approach is taken that combines a binary stub with a launcher script.
  
  Adding wrappers for user scripts
  
    The script wrapping machinery is not limited to scripts shipped with 
    TeX Live.  You can also use it for script programs from manually 
    installed packages, which should minimize the problems when using 
    them with TeX Live. 
    
    First, make sure that there is an interpreter program available on 
    your system for the script you want to use.  Interpreters for Perl 
    and Lua are bundled with TeX Live, all others have to be installed 
    independently.  Lua scripts are the most efficient to run, so if you
    consider writing a new script, that would be the recommended choice.
    
    The following script types and their file extensions are currently 
    supported and searched in that order:
    
      Lua      (.tlu;.texlua;.lua) --  included
      Perl     (.pl)               --  included
      Ruby     (.rb)               --  requires installation
      Python   (.py)               --  requires installation
      Tcl      (.tcl)              --  requires installation
      Java     (.jar)              --  requires installation
      VBScript (.vbs)              --  part of Windows
      Jscript  (.js)               --  part of Windows
      Batch    (.bat;.cmd)         --  part of Windows
    
    For Unix-style extensionless scripts the first line of the script is 
    consulted for a she-bang (#!) specification of the interpreter 
    program.  This can be an arbitrary program but it must be present on 
    the search path.
    
    Next, the script program needs to be installed somewhere below the 
    'scripts' directory under one of the TEXMF trees (consult the 
    documentation or texmf/web2c/texmf.cnf file for a list).  You may 
    need to update the file search database afterwards with:
    
      mktexlsr [TEXMFDIR]
      
    Test if the script can be located with:
    
      kpsewhich --format=texmfscripts <script-name>.<ext>
    
    Once installed the script can be called immediately with:
    
      runscript <script-name> [script arguments]
    
    If you prefer to call the script program simply by its name, copy 
    and rename bin/win32/runscript.exe to <script-name>.exe and put it 
    in bin/win32/ directory of your TeX Live installation or, if you 
    don't have the write permissions there, somewhere else on the search 
    path.]]

local docstr = [[

  Wrapper structure
  
    Wrappers consist of small binary stubs and a common texlua script.  
    The binary stubs are all the same, just different names (but CLI 
    and GUI stubs differ, see below, and GUI stubs are actually all 
    different due to different embedded icons).
    
    The job of the binary stub is twofold: (a) call the texlua launcher
    script 'runscript.tlu' from the same directory (or more precisely 
    from the directory containing 'runscript.dll') and (b) pass to it 
    argv[0] and the unparsed argument string as the last two arguments 
    (after adding a sentinel argument, which ends with a new line 
    character).  Arbitrary C strings can be passed, because the script 
    is executed by linking with luatex.dll and calling the lua 
    interpreter internally rather than by spawning a new process.
    
    There are two flavours of the binary stub: one for CLI programs 
    and another one for GUI programs.  The GUI variant does not open 
    a console window nor does it block the command prompt if started 
    from there.  It also uses a dialog box to display an error message 
    in addition to outputting to stderr.
    
    The stubs are further split into a common DLL and EXE proxies 
    to it.  This is for maintenance reasons - updates can be done by 
    replacement of a single DLL rather than all binary stubs.
    
    The launcher script knows, which variant has been used to invoke it 
    based on the sentinel argument.  The lack of this argument means 
    that it was invoked in a standard way, i.e. through texlua.exe. 
    
    All the hard work of locating a script/program to execute happens 
    in the launcher script.  The located script/program is always 
    executed directly by spawning its interpreter (or binary) in a new 
    process. The system shell (cmd.exe) is never called (except for 
    batch scripts, of course).  If the located script happens to be 
    a (tex)lua script, it is loaded and called internally from within 
    this script, i.e. no new process is spawned.
    
  Source files
  
    runscript.tlu     launcher script for locating and dispatching 
                      target scripts/programs
    runscript_dll.c   common DLL part of the binary stubs; locates and
                      calls the launcher script
    runscript_exe.c   EXE proxy to the common DLL for CLI mode stubs
    wrunscript_exe.c  EXE proxy to the common DLL for GUI mode stubs
  
  Compilation of binaries (requires luatex.dll in the same directory)

    with gcc (size optimized):
    
    gcc -Os -s -shared -o runscript.dll runscript_dll.c -L./ -lluatex
    gcc -Os -s -o runscript.exe runscript_exe.c -L./ -lrunscript
    gcc -mwindows -Os -s -o wrunscript.exe wrunscript_exe.c -L./ -lrunscript

    with tcc (extra small size):
    
    tiny_impdef luatex.dll
    tcc -shared -o runscript.dll runscript_dll.c luatex.def
    tcc -o runscript.exe runscript_exe.c runscript.def
    tcc -o wrunscript.exe wrunscript_exe.c runscript.def

  License
  
    Originally written in 2009 by Tomasz M. Trzeciak, Public Domain.
    
    Prior work:
    'tl-w32-wrapper.texlua' by Reinhard Kotucha and Norbert Preining.
    'tl-w32-wrapper.cmd' by Tomasz M. Trzeciak.
  
  Changelog
  
    2009/12/04 
        - initial version
    2009/12/15 
        - minor fixes for path & extension list parsing
    2010/01/09 
        - added support for GUI mode stubs
    2010/02/28 
        - enable GUI mode stubs for dviout, psv and texworks;
        - added generic handling of sys programs
        - added restricted repstopdf to alias_table
    2010/03/13 
        - added 'readme.txt' and changelog
        - added support and docs for calling user added scripts; 
          (use path of 'runscript.dll' instead of .exe stub to 
          locate 'runscript.tlu' script)
        - limit search for shell_escape_commands to system trees
        - added function for creating directory hierarchy 
        - fixed directory creation for dviout & texworks aliases
        - fixed arg[0] of repstopdf & rpdfcrop
    2010/03/28
        - restructured docs, added --help and --version options 
          (available only when invoked under 'runscript' name)
        - use TEXMF_RESTRICTED_SCRIPTS kpse var for searching
          shell_escape_commands
        - changed command validation to handle a list of commands
        - prepend GUI mode command(s) to the command list
        - added support for .tcl scripts
    2010/03/31
        - fixed fatal bug in extention_map definition for GUI mode
]]

-- HELPER SUBROUTINES --

-- quotes string with spaces
local function _q(str)
  str = string.gsub(str, '"', '') -- disallow embedded double quotes
  return string.find(str, "%s") and '"'..str..'"' or str
end

-- checks if path is absolute (but not if it actually exists)
local function is_abs_path(fpath)
  return string.find(fpath, '^[a-zA-Z]:[/\\]') and true or false
end

-- prepends directories to path if they are not already there
local function prepend_path(path, ...)
  local pathcmp = string.lower(string.gsub(path, '/', '\\'))..';'
  for k = 1, select('#', ...) do 
    local dir = string.lower(string.gsub(select(k, ...), '/', '\\'))..';'
    if not string.find(pathcmp, dir, 1, true) then path = dir..path end
  end
  return path
end

-- searches the PATH for a file
local function search_path(fname, PATH, PATHEXT)
  if string.find(fname, '[/\\]') then 
    return nil, "directory part not allowed for PATH search: "..fname 
  end
  PATH = PATH or os.getenv('PATH')
  PATHEXT = PATHEXT or '\0' -- '\0' for no extension
  for dir in string.gmatch(PATH, '[^;]+') do
    local dirsep = (string.find(dir, '\\') and '\\' or '/')
    for ext in string.gmatch(PATHEXT, '[^;]+') do
      local f = dir..dirsep..fname..ext
      if lfs.isfile(f) then return f, ext end
    end
  end
  return nil, "file or program not on PATH: "..fname
end

-- locates texmfscript to execute
local function find_texmfscript(progname, ext_list)
  for ext in string.gmatch(ext_list, '[^;]+') do
    local progfullname = kpse.find_file(progname..ext, 'texmfscripts')
    if progfullname then return progfullname, ext end
  end
  return nil, "no appropriate script or program found: "..progname
end

-- converts the #! line to arg table 
-- used for scripts w/o extension
-- only the two most common cases are considered:
-- #! /path/to/command [options]
-- #! /usr/bin/env command [options]
-- ([options] after the command are retained as well)
local function shebang_to_argv(progfullname)
  local fid, errmsg = io.open(progfullname, 'r')
  if not fid then return nil, errmsg end
  local fstln = fid:read('*line')
  fid:close()
  if (string.sub(fstln, 1, 2) ~= '#!') then
    return nil, "don't know how to execute script: "..progfullname
  end
  local argv = string.explode( string.sub(fstln, 3) ) -- split on spaces
  argv[1] = string.match(argv[1], '[^/]+$')
  if (argv[1] == 'env') then table.remove(argv, 1) end
  return argv
end

-- checks if command exists on the path and returns its full path
local function check_command(cmdlist, PATH)
  for cmd in string.gmatch(cmdlist, '%S+') do
    local cmdext = cmd..(string.find(cmd, '%.[^\\/.]*$') and '' or '.exe')
    local fullcmd = is_abs_path(cmdext) and lfs.isfile(cmdext) and 
                    cmdext or search_path(cmdext, PATH)
    if fullcmd then 
      return fullcmd, cmd
    end
  end
  return nil, 'program not found (not part of TeX Live): '..cmdlist
end

-- creates directory or directory hierarchy
local function mkdir_plus(dir)
  if lfs.mkdir(dir) then
    return true
  end
  -- try with system's mkdir in case we need to create intermediate dirs too
  local ret = os.spawn({[0]=search_path("cmd.exe"), 
                        string.format('cmd.exe /x /c mkdir "%s"', dir)})
  if ret == 0 then
    return true
  else
    return nil, "cannot create directory: " .. dir
  end
end

-- MAIN CHUNK --

-- localize the assert function (it will be replaced in gui mode)
local assert = assert 
local guimode = false
local argline = ''
-- check for the sentinel argment coming from the .exe stub 
if arg[#arg-2] and ( string.sub(arg[#arg-2], -1) == '\n' ) then
  -- argv[0] and unparsed argument line are passed 
  -- from the .exe stub as the two last arguments 
  -- pop them up from the arg table
  argline = table.remove(arg) -- pop unparsed arguments
  arg[0] = table.remove(arg) -- pop C stub's argv[0]
  guimode = (table.remove(arg) == 'GUI_MODE\n') -- pop sentinel argument
  if guimode then
    -- replace the assert function, if we are running w/o console window
    function assert(...)
      if select(1, ...) then return ... end
      local error_msg = select(2, ...)
      if type(error_msg) ~= 'string' then 
        error_msg = "assertion failed!" 
      end
      -- store the error message in an env var and throw an error 
      -- it will be catched on the C side at exit and displayed 
      -- in a message box
      os.setenv('RUNSCRIPT_ERROR_MESSAGE', error_msg)
      error(error_msg, 2)
    end
  end
else
  -- we must be called as: texlua runscript.tlu progname ...
  -- this is treated the same as: runscript[.exe] progname ...
  -- we don't have the unparsed arument line in this case, so construct one
  for k = 1, #arg do argline = argline..' '.._q(arg[k]) end
end

-- special behaviour when called under 'runscript' name

-- ( lower arg[0] : get file name part : remove extension ) == 'runscript'
if (string.lower(arg[0]):match('[^\\/]+$'):gsub('%.[^.]*$', '') == 'runscript') then
  -- we are called as: runscript progname ...
  -- or as: runscript --help|-h|--version ...
  -- handle options first (only -help and -version)
  local opt = { h = false, help = false, v = false, version = false }
  local param, k
  while true do
    -- remove the first argument from the arg table and from the argline string
    -- (this argument should have no embedded spaces!)
    param = table.remove(arg, 1)
    if not param then break end
    argline = string.gsub(argline, '^%S+%s*', '')
    param, k = string.lower(param):gsub('^%-%-?', '')
    -- if an option was given
    if (k > 0) then 
      if opt[param] == nil then
        assert(nil, "unknown option: "..param.."\n"..bannerstr)
      end
      opt[param] = true
    else
      break
    end
  end
  if opt.help or opt.h then
    print(helpstr)
    if opt.v then print(docstr) end
    os.exit(0)
  elseif opt.version or opt.v then
    print(bannerstr)
    os.exit(0)
  end
  -- make sure we a new arg[0] is given and proceed
  arg[0] = assert(param, "not enough arguments!\n"..bannerstr)
end

-- kpse and environment set-up

-- program name
local progname, k = string.lower(arg[0]):match('[^\\/]+$'):gsub('%.[^.]*$', ''):gsub('%-sys$', '')
local sysprog = (k > 0) -- sys type program/script (there was -sys suffix removed)
if (progname == 'runscript') then -- prevent recursive calls to this script
  assert(nil, "oops! wrapping the wrapper?")
end
-- init kpathsea 
local k = -1 
while arg[k-1] do k = k - 1 end -- in case of a call: luatex --luaonly ...
local lua_binary = arg[k]
kpse.set_program_name(lua_binary, progname)
-- various dir-vars
local TEXDIR = kpse.var_value('SELFAUTOPARENT')
local BINDIR = kpse.var_value('SELFAUTOLOC')
local PATH = os.getenv('PATH') or ''
-- perl stuff
local scripts4tlperl = {
  ['updmap-sys'] = true, 
  updmap = true, 
}
local PERLEXE = search_path('perl.exe', PATH)
if not PERLEXE or guimode or scripts4tlperl[progname] then 
  PERLEXE = TEXDIR..'/tlpkg/tlperl/bin/perl.exe'
  os.setenv('PERL5LIB', TEXDIR..'/tlpkg/tlperl/lib')--[[
  local PERL5SHELL = os.getenv('COMSPEC')
  PERL5SHELL = string.gsub(PERL5SHELL, '\\', '\\\\') 
  PERL5SHELL = string.gsub(PERL5SHELL, ' ', '\\ ') 
  os.setenv('PERL5SHELL', PERL5SHELL..' /x /c')--]]
  PATH = prepend_path(PATH, TEXDIR..'/tlpkg/tlperl/bin')
end
-- gs stuff
os.setenv('GS_LIB', TEXDIR..'/tlpkg/tlgs/lib;'..TEXDIR..'/tlpkg/tlgs/fonts')
os.setenv('GS_DLL', TEXDIR..'/tlpkg/tlgs/bin/gsdll32.dll')
-- path
PATH = prepend_path(PATH, TEXDIR..'/tlpkg/tlgs/bin', BINDIR)
os.setenv('PATH', PATH);
-- sys stuff
if sysprog then
  os.setenv('TEXMFVAR', kpse.var_value('TEXMFSYSVAR'))
  os.setenv('TEXMFCONFIG', kpse.var_value('TEXMFSYSCONFIG'))
end
-- restricted programs
local shell_escape_commands = string.lower(kpse.var_value('shell_escape_commands'))
if string.find(','..shell_escape_commands..',', ','..progname..',', 1, true) then
  -- limit search path to the restricted (system) trees 
  -- (not really necessary for entries in the alias_table, 
  -- because they are not searched for with kpathsea)
  os.setenv('TEXMFSCRIPTS', kpse.var_value('TEXMF_RESTRICTED_SCRIPTS'))
end

-- alias table (programs with special needs)

local alias_table = {--[[
  ['fmtutil-sys'] = {[0]=BINDIR..'/fmtutil.exe', 'fmtutil-sys', argline},
  ['updmap-sys'] = 
    function ()
      os.setenv('TEXMFVAR', kpse.var_value('TEXMFSYSVAR'))
      os.setenv('TEXMFCONFIG', kpse.var_value('TEXMFSYSCONFIG'))
      return {[0]=PERLEXE, 'perl', 
      _q(TEXDIR..'/texmf/scripts/tetex/updmap.pl'), argline}
    end,--]]
  asy = 
    function ()
      -- TODO: check if ASYMPTOTE_GS requires quoting of names with spaces
      --os.setenv('ASYMPTOTE_GS', _q(TEXDIR..'/tlpkg/tlgs/bin/gswin32c.exe'))
      os.setenv('ASYMPTOTE_GS', 'gswin32c.exe')
      os.setenv('CYGWIN', 'nodosfilewarning')
      return {[0]=TEXDIR..'/tlpkg/asymptote/asy.exe', 'asy', argline}
    end,
  --dvigif = {[0]=BINDIR..'/dvipng.exe', 'dvigif', argline},
  dviout = 
    function ()
      local fontsdir = kpse.var_value('TEXMFVAR') .. '/fonts'
      if (lfs.attributes(fontsdir, 'mode') ~= 'directory') then 
        assert(mkdir_plus(fontsdir))
      end
      local tfmpath = kpse.show_path('tfm')
      tfmpath = string.gsub(tfmpath, '!!', '')
      tfmpath = string.gsub(tfmpath, '/', '\\')
      local texrt = {}
      for d in string.gmatch(tfmpath, '([^;]+\\fonts)\\tfm[^;]*') do
        if (lfs.attributes(d, 'mode') == 'directory') then 
          table.insert(texrt, d)
        end
      end
      local dvioutpar = [[-NULL -dpi=600 "-gen=']] .. string.gsub(TEXDIR, '/', '\\') .. [[\tlpkg\dviout\gen_pk'" ]] ..
      [["-TEXPK=^r\pk\\^s.^dpk;^r\tfm\\^s^tfm;^r\vf\\^s.vf;^r\ovf\\^s.ovf;^r\tfm\\^s.tfm" "-TEXROOT=']] ..
      table.concat(texrt, ';') .. [['"]]
      return {[0]=TEXDIR..'/tlpkg/dviout/dviout.exe', 'dviout', dvioutpar, argline}
    end,
  psv = {[0]=TEXDIR..'/tlpkg/tlpsv/gswxlua.exe', 'gswxlua', 
    '-l', _q(TEXDIR..'/tlpkg/tlpsv/psv.wx.lua'),
    '-p', _q(TEXDIR..'/tlpkg/tlpsv/psv_view.ps'), '-sINPUT='..argline},
  repstopdf = {[0]=PERLEXE, 'perl', 
    _q(TEXDIR..'/texmf-dist/scripts/epstopdf/epstopdf.pl'), 
    '--restricted', argline},
  rpdfcrop = {[0]=PERLEXE, 'perl', 
    _q(TEXDIR..'/texmf-dist/scripts/pdfcrop/pdfcrop.pl'), 
    '--restricted', argline},
  sam2p = {[0]=TEXDIR..'/tlpkg/sam2p/sam2p.exe', 'sam2p', argline},
  texworks = 
    function ()
      local TW_LIBPATH = kpse.var_value('TW_LIBPATH') or 
                         kpse.var_value('TEXMFCONFIG')..'/texworks'
      local TW_INIPATH = kpse.var_value('TW_INIPATH') or TW_LIBPATH
      os.setenv('TW_LIBPATH', TW_LIBPATH)
      os.setenv('TW_INIPATH', TW_INIPATH)
      if (TW_INIPATH and lfs.attributes(TW_INIPATH, 'mode') ~= 'directory') then
        assert(mkdir_plus(TW_INIPATH))
      end
      return {[0]=TEXDIR..'/tlpkg/texworks/texworks.exe', 'texworks', argline}
    end,
}

-- extension to interpeter mapping

-- the extension is mapped to argv table
-- the command to execute is given as the first element of the table  
-- (it can be a whitespace separated list of names to try)
local extension_map = {
  ['.bat'] = {'cmd', '/c', 'call'},
  ['.jar'] = {'java.exe', '-jar'},
  ['.pl' ]  = {'perl.exe'},
  ['.py' ]  = {'python.exe'},
  ['.rb' ]  = {'ruby.exe'},
  ['.tcl'] = {'tclsh.exe tclsh85.exe tclsh84.exe'},
  ['.vbs'] = {'cscript.exe', '-nologo'},
}
if guimode then
  -- for GUI mode wrappers we try GUI mode interpeters where possible
  extension_map['.jar'][1] = 'javaw.exe '   .. extension_map['.jar'][1]
  extension_map['.pl' ][1] = 'wperl.exe '   .. extension_map['.pl' ][1]
  extension_map['.py' ][1] = 'pythonw.exe ' .. extension_map['.py' ][1]
  extension_map['.py' ][1] = 'rubyw.exe '   .. extension_map['.py' ][1]
  extension_map['.tcl'][1] = 'wish.exe wish85.exe wish84.exe ' .. extension_map['.tcl'][1]
  extension_map['.vbs'][1] = 'wscript.exe ' .. extension_map['.vbs'][1]
end
extension_map['.cmd'] = extension_map['.bat']
extension_map['.js']  = extension_map['.vbs']

-- set up argv table

local program = alias_table[progname]
if program then -- special case (alias)
  if (type(program) == 'function') then 
    program = program() 
  end
else -- general case (no alias)
  local progfullname, ext = search_path(progname, BINDIR, ".tlu;.bat;.cmd")
  if not progfullname then
    progfullname, ext = assert(find_texmfscript(progname, 
      ".tlu;.texlua;.lua;.pl;.rb;.py;.tcl;.jar;.vbs;.js;.bat;.cmd;\0"))
  end
  if (ext == '.lua') or (ext == '.tlu') or (ext == '.texlua') then
    -- lua script
    arg[0] = progfullname
    program = progfullname
  else
    program = extension_map[ext] or assert(shebang_to_argv(progfullname)) 
    table.insert(program, _q(progfullname))
    table.insert(program, argline)
    if not program[0] then
      program[0], program[1] = assert(check_command(program[1], PATH))
    end
  end
end

-- run the program/script

if (type(program) == 'table') then
  os.exit(assert(os.spawn(program)))
elseif (type(program) == 'string') then
  program = assert(loadfile(program))
  program() 
else
  assert(nil, "unexpected argument type: "..type(program))
end