Inferno shell

    FAQ: What is OS Inferno and why is it needed?

    The Inferno OS shell has been causing me extremely negative emotions for many years. And I never realized that in Inferno sh some people were delighted . But, as they say, better late than never - today I decided to carefully deal with the shell, and as a result I was also impressed - this is really a unique thing! Incredibly elegant and simple.

    Still, I’ll start with the shortcomings, for greater objectivity. The main thing is that the shell is very slow. It is not known why, but all shells are inferno (there are actually several of them, but now we are talking about/dis/sh.dis) are very slow. This is extremely strange because usually the speed of applications written in Limbo (in JIT mode) is between the speed of C and fast scripting languages ​​like Perl. Therefore, full-fledged applications for shwriting will not work. But, nevertheless, it is also inconvenient to uncover Limbo for every sneeze, so all sorts of starting scripts and other small things still have to be written in sh. The second serious drawback is the inconvenience of using it in the text console, the lack of command history, auto-completion, convenient editing when typing is very annoying (but I was just told about the rlwrap utility , running emu throughrlwrap -aseems to be able to solve this problem). The third - the syntax of this shell is unusual using unpaired quotes, which when trying to highlight the syntax of its scripts using the backlight for absolutely any other shell (due to the lack of a ready-made backlight for the infernal) leads to a complete nightmare. I was going to solve this problem today by implementing syntax highlighting for vim, for which I sat down to deal with the shell ... and as a result, instead of syntax highlighting, I could not resist, and I am writing this article. :)

    Content




    From what, from what, from what are our scripts made of?


    The functionality supported /dis/sh.dis“out of the box” is impressive! There are not even conditional statements and loops! No features. And then what is there, and how can one exist in these conditions? See now. So what is:
    • launch applications (including pipelines and I / O redirection)
    • lines, line lists, command blocks
    • environment variables
    • file name templates (*,?, [...])
    • and some built-in commands and commands for working with strings
    Perhaps, having seen the last point, you thought, “Aha! Some special teams. Surely they do the rest, everything is corny. ”... but no, you didn’t guess. Usually three are used built-in commands: exit, runand load; and commonly used string commands are quoteand unquote. As for run, it simply executes the specified script in the current shell (analogue .or sourcein bash).

    And here load- yes, it's a bomb! It allows you to load additional modules that are written on Limbo into the current shell and allow you to add absolutely any functionality to the shell - if, for, functions, exceptions, regular expressions, mathematical operations, etc. etc. There is still a teamunload, allowing dynamically unload these modules. :) Nevertheless, even without loadable modules, the shell is absolutely full and functional - which I will prove at the end of the article by implementing if and for on the “bare sh”!

    Oh, something else I forgot to mention. (Do you think now it’s for sure “aha!” Nah.) He still supports the comments. Beginning with #. :)

    Launch applications


    Much identical to any * nix shell:
    • runs applications ( .disfiles) and scripts (files in which the first line is shebang#! )
    • teams are divided ;
    • running commands in the background through &
    • command pipelines through |
    • redirecting stdin / stdout through >, >>and<

    ; echo one two | wc
        1       2       8
    

    But there are differences.

    Additional Command I / O Redirections

    • specifying a file descriptor (for redirecting stderr or opening additional descriptors plus to 0, 1 and 2 available by default)
      • cmd stdout.txt >[2]stderr.txt
      • cmd >[1=2] (redirecting stdin to stderr)
      • cmd <[3]file.txt (the command is launched with an additional file descriptor 3 open for reading from the file)
      • cmda |[2] cmdb (instead of stdout of the first command, its stderr is sent to the pipeline input, and stdout is simply displayed on the screen)
      • cmda |[1=2] cmdb (stderr is sent to the pipeline again, not stdout cmda, but it connects not to stdin, but to stdout cmdb - which opens for reading, not writing, of course)

    • opening at the same time for reading and writing
      • cmd <>in_pipe <>[1]out_pipe (stdin and stdout are open at the same time for reading and writing to different files)

    • nonlinear conveyors
      • cmd <{первый;блок;команд} >{второй;блок;команд}(both blocks of commands are launched in parallel with cmd, while cmd receives two parameters - the names of the files a la /fd/номер, which are connected via pipe to stdout of the first block of commands and stdin of the second block of commands)

    The last feature is especially interesting. For example, a standard command compare two files cmpwith its help files can not be compared, and the results of two other commands: cmp <{ ls /dir1 } <{ ls /dir2 }.

    $ status

    An infernally unusual approach to implementing an exit status code for an application. In traditional operating systems, any program ends with a number: 0 if everything is in order, and any other if it fails. In inferno, any program either simply terminates (if everything is in order) or throws an exception, the value of which is a string with the error text.

    An agreement has been adopted, according to which this line should begin with “fail:” if this is a regular error - i.e. the application did not “crash”, but simply wants to exit by returning this error to the one who launched this application (usually this is a shell). If the error does not start with “fail:”, then after the application finishes, its process will remain in memory (in the “broken” state) so that it can be examined by debuggers and find out why this exception occurred (analogous to core dumps, only instead of saving they hang on a disk in memory).

    So, after the running command completes its “exit code” - i.e. the error-exception text will be in the environment variable $status(the “fail:” prefix will be automatically deleted from it; if the exception text except “fail:” did not contain anything else, then the $statusline will be “failed”).

    Lines, line lists, command blocks


    The Infernov shell only operates with strings and lists of strings.

    Line

    A string is either a word that does not contain spaces or some special characters, or any characters enclosed in single quotes. The only "escapable" character in such a string is the single quote itself, and it is written with two such quotes:
    ; echo 'quote   ''   <-- here'
    quote   '   <-- here
    

    No \ n, \ t, interpolation of variables, etc. in rows is not supported. If you need to insert a linefeed character in a string, either simply paste it as is (by pressing Enter) or concatenate the string with a variable containing this character. In inferno, there is a unicode (1) utility that can output any character by its code, so it is done with its help (the construction "{команда}is described below, but for now consider this an analog of `команда`bash):
    ; cr="{unicode -t 0D}
    ; lf="{unicode -t 0A}
    ; tab="{unicode -t 09}
    ; echo -n 'a'$cr$lf'b'$tab'c' | xd -1x
    0000000 61 0d 0a 62 09 63
    0000006
    

    Double quotes are not used for strings (incidentally, paired double quotes in a shell infernovskom not used for anything at all).

    Command block

    A block of commands is written inside curly brackets, and the entire block is processed by the shell as one line (including characters of curly brackets).
    ; { echo one; echo two }
    one
    two
    ; '{ echo one; echo two }'
    one
    two
    

    The only difference from a string in single quotes is that it shchecks the syntax of command blocks and reformatts them into more compact lines.
    ; echo { cmd | }
    sh: stdin: parse error: syntax error
    ; echo { cmd | cmd }
    {cmd|cmd}
    ; echo {
    echo     one
    echo two
    }
    {echo one;echo two}
    


    List of lines

    Lists of strings are simply zero or more strings separated by whitespace characters. They can be enclosed in parentheses, but in the vast majority of cases this is not necessary.

    Any shell command is just a list of lines , where the first line contains a command or a block of code, and the remaining lines are parameters for them.
    ; echo one two three
    one two three
    ; echo (one two) (three)
    one two three
    ; (echo () (one (two three)))
    one two three
    ; ({echo Hello, $1!} World)
    Hello, World!
    

    An operator is used to concatenate string lists ^. It can be applied to two lists containing either the same number of elements (then the elements are concatenated in pairs), or one of the lists should contain only one element, then it will be concatenated with each element of the second list. As a special case, if both lists contain only one element each, the usual concatenation of two lines is obtained.
    ; echo (a b c) ^ (1 2 3)
    a1 b2 c3
    ; echo (a b) ^ 1
    a1 b1
    ; echo 1 ^ (a b)
    1a 1b
    ; echo a ^ b
    ab
    

    Usually one shell command should be written on one line - there are no special characters to indicate "continue the command on the next line". But splitting a command into several lines is very simple without it - it’s enough to open and not close the line (with a single quote) or the command block (curly brace) or the list of lines (parenthesis) on the previous line, or end the previous line with the list concatenation operator.

    Environment variables


    Working with variables in an infernal shell looks deceptively similar to traditional shells, but don't let yourself be fooled by this!
    ; a = 'World'
    ; echo Hello, $a!
    Hello, World!
    


    / env

    Environment variables in the Inferno OS are implemented differently than in traditional OSs. Instead of introducing an additional POSIX API for working with environment variables and passing them to each process through a nightmare, like int execvpe(const char *file, char *const argv[], char *const envp[]);environment variables, these are just files in a directory /env. This provides interesting possibilities, for example, an application can provide access to its environment variables to other applications over the network - simply by exporting (via listen (1) and export (4) ) its own /env.

    You can create / delete / modify environment variables by creating, deleting and modifying files in /env. But it is worth considering that the current sh holds in memory a copy of all environment variables, so the changes made through the files in/envthey will see launched applications, but not the current sh. On the other hand, changing the variables in the usual way for the shell (via the operator =), you automatically update the files in /env.

    Shell variable names can contain any characters, including those that are not allowed for file names (for example, names ., ..and containing /). Variables with such names will be available only in the current sh, and launched applications will not see them.

    Lists

    Another important difference is that all variables contain only lists of strings . You cannot put a single line in a variable - it will always be a list of one line.

    Saving an empty list into a variable (explicitly through assignment a=()or implicitly through a=) is the deletion of the variable.
    • $var this is getting a list of lines from the var variable, and there can be zero, one or more of these lines in it
    • $#var this is getting the number of items in a list of strings $var
    • $"varit is a concatenation of all elements of the list of lines from $varto one line (separated by spaces)

    ; var = 'first   str'  second
    ; echo $var
    first   str second
    ; echo $#var
    2
    ; echo $"var
    first   str second
    

    As you can see, the output result $varand $"varvisually different because echodisplays all its parameters separated by a space, and $"also unites all elements of the variable through a space. But the first team echoreceived two parameters, and the last one.

    Whenever you somehow implicitly concatenate the value of a variable with something, the shell inserts the concatenation operator ^(which, as you recall, works with lists of strings, not strings). This is convenient, although out of habit it may be unexpected:
    ; flags = a b c
    ; files = file1 file2
    ; echo -$flags $files.b
    -a -b -c file1.b file2.b
    ; echo { echo -$flags $files.b }
    {echo -^$flags $files^.b}
    

    Assigning to a list of variables also works. If on the right side the list consists of more elements than the variables on the left, the last variable will receive the rest of the list, and in the previous variables there will be one element in each.
    ; list = a b c d
    ; (head tail) = $list
    ; echo $head
    a
    ; echo $tail
    b c d
    ; (x y) = (3 5)
    ; (x y) = ($y $x)
    ; echo 'x='^$x 'y='^$y
    x=5 y=3
    

    List of all the script settings, or any block of code is variable $*, plus the individual parameters are variable $1, $2....

    Scopes

    Each block of code forms its own scope. You can assign values ​​to variables with the =and operators :=. The first will change the value of an existing variable or create a new variable in the current scope. The second always creates a new variable, overriding the old value of the variable with this name (if it existed) until the end of the current block.
    ; a = 1
    ; { a = 2 ; echo $a }
    2
    ; echo $a
    2
    ; { a := 3; echo $a }
    3
    ; echo $a
    2
    ; { b := 4; echo $b }
    4
    ; echo b is $b
    b is
    


    Variable References

    A variable name is just a line after a character $. And it can be any string - in single quotes or another variable.
    ; 'var' = 10
    ; echo $var
    10
    ; ref = 'var'
    ; echo $$ref
    10
    ; echo $'var'
    10
    ; echo $('va' ^ r)
    10
    


    Intercept command output


    Actually, this topic refers more to the section describing the launch of commands, but in order to make it clear what we were talking about, we had to first describe how to work with lists of strings, so we had to postpone it until now.

    Meet the very unpaired quotes that forever break syntax highlighting.
    • `{команда}(executes the command, and returns what it displayed on stdout as a list of strings; list items are separated using the value of the variable $ifs; if it is not defined, then it is considered that it has a string of three characters: space, tab and line feed)
    • "{команда} (executes the command, and returns what it printed to stdout as a single line - concatenation of all lines output by the command)
    As an example, load the list of files through a call ls. Considering that file names may contain spaces, the standard behavior `{}will not work for us - we need to separate the list items only by line breaks.
    ; ifs := "{unicode -t 0A}
    ; files := `{ ls / }
    ; echo $#files
    82
    ; ls / | wc -l
    	82
    

    But actually it is made easier and more reliable through file name templates that are expanded by the shell into the list of file names:
    ; files2 := /*
    ; echo $#files2
    82
    


    Built-in commands


    There are two types of built-in commands: regular and for working with strings.

    Regular commands are invoked the same way applications / scripts are launched:
    ; run /lib/sh/profile
    ; load std
    ; unload std
    ; exit
    

    Commands for working with strings are called through ${команда параметры}and return (they do not output to stdout, namely, they return - as an example, an appeal to the value of a variable does) an ordinary string - i.e. they must be used in the parameters of regular commands or on the right side of assigning values ​​to variables. For example, the command ${quote}screens the list of strings passed to it in one line, and ${unquote}performs the inverse operation turning one line into a list of strings.
    ; list = 'a  b'  c d
    ; echo $list
    a  b c d
    ; echo ${quote $list}
    'a  b' c d
    ; echo ${unquote ${quote $list}}
    a  b c d
    


    Add if, for, functions


    As I promised, I show how to make my implementations of these extremely necessary things on the “bare sh”. Of course, in real life this is not required, the std loadable module provides everything you need, and it is much more convenient to use it. But, nevertheless, this implementation is of interest as a demonstration of the capabilities of naked sh.

    Everything is implemented using exclusively:
    • variables
    • lines and line lists
    • code blocks and their parameters


    We do our “functions”

    As I mentioned, any shell command is just a list of lines, where the first line is the command, and the rest of the lines are its parameters. And the shell command block is just a string, and it can be saved in a variable. And the parameters of any command unit receives $*, $1, etc.
    ; hello = { echo Hello, $1! }
    ; $hello World
    Hello, World!
    

    Moreover, we can even do function currying in the best spirit of functional programming. :)
    ; greet = {echo $1, $2!}
    ; hi    = $greet Hi
    ; hello = $greet Hello
    ; $hi World
    Hi, World!
    ; $hello World
    Hello, World!
    

    Another example - you can use the block parameters to get the desired list item by number:
    ; list = a b c d e f
    ; { echo $3 } $list
    c
    


    Making your for

    I didn’t try to do a full-fledged convenient if, I was interested to implement for, and if I made a minimally functional one because it was necessary for for (the loop must be stopped once, and without if it is problematic to do this).
    ; do = { $* }
    ; dont = { }
    ; if = {
        (cmd cond) := $*
        { $2 $cmd } $cond $do $dont
    }
    ; for = {
        (var in list) := $*
        code := $$#*
        $iter $var $code $list
    }
    ; iter = { 
        (var code list) := $*
        (cur list) := $list
        (next unused) := $list
        $if {$var=$cur; $code; $iter $var $code $list} $next
    }
    ; $for i in 10 20 30 { echo i is $i }
    i is 10
    i is 20
    i is 30
    ;
    


    Interesting little things


    By default, the shell forks the namespace when the script starts, so the script cannot change the namespace of the process that launched it. This does not fit scripts whose task is just to configure the namespace of their parent. Such scripts should begin on #!/dis/sh -n.

    The built-in command loadedlists all built-in commands from all loaded modules. A built-in command whatisdisplays information on variables, functions, commands, etc.

    If you create a variable $autoloadwith a list of shell modules, then these modules will be automatically loaded into each shell launched.

    There is syntactic sugar support in the shell: &&and ||. These statements are available in bare sh, but are converted to calling built-in commands andandor, Which "naked sh» no, they are from the module the std (so use &&and ||only after load std).
    ; echo { cmd && cmd }
    {and {cmd} {cmd}}
    ; echo { cmd || cmd }
    {or {cmd} {cmd}}
    

    Applications written in Limbo received, for example, when starting with a command line parameter a line with a block of sh commands can very easily execute it using the sh (2) module .

    The style of work of the shell with variables and lists of strings leads to the fact that, unlike traditional shells, in inferno one has to think about shielding the parameters and values ​​of variables only once - when they are first mentioned. And then you can transfer them wherever and as you like without thinking about what symbols they have.

    Summary


    Эта небольшая статья — практически полный reference guide по инферновскому шеллу. В смысле, описана вся функциональность базового шелла, и довольно подробно — со всеми нюансами и примерами. Если вы прочитаете sh(1), то увидите, что я не упомянул разве что переменные $apid, $prompt, пару-тройку встроенных команд, опции самого sh да полный список спец.символов, которые нельзя использовать в строках вне одинарных кавычек.

    Если не брать довольно продвинутые возможности по перенаправлению ввода/вывода, то используя всего лишь:
    • строки с тривиальнейшими правилами экранирования
    • блоки команд (а по сути это просто те же строки)
    • списки строк с одним оператором ^
    • переменные с одним оператором =
    • обращение к переменным через $var, $#var и $"var
    quite full shell is implemented! Full-fledged even in the "bare" form, which has been convincingly proved by the ability to implement functions on it, if and for (and I also figured out how to make an analogue of the raise command from the std module - that is, an analogue of the traditional /bin/false:) but this is a hack through runand to the article I did not turn it on).

    And when we start loading modules to it, the possibilities and usability of the shell increase by an order of magnitude. For example, the same sh-std (1) module adds:
    • Several conditional operators ( and, or, if)
    • Teams compare and check the conditions ( !, ~, no)
    • several operators cycle ( apply, for, while, getlines)
    • functions of both types ( fn, subfn)
    • work with exceptions and status ( raise, rescue, status)
    • work strings, and lists ( ${hd}, ${tl}, ${index}, ${split}, ${join})
    • etc.
    But all these additional commands do not complicate the syntax of the shell itself, it remains the same trivial and elegant!

    Also popular now: