Hi John,

I'm a bit busy right now, so will reply properly later...

> A bash detail puzzles me.  I used the (multi-)command in the bug
> report and put sudo in front of it.  That gives:
>
>      $ sudo for FILE in $(dpkg-divert --list | grep nvidia-340 | awk '{print 
> $3}'); do
>      >     dpkg-divert --remove $FILE
>      > done
>      bash: syntax error near unexpected token `do'
>
> It works OK when I put the command in a file.

...but that's an easy one.

The shell's main purpose is to run programs, and it looks for those
along the directories in the PATH environment variable, e.g. `ls' is
found as /bin/ls and executed.

Some things that may appear similar are built in to the shell because of
Unix's design.  `cd' is a key one.  Each process has a current working
directory and that can only be affected by the process.  One can write a
/bin/cd that processes its argument and calls chdir(2) but that changes
the directory of the process running the /bin/cd executable and when the
process exits, the change dies with it leaving the shell in the same
working directory as before.  So `cd' has to be a shell built in.

Over time, other commands that can work as /bin/foo were also built in
to some shells for speed.

Control flow commands weren't initially built in to the shell as there
was no room for the extra code.  goto(1) used to manipulate the shell's
file offset into the script after seeking out the corresponding :(1)
label.  As space became available, control flow moved into the shell.
The `for' above is a shell keyword for the control-flow loop and is
built in.

type(1) is a shell built-in command that describes what the shell thinks
of given names.

    $ type :
    : is a shell builtin
    $ type for
    for is a shell keyword
    $ type ls
    ls is aliased to `ls --color=auto'
    $ type diff
    diff is /usr/bin/diff
    $ type sudo
    sudo is hashed (/usr/bin/sudo)
    $

help(1) will give the man-page extract for a built-in in bash, e.g.
`help time'.

The shell is attempting to parse all of the entered line before running
any of it.  There's a grammar involved that's more complex than just
every command is a run of words.  `do' is a reserved word and only
appears in grammar productions within a for-loop or similar.  Here's an
extract from
http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html

    for_clause   : For name                                      do_group
                 | For name                       sequential_sep do_group
                 | For name linebreak in          sequential_sep do_group
                 | For name linebreak in wordlist sequential_sep do_group
                 ;
    while_clause : While compound_list do_group
                 ;
    until_clause : Until compound_list do_group
                 ;
    do_group     : Do compound_list Done
                 ;
    %token  If    Then    Else    Elif    Fi    Do    Done
    /*      'if'  'then'  'else'  'elif'  'fi'  'do'  'done'   */
    %token  Case    Esac    While    Until    For
    /*      'case'  'esac'  'while'  'until'  'for'   */

The shell cannot pass your command, which boils down to

    $ sudo for; do bar; done
    -bash: syntax error near unexpected token `do'
    $

`for' is only recognised as the start of a for_clause when it appears in
the right place according to the grammar.  Otherwise, `echo for ever'
wouldn't work.  Thus the `sudo for ...' means no for-loop has been
started and `do' isn't valid at the start of a command.

sudo(1) is an example of a command that takes another command to run as
its parameters.  env(1) and nice(1) are others.  sudo isn't built in to
the shell and passes its arguments to execve(2) for the kernel to
overwrite the sudo executable in the current process with the new one
given in the arguments.  There is no /bin/for executable so the
approaches are to use a script, as you did, or to have sudo run a shell
and specify the shell commands as a parameter.

    $ sudo sh -c 'for f in 3 1 4; do >/tmp/$f; done'
    [sudo] password for ralph:
    $ ls -l /tmp/{3,1,4}
    -rw-r--r-- 1 root root 0 Mar  1 11:21 /tmp/1
    -rw-r--r-- 1 root root 0 Mar  1 11:21 /tmp/3
    -rw-r--r-- 1 root root 0 Mar  1 11:21 /tmp/4
    $

This method requires quoting the arguments correctly so interpretation
of their contents happens either by the shell running sudo, or the shell
run by sudo, as desired.  Also, here I used sh(1) as it was adequate.
bash(1) might sometimes be needed.

In the case of your original command above, the sudo isn't required for
the first dpkg-divert(1) or awk(1), so it could have been

    for FILE in $(dpkg-divert --list | grep nvidia-340 | awk '{print $3}'); do
        sudo dpkg-divert --remove $FILE
    done

though that would add some overhead if the loop's body was run many
times.

-- 
Cheers, Ralph.

--
  Next meeting: BEC, Bournemouth, Tuesday, 2019-03-05 20:00
  Check to whom you are replying
  Meetings, mailing list, IRC, ...  http://dorset.lug.org.uk/
  New thread, don't hijack:  mailto:dorset@mailman.lug.org.uk

Reply via email to