NGS unique features – exit code handling

smilies-1607163_640

How other languages treat exit codes?

Most languages that I know do not care about exit codes of processes they run. Some languages do care … but not enough.

Update / Clarification / TL;DR

  1. Only NGS can throw exceptions based on fine grained inspection of exit codes of processes it runs out of the box. For example, exit code 1 of test will not throw an exception while exit code 1 of cat will throw an exception by default. This allows to write correct scripts which do not have explicit exit codes checking and therefore are smaller (meaning better maintainability).
  2. This behaviour is highly customizable.
  3. In NGS, it is OK to write if $(test -f myfile) ... else ... which will throw an exception if exit code of test is 2 (test expression syntax error or alike) while for example in bash and others you should explicitly check and handle exit code 2 because simple if can not cover three possible exit codes of test (zero for yes,  one for no, two for error). Yes, if /usr/bin/test ...; then ...; fi in bash is incorrect! By the way, did you see scripts that actually do check for three possible exit codes of test? I haven’t.
  4. When -e switch is used, bash can exit (somewhat similar to uncaught exception) when exit code of a process that it runs is not zero. This is not fine grained and not customizable.
  5. I do know that exit codes are accessible in other languages when they run a process. Other languages do not act on exit codes with the exception of bash with -e switch. In NGS exit codes are translated to exceptions in a fine grained way.
  6. I am aware that $? in the examples below show the exit code of the language process, not the process that the language runs. I’m contrasting this to bash (-e) and NGS behaviour (exception exits with non-zero exit code from NGS).

Let’s run “test” binary with incorrect arguments.

Perl

> perl -e '`test a b c`; print "OK\n"'; echo $?
test: ‘b’: binary operator expected
OK
0

Ruby

> ruby -e '`test a b c`; puts "OK"'; echo $?
test: ‘b’: binary operator expected
OK
0

Python

> python
>>> import subprocess
>>> subprocess.check_output(['test', 'a', 'b', 'c'])
... subprocess.CalledProcessError ... returned non-zero exit status 2
>>> subprocess.check_output(['test', '-f', 'no-such-file'])
... subprocess.CalledProcessError: ... returned non-zero exit status 1

bash

> bash -c '`/usr/bin/test a b c`; echo OK'; echo $?
/usr/bin/test: ‘b’: binary operator expected
OK
0

> bash -e -c '`/usr/bin/test a b c`; echo OK'; echo $?
/usr/bin/test: ‘b’: binary operator expected
2

Used /usr/bin/test for bash to make examples comparable by not using built-in test in bash.

Perl and Ruby for example, do not see any problem with failing process.

Bash does not care by default but has -e switch to make non-zero exit code fatal, returning the bad exit code when exiting from bash.

Python can differentiate zero and non-zero exit codes.

So, the best we can do is distinguish zero and non-zero exit codes? That’s just not good enough. test for example can return 0 for “true” result, 1 for “false” result and 2 for exceptional situation. Let’s look at this bash code with intentional syntax error in “test”:

if /usr/bin/test --f myfile;then
  echo OK
else
  echo File does not exist
fi

The output is

/usr/bin/test: missing argument after ‘myfile’
File does not exist

Note that -e switch wouldn’t help here. Whatever follows if is allowed to fail (it would be impossible to do anything if -e would affect if and while conditions)

How NGS treats exit codes?

> ngs -e '$(test a b c); echo("OK")'; echo $?
test: ‘b’: binary operator expected
... Exception of type ProcessFail ...
200

> ngs -e '$(nofail test a b c); echo("OK")'; echo $?
test: ‘b’: binary operator expected
OK
0

> ngs -e '$(test -f no-such-file); echo("OK")'; echo $?
OK
0

> ngs -e '$(test -d .); echo("OK")'; echo $?
OK
0

NGS has easily configurable behaviour regarding how to treat exit codes of processes. Built-in behaviour knows about false, test, fuser and ping commands. For unknown processes, non-zero exit code is an exception.

If you use a command that returns non-zero exit code as part of its normal operation you can use nofail prefix as in the example above or customize NGS behaviour regarding the exit code of your process or even better, make a pull request adding it to stdlib.

How easy is to customize exit code checking for your own command? Here is the code from stdlib that defines current behaviour. You decide for yourself (skipping nofail as it’s not something typical an average user is expected to do).

F finished_ok(p:Process) p.exit_code == 0

F finished_ok(p:Process) {
    guard p.executable.path == '/bin/false'
    p.exit_code == 1
}

F finished_ok(p:Process) {
    guard p.executable.path in ['/usr/bin/test', '/bin/fuser', '/bin/ping']
    p.exit_code in [0, 1]
}

Let’s get back to the bash if test ... example and rewrite the it in NGS:

if $(test --f myfile)
    echo("OK")
else
    echo("File does not exist")

… and run it …

... Exception of type ProcessFail ...

For if purposes, zero exit code is true and any non-zero exit code is false. Again, customizable. Such exit code treatment allows the if ... test ... NGS example above to function properly, somewhat similar to bash but with exceptions when needed.

NGS’ behaviour makes much more sense for me. I hope it makes sense for you.

Update: Reddit discussion.


