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:
!!
last command!$
last argument of the last command!*
arguments of the last command
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
-i
case-insensitive-w
whole words
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 fork
ing 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 fork
s and exec
s.