summaryrefslogtreecommitdiff
path: root/docs/handbook/corebinutils/test.md
blob: 11b429ab2a496e97a89449ae12c70482137738a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# test — Evaluate Conditional Expressions

## Overview

`test` (also invoked as `[`) evaluates file attributes, string comparisons,
and integer arithmetic, returning an exit status of 0 (true) or 1 (false).
It uses a recursive descent parser with short-circuit evaluation and
supports both POSIX and BSD extensions.

**Source**: `test/test.c` (single file)
**Origin**: BSD 4.4, University of California, Berkeley
**License**: BSD-3-Clause

## Synopsis

```
test expression
[ expression ]
```

When invoked as `[`, the last argument must be `]`.

## Source Analysis

### Parser Architecture

```c
struct parser {
    int argc;
    char **argv;
    int pos;     /* Current argument index */
};

enum token {
    TOK_OPERAND,   /* String/number operand */
    TOK_UNARY,     /* Unary operator (-f, -d, etc.) */
    TOK_BINARY,    /* Binary operator (-eq, =, etc.) */
    TOK_NOT,       /* ! */
    TOK_AND,       /* -a */
    TOK_OR,        /* -o */
    TOK_LPAREN,    /* ( */
    TOK_RPAREN,    /* ) */
    TOK_END,       /* End of arguments */
};
```

### Operator Table

```c
struct operator {
    const char *name;
    enum token type;
    int (*eval)(/* ... */);
};
```

### Recursive Descent Grammar

```
parse_expr()
  └── parse_oexpr()      /* -o (OR, lowest precedence) */
        └── parse_aexpr()    /* -a (AND) */
              └── parse_nexpr()  /* ! (NOT) */
                    └── parse_primary() /* atoms, ( expr ) */
```

### Functions

| Function | Purpose |
|----------|---------|
| `main()` | Entry: handle `[`/`test` invocation, drive parser |
| `current_arg()` | Return current argument |
| `peek_arg()` | Look at next argument |
| `advance_arg()` | Consume current argument |
| `lex_token()` | Classify current argument as token type |
| `find_operator()` | Look up operator in table |
| `parse_primary()` | Parse `( expr )`, unary ops, binary ops |
| `parse_nexpr()` | Parse `! expression` |
| `parse_aexpr()` | Parse `expr -a expr` |
| `parse_oexpr()` | Parse `expr -o expr` |
| `parse_binop()` | Evaluate binary operators |
| `evaluate_file_test()` | Evaluate file test primaries |
| `compare_integers()` | Integer comparison |
| `compare_mtime()` | File modification time comparison |
| `newer_file()` | `-nt` test |
| `older_file()` | `-ot` test |
| `same_file()` | `-ef` test |
| `parse_int()` | Parse integer with error checking |
| `effective_access()` | `eaccess(2)` or `faccessat(AT_EACCESS)` |

### File Test Primaries

| Operator | Test | System Call |
|----------|------|------------|
| `-b file` | Block special | `stat(2)` + `S_ISBLK` |
| `-c file` | Character special | `stat(2)` + `S_ISCHR` |
| `-d file` | Directory | `stat(2)` + `S_ISDIR` |
| `-e file` | Exists | `stat(2)` |
| `-f file` | Regular file | `stat(2)` + `S_ISREG` |
| `-g file` | Set-GID bit | `stat(2)` + `S_ISGID` |
| `-h file` | Symbolic link | `lstat(2)` + `S_ISLNK` |
| `-k file` | Sticky bit | `stat(2)` + `S_ISVTX` |
| `-L file` | Symbolic link | `lstat(2)` + `S_ISLNK` |
| `-p file` | Named pipe (FIFO) | `stat(2)` + `S_ISFIFO` |
| `-r file` | Readable | `eaccess(2)` or `faccessat(2)` |
| `-s file` | Non-zero size | `stat(2)` + `st_size > 0` |
| `-S file` | Socket | `stat(2)` + `S_ISSOCK` |
| `-t fd` | Is a terminal | `isatty(3)` |
| `-u file` | Set-UID bit | `stat(2)` + `S_ISUID` |
| `-w file` | Writable | `eaccess(2)` or `faccessat(2)` |
| `-x file` | Executable | `eaccess(2)` or `faccessat(2)` |
| `-O file` | Owned by EUID | `stat(2)` + `st_uid == geteuid()` |
| `-G file` | Group matches EGID | `stat(2)` + `st_gid == getegid()` |

