Shell notes

Even if you’ve been using a shell for a long time there are a few things you might still have to (re)learn. These things apply to both bash and zsh (which I’ve been using for quite some time).

Parameter expansion

For our exploration write this little C program:

#include <stdio.h>

int main(int argc, char *argv[]) {
  for(int i = 1; i < argc; i++) {
    printf("%d: %s\n", i, argv[i]);
  }
  return 0;
}

Save it in a file called show-args.c and compile it with

gcc show-args.c -o show-args

All it does is write the arguments it is receiving (note that we’re deliberately excluding argv[0], which is the program name itself).

$ ./show-args foobar
1: foobar

$ ./show-args foo bar
1: foo
2: bar

So. When you do something like

./show-arg *.txt

The command is not receving an *.txt argument: it is expanded by the shell. The command will believe it’s being called as

./show-arg 1.txt 2.txt

Its ARGV contains foo.txt and bar.txt, not *.txt. But if the expansion doesn’t find any match then the argument is passed verbatim:

$ ./show-args *.txt
1: 1.txt
2: 2.txt

$ ./show-args *.foo
1: *.foo

Variables

The shell takes care of replacing a variable name with its value before executing the command. The command itself isn’t aware of that.

$ printenv USER
pzac

$ echo hello $USER
hello pzac

stdin, stdout, stderr

I’ve always been wondering what was the difference between

wc foo.txt

and

wc < foo.txt

The former passes the filename to the command. It will have to take care of opening it, reading it, etc. The latter will use the contents of the file as stdin: to the command this will be analogous to the user manually typing the contents of the file. A lot of utilities are able to handle both forms.

The modern way to redirect both stdin and stdout is:

foo &> bar.txt

Or, to append:

foo &>> bar.txt

We no longer need the confusing

foo >> bar.txt 2>&1

Aliases

You can call the unaliased version of a command using a \ prefix.

History

Bash will expand these tokens:

The nice thing is that they’re printed and won’t be executed unless you press enter. You can also fix the last command by replacing strings with ^, and again the substitution is printed and not executed :

$ tac 1.txt
zsh: command not found: tac
$ ^tac^cat
$ cat 1.txt

Grep and friends

grep foo bar.txt

By default foo is a regexp! To make it verbatim use the -F flag. Other useful flags

These flags also work with rg (ripgrep).

Shells, child shells and subshells

A subshell is a copy of the parent shell that preserves variables, functions, etc. You’re basically forking the current shell. You fire it by enclosing in parentheses your command:

$ foo=123

# current shell
$ echo $foo
> 123

# child shell
$ sh -c 'echo $foo'
>

# subshell
$ (echo $foo)
> 123

A child shell doesn’t inherit variables, functions etc. It basically forks and execs.