This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
statistician's profile picture

Accessing shell API from shell-less environment

Started by statistician, 10 September 2015 - 02:03 PM
statistician #1
Posted 10 September 2015 - 04:03 PM
Hello. I'm writing a program that has a main menu with buttons to different programs. I use the fileload function to load and run these programs. I don't use shell.run because I want to have my custom error handling by using pcall (pcall doesn't work with programs ran with shell.run. If there is a way, however, please tell me). This raises problems, because this makes me unable to call shell.run from these programs, which is neccessary to run, for example, the builtin pastebin program(s) (I'm reluctant to write my own)

My current solution works by getting the environment from the startup program (where shell is available) using getfenv, making it available to the menu program by using os.run, and then passing it back and forth so it can be accessed by the function I use to load programs when neccessary.

Is there a way to get access to the shell api from the fileloading function without passing the appropriate environment as a function parameter? One possibility: I know I can get the environment from different stack levels, for example, getfenv(2) gets the environment that called the current function (as I understood it. I just don't know how to determine the *first* caller.

Thanks for your help. Please let me know if you need clarification and I will edit my question!
Lyqyd #2
Posted 10 September 2015 - 04:18 PM
If you could call shell.run to run the programs, you don't need to use getfenv. You can just construct the environment table and use it:


local env = setmetatable({shell = shell, multishell = multishell}, {__index = _G})

Bear in mind that you are breaking the functionality of the shell "API" when you do this, as any programs you are running this way will not get sane values when calling shell.getRunningProgram().
statistician #3
Posted 10 September 2015 - 06:27 PM
If you could call shell.run to run the programs, you don't need to use getfenv. You can just construct the environment table and use it:


local env = setmetatable({shell = shell, multishell = multishell}, {__index = _G})

Bear in mind that you are breaking the functionality of the shell "API" when you do this, as any programs you are running this way will not get sane values when calling shell.getRunningProgram().

Hi, thanks for your answer. I don't think I understood your suggestion correctly; I tried the following to handle the loading:


-- first try
function runFile(path)
  local file = fs.open(path, 'r').readAll();
  file = "local env = setmetatable({shell = shell, multishell = multishell}, {__index = _G}); "..file
  local func = loadstring(file)
  func()
end
-- second try
function runFile(path)
  local func = loadfile(path)
  local env = setmetatable({shell = shell, multishell = multishell}, {__index = _G})
  setfenv(func, env)
  func()
end

Alas, neither worked. Can you elaborate on your answer?
Edited on 10 September 2015 - 07:01 PM
Lyqyd #4
Posted 10 September 2015 - 08:09 PM
The second try is much closer. Don't prepend it to the loaded file, though. You also still need to use loadstring instead of loadfile.
statistician #5
Posted 10 September 2015 - 08:59 PM
The second try is much closer. Don't prepend it to the loaded file, though. You also still need to use loadstring instead of loadfile.

There was an error with my paste, I'll correct it. Can you answer based on the edited reply?
Also, I was able to make the following code work:


function runFile(path)
  local n = 1
  local function tryenv()
    return getfenv(n)
  end

  while pcall(tryenv) do
    n = n + 1
    if getfenv(n).shell then
	  print("environment containing shell found at level: "..n)
	  break
    end
  end

  local func = loadfile(path)
  setfenv(func, getfenv(n))
  func()
end

I'm still interested in the method you're describing, though. If I understood correctly, however, the above method isn't breaking shell API's functionality like the one we are discussing.
Exerro #6
Posted 10 September 2015 - 10:03 PM
To get a shell, you could use this:

local n = 0
while getfenv(n) and not getfenv(n).shell do
    n = n + 1
end
local _shell = getfenv(n).shell
Seems a bit shorter to me, and I don't think getfenv() can error?

Your second attempt (now edited I believe) should work just fine, although you might want to switch to using load() for future compatibility. In future CC versions, setfenv() won't be supported. Instead, you can use load() which works like this:

load( string code, string source, _ignore_this_, table env )
With the arguments past the first one all being optional. That'll work like loadstring(), but allow you to set its environment.

The reason that shell may break is because the thing that shell.getRunningProgram() returns will be the path to your program rather than the one you run using runFile(). You could load up a custom shell API yourself, or even use:

env.shell = setmetatable( { getRunningProgram = customGetRunningProgram }, { __index = shell } )
to fix it. Then again, it might not be a huge issue, and you might be able to leave it. I generally write programs with this in mind so they don't rely too heavily on shell.getRunningProgram().
Lyqyd #7
Posted 11 September 2015 - 12:54 AM
You should always present a sane environment to any programs run under your coroutine manager. In LyqydOS, I spawn a new shell instance for each process so that the shell "API" will behave as it is intended to do.
statistician #8
Posted 11 September 2015 - 10:42 AM
I have it working like follows at the moment:


function runFile(path)
  local n = 0
  while getfenv(n) and not getfenv(n).shell do
    n = n + 1
  end

  local func = loadfile(path)
  setfenv(func, getfenv(n))
  func()
end

which is pretty much what awsumben13 suggested. I had it like this before, but it didn't work then; I guess there was something else wrong. Even so, this code still doesn't work:


function runFile(path)
  local func = loadfile(path)
  local env = setmetatable({shell = shell, multishell = multishell}, {__index = _G})
  setfenv(func, env)
  func()
end
Exerro #9
Posted 11 September 2015 - 03:56 PM
Is it possible that shell is nil in the environment where that is called?
statistician #10
Posted 11 September 2015 - 04:22 PM
Is it possible that shell is nil in the environment where that is called?

Ah, that's true. The startup file calls a "controlpanel" program with shell.run, but the "controlpanel" program loads "utils" (where the runFile method resides) with os.loadAPI. shell isn't available to apis, am I correct? Is there an easy way to bypass this?
Exerro #11
Posted 11 September 2015 - 04:32 PM
Yeah os.loadAPI loads functions in an environment without the shell API. You can bypass it by using a custom os.loadAPI (take a look at bios.lua where it's defined) or by doing this:

local _shell = _G.shell
_G.shell = shell
os.loadAPI( "SomeAPI" )
_G.shell = _shell
and in 'SomeAPI'

local shell = _G.shell
...
Lyqyd #12
Posted 11 September 2015 - 05:57 PM
The shell functions are not a "real API", and should not be placed in _G. There is no "authoritative" shell instance. If you called the API function that uses it from within a different shell instance, the result may not be as desired. When an API needs the shell, it should be passed the current instance by the caller. Mishandling the shell "API" is a good way to accidentally break programs that users are trying to run.
Creator #13
Posted 12 September 2015 - 04:40 PM
How comes the shell api is different? At the most basic level, it is only a table.
FUNCTION MAN! #14
Posted 12 September 2015 - 05:52 PM
How comes the shell api is different? At the most basic level, it is only a table.

The shell API doesn't exist in the global namespace. It resides in _ENV only for programs ran through the shell/started with shell.run.
Lyqyd #15
Posted 12 September 2015 - 11:45 PM
How comes the shell api is different? At the most basic level, it is only a table.

Because the shell "API" is an interface exposed by a specific shell instance to programs running under it. Changes to one instance would not affect any other instances, and programs running contemporaneously should have their own instance to work with. For example, if you were using multishell and started a second shell in a new tab, you could modify the path, or the aliases, or run a program, and you would not see any changes to the values the shell "API" would return in the other shell instance.
Creator #16
Posted 13 September 2015 - 10:21 AM
I see. Thanks!