### String Operators

| Operator | Description |
|----------|-------------|
| `-z string` | String is zero length |
| `-n string` | String is non-zero length |
| `s1 = s2` | Strings are identical |
| `s1 == s2` | Strings are identical (alias) |
| `s1 != s2` | Strings differ |
| `s1 < s2` | String less than (lexicographic) |
| `s1 > s2` | String greater than (lexicographic) |

### Integer Operators

| Operator | Description |
|----------|-------------|
| `n1 -eq n2` | Equal |
| `n1 -ne n2` | Not equal |
| `n1 -lt n2` | Less than |
| `n1 -le n2` | Less or equal |
| `n1 -gt n2` | Greater than |
| `n1 -ge n2` | Greater or equal |

### File Comparison Operators

| Operator | Description |
|----------|-------------|
| `f1 -nt f2` | f1 is newer than f2 |
| `f1 -ot f2` | f1 is older than f2 |
| `f1 -ef f2` | f1 and f2 are the same file (device + inode) |

### Short-Circuit Evaluation

```c
static int
parse_oexpr(struct parser *p)
{
    int result = parse_aexpr(p);

    while (current_is(p, "-o")) {
        advance_arg(p);
        int right = parse_aexpr(p);
        result = result || right;  /* Short-circuit */
    }

    return result;
}

static int
parse_aexpr(struct parser *p)
{
    int result = parse_nexpr(p);

    while (current_is(p, "-a")) {
        advance_arg(p);
        int right = parse_nexpr(p);
        result = result && right;  /* Short-circuit */
    }

    return result;
}
```

### Bracket Mode

```c
int main(int argc, char *argv[])
{
    /* If invoked as "[", last arg must be "]" */
    const char *progname = basename(argv[0]);
    if (strcmp(progname, "[") == 0) {
        if (argc < 2 || strcmp(argv[argc - 1], "]") != 0)
            errx(2, "missing ]");
        argc--;  /* Remove trailing ] */
    }

    if (argc <= 1)
        return 1;  /* No expression → false */

    struct parser p = { argc - 1, argv + 1, 0 };
    int result = parse_oexpr(&p);

    if (p.pos < p.argc)
        errx(2, "unexpected argument: %s", current_arg(&p));

    return !result;  /* 0 = true, 1 = false */
}
```

## System Calls Used

| Syscall | Purpose |
|---------|---------|
| `stat(2)` | File attribute tests |
| `lstat(2)` | Symlink tests (`-h`, `-L`) |
| `eaccess(2)` / `faccessat(2)` | Permission tests (`-r`, `-w`, `-x`) |
| `isatty(3)` | Terminal test (`-t`) |
| `geteuid(3)` / `getegid(3)` | Ownership tests (`-O`, `-G`) |

## Examples

```sh
# File exists
test -f /etc/passwd && echo "exists"

# Using [ syntax
[ -d /tmp ] && echo "is a directory"

# String comparison
[ "$var" = "hello" ] && echo "match"

# Integer comparison
[ "$count" -gt 10 ] && echo "more than 10"

# Combined with AND
[ -f file.txt -a -r file.txt ] && echo "readable file"

# File newer than another
[ config.new -nt config.old ] && echo "config updated"

# Negation
[ ! -e /tmp/lockfile ] && echo "no lock"

# Parenthesized expression
[ \( -f a -o -f b \) -a -r c ]
```

## Exit Codes

| Code | Meaning |
|------|---------|
| 0    | Expression is true |
| 1    | Expression is false |
| 2    | Invalid expression (syntax error) |