andrewchambers / janet-sh

Shorthand shell like functions for janet.
82 stars 6 forks source link

(sh/$ cd /tmp) → error: spawn failed: "No such file or directory" #21

Open MaxGyver83 opened 6 months ago

MaxGyver83 commented 6 months ago

Hi! I'm new to both janet and janet-sh. Why doesn't the following command work?

repl:2:> (sh/$ cd /tmp)
error: spawn failed: "No such file or directory"
  in posix-spawn/spawn
  in spawn2 [/usr/lib/janet/posix-spawn.janet] on line 36, column 3
  in spawn-spec [/usr/lib/janet/sh.janet] on line 152, column 23
  in <anonymous> [/usr/lib/janet/sh.janet] on line 157, column 9
  in <anonymous> [/usr/lib/janet/sh.janet] on line 155, column 5
  in run* [/usr/lib/janet/sh.janet] on line 154, column 3
  in $* [/usr/lib/janet/sh.janet] on line 253, column 13
  in _thunk [repl] (tailcall) on line 2, column 1
MaxGyver83 commented 6 months ago

This works:

(sh/$ sh -c "cd /tmp && echo $PWD")

But then I don't know how to use janet variables within the double quotes. This fails:

(sh/$ sh -c "cd ,dir && git clone ,url")
ianthehenry commented 6 months ago

cd is not an executable -- there is no /bin/cd for example -- it's a shell builtin provided by shells like bash. sh/$ just spawns subprocesses, which can't change their parent's working directory.

If you want to change the working directory of your running process, use (os/cd "/tmp") instead.

As for:

(sh/$ sh -c "cd ,dir && git clone ,url")

I would recommend against building strings to pass to /bin/sh directly because now you have to deal with shell quoting problems, but you can do this if you construct the whole argument string dynamically.

(sh/$ sh -c ,(string "cd " dir " && git clone " url))

But now you have to deal with shell quoting and escaping issues, which is kind of a nightmare.

MaxGyver83 commented 6 months ago

Thank you for your quick reply!

cd is not an executable -- there is no /bin/cd for example -- it's a shell builtin provided by shells like bash. sh/$ just spawns subprocesses, which can't change their parent's working directory.

At first, I thought that maybe /tmp wasn't accessible within the REPL. But then I suspected that the error is about cd. I'm aware that cd is a builtin. I just assumed that sh/$ was starting shell. The macro name looks like that :-)

If you want to change the working directory of your running process, use (os/cd "/tmp") instead.

Makes sense. Actually, in my bash script, I do this:

function clone_repo {
    sleep 0.1
    cd "$1" && git clone $3 "$2" 2> /dev/null
}

clone_repo "$dir" "$url" "$params" &

This means, the cd happens in a subshell and doesn't change the working directory in the main process. Now I have this workaround:

(defn clone-repo [dir url params]
  "Clone repo."
  (os/sleep 0.1)
  (os/cd dir)
  (sh/$ git clone ,;params ,url))

This changes the working directory permanently. What's a good solution to this problem?

But now you have to deal with shell quoting and escaping issues, which is kind of a nightmare.

I see.

ianthehenry commented 6 months ago

So unfortunately posix_spawn, which janet-sh is a wrapper around, doesn't give you any way to spawn a child process with a different working directory than the parent process. So as far as I know the only way to do this is to change your working directory, spawn a subprocess, and then change it back (even if the subprocess raises!).

Here's a macro that might work (I haven't tested it):

(defmacro cdo [new-dir & body]
  (with-syms [$cwd]
    ~(let [,$cwd (,os/cwd)]
      (,os/chdir ,new-dir)
      (as-macro ,defer (,os/chdir ,$cwd)
        ,;body))))

That uses defer to unconditionally change the working directory back, even if the body raises. Then you can write code like this:

(cdo "/tmp"
  (sh/$ git clone ,;params ,url))

I'm not really happy with this because it doesn't interact very nicely with the event loop: os/chdir is effectively setting a global variable, so if you yield to another fiber during that body, it could revert the working directory or at least read the wrong thing. This might matter or not depending on what the rest of your script is doing.

Maybe sh -c is the best way to do this, at least for a simple command?

(defn run-in [path & command]
  (sh/$ sh -c `cd "$0"; "$@"` ,path ,;command)

That bypasses all of the shell quoting issues that you'd get from using string interpolation to build the argument to sh -c... but this doesn't let you use all the nice features of sh/$. So I dunno.

MaxGyver83 commented 6 months ago

Here's a macro that might work (I haven't tested it):

Thank you. This works well (with os/chdir changed to os/cd).

Maybe sh -c is the best way to do this, at least for a simple command?

(defn run-in [path & command]
  (sh/$ sh -c `cd "$0"; "$@"` ,path ,;command)

This works well, too.

The first option is nicer to type but I have to think if this global working directory is a problem for me.

Anyway, your answer helps me a lot understanding janet better!