Have a nice weekend!

4 thoughts on “NGS unique features – exit code handling

  1. Hi.
    I’ve been following your posts on this language you are developing for some time now.

    I didn’t want to nitpick before because I have a tendency to give really negative feedback, but this time I really couldn’t understand how this new syntax improves anything.

    I read your post twice. It looks like you simply don’t know BASH or Python well enough to know how to properly handle exit codes in these languages.

    Did you read the Python documentation of the subprocess module?
    https://docs.python.org/2/library/subprocess.html

    First, it has a ‘call’ method, which simply returns the return code, so you can handle any code any way you want.
    It is pretty much same thing as your ‘nofail’.

    Second, the call_check method still lets you get at the error code, because it is stored in the CalledProcessError.returncode

    The only thing you seem to add here is existing handlers for common commands.

    But this is a recipe for many problems:
    1. You need to keep track of all version of the programs you recognize, in case their developers ever decide to change the return codes for some reason.

    2. You need to check for compatibility, because some times a tool with the same name will be a different program on different platforms (example: some Linux distros simlink nano as vi by default, until you install vim and then it takes over the simlink)

    3. The more programs you want to keep track of, the bulkier your stdlib will be.
    Which ties directly in to 4:

    4. If you keep constantly updating stdlib in a way that changes behavior of scripts, your language will be unusable, and that is exactly what you are proposing by inviting pull requests for built in return code recognition.

    And finally:

    You do know about the && operator in BASH and about the case syntax, right?
    Because it would be very simple to write:
    /usr/bin/test a b c && echo $?
    which will give the same result as your ‘test’ without using the -e switch for BASH.

    And there is even this:
    /usr/bin/test a b c
    if [ $? -gt 1 ]; then
    echo “This script failed miserably!”
    fi

    Just in case you do not want to write a whole “case list” to handle each code separately, but just want anything other then 0 and 1 to fail the script.

    But maybe I am missing something?
    Did I not understand your syntax correctly?
    Is there more to it?

    Let me know!

    Like

    • Hi.

      I’ve been following your posts on this language you are developing for some time now.

      Thanks!

      It looks like you simply don’t know BASH or Python well enough

      My intention to focus only on specific aspect of exit codes handling (no exceptions thrown automatically) might be the root of this suspicion.

      Did you read the Python documentation of the subprocess module?

      Several times during the years that I use Python. Never seen there anything about handling error codes in more precise way than zero/non-zero.

      returns the return code, so you can handle any code any way you want.

      That’s exactly the part which I want to be more convenient. I want my scripts to be smaller and not to have to handle all possible errors all the time (that’s the part I don’t like in Go).

      The only thing you seem to add here is existing handlers for common commands.

      Yes, specifically by possibly throwing exceptions in exceptional situations (like test a b c or test --f somefile)

      But this is a recipe for many problems

      Before we dive into the points I’d like to remind that special handling in stdlib is only for programs that do not follow the normal exit code convention of zero for OK and non-zero is failure. That’s diff, ping and alikes.

      You need to keep track of all version of the programs you recognize, in case their developers ever decide to change the return codes for some reason.

      Yes, I expect it to be very rare case considering the small size of the list of “special” programs in stdlib.

      You need to check for compatibility, because some times a tool with the same name will be a different program on different platforms

      Same answer as above

      The more programs you want to keep track of, the bulkier your stdlib will be.

      Correct, I do estimate that 10-20 utilities in stdlib will cover most of the use cases.

      If you keep constantly updating stdlib

      That’s fixing. This point invites me to think how such fixes should be made in the most convenient way for the users. I’ll be definitely thinking about this. Thanks!

      You do know about the && operator in BASH and about the case syntax, right?

      Yes. It’s interesting how many people think that a person that tries to make alternative shell does not know this basic syntax.

      which will give the same result as your ‘test’ without using the -e switch for BASH.

      Sorry, I don’t understand. Can you please explain what you mean here?

      if [ $? -gt 1 ]; then
      echo “This script failed miserably!”
      fi

      Exactly the part I’m trying to make optional. I’d rather most of my scripts to stop with exception than having to have this all over, which you should if you are using test or [ because they might fail because of syntax error.

      Did I not understand your syntax correctly?

      Based on your comment here, I would assume yes.

      Is there more to it?

      Let’s summarize here: the feature enables to write more correct shorter code. If you have such shorter code in other languages it will be less correct.

      Like

  2. I have some issues with the starting of the post, regarding languages.

    For example with Ruby, you have the following variable: $? (yes, like with Bash), it holds the process status, including it’s PID, exist code etc…

    With Perl, you also have the $? variable, like with Ruby or Bash.

    With python you can use “call” or as you described, and still you have the return value.

    The thing is, that they are programming languages, and not shell languages.
    It means that the requires you to validate the return value.

    If you want to send back error when the program exits, you have things like OS.exit or exit with a return code.

    The thing is, that you didn’t return your own exit code on the examples you have provided. You have checked for the Ruby and Perl processes exit code, rather then an error of the shell code.

    BTW, with Bash it is the same thing, and so does ZSH, If your script does not return a specific exit code, you’ll get the default that is usually 0.

    Like

Leave a